Intercepting exceptions and changing the response .NET 6
I have recently come across an issue where I wanted to return status codes by throwing an exception. So for example, if I throw a NotFoundException (this would be a custom exception) then I would want that to return a 404 error. As a result, I wanted to better handle the status codes that are returned instead of the generic 500 error when an exception is thrown. This is so that we can handle and return appropriate error messages and status codes depending on the error that occurred on the backend.
In .NET there is a class that you can inherit called ExceptionFilterAttribute and the code in here will be run after an exception is thrown but before the response body is written. This is perfect because what I want to do is throw an appropriate error code so for example if we throw a NotFoundException, I want to return the 404 status code.
In the Web API project create a class called ApiExceptionFilterAttribute that inherits ExceptionFilterAttribute.
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
We then want to create a dictionary with the type of exception and the method that we want to call when this exception is triggered. That is going to look like the below.
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IDictionary<Type, Action<ExceptionContext>> _exceptionHandlers;
public ApiExceptionFilterAttribute()
{
_exceptionHandlers = new Dictionary<Type, Action<ExceptionContext>>()
{
{typeof(NotFoundException), HandleNotFoundException},
{typeof(ValidationException), HandleValidationException}
};
}
public override void OnException(ExceptionContext context)
{
HandleException(context);
base.OnException(context);
}
private void HandleException(ExceptionContext context)
{
Type type = context.GetType();
if (_exceptionHandlers.ContainsKey(type))
{
// _exceptionHandlers[type] gets us the value for that key and this gives us the method. We then pass the context to the method.
// When run the below code compiles to "HandleValidationException(context)" if NotFoundException was passed for example.
_exceptionHandlers[type].Invoke(context);
return;
}
}
}
Above, on line 18 you can see that we assign the NotFoundException class the HandleNotFoundException method. In HandleException you can see that if the exception is in the list we then get the value with that key so in this case, it will get the method. We then invoke that method and pass the context.
Next, we have to describe what we want that exception to return. In the example below, you can see that for a validation exception, we return a BadRequestObjectResult.
private void HandleValidationException(ExceptionContext context)
{
var exception = (ValidationException) context.Exception;
var details = new ValidationProblemDetails(exception.Errors)
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
};
context.Result = new BadRequestObjectResult(details);
context.ExceptionHandled = true;
}
An example of the NotFoundException would look like the below. Notice the ProblemDetails class.
private void HandleNotFoundException(ExceptionContext context)
{
var exception = (NotFoundException)context.Exception;
var details = new ProblemDetails()
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Title = "The specified resource was not found.",
Detail = exception.Message
};
context.Result = new NotFoundObjectResult(details);
context.ExceptionHandled = true;
}
Lastly, we have to register this class to the filters in the Startup.cs file, and then we are done.
services.AddControllers(options => options.Filters.Add<ApiExceptionFilterAttribute>())
.AddFluentValidation();
Final Thoughts
Using the ExceptionFilterAttribute is a really handy way of making sure that the error codes returned are logical for the type of error you are throwing. This gives greater flexibility in the front end because we can perform different actions and show different error messages depending on the status code returned.