-
Notifications
You must be signed in to change notification settings - Fork 29
User error reporting
Koen edited this page Nov 29, 2020
·
1 revision
If we use triggers for validation, we may want to abort a save and report a particular error to the user. The easiest way to accomplish this is by the use of Exceptions, e.g.:
public class VerifyUniqueEmailAddress : IBeforeSaveTrigger<User> {
readonly IApplicationDbContext _applicationDbContext;
public VerifyUniqueEmailAddress(ApplicationDbContext applicationDbContext) {
_applicationDbContext = applicationDbContext;
}}
public Task BeforeSave(ITriggerContext<User> context, CancellationToken cancellationToken) {
if (context.ChangeType == ChangeType.Added || context.ChangeType == ChangeType.Modified && context.Entity.EmailAddress != context.UnmodifiedEntity.EmailAddress)
{
var exists = _applicationDbContext.Users.Any(x => x.EmailAddress == context.Entity.EmailAddress);
if (exists) {
throw new ValidationException("Email address is already in use");
}
}
}
}
This simply requires the calling code from catching the ValidationException
and working with it. This is great for simple cases however you may want have a validation report of all Triggers, we can then rely on a shared service that can hold validation issues:
services.AddScoped<ValidationController>();
services.AddTransient<IBeforeSaveTrigger<User>, VerifyMinimumAge>();
services.AddTransient<IVerifyUniqueEmailAddress<User>, VerifyUniqueEmailAddress>();
services.AddTransient<IBeforeSaveTrigger<object>, AbortOnValidationErrors>();
public class ValidationController {
public List<string> Errors { get; } = new List<string>();
}
public class VerifyMinimumAge : IBeforeSaveTrigger<User> {
readonly ValidationController _validationController;
public VerifyMinimumAge(ValidationController validationController) {
this._validationController = validationController;
}
public Task BeforeSave(ITriggerContext<User> context, CancellationToken cancellationToken) {
if (context.Entity.Age <= 3) {
_validationController.Errors.Add("Toddlers not allowed!");
}
}
}
public class VerifyUniqueEmailAddress : IBeforeSaveTrigger<User> {
readonly IApplicationDbContext _applicationDbContext;
readonly ValidationController _validationController;
public VerifyUniqueEmailAddress(ApplicationDbContext applicationDbContext, ValidationController validationController) {
_applicationDbContext = applicationDbContext;
}}
public Task BeforeSave(ITriggerContext<User> context, CancellationToken cancellationToken) {
if (context.ChangeType == ChangeType.Added || context.ChangeType == ChangeType.Modified && context.Entity.EmailAddress != context.UnmodifiedEntity.EmailAddress)
{
var exists = _applicationDbContext.Users.Any(x => x.EmailAddress == context.Entity.EmailAddress);
if (exists) {
_validationController.Errors.Add("Email address is already in use");
}
}
}
}
public class AbortOnValidationErrors : IBeforeSaveTrigger<object>, ITriggerPriority {
readonly ValidationController _validationController;
public AbortOnValidationErrors(ValidationController validationController) {
this._validationController = validationController;
}
public int Priority => CommonTriggerPriority.Late;
public Task BeforeSave(ITriggerContext<User> context, CancellationToken cancellationToken) {
if (_validationController.Errors.Any()) {
var errorMessage = string.Join(", ", _validationController.Errors);
_validationController.Errors.Clear(); // In case we run SaveChanges again within the same session
throw new ValidationException(_validationController.Errors);
}
}
}
In this example we're using a scoped service called ValidationController
that triggers use to share state. We've also implemented an AbortOnValidationErrors
trigger which runs with a Late priority and throws our earlier seen Exceptions approach to break off a call to SaveChanges.