How to define one specifc exception handler in Mediatr for all requests

Viewed 6955

I use Mediatr for an ASP.NET core project to handle all requests. I have several Request/Response/Handlers implemented. Each of them can throw a specific exception, let's call this "MyException" class. I defined an exception handler as

    public class MyExceptionHandler : RequestExceptionHandler<MyRequest<MyResponse>, MyResponse, MyException>
    {
        protected override void Handle(MyRequest<MyResponse> request, MyException exception, RequestExceptionHandlerState<MyResponse> state)
        {
            MyResponse response = new MyResponse();
            //Set some specific properties here in the response to indicate an error occurred
            state.SetHandled(response);
        }
    }

I add this exception handler to the (Autofac) dependency container and this works if I throw a MyException in the MyHandler for MyRequest and MyResponse. However, I have dozens of requests, responses and corresponding handlers. So how can I register this exception handler for all of them for this specific exception (note: all responses are derived from a same base class). It tried something like the below, but, that does not get called. It only works if I give the actual types, but that would mean that I have to create an exception handler for each of the types, which is far from practical. Any ideas on how to solve this?

    public class MyExceptionHandler : RequestExceptionHandler<IRequest<BaseResponse>, BaseResponse, MyException>
    {
        protected override void Handle(IRequest<BaseResponse> request, MyException exception, RequestExceptionHandlerState<BaseResponse> state)
        {
            BaseResponse response = new BaseResponse();
            //Set some specific properties here in the response to indicate an error occurred
            state.SetHandled(response);
        }
    }
3 Answers

AFAIK, the handlers are meant to be used for specific typed requests, responses and exceptions. MediatR in this case is not able to execute a generic variant, because that's not how they are being resolved, when an exception occurs.

So basically, what you have done is pretty close to what you are trying to achieve. You can create a generic abstract handler and have as many derived typed implementations as you need, but you will always have to create that boilerplate. Here a sample for a handler that catches an InvalidOperationException.

public class BaseResponse
{
    public bool Error { get; set; }

    public string ErrorMessage { get; set; } = null!;
}

public class SomeResponse : BaseResponse {}

public class BaseRequest<TResponse> : IRequest<TResponse> where TResponse : BaseResponse {}

public class SomeRequest : BaseRequest<SomeResponse> {}

public class SomeRequestHandler : IRequestHandler<SomeRequest, SomeResponse>
{
    public Task<SomeResponse> Handle(SomeRequest request, CancellationToken cancellationToken)
    {
        throw new InvalidOperationException();
    }
}

public abstract class
    AbstractInvalidOperationExceptionHandler<TRequest, TResponse> : RequestExceptionHandler<TRequest, TResponse, InvalidOperationException>
    where TRequest : BaseRequest<TResponse>
    where TResponse : BaseResponse, new()
{
    protected override void Handle(TRequest request, InvalidOperationException exception, RequestExceptionHandlerState<TResponse> state)
    {
        var response = new TResponse
        {
            Error = true, 
            ErrorMessage = exception.Message
        };

        state.SetHandled(response);
    }
}

public class SomeInvalidOperationExceptionHandler : AbstractInvalidOperationExceptionHandler<SomeRequest, SomeResponse>
{
    
}

This should work as desired, requiring you to add one derived type for each combination of TRequest + TResponse. Each of those derived types also have to be manually registered, if you don't use the Autofac-PlugIn for MediatR, which uses assembly-scanning to register all implementations of RequestExceptionHandler<,,,>.

There is a second option as well, which is similar to the suggested middleware but rather than writing a middleware, you would create one implementation for each exception of the interface IExceptionFilter (or IAsyncExceptionFilter if async is required here).

Given again handling the exception of type InvalidOperationFilter it would look something like that:

public class InvalidOperationExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        if (context.Exception is not InvalidOperationException invalidOperationException)
        {
            return;
        }

        context.ExceptionHandled = true; // this follows the same principle as MediatR, you need to set it as handled, for the pipeline to stop executing!
        // this will set the response to 409
        context.Result = new ConflictObjectResult(new
        {
            Message = invalidOperationException.Message
        });
    }
}

Now this works pretty fine and that filter only needs to be registered once but it might not be the desired way of handling errors and returning them to the client. It also (tightly) couples your exception-handling logic to the framework.

An alternative approach when you have the same type of exception being thrown in your MediatR handlers is to forgo the MediatR exception handlers and handle it globally via middleware. By doing that you ensure that you don't have the same piece of error-handling code scattered in multiple places.

Of course, you should consider will you always (to your best knowledge right now) handle the exception the exact same way, because if not that means that the middleware solution will most likely make your code less maintainable and clean if you start checking for custom conditions on the same exception.

So, here is an example middleware:

public class ValidationExceptionHandlerMiddleware
{
    private readonly RequestDelegate next;

    public ValidationExceptionHandlerMiddleware(RequestDelegate next) => this.next = next;

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await this.next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        switch (exception)
        {
            string result = "";
            case MyException validationException:
                //custom handling of my exception
                context.Response.ContentType = "application/json";
                context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
                result = //your serialized json object with error data
                break;
            default:
                context.Response.ContentType = "application/json";
                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                result = //your serialized json object with error data
                break;
        }

        return context.Response.WriteAsync(result);
    }
}

What you then need to do is register this middleware in the pipeline where you see fit. You have to register it before the middlewares you want to be handled.

You can do it in the Configure() method in your Startup.cs:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    builder.UseMiddleware<ValidationExceptionHandlerMiddleware>();
    ...
}

So now, all thrown and unhandled exceptions in the application will be handled by the middleware.

Take note that this is a lower-level solution that writes directly to the response, so if you need to execute business logic when handling the exception this might not be suitable.

You can create a generic one like this:

public class ExceptionLoggingHandler<TRequest, TResponse, TException> : IRequestExceptionHandler<TRequest, TResponse, TException>
         where TRequest : IRequest<TResponse>
         where TException : Exception
    {
        private readonly ILogger<ExceptionLoggingHandler<TRequest, TResponse, TException>> _logger;

        public ExceptionLoggingHandler(ILogger<ExceptionLoggingHandler<TRequest, TResponse, TException>> logger)
        {
            _logger = logger;
        }

        public Task Handle(TRequest request, TException exception, RequestExceptionHandlerState<TResponse> state, CancellationToken cancellationToken)
        {
            _logger.LogError(exception, "Something went wrong while handling request of type {@requestType}", typeof(TRequest));

            // TODO: when we want to show the user somethig went wrong, we need to expand this with something like
            // a ResponseBase where we wrap the actual response and return an indication whether the call was successful or not.
            state.SetHandled(default!);

            return Task.CompletedTask;
        }
    }

And then register it like this on a IerviceC:

services.AddTransient(typeof(IRequestExceptionHandler<,,>), typeof(ExceptionLoggingHandler<,,>))

Because the RequestExceptionProcessorBehavior class will look for this type with 3 generics, instead of the IRequestExceptionHandler<,> one.

Related