ASP.NET Core 8: IExceptionHandler für APIs

ASP.NET Core 8: IExceptionHandler für APIs
Foto von cottonbro studio: https://www.pexels.com/de-de/foto/person-kurze-hose-beine-strasse-5244340/

Einleitung

Seit der ersten Version von .NET Core bin ich dabei und habe die Verbesserungen während der Entwicklung erlebt.

Derzeit plane ich eine neue Basis für mein System und überprüfe daher regelmäßig verschiedene Teilbereiche und deren Lösungen in den neueren .NET Core-Versionen. Eine dieser Verbesserungen ist das Exception Handling.

In diesem Beitrag unterscheide ich zwei Arten von Fehlern: Systemfehler und Logikfehler. Systemfehler umfassen unerreichbare Systeme wie Datenbanken, Caches oder externe Dienste sowie Programmierfehler. Logikfehler hingegen stehen im Zusammenhang mit der Geschäftslogik. Zum Beispiel, wenn ein Benutzer versucht, ein bereits storniertes Ticket zu stornieren, ohne die nötigen Berechtigungen zu haben.

Bei Systemfehlern sollten keine Details preisgegeben werden. Ist beispielsweise der SQL Server nicht erreichbar, sollte der Nutzer höchstens erfahren, dass es ein Datenbankproblem gibt, ohne weitere Einzelheiten. Die Weitergabe von Fehlermeldungen könnte sonst die Adresse des SQL-Servers, den Datenbanknamen und möglicherweise den Benutzernamen offenlegen, was ein Sicherheitsrisiko darstellt. Diese Daten müssen daher bereinigt werden.

Bei Logikfehlern sollte der Benutzer hingegen mehr Informationen erhalten. Wenn ein Benutzer versucht, ein storniertes Ticket zu stornieren, sollte ihm mitgeteilt werden, dass dies nicht möglich ist. Im Idealfall sollte auch die fehlende Berechtigung angezeigt werden.

💡
Die Frage, ob Logikfehler durch Exceptions oder Result Pattern behandelt werden sollten, ist eine separate Diskussion und wird daher hier nicht weitergeführt 1.

Umsetzung

💡
Ich werde nur Code-Fragmente vorstellen, keine kompletten Klassen oder Funktionen. Sie sollen ausschließlich als Inspiration dienen und sind für erfahrene Entwickler konzipiert. Zudem bezieht sich alles auf .Net Core 8.

Das zuvor erwähnte anzeigen von Server-Details, einschließlich der SQL-Verbindungsdaten, müssen etwas präzisiert werden. Asp.Net Core unterscheidet zwischen drei Umgebungen: Development, Stage und Production. Das Verhalten der Anwendung variiert je nach Umgebung. Ist die Umgebung auf Production eingestellt, werden keinerlei Informationen angezeigt, nicht einmal der Text der Ausnahme.

Vor .Net Core 8

In ASP.NET Core vor Version 8 konnten Fehler mittels Middleware abgefangen werden. Dabei wurde eine Middleware mit einem Try-Catch-Block implementiert, wobei die Fehlerbehandlung im Catch-Teil erfolgte.

public class ExceptionHandlingMiddleware(RequestDelegate next)
{
    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            ILogger<ExceptionHandlingMiddleware> logger = context.RequestServices.GetRequiredService<ILogger<ExceptionHandlingMiddleware>>();

            logger.LogError(ex, "An unexpected error occurred");

            ProblemDetails problemDetails = new()
            {
                Status = StatusCodes.Status500InternalServerError,
                Type = ex.GetType().Name,
                Title = "An unexpected error occurred",
                Detail = ex.Message,
                Instance = $"{context.Request.Method} {context.Request.Path}"
            };

            context.Response.StatusCode =
                StatusCodes.Status500InternalServerError;

            await context.Response.WriteAsJsonAsync(problemDetails);
        }
    }
}
app.UseMiddleware<ExceptionHandlingMiddleware>();

IExceptionHandler

In Asp.Net Core 8 wurde mit dem IExceptionHandler eine neue Methode zur Fehlerbehandlung eingeführt.

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

IExceptionHandler Interface

public class DefaultExceptionHandler(ILogger<DefaultExceptionHandler> logger) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception, CancellationToken cancellationToken)
    {
        logger.LogError(exception, "An unexpected error occurred");

        await context.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Type = exception.GetType().Name,
            Title = "An unexpected error occurred",
            Detail = exception.Message,
            Instance = $"{context.Request.Method} {context.Request.Path}"
        });

        return true;
    }
}
  builder.Services.AddExceptionHandler<DefaultExceptionHandler>();
  builder.Services.AddProblemDetails();
   app.UseExceptionHandler();
Ergebnis

Verkettung (Chaining)

ExceptionHandler können miteinander verkettet werden, indem der Rückgabewert der Funktion genutzt wird: true bedeutet, dass der Fehler behandelt wurde, und false, dass der Fehler nicht behandelt wurde und ein anderer ExceptionHandler eingreifen muss.

Dies bietet den Vorteil, dass Bibliotheken eigene ExceptionHandler für ihre spezifischen Exceptions bereitstellen können.