MediatR fluent validation response from pipeline behavior
Asked Answered
J

3

6

I have a MediatR Pipeline behavior for validating commands with the FluentValidation library. I've seen many examples where you throw a ValidationException from the behavior, and that works fine for me. However in my scenario I want to update my response object with the validation errors.

I am able to build and run the following code. When I set a break point within the if statement the CommandResponse is constructed with the validation errors as expected - but when the response is received by the original caller it is null:

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

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

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

        // Run the associated validator against the request
        var failures = _validators
            .Select(v => v.Validate(context))
            .SelectMany(result => result.Errors)
            .Where(f => f != null)
            .ToList();

        if(failures.Count != 0)
        {
            var commandResponse = new CommandResponse(failures) { isSuccess = false };
            return commandResponse as Task<TResponse>;
        }
        else
        {   
            return next();
        }
    }
}

I think it has to do with my attempt to cast it as Task - but without this I get compiler errors. I'm returning the same type that my command handler would if validation passes so I am at a loss as to why it returns a null instance of the expected response. I feel like there is a better way to handle this, but I've tried a number of variations to no avail. Any suggestions? Is there a better pattern to use? I'd prefer to keep this in the pipeline as it will be reused a lot.

Jasun answered 9/1, 2019 at 6:8 Comment(1)
should the Handle method be async? Then you wouldn't need the cast to Task<>Remediable
J
10

I ended up adding exception handling middleware to the MVC project. Instead of trying to pass back the validation errors as an object I throw a ValidationException inside of the pipeline behavior and the middleware handles any and all exceptions across the entire project. This actually worked out better as I handle all exceptions in one place higher up in the processing chain.

Here is the updated portion of the code I posted:

if(failures.Count != 0)
{
    // If any failures are found, throw a custom ValidationException object
    throw new ValidationException(failures);
}
else
{   
    // If validation passed, allow the command or query to continue:
    return next();
}

Here is the exception handling middleware:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate next;

    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }


    private static Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        // Log issues and handle exception response

        if (exception.GetType() == typeof(ValidationException))
        {
            var code = HttpStatusCode.BadRequest;
            var result = JsonConvert.SerializeObject(((ValidationException)exception).Failures);
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = (int)code;
            return context.Response.WriteAsync(result);

        }
        else
        {
            var code = HttpStatusCode.InternalServerError;
            var result = JsonConvert.SerializeObject(new { isSuccess = false, error = exception.Message });
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = (int)code;
            return context.Response.WriteAsync(result);
        }
    }
}

You then register the middleware in your Startup before MVC is added:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseMiddleware(typeof(ErrorHandlingMiddleware));
    app.UseMvc();
}

Note: You can also create an extension method for your middleware:

public static class ErrorHandlingMiddlewareExtension
{
    public static IApplicationBuilder UseErrorHandlingMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ErrorHandlingMiddleware>();
    }
}

Which allows you to register it like this:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseErrorHandlingMiddleware();
    app.UseMvc();
}
Jasun answered 11/1, 2019 at 20:5 Comment(3)
I have added this middleware at the start of the Configure method in Startup but it does not get invoked when exception is thrown. I am using .Net Core 3.1. Am I missing anything?Shrove
Can you verify that you are throwing the exception so that the middleware catches it? Be sure you aren't just allowing the exception to silently fail and continue running the remainder of your process. If that looks good maybe post your code so it can be reviewed?Jasun
Code smells for me exception management antipattern. Handling exceptions is very slow.Rocher
S
1

I am using .Net core 3.1 and I was not able to catch the exceptions when I added the middleware before the following block in Configure function of Startup

  if (env.IsDevelopment())          
  {
    app.UseDeveloperExceptionPage();
  }

check in the configure method. Make sure to register it after the above statement like this. It is quite obvious but may help someone like me.

 if (env.IsDevelopment())
 {
    app.UseDeveloperExceptionPage();
 }
 app.UseMiddleware<ErrorHandlingMiddleware>(); 
Shrove answered 28/7, 2020 at 17:24 Comment(0)
D
0

I think you are using FluentValidation.AspNetCore higher than 8.0 there was some changes try using var context = new ValidationContext(request); it worked for me, good luck

Dehlia answered 9/3, 2024 at 9:40 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.