Middleware-Based Exception Handling in ASP.NET Core

Creating a robust and resilient web application is paramount in today's digital age, where downtime or malfunctions can lead to significant losses or a dent in reputation.

One of the critical aspects of ensuring an application's reliability is handling exceptions gracefully. An unhandled exception can crash an application, making it unavailable to users and disrupting the service.

This article delves into the importance of exception handling in ASP.NET Core applications, presenting a middleware approach to catch all exceptions and prevent the application from crashing.

Why Exception Handling is Crucial

In any application, exceptions are inevitable. They can occur due to various reasons, such as invalid user inputs, database connection issues, or unexpected system failures.

If an application is not designed to handle these exceptions properly, it can crash, leading to service disruption. Moreover, unhandled exceptions can expose sensitive information about the application's internal workings, posing a security risk.

Therefore, implementing a robust exception handling mechanism is crucial to maintain the application's integrity, security, and availability.

Introduction to ASP.NET Core Middleware

ASP.NET Core introduced middleware components in its initial release, which are software components that are assembled into an application pipeline to handle requests and responses.

One of the primary goals of middleware is to encapsulate application concerns, such as exception handling, logging, and authentication, making the application modular and easy to manage.

The exception handling middleware in ASP.NET Core is designed to catch unhandled exceptions that occur in the pipeline, allowing the application to respond appropriately instead of crashing.

In ASP.NET Core, middleware components are configured using the UseMiddleware extension method. This method is called on the application builder in the Configure method of the Startup class or in the Program.cs file for .NET 6 onwards. By using this method, developers can insert custom middleware into the application pipeline.

To implement the exception handling middleware, you can start by adding it to the application pipeline in the Program.cs file as follows:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseMiddleware<MyMiddleware>();

To make things clearer, we're going to implement a global middleware that catches all the application's exceptions.

Implementation of a Exception Handling Middleware

In the context of middleware, a RequestDelegate is a delegate that processes an HTTP request. It represents the next middleware in the pipeline.

When a middleware component is executed, it can perform operations before and after calling the next middleware in the pipeline.

This is achieved by invoking the next(context) method, where context is an instance of HttpContext, representing the current HTTP context.

public class ExceptionHandlingMiddleware(
    RequestDelegate next,
    ILogger<ExceptionHandlingMiddleware> logger)
{
    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception exception)
        {
            logger.InternalFailed(exception);
            
            // DO SOMETHING
        }
    }
}

When an exception is caught, it's essential to return a meaningful response to the client without exposing sensitive information.

ASP.NET Core provides the ProblemDetails class for this purpose, which can be used to return standardized error responses for API calls.

The following code snippet shows how to modify the catch block to return a 500 Internal Server Error response with a ProblemDetails object:

catch (Exception exception)
{
    logger.InternalFailed(exception);
    
    var problemDetails = new ProblemDetails
    {
        Status = StatusCodes.Status500InternalServerError,
        Title = "Server Error",
        Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1"
    };
    
    context.Response.StatusCode = StatusCodes.Status500InternalServerError;
    await context.Response.WriteAsJsonAsync(problemDetails);
}

To ensure that the exception handling middleware works as expected, you can write a test using the WebApplicationFactory class. This class allows you to create an instance of your web application for testing.

The following test sends an HTTP request that triggers an exception and verifies that the response contains the expected ProblemDetails object:

[Fact]
public async Task GivenHttpCall_WhenThrowException_ThenReturnInternalServerErrorResponse()
{
    var webApplicationFactory = new DebugWebApplicationFactory();
    var client = webApplicationFactory.CreateClient();
    var uri = new Uri("FAKE-GUID"); // For example on a /users/FAKE-GUID to get a user
    var response = await client.GetAsync(uri) ?? throw new NullReferenceException("Response is null");
    response.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
    var content = await response.Content.ReadFromJsonAsync<ProblemDetails>();
    content.Should().NotBeNull();
}

Conclusion

Implementing a middleware to catch all exceptions in an ASP.NET Core application is a best practice that enhances the application's reliability and security.

By using the ExceptionHandlingMiddleware and the ProblemDetails class, developers can ensure that their application handles exceptions gracefully, providing a better user experience and maintaining the service's availability.

Remember, the goal is not to prevent exceptions but to manage them effectively when they occur.

To go further:

Write custom ASP.NET Core middleware
Learn how to write custom ASP.NET Core middleware.

Have a goat day 🐐