9

I am currently working with Pipeline behavior in Mediatr 3 for request validation. All the examples that I came across were throwing ValidationException if any failures happening, instead of doing that I want to return the response with the error. Anyone has idea on how to do it?

Below is the code for the validation pipeline:

public class ValidationPipeline<TRequest, TResponse> :
    IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationPipeline(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next)
    {
        var failures = _validators
            .Select(v => v.Validate(request))
            .SelectMany(result => result.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Any())
        {
            throw new ValidationException(failures);
        }

        return next();
    }
}

Note: I found this question Handling errors/exceptions in a mediator pipeline using CQRS? and I am interested in the 1st option on the answer, but no clear example on how to do that.

This is my response class:

public class ResponseBase : ValidationResult
{
    public ResponseBase() : base() { }

    public ResponseBase(IEnumerable<ValidationFailure> failures) : base(failures)  {
    }
}

and I added below signature in the validation pipeline class:

public class ValidationPipeline<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> 
where TResponse : ResponseBase

I did this then in the Handle method:

var response = new ResponseBase(failures);
return Task.FromResult<TResponse>(response);

But that gave me error 'cannot convert to TResponse'.

Community
  • 1
  • 1
anggiputper
  • 193
  • 1
  • 9
  • `return the response with the error` - what does the `response` look like? – Alex May 19 '17 at 08:40
  • The pipeline `TResponse` will be the same as the underlying handler `TResponse` - the `ResponseBase : ValidationResult` feels like a code smell to me. I'm not clear on what you're trying to achieve . – Alex May 19 '17 at 09:32
  • I am trying to inherit from ValidationResult class of FluentValidation – anggiputper May 19 '17 at 09:38
  • That implies that the WHOLE pipeline (including your main handler) will return this type, is that correct? – Alex May 19 '17 at 09:39
  • Yes, exactly. Thanks for pointing that out. So I decided to not inherit ResponseBase from any class and it now works! Thanks a lot. – anggiputper May 19 '17 at 12:11

3 Answers3

4

Several years ago, I created general Result object, which I am constantly improving. It is quite simple, check https://github.com/martinbrabec/mbtools.

If you will be ok with the Result (or Result<>) being the return type every method in Application layer, then you can use the ValidationBehavior like this:

public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
    where TResponse : Result, new()
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        if (_validators.Any())
        {
            var context = new ValidationContext(request);

            List<ValidationFailure> failures = _validators
                .Select(v => v.Validate(context))
                .SelectMany(result => result.Errors)
                .Where(f => f != null)
                .ToList();

            if (failures.Any())
            {
                TResponse response = new TResponse();

                response.Set(ErrorType.NotValid, failures.Select(s => s.ErrorMessage), null);

                return Task.FromResult<TResponse>(response);
            }
            else
            {
                return next();
            }
        }

        return next();
    }

}

Since all your handlers return Result (or Result<>, which is based upon Result), you will be able to handle all validation errors without any exception.

Martin Brabec
  • 3,720
  • 2
  • 23
  • 26
3

Simply don't call next if there's any failures:

public Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next)
{
    var failures = _validators
        .Select(v => v.Validate(request))
        .SelectMany(result => result.Errors)
        .Where(f => f != null)
        .ToList();

    if (failures.Any())
    {
        var response = new Thing(); //obviously a type conforming to TResponse
        response.Failures = failures; //I'm making an assumption on the property name here.

        return Task.FromResult(response);
    }
    else
    {
        return next();
    }
}

Note:
Your class (Thing in my example) must be of type TResponse

Alex
  • 37,502
  • 51
  • 204
  • 332
  • Hi, I'll answer your question above here. My response class look like this: `public class ResponseBase : ValidationResult { public ResponseBase() : base() { } public ResponseBase(IEnumerable failures) : base(failures) { } }` I did this, but I got error when try to convert ResponseBase to TResponse var response = new ResponseBase(failures); return Task.FromResult(response); – anggiputper May 19 '17 at 09:17
  • I am working on it, still struggle with the formatting, sorry :/ – anggiputper May 19 '17 at 09:24
  • The main issue here is that the TResponse is not always known or in some cases a Generic Type (it's a design decision) – Verbe Apr 06 '19 at 00:04
  • This will act like the response is a normal one, due to its return code being 200. Just the return text message suggests a validation error. This is not ideal. I'm afraid the only perfect way to achieve this goal is to enhance MediatR to support writing response directly, which it lacks as of now. – Nico Mar 25 '22 at 00:18
0

You can configure validation handling using package https://www.nuget.org/packages/MediatR.Extensions.FluentValidation.AspNetCore

Just insert in configuration section:

services.AddFluentValidation(new[] {typeof(GenerateInvoiceHandler).GetTypeInfo().Assembly});

GitHub

GetoX
  • 4,225
  • 2
  • 33
  • 30
  • 7
    That’s not what was asked. They already know how to throw validation exceptions from the pipeline. They’re asking specifically how not to throw exceptions, but return the errors instead. – Jordan Walker Oct 17 '20 at 22:26