What every ASP.NET Core Web API project needs - Part 3 - Exception handling middleware

moesmp

Mohsen Esmailpour

Posted on March 6, 2021

What every ASP.NET Core Web API project needs - Part 3 - Exception handling middleware

In the previous article, I wrote about API versioning and how to add Swagger to the sample project with support of API versioning. In this article, I show how to add custom middleware to handle exceptions globally and create a custom response when an error occurred.

Who can write bug-free codes? at least not me. While unhandled exceptions may occur in each system, it's really important to trap errors to log and fix them and showing proper response to the client. Exception handling middleware helps us to catch exceptions in a single place and avoid duplicate exception handling code through the application.

Step 1 - Implement exception handling middleware

First, add a new folder to the Infrastructure folder and call it Middlewares then add a new file ApiExceptionHandlingMiddleware.cs. Add following codes:

public class ApiExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ApiExceptionHandlingMiddleware> _logger;

    public ApiExceptionHandlingMiddleware(RequestDelegate next, ILogger<ApiExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

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

    private async Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        _logger.LogError(ex, $"An unhandled exception has occurred, {ex.Message}");

        var problemDetails = new ProblemDetails
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
            Title = "Internal Server Error",
            Status = (int)HttpStatusCode.InternalServerError,
            Instance = context.Request.Path,
            Detail = "Internal server error occured!"
        };

        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        var result = JsonSerializer.Serialize(problemDetails);

        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Alongside setting the status code of response to 500 context.Response.StatusCode = (int)HttpStatusCode.InternalServerError, a message in format of ProblemDetails exist in body of response.

Prior to ASP.NET Core 2.2 the default response type for an HTTP 400 (BadRequest(ModelState) was this:

{
  "": [
    "A non-empty request body is required."
  ]
}
Enter fullscreen mode Exit fullscreen mode

According to the Internet Engineering Task Force (IETF) RFC-7231 document, the ASP.NET Core team has implemented ProblemDetails, a machine-readable format for specifying errors in web API responses and complies with the RFC 7807 specification.

Step 2 - Register middleware

  • Create an extension method to register middleware:
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseApiExceptionHandling(this IApplicationBuilder app)
        => app.UseMiddleware<ApiExceptionHandlingMiddleware>();
}
Enter fullscreen mode Exit fullscreen mode
  • Open the Startup.cs class and in the Configure method add the middleware:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        ...
    }

    app.UseApiExceptionHandling();
Enter fullscreen mode Exit fullscreen mode

As you know the order of adding middleware components is important. If you are using UseDeveloperExceptionPage for the development environment, then add ApiExceptionHandling middleware after that.

Step 3 - Translate a business error into a domain exception

There are lots of arguments about when to throw an exception, however, when an exception should be thrown:

  • The first and foremost reason is completing the process and giving result is impossible (Fail Fast):
private async Task AddProductToBasketAsync(Guid productId)
{
    var product = await _repository.GetProductByIdAsync(productId);
    if(product == null)
        throw new DomainException($"Product with id {productId} could not be found."); 
        // Or simply return null?
        // Or return an error code or warping response into another object that has `Succeeded` property like `IdentityResult`?
        // Or return a tuple (false, "Product with id {productId} could not be found.")?
Enter fullscreen mode Exit fullscreen mode
  • One of the disadvantages of throwing an exception is that exception has a performance cost. If you are writing a high-performance application, throwing an exception can hurt the performance.

    To reiterate: exceptions should be truly exceptional.
  • What are the disadvantages of using error code or wrapper object?

    • Well throwing an exception is safer because the caller code may forget to check the result for error code or null and go ahead with the execution. You have to check the result of method calls from bottom to up
    • If you wrap the result into another object like IdentityResult, you should pay the extra heap allocation. For each call, an extra object should be initialized even for the successful operation. If you call an API 100 times with different inputs, how many times an exception may be thrown? So the rate of throwing an exception with the extra object initializing (and heap allocation) is not the same

Step 4 - Add DomainException class

  • Create a new folder at the project root and name it Domain then add another folder Exception
  • Add new file DomainException.cs to Exception folder:
public class DomainException : Exception
{
    public DomainException(string message)
        : base(message)
    {
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Catch DomainException and translate to bad request result in the exception handling middleware:
private async Task HandleExceptionAsync(HttpContext context, Exception ex)
{
    string result;

    **if (ex is DomainException)
    {
        var problemDetails = new ValidationProblemDetails(new Dictionary<string, string[]> { { "Error", new[] { ex.Message } } })
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
            Title = "One or more validation errors occurred.",
            Status = (int)HttpStatusCode.BadRequest,
            Instance = context.Request.Path,
        };
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        result = JsonSerializer.Serialize(problemDetails);
    }**
    else
    {
        _logger.LogError(ex, $"An unhandled exception has occurred, {ex.Message}");
        var problemDetails = new ProblemDetails
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
            Title = "Internal Server Error.",
            Status = (int)HttpStatusCode.InternalServerError,
            Instance = context.Request.Path,
            Detail = "Internal Server Error!"
        };
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        result = JsonSerializer.Serialize(problemDetails);
    }

    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}
Enter fullscreen mode Exit fullscreen mode

I translated the DomainException to ValidationProblemDetails just like unhandled exceptions. I will use DomainException later on. Let's test to domain exception in action:

[HttpGet("throw-domain-exception")]
public IActionResult ThrowDomainError()
{
    throw new DomainException("Product could not be found");
}
Enter fullscreen mode Exit fullscreen mode

Alt Text

You can find the source code for this walkthrough on Github.

💖 💪 🙅 🚩
moesmp
Mohsen Esmailpour

Posted on March 6, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related