grokky
grokky

Reputation: 9255

Double validation in MediatR pipeline

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();
}

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.

Upvotes: 1

Views: 3777

Answers (3)

DeLucas
DeLucas

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

grokky
grokky

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

Pavel
Pavel

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

Related Questions