1

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();
}
  • The command is validated (I'm using FluentValidation)
  • HandleInvalidCommand checks ModelState.IsValid, and if invalid then redirects to the view for the user to correct the data
  • Else the action runs
  • The command is sent into the pipeline
  • The pipeline validates the command, AGAIN

So 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.

Community
  • 1
  • 1
grokky
  • 8,537
  • 20
  • 62
  • 96
  • so does the `HandleInvalidCommand` attribute run the validation itself? – Mickaël Derriey Feb 21 '17 at 22:43
  • @MickaëlDerriey Nope FluentValidation automatically validates the arguments during model binding. That attribute simply checks `ModelState.IsValid` and redirects back the view. I've edited to make it clearer. – grokky Feb 22 '17 at 07:35
  • 1
    OK. so yeah, I guess you have to choose between running the validation as part of the model binding or as part of the MediatR pipeline. It's really a *"it depends"* situation. On one side, having it in during model binding makes it easier to deal at the MVC level with errors. On the other side, having it as part of the MediatR pipeline makes it a cross-cutting concern, so applicable if you choose to plug the pipeline in another application/framework. Dealing with errors could be trickier, though. The validation behavior could throw an exception that an action filter could catch and handle. – Mickaël Derriey Feb 22 '17 at 11:01
  • @MickaëlDerriey Yeah that is the problem. But I added a compromise solution below. Curious to know what you think of it when you get a chance. – grokky Feb 22 '17 at 13:08

3 Answers3

1

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

Pavel
  • 616
  • 1
  • 7
  • 13
1

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.

grokky
  • 8,537
  • 20
  • 62
  • 96
  • Why dont you just remove Mediatr validation pipeline from your app? If you handle every command with `Interceptor`, validation pipeline is useless. – Pavel Feb 22 '17 at 16:59
  • @Pavel See my explanation in the edit above: 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. – grokky Feb 22 '17 at 18:02
  • when you say the command could come from the app itself, do you mean you would send a second command from a query/command handler? – Mickaël Derriey Feb 22 '17 at 21:18
  • @MickaëlDerriey Maybe. But what I mean in general is we have an app with lots of interconnected parts, and only one of them is the UI (MVC controllers, etc.) There are lots of non-UI things. Some of them also need to create/edit/etc. entities and queue things, and do things. So why not reuse the commands and handlers? They are already there, already tested. So if a command comes from within the app itself, it also needs to be validated. And this way allows validation from the UI, and from stuff generated within the app as well. Maybe not the best design, but it's clean and maintainable. – grokky Feb 23 '17 at 03:48
  • 1
    in this case, I'd suggest to remove the validation from MVC, so the only place where it happens is in the MediatR pipeline. You'll then need to translate validation errors from FluentValidation to the MVC model state and find a way to show the page with errors to the user. I personally don't like the double validation thing, even after the change to make it run only once. – Mickaël Derriey Feb 23 '17 at 05:19
  • @MickaëlDerriey I agree with you. That was my first approach, but it is very complicated. Mapping validation errors to ModelState is easy, but getting client-side metadata to work as well (so views show the errors) is not easy at all, because you need to do it manually (FV doesn't do it unless you opt-in for the whole configuration). So I chose this approach instead. If the FV library makes it easier at some point, I'll switch. This is basically a "compromise" solution. – grokky Feb 23 '17 at 08:16
1

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.

DeLucas
  • 101
  • 1
  • 6