Zum Fußzeileninhalt springen
Iron Academy Logo
Lernen Sie C#
Lernen Sie C#

Andere Kategorien

Globale Fehlerbehandlung in minimalen C# APIs

[[academy-video-youtube({"vid": "B5NsgtdwOlg", "start_time": "0", "title": "Global Error Handling in C# Minimal APIs", "creator": "Tim Corey", "length": "13m 30s"})]]

Eine Web-API, die eine unbehandelte Ausnahme wirft, wird standardmäßig die Art von Fehlerseite zurückgeben, die einem Entwickler beim lokalen Debuggen hilft und einem Fremden die Möglichkeit gibt, Ihren Callstack nachzuvollziehen. Zeilennummern, Typnamen und der Pfad zur Quelldatei gehen an den Absender der Anfrage zurück. Jeden Fehler am Endpunkt abzufangen, an dem er auftreten könnte, ist der richtige Ansatz, aber es funktioniert nur bis zum nächsten vergessenen try/catch. Ein globaler Handler ist das Sicherheitsnetz, das auffängt, was der Endpunkt übersehen hat.

In seinem Video "Global Error Handling in C# Minimal APIs" entwickelt Tim Corey eine kleine minimale API mit einem absichtlich defekten Endpunkt, demonstriert die Entwickler-Fehlerseite, die ohne Schutz zurückgegeben wird, und bindet dann app.UseExceptionHandler ein, um jede nicht abgefangene Ausnahme abzufangen und mit einem generischen 500 zu antworten. Er unterstreicht auch, warum die Behandlung auf Endpunktearnebene der bevorzugte Weg bleibt: Der globale Handler ist der Rückfall, nicht die Strategie. Jeder, der eine Minimal-API ausliefert, die sicherstellen möchte, dass kein Stack-Trace jemals den Server verlässt, findet unten das Middleware-Setup und die Design-Grundlagen dazu.

Eine Minimal-API mit einem defekten Endpunkt erstellen

[1:08 - 3:01] Tim beginnt mit einem neuen .NET 8 ASP.NET Core WebAPI-Projekt namens ErrorDemoApp. Die Projektvorlagenoptionen bleiben nahe an den Standardeinstellungen: HTTPS an, OpenAPI an, keine Authentifizierung, top-level Anweisungen, die aktiviert bleiben, und das Controller-Kontrollkästchen nicht aktiviert, da dies eine Minimal-API ist. Das generierte Program.cs behält Swagger bei, aber der Wettervorhersage-Beispielendpunkt und dessen Aufzeichnung werden gelöscht, damit die Datei nur die Grundlagen zeigt.

Anstelle des Beispiels fügt er einen einzigen Endpunkt bei /demo hinzu, dessen einziger Zweck es ist zu scheitern:

app.MapGet("/demo", () =>
{
    throw new Exception("This is a demo exception");
});
app.MapGet("/demo", () =>
{
    throw new Exception("This is a demo exception");
});

Das Ausführen des Projekts mit Strg+F5 (Starten ohne Debugging) verhindert, dass der Visual Studio-Debugger die Ausnahme abfängt, sodass der Fehler so auftaucht, wie es für einen tatsächlichen HTTP-Anrufer der Fall sein würde. Swagger öffnet sich, der /demo Endpunkt ist der einzige verfügbare, und das Ausführen gibt eine 500-Antwort zurück. Der Antwortkörber trägt den Ausnahmetyp, die Nachricht und einen Verweis auf Zeile 18 von Program.cs.

Warum die Standard-Fehlermeldung Implementierungsdetails verrät

[3:01 - 5:00] Das direkte Ansteuern von /demo im Browser (ohne den ?message= Swagger-Wrapper) zeigt die Entwicklerinformationsseite anstelle der JSON-Antwort. Die Seite rendert den Ausnahme-Namen, die Nachricht, den Dateipfad und die Zeilennummer, bei der die Ausnahme ausgelöst wurde, die rohen Ausnahmedetails und die Stack-Frames über dem Auslösen. Für einen Entwickler, der lokal arbeitet, ist dies Gold wert. Für alle anderen ist es eine freie Karte des Codes.

Tims Punkt kommt ohne Ausschmückung an: Diese Seite existiert, um Entwicklern zu helfen, und sie sollte niemals Endbenutzer erreichen. Die Tatsache, dass sie es manchmal tut, ist der Grund, warum ein globaler Handler wichtig ist. Sogar Teams, die jeden Endpunkt sorgfältig in Ausnahmebehandlung einwickeln, übersehen irgendwann einen, und die Kosten dafür sind, dass der gesamte Stack-Trace an den Absender der Anfrage geht.

Fehler zuerst auf Endpunktebene abfangen

[5:00 - 6:30] Bevor der globale Handler installiert wird, umwickelt Tim den Demo-Endpunkt in einem try/catch, um den bevorzugten Weg zu veranschaulichen. Der Handler gibt Results.BadRequest(ex.Message) für jede geworfene Ausnahme zurück:

app.MapGet("/demo", () =>
{
    try
    {
        throw new Exception("This is a demo exception");
    }
    catch (Exception ex)
    {
        return Results.BadRequest(ex.Message);
    }
});
app.MapGet("/demo", () =>
{
    try
    {
        throw new Exception("This is a demo exception");
    }
    catch (Exception ex)
    {
        return Results.BadRequest(ex.Message);
    }
});

Das Ergebnis ist ein 400, das nur die Nachrichtenkette enthält. Kein Stack-Trace, kein Dateipfad, keine Zeilennummer. Ob die Nachricht selbst offenbart werden sollte, hängt von der Anwendung ab; Für eine öffentliche API kann selbst die Nachricht mehr preisgeben, als das Team möchte, in diesem Fall ersetzt der Handler sie durch eine generische Zeichenkette. Der lokale Catch gibt dem Endpunkt die volle Kontrolle darüber, was der Anrufer sieht, einschließlich der Möglichkeit, einen spezifischeren Statuscode als 500 zurückzugeben, wenn der Fehlermodus tatsächlich bekannt ist.

Was dieses Muster nicht kann, ist das abzufangen, was der Endpunkt zu umschließen vergessen hat. Jeder neue Codepfad, jedes erneute Werfen aus einer tieferen Schicht, jedes Task, das in einem separaten Thread wirft, umgehen das try/catch des Endpunkts. Diese Lücke füllt die Middleware.

Die UseExceptionHandler-Middleware anschließen

[6:30 - 10:00] Direkt unter der app.UseHttpsRedirection()-Zeile wird der Handler mit app.UseExceptionHandler registriert. Die Überladung, die eine Builder-Aktion annimmt, legt die zugrundeliegende Pipeline frei, wodurch der Handler die Antwortform explizit festlegen kann:

app.UseExceptionHandler(appError =>
{
    appError.Run(async context =>
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/json";

        var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
        if (contextFeature is not null)
        {
            Console.WriteLine($"Error: {contextFeature.Error}");
        }

        await context.Response.WriteAsJsonAsync(new
        {
            StatusCode = context.Response.StatusCode,
            Message = "Internal Server Error"
        });
    });
});
app.UseExceptionHandler(appError =>
{
    appError.Run(async context =>
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/json";

        var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
        if (contextFeature is not null)
        {
            Console.WriteLine($"Error: {contextFeature.Error}");
        }

        await context.Response.WriteAsJsonAsync(new
        {
            StatusCode = context.Response.StatusCode,
            Message = "Internal Server Error"
        });
    });
});

Einige Entscheidungen in diesem Block sind entscheidend. Den Statuscode auf 500 zu erzwingen bedeutet, dass der Anrufer aus der Antwortnummer nichts schließen kann; welcher Innentyp der Ausnahme auch immer vorhanden war, die Oberfläche sieht gleich aus. Den Inhaltstyp auf application/json zu erzwingen passt zu den restlichen Antworten der API, was es den Clients ermöglicht, einen einzelnen Parser zu verwenden. Der IExceptionHandlerFeature gibt die ursprüngliche Ausnahme frei, damit ein echter Handler sie protokollieren kann; Tim verwendet Console.WriteLine hier als Platzhalter für den Logger, den das Projekt tatsächlich nutzen würde.

Der abschließende WriteAsJsonAsync-Aufruf gibt ein anonymes Objekt mit dem Statuscode und der generischen Nachricht zurück. Der Body sagt nichts darüber aus, was fehlgeschlagen ist, außer der Tatsache, dass etwas schiefgegangen ist, was der Punkt ist. Interne Diagnosen gehören ins Protokoll, nicht in die Antwort.

Testen der behandelten und unbehandelten Pfade

[10:00 - 13:14] Mit dem try/catch weiterhin in Kraft führt der Endpunkt den lokalen Pfad aus: Swagger zeigt ein 400 mit "Dies ist eine Demo-Ausnahme". Die Middleware sieht den Wurf nie, da der catch-Block ihn zuerst auflöst. Dies ist das Design, das Tim standardmäßig möchte: lokale Handler erledigen ihre Arbeit und der globale Handler bleibt inaktiv.

Das Entfernen des try/catch und erneutes Ausführen trainiert die Rückfalloptionen. Die gleiche Anfrage liefert jetzt eine 500 mit dem JSON-Körper { "statusCode": 500, "message": "Internal Server Error" } zurück. Nichts in der Antwort zeigt, wo die Ausnahme ausgelöst wurde oder um welchen Typ es sich handelte. Das Konsolenfenster von Visual Studio zeigt jedoch den ursprünglichen Ausnahmetext, der über den Console.WriteLine-Platzhalter protokolliert wurde, einschließlich des Dateipfads und der Zeilennummer. Die vollständige Diagnose bleibt dort, wo Entwickler sie lesen können; die Antwort bleibt dort, wo sie nicht durchgesickert werden kann.

Dieses Muster zieht sich bis zu einer Minimal-API mit Validierungs-Middleware, einer benutzerdefinierten Authentifizierung oder einer anderen Pipeline-Komponente durch. Der Ausnahme-Handler sitzt früh in der Pipeline und fängt alles ab, was von einem späteren Stadium aus propagiert wird.

Abschluss: Verteidigung in die Tiefe

[13:14 - 13:30] Lokale und globale Behandlung sind keine Alternativen; sie sind Schichten. Der lokale Handler gibt dem Endpoint die Möglichkeit, sinnvoll zu reagieren, wenn der Fehlermodus bekannt ist. Der globale Handler stellt sicher, dass alles, was die lokale Schicht nicht erfasst hat, eine Antwort liefert, die konsistent, generisch und sicher ist. Controller-basierte APIs verwenden die gleiche Idee mit kleineren Syntaxanpassungen, aber die Minimal-API-Form ist diejenige, mit der man sich zuerst vertraut machen sollte, da der Umfang klein genug ist, um das ganze Bild in einer Datei zu sehen.

Abschluss

[13:14 - 13:30] Das Einrichten eines globalen Fehlerhandlers in einer minimalen API erfolgt in drei Schritten: UseExceptionHandler frühzeitig in der Pipeline registrieren, den Antwortstatus und den Inhaltstyp im Handler festlegen und einen absichtlich generischen Körper schreiben, damit keine Implementierungsdetails entweichen. Kombinieren Sie dies mit lokalen try/catch-Blöcken um die wahrscheinlichsten fehlerhaften Codepfade, und Sie haben ein Verteidigungs-in-der-Tiefe-Modell, in dem der globale Handler das Sicherheitsnetz ist und nicht die Strategie.

Beispiel-Tipp: Wenn der Handler auf einen echten Logger zugreift, übergeben Sie das gesamte Ausnahme-Objekt (nicht nur die Nachricht), damit die strukturierte Logging-Pipeline den Typ, den Stack und alle inneren Ausnahmen erfasst. Ein Logger wie Serilog behält all dies als abfragbare Eigenschaften, was bedeutet, dass der Alarm, der für eine Produktions-500 ausgelöst wird, genügend Kontext bietet, um ihn lokal nachzuvollziehen, ohne dass jemand die Anfrage erneut ausführen muss.

Schauen Sie sich das vollständige Video auf seinem YouTube-Kanal an und gewinnen Sie weitere Einblicke in den Aufbau von produktionsbereiten Minimal-APIs in der 10-Minuten-Trainingsserie.

Hero Worlddot related to Globale Fehlerbehandlung in minimalen C# APIs
Hero Affiliate related to Globale Fehlerbehandlung in minimalen C# APIs

Verdienen Sie mehr, indem Sie teilen, was Sie lieben

Erstellen Sie Inhalte für Entwickler, die mit .NET, C#, Java, Python oder Node.js arbeiten? Verwandeln Sie Ihr Fachwissen in ein zusätzliches Einkommen!

Iron-Support-Team

Wir sind 24 Stunden am Tag, 5 Tage die Woche online.
Chat
E-Mail
Rufen Sie mich an