Reputation: 9255
I'm on ASP.NET Core and the new MediatR which supports pipelines. My pipeline includes validation.
Consider this action:
[HttpPost]
[HandleInvalidCommand]
public IActionResult Foo(Command command)
{
await _mediator.Send(command);
return View();
}
HandleInvalidCommand
checks ModelState.IsValid
, and if invalid then redirects to the view for the user to correct the dataSo if the command is valid, then validation occurs twice (and validators are expensive to run).
How best can I deal with this?
EDIT: The obvious way is to remove validation from the pipeline, but that is no good because the command may come from the UI, but also from the app itself. And you want validation in both cases.
Upvotes: 1
Views: 3777
Reputation: 101
I think the ideal solution would be to separate the classes that you use between the command/query and the model coming in from the client. This is probably the most correct design as it keeps your command/query classes dedicated to your application core inputs, and therefore won't be modified to suit the client and change over time. This would keep your CQRS classes more pure when it comes to your application core.
However that does mean more duplication of classes to provide more classes for the client's inputs.
Upvotes: 1
Reputation: 9255
I found another way. Maybe not the best, but it works.
Define this interface
public interface IValidated
{
bool AlreadyValidated { get; }
}
Decorate the request
public class Command : IRequest, IValidated
{
public bool AlreadyValidated { get; set; }
// etc...
}
Update the request's validator to use an interceptor:
public class CommandValidator : AbstractValidator<Command>, IValidatorInterceptor
{
public CommandValidator() {
// validation rules etc.
}
public ValidationContext BeforeMvcValidation(ControllerContext controllerContext, ValidationContext validationContext)
{
return validationContext;
}
public ValidationResult AfterMvcValidation(ControllerContext controllerContext, ValidationContext validationContext, ValidationResult result)
{
var command = validationContext.InstanceToValidate as Command;
if (command != null) command.AlreadyValidated = true;
return result;
}
}
Update the pipeline:
public class MyPipeline<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>, IValidated // update here
{
public async Task<TResponse> Handle(
TRequest message,
RequestHandlerDelegate<TResponse> next)
{
if (!message.AlreadyValidated) // update here
{
var context = new ValidationContext(message);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(e => e.Errors)
.Where(e => e != null)
.ToList();
if (failures.Count != 0)
throw new ValidationException(failures);
}
return await next();
}
}
So after validation by MVC/FluentValidation, it sets the flag. Then in the CQRS pipeline, if that flag is set, it doesn't perform validation again.
However I'm not sure I like this, as I'm leaking stuff into the command that shouldn't be there.
Upvotes: 1
Reputation: 656
FluentValidation
does not stop handling your command even if validation fails - it just registers rules.
Mediatr Validation Pipeline
checks for existing validation errors and stops sending command - Handler wont fire if errors exist.
But you implemented your own logic - HandleInvalidCommand
. You should choose one option - mediatr pipiline or implementing own logic with ModelState.IsValid
Upvotes: 1