What every ASP.NET Core Web API project needs - Part 3 - Exception handling middleware
Tags: architecture, dotnet, aspnetcore, webapi
Published: 2021-03-06 23:12:36

In the [previous article](What every ASP.NET Core Web API project needs - Part 2 - API versioning and Swagger), I wrote about API versioning and how to add Swagger to the [sample project](https://github.com/mo-esmp/cool-webapi) 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:
```csharp
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);
    }
}
``` 
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:
```json
{
  "": [
    "A non-empty request body is required."
  ]
}
```
According to the Internet Engineering Task Force (IETF) [RFC-7231 document](https://tools.ietf.org/html/rfc7231), the ASP.NET Core team has implemented [ProblemDetails](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails?view=aspnetcore-5.0), a machine-readable format for specifying errors in web API responses and complies with the [RFC 7807](https://tools.ietf.org/html/rfc7807) specification.

###Step 2 - Register middleware
 - Create an extension method to register middleware:
```csharp
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseApiExceptionHandling(this IApplicationBuilder app)
        => app.UseMiddleware<ApiExceptionHandlingMiddleware>();
}
```
- Open the `Startup.cs` class and in the `Configure` method add the middleware:
```csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        ...
    }

    app.UseApiExceptionHandling();
```
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):
```csharp
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.")?
```    
- 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](https://github.com/aspnet/Identity/blob/master/src/Core/IdentityResult.cs), 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:
```csharp
public class DomainException : Exception
{
    public DomainException(string message)
        : base(message)
    {
    }
}
```
- Catch `DomainException` and translate to bad request result in the exception handling middleware:
```csharp
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);
}
```
I translated the `DomainException` to `ValidationProblemDetails` just like unhandled exceptions. I will use `DomainException` later on. Let's test to domain exception in action:
```csharp
[HttpGet("throw-domain-exception")]
public IActionResult ThrowDomainError()
{
    throw new DomainException("Product could not be found");
}
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p1t69vn7e9qgac6n9w1y.jpg)

You can find the source code for this walkthrough on [Github](https://github.com/mo-esmp/cool-webapi).