I see your concern, I also found myself in this situation. I wanted to separate my validators from handlers while also keeping them in the domain/business project. Also I didn't want to throw exceptions just to handle bad request or any other custom business exception.
You have the right idea by
I mean a specific validator must be executed for a specific request
For this, you need to set up a mediator pipeline, so for every Command you can find the appropriate the appropriate validator, validate and decide whether to execute the command or return a failed result.
First, create an interface(although not necessary but it is how I did it) of ICommand
like this:
public interface ICommand<TResponse>: IRequest<TResponse>
{
}
And, ICommandHandler
like:
public interface ICommandHandler<in TCommand, TResponse>: IRequestHandler<TCommand, TResponse>
where TCommand : ICommand<TResponse>
{
}
This way we can only apply validation to commands. Instead of iheriting IRequest<MyOutputDTO>
and IRequestHandler<MyCommand, MyOutputDTO>
you inherit from ICommand
and ICommandHandler
.
Now create a ValidationBehaviour
for the mediator as we agreed before.
public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : class, ICommand<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators) => _validators = validators;
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
if (!_validators.Any())
return await next();
var validationContext = new ValidationContext<TRequest>(request);
var errors = (await Task.WhenAll(_validators
.Select(async x => await x.ValidateAsync(validationContext))))
.SelectMany(x => x.Errors)
.Where(x => x != null)
.Select(x => x.CustomState)
.Cast<TResponse>();
//TResponse should be of type Result<T>
if (errors.Any())
return errors.First();
try
{
return await next();
}
catch(Exception e)
{
//most likely internal server error
//better retain error as an inner exception for debugging
//but also return that an error occurred
return Result<TResponse>.Failure(new InternalServerException(e));
}
}
}
This code simply, excepts all the validators in the constructor, because you register all your validator from assembly for your DI container to inject them.
It waits for all validations to validate async(because my validations mostly require calls to db itself such as getting user roles etc).
Then check for errors and return the error(here I have created a DTO to wrap my error and value to get consistent results).
If there were no errors simply let the handler do it's work return await next();
Now you have to register this pipeline behavior and all the validators.
I use autofac so I can do it easily by
builder
.RegisterAssemblyTypes(_assemblies.ToArray())
.AsClosedTypesOf(typeof(IValidator<>))
.AsImplementedInterfaces();
var mediatrOpenTypes = new[]
{
typeof(IRequestHandler<,>),
typeof(IRequestExceptionHandler<,,>),
typeof(IRequestExceptionAction<,>),
typeof(INotificationHandler<>),
typeof(IPipelineBehavior<,>)
};
foreach (var mediatrOpenType in mediatrOpenTypes)
{
builder
.RegisterAssemblyTypes(_assemblies.ToArray())
.AsClosedTypesOf(mediatrOpenType)
.AsImplementedInterfaces();
}
If you use Microsoft DI, you can:
services.AddMediatR(typeof(Application.AssemblyReference).Assembly);
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly); //to add validators
Example usage:
My generic DTO Wrapper
public class Result<T>: IResult<T>
{
public Result(T? value, bool isSuccess, Exception? error)
{
IsSuccess = isSuccess;
Value = value;
Error = error;
}
public bool IsSuccess { get; set; }
public T? Value { get; set; }
public Exception? Error { get; set; }
public static Result<T> Success(T value) => new (value, true, null);
public static Result<T> Failure(Exception error) => new (default, false, error);
}
A sample Command:
public record CreateNewRecordCommand(int UserId, string record) : ICommand<Result<bool>>;
Validator for it:
public class CreateNewRecordCommandValidator : AbstractValidator<CreateNewRecordCommand>
{
public CreateNewVoucherCommandValidator(DbContext _context, IMediator mediator) //will be injected by out DI container
{
RuleFor(x => x.record)
.NotEmpty()
.WithState(x => Result<bool>.Failure(new Exception("Empty record")));
//.WithName("record") if your validation a property in array or something and can't find appropriate property name
RuleFor(x => x.UserId)
.MustAsync(async(id, cToken) =>
{
//var roles = await mediator.send(new GetUserRolesQuery(id, cToken));
//var roles = (await context.Set<User>.FirstAsync(user => user.id == id)).roles
//return roles.Contains(MyRolesEnum.CanCreateRecordRole);
}
)
.WithState(x => Result<bool>.Failure(new MyCustomForbiddenRequestException(id)))
}
}
This way you always get a result object, you can check if error is null
or !IsSuccess
and then create a custom HandleResult(result)
method in your Controller base which can switch on the exception to return BadReuqestObjectResult(result)
or ForbiddenObjectResult(result)
.
If you prefer to throw, catch and handle the exceptions in the pipeline or you wan't non-async implementation, read this https://code-maze.com/cqrs-mediatr-fluentvalidation/
This way all your validations are very far from your handler while maintaining consistent results.