ASP.NET Core 8: Improved exception handling with IExceptionHandler

[删除(380066935@qq.com或微信通知)]

更好的阅读体验请查看原文:https://anthonygiretti.com/2023/06/14/asp-net-core-8-improved-exception-handling-with-iexceptionhandler/

Introduction

ASP.NET Core 8 is coming soon and bring great improvements! In this post I will show how Exception handling is improved with the new interface IExceptionhandler.

No more middleware to handle exceptions!

The great news is you no longer need to handle global exceptions in a middleware since ASP.NET Core brought middleware. To remind you what’s a middleware and how you can handle Exception with, you can read my previous post on exception handling here: https://anthonygiretti.com/2018/11/18/common-features-in-asp-net-core-2-1-webapi-error-handling/.

With ASP.NET Core 8 you can design a class that implements the IExceptionhandler interface. The latter describe the following contract:

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

You can implement the logic you want and return the response you desire to the client. important thing. You must return True or False. If you return True, the pipeline execution will end and other middlewares in the pipeline won’t be invoked. If you return False, the pipeline will continue its execution. Let’s see a example of an implementation:

using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using System.Net;
namespace DemoAspNetCore8.ErrorHandling;
public class DefaultExceptionHandler : IExceptionHandler
{
private readonly ILogger<DefaultExceptionHandler> _logger;
public DefaultExceptionHandler(ILogger<DefaultExceptionHandler> logger)
{
_logger = logger;
}
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
_logger.LogError(exception, "An unexpected error occurred");
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = (int)HttpStatusCode.InternalServerError,
Type = exception.GetType().Name,
Title = "An unexpected error occurred",
Detail = exception.Message,
Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}"
});
return true;
}
}

In this class (which supports Dependency Injection) you can add logging and return a response with the WriteAsJsonAsync which takes in parameter a ProblemDetails instance. You can add anything in the response, I chose ProblemDetails because it’s a RFC standard to handle errors returned on HTTP APIs responses. You can find the RFC here: https://datatracker.ietf.org/doc/html/rfc7807.

Note that the default HTTP Status code return is 500, you customize it as follow if you need:

httpContext.Response.StatusCode = (int)HttpStatusCode.RequestTimeout;

To register your Exception handler, proceed as follow:

using DemoAspNetCore8.ErrorHandling;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddExceptionHandler<DefaultExceptionHandler>();
var app = builder.Build();
app.UseExceptionHandler(opt => { });
app.MapGet("/Exception", () => {
throw new NotImplementedException();
});
app.Run();
view rawProgram.cs hosted with ❤ by GitHub

Use the method named AddException<T>() to register your handler, and use the method named UseExceptionHandler() which needs options paramters (that can remain empty) to work. If you run the app with invoke the /exception in my sample you will get following response:

Chaining Exception handlers

A great thing is that you can you chain exception handlers. How? By returning False, the pipeline will pursue its execution and invoke the next Exception handler. If every Exception handlers return False, the pipeline will execute all middlewares that remains in the pipeline. You can definitely combine Exception handlers and middlewares if you were wondering if it’s possible.

To chain your Exception handlers and only want to handle exception with them, chain them, but you HAVE TO define a default Exception handler that will run (and placed in the last position) to handle any Exception that has been handled by the previous handlers. The order matters! The following example shows an Exception handler that handles (only) TimeOutException and breaks the pipeline execution if the Exception is effectively a TimeOutException. If not the pipeline execution will continue and reach the default Exception handler as follow:

using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using System.Net;
namespace DemoAspNetCore8.ErrorHandling;
public class TimeOutExceptionHandler : IExceptionHandler
{
private readonly ILogger<DefaultExceptionHandler> _logger;
public TimeOutExceptionHandler(ILogger<DefaultExceptionHandler> logger)
{
_logger = logger;
}
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
_logger.LogError(exception, "A timeout occurred");
if (exception is TimeoutException)
{
httpContext.Response.StatusCode = (int)HttpStatusCode.RequestTimeout;
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = (int)HttpStatusCode.RequestTimeout,
Type = exception.GetType().Name,
Title = "A timeout occurred",
Detail = exception.Message,
Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}"
});
return true;
}
return false;
}
}
using DemoAspNetCore8.ErrorHandling;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddExceptionHandler<TimeOutExceptionHandler>();
builder.Services.AddExceptionHandler<DefaultExceptionHandler>();
var app = builder.Build();
app.UseExceptionHandler(opt => { });
app.MapGet("/Exception", () => {
throw new NotImplementedException();
});
app.MapGet("/Timeout", () => {
throw new TimeoutException();
});
app.Run();
view rawProgram.cs hosted with ❤ by GitHub

If you try now to invoke the /timeout endpoint the right response will be sent to the client and the default Exception handler won’t be invoked:

If you try to invoke again the /exception endpoint, the pipeline will still reach first the TimeOutException handler but the latter won’t manage the response since it’s not a TimeOutException and will return False in order to tell the pipeline to pursue its exception and reach the default Exception handler.

Hope, like me, you will enjoy this new feature 🙂