How to Use IExceptionHandler in ASP.NET Core

By FoxLearn 11/13/2024 7:19:16 AM   120
In ASP.NET Core, IExceptionHandler is used to handle exceptions that occur during the processing of a request.

In .NET 8.0, `IExceptionHandler` was introduced as a way to integrate custom exception-handling logic into the exception-handling middleware pipeline in ASP.NET Core. It follows a similar pattern to previous error-handling approaches, such as using `UseExceptionHandler`, but with enhanced flexibility.

What Is IExceptionHandler in .NET?

`IExceptionHandler` is an interface in ASP.NET Core for handling exceptions in a customized way. It allows developers to implement custom logic for handling specific exceptions or groups of exceptions based on their types. This flexibility enables tailored responses, custom error messages, and enhanced logging, improving error handling in the application.

Why use IExceptionHandler?

`IExceptionHandler` in .NET offers a powerful and flexible approach to exception handling in APIs, enabling the creation of more robust and user-friendly applications.

Additionally, `IExceptionHandler` eliminates the need to create custom global exception handlers, as .NET 8 already provides built-in middleware for exception handling through this interface.

By implementing `IExceptionHandler`, developers can handle different types of exceptions in a modular and maintainable way, improving the overall structure of the application.

We'll simulate a library service by creating a `Book` class. This class will serve as a model to represent a book in the library system.

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    //...
}

Next, we'll create an `IBookService` interface, which will define two key methods for our library service.

public interface IBookService
{
    Book GetById(int id);
    List<Book> GetList();
}

Next, we'll add an endpoint to test our library service implementation.

app.MapGet("/books", async context =>
{
    var bookService = context.RequestServices.GetRequiredService<IBookService>();
    var books = bookService.GetList();
    await context.Response.WriteAsJsonAsync(books);
});

Now, we can run our application and test the `/books` endpoint to retrieve a list of books.

[
  {
    "id": 1,
    "title": "ASP.NET Core Programming"
  }
]

Using the `IExceptionHandler` Middleware for Advanced Exception Handling in ASP.NET Core Applications

With ASP.NET Core 8, you can design a class that implements the `IExceptionHandler` interface. This interface defines a contract for handling exceptions within the application.

public interface IExceptionHandler
{
    ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken);
}

Next, we'll add the `CustomExceptionHandler` class, which implements the `IExceptionHandler` interface. This class will allow us to define custom exception-handling logic, such as handling different types of exceptions, logging them, and providing tailored error responses.

public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
    _logger.LogError($"An error occurred while processing your request: {exception.Message}");
    var errorResponse = new ErrorResponse { Message = exception.Message };
    switch (exception)
    {
        case BadHttpRequestException:
            errorResponse.StatusCode = (int)HttpStatusCode.BadRequest;
            errorResponse.Title = exception.GetType().Name;
            break;

        default:
            errorResponse.StatusCode = (int)HttpStatusCode.InternalServerError;
            errorResponse.Title = "Internal Server Error";
            break;
    }
    httpContext.Response.StatusCode = errorResponse.StatusCode;
    await httpContext.Response.WriteAsJsonAsync(errorResponse, cancellationToken);
    return true;
}

You can implement custom logic in your IExceptionHandler implementation and return the desired response to the client. An important aspect to note is that the handler must return either True or False.

  • If you return True, the exception handling will be considered complete, and the middleware pipeline will stop, preventing further middleware from executing.
  • If you return False, the pipeline will continue executing other middlewares after the exception handler.

You can incorporate logging and return a detailed error response using `WriteAsJsonAsync`. This method takes a `ProblemDetails` instance as a parameter, which is used to standardize error responses in HTTP APIs. `ProblemDetails` is an RFC-compliant format for error handling, providing a consistent structure for conveying error information, such as status codes, titles, and descriptions, in API responses.

Finally, we'll register our CustomExceptionHandler in the dependency injection container. This will allow the exception handler to be injected into the application's middleware pipeline, ensuring that it is used to handle exceptions throughout the application.

builder.Services.AddExceptionHandler<CustomExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler();

In the `TryHandleAsync()` method, the `errorResponse` object is used to return the exception details from the API, in contrast to the previous handler where `ProblemDetails` was used to standardize the error response. This allows for a custom structure for the error details. After implementing the exception handler, we need to register it as a service in the dependency injection container, enabling it to be used globally across the application to handle exceptions.

builder.Services.AddExceptionHandler<BadRequestExceptionHandler>();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();

For example endpoint get book by id

app.MapGet("/books/{id}", async context =>
{
    var id = int.Parse(context.Request.RouteValues["id"].ToString());
    var bookService = context.RequestServices.GetRequiredService<IBookService>();
    var book = bookService.GetById(id);
    if (book is null)
    {
        throw new BadHttpRequestException("Book is not found.", StatusCodes.Status400BadRequest);
    }
    await context.Response.WriteAsJsonAsync(book);
});

This endpoint retrieves a book by its Id, which is passed as a route parameter. If the book is found, it returns a 200 OK response with the book data. However, if the book doesn't exist, it throws a BadHttpRequestException, resulting in an error response indicating that the request was invalid.

You get a response with the book details

For example:

https://localhost:7036/books/1

{
  "id": 1,
  "title": "ASP.NET Core Programming"
}

If you test with an ID that doesn’t exist, the endpoint will throw a BadHttpRequestException.

{
  "title": "BadHttpRequestException",
  "statusCode": 400,
  "message": "Book is not found."
}

We use a switch statement to handle different exception types, allowing us to extend the logic to manage additional exceptions as needed. If no specific exception type matches, the application defaults to throwing an InternalServerError exception.