'Fluent validation divide bussines validation from auth validaton
I'm using ASP, CQRS + MediatR and fluent validation. I want to implement user role validation, but I don't want to mix it with business logic validation. Do you have any idea how to implement this? I mean a specific validator must be executed for a specific request. Something tells me the solution lies in IEnumerable< IValidator>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) => _validators = validators;
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
if (_validators.Any())
{
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToArray();
if (failures.Any())
{
var errors = failures
.Select(x => new Error(x.ErrorMessage, x.ErrorCode))
.ToArray();
throw new ValidationException(errors);
}
}
return await next();
}
}
Solution 1:[1]
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.
Solution 2:[2]
I think that your initial approach its right. When you say that you want to keep the auth validations apart from the other business validation, do you mean like returning a http error like 403 and 401 right? If thats the case try marking the auth validations with and interface to identify they, and do not run all the validations at once. Search first in the collection for a validation with that interface, and if it fails send a custom exception that you can identity in a IActionFilter to set the wanted result. This code does not do that exactly but you can make an idea.
public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
{
private ISystemLogger _logger;
public HttpResponseExceptionFilter()
{
}
public int Order { get; } = int.MaxValue - 10;
public void OnActionExecuting(ActionExecutingContext context) { }
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.Exception is PipelineValidationException exception)
{
context.Result = new ObjectResult(new Response(false, exception.ValidationErrors.FirstOrDefault()?.ErrorMessage ?? I18n.UnknownError));
context.ExceptionHandled = true;
}
else if (context.Exception != null)
{
_logger ??= (ISystemLogger)context.HttpContext.RequestServices.GetService(typeof(ISystemLogger));
_logger?.LogException(this, context.Exception, methodName: context.HttpContext.Request.Method);
context.Result = new ObjectResult(new Response(false, I18n.UnknownError));
context.ExceptionHandled = true;
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | José Ramón Barro |