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.
Let’s start.
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: TRequest
, TResponse
, 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:
After that, we can inspect the response:
As we expected the response resulted in a 500 Internal Server Error
.
Let’s review our console log for recorded exceptions:
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.