Skip to content

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.

Clone this wiki locally