In this article, we are going to dive into global exception handling within an ASP.NET Core Web API controller using the MediatR library.

Particularly, we will focus on the MediatR way of handling exceptions. There is also another way of using Middlewares in ASP.NET Core to do a similar thing, which you can explore in our article here. If you want to know more about MediatR we have an article for you which you can check out here.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Implementing a Global Exception Handler

Even though using try-catch blocks in individual classes can be helpful some exceptions can pass unnoticed. A global exception handler acts as a safety net for our application, ensuring no unexpected errors disrupt its functionality.

The Implementation

We will be using MediatR’s IRequestExceptionHandler<TRequest, TResponse, TException> interface:

public interface IRequestExceptionHandler<in TRequest, TResponse, in TException>
    where TRequest : notnull
    where TException : Exception
{
    Task Handle(
        TRequest request, 
        TException exception, 
        RequestExceptionHandlerState<TResponse> state,
        CancellationToken cancellationToken);
}

This is essentially an interface that defines how to handle exceptions of a specific type TException that might occur when handling a particular type of request TRequest together with the return type TResponse.

As we can see, the IRequestExceptionHandler interface has three type parameters: TRequestTResponse, and TException.

The TRequest type parameter must implement the IRequest<TResponse> interface, which is used to define a request that can be handled by a request handler, and also specifies the type of response returned.

Next, the TResponse type parameter represents the response type returned when we handle the request.

Finally, the TException type represents the type of exception our middleware will handle later. We can also specify the type of exception we want to catch more concrete, for more accurate handling.

Following up on that, we define our custom request:

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

public class GetWeatherRequest : BaseRequest<WeatherResponse> { }

We also need a response:

public class BaseResponse
{
    public bool HasError { get; set; }
    public string Message { get; set; } = null!;
}

public class WeatherResponse : BaseResponse { }

Now, let’s add it all together and create our GlobalRequestExceptionHandler class, which implements the aforementioned IRequestExceptionHandler interface.

Firstly, we need to install a package:

dotnet add package Ben.Demystifier

With this package installed, we can start creating our GlobalRequestExceptionHandler class:

public class GlobalRequestExceptionHandler<TRequest, TResponse, TException> 
  : IRequestExceptionHandler<TRequest, TResponse, TException>
      where TResponse : BaseResponse, new()
      where TException : Exception
{
    private readonly ILogger<GlobalRequestExceptionHandler<TRequest, TResponse, TException>> _logger;

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

    public Task Handle(TRequest request, TException exception, RequestExceptionHandlerState<TResponse> state,
        CancellationToken cancellationToken)
    {
        var ex = exception.Demystify();

        _logger.LogError(ex, "Something went wrong while handling request of type {@requestType}", typeof(TRequest));

        var response = new TResponse
        {
            HasError = true,
            Message = "A server error ocurred",
        };

        state.SetHandled(response);

        return Task.CompletedTask;
    }
}

IRequestExceptionHandler requires us to implement the Handle() method, which takes four parameters: the request that caused the exception, the thrown exception itself, a state object that allows us to set the response for the request and a cancellationToken.

With the help of the where keyword, we specify our previously created response.

Inside the Handle() method we start by invoking the Demystify() method on the caught exception. This method is part of the Ben.Demystifier NuGet package we installed. This helps us to understand the exceptions better, preventing confusion on potential long stack traces.

Afterward, we simply log the raised exception.

Next, we want to use our state object of the RequestExceptionHandlerState<TReponse> type, mark the exception as handled, and pass our response as an argument to the SetHandled() method. This will return the created response in case of an error.

Registering Our Global Exception Handler

Once we’ve created our exception handler, we need to register it as a service within the dependency injection container inside our Program.cs:

builder.Services.AddTransient(typeof(IRequestExceptionHandler<,,>), typeof(GlobalRequestExceptionHandler<,,>));

This basically means that whenever an instance of IRequestExceptionHandler<,,> with a specific type of argument is needed, provide an instance of GlobalRequestExceptionHandler<,,>.

Testing the Global Exception Handler

Let’s attempt to generate an exception when we are using MediatR in a controller:

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly IMediator _mediator;

    public WeatherForecastController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet(Name = "GetWeather")]
    public async Task<ActionResult<WeatherResponse>> GetWeather()
    {
        var result = await _mediator.Send(new GetWeatherRequest());

        return result.HasError ? Problem(result.Message) : Ok(result);
    }
}

Our application consists of a single WeatherForecastController together with an IMediator instance. We also want to return a Problem() when an error has been raised, which is indicated by the property HasError on our result.

Once the HTTP request reaches the WeatherForecastController, it passes it to the handler:

public class GetWeatherHandler : IRequestHandler<GetWeatherRequest, WeatherResponse>
{
    public Task<WeatherResponse> Handle(GetWeatherRequest request, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

To emulate the behavior of an unexpected exception, inside the Handle() method, we throw a new NotImplementedException(). We can use any type of exception; the purpose here is to imitate unexpected errors.

Let’s run our application and call our WeatherForecast endpoint through Swagger:

Generic Exception Handling with MediatR: Swagger request to the WeatherForecastController

After that, we can inspect the response:

Generic Exception Handling with MediatR: Error response after executing our endpoint.

As we expected the response resulted in a 500 Internal Server Error.

Let’s review our console log for recorded exceptions:

Generic Exception Handling with MediatR: Error in console log showing the exception raised when requesting weather forecast

It appears that we have encountered an exception of the type System.NotImplementedException, which is exactly what we’ve thrown inside our GetWeatherHandler invoked by MediatR, together with the entire stack trace.

Conclusion

Wrapping up, in this article we’ve covered a generic global exception-handling mechanism in an ASP.NET Core application when using the MediatR library, preventing application crashes and unauthorized exposure of sensitive details to users.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!