ASP.NET Core Web API-Ausnahmebehandlung

Ich begann mit ASP.NET Core für mein neues REST-API-Projekt nach der Verwendung von regelmäßigen ASP.NET Web API für viele Jahre. Ich sehe keinen guten Weg, um Ausnahmen in ASP.NET Core Web API zu behandeln. Ich habe versucht, Ausnahmebehandlung Filter/Attribut zu implementieren:

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        HandleExceptionAsync(context);
        context.ExceptionHandled = true;
    }

    private static void HandleExceptionAsync(ExceptionContext context)
    {
        var exception = context.Exception;

        if (exception is MyNotFoundException)
            SetExceptionResult(context, exception, HttpStatusCode.NotFound);
        else if (exception is MyUnauthorizedException)
            SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
        else if (exception is MyException)
            SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
        else
            SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
    }

    private static void SetExceptionResult(
        ExceptionContext context, 
        Exception exception, 
        HttpStatusCode code)
    {
        context.Result = new JsonResult(new ApiResponse(exception))
        {
            StatusCode = (int)code
        };
    }
}

Und hier ist meine Startup-Filter-Registrierung:

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

Das Problem, das ich hatte, ist, dass wenn eine Ausnahme in meinem AuthorizationFilter auftritt, diese nicht von ErrorHandlingFilter behandelt wird. Ich hatte erwartet, dass es dort abgefangen wird, so wie es bei der alten ASP.NET Web API funktioniert hat.

Wie kann ich also alle Anwendungsausnahmen sowie alle Ausnahmen von Aktionsfiltern abfangen?

Lösung

Middleware für die Behandlung von Ausnahmen

Nach vielen Experimenten mit verschiedenen Ansätzen zur Ausnahmebehandlung habe ich mich für Middleware entschieden. Sie hat sich für meine ASP.NET Core Web API-Anwendung am besten bewährt. Sie behandelt sowohl Anwendungsausnahmen als auch Ausnahmen von Aktionsfiltern und ich habe die volle Kontrolle über die Ausnahmebehandlung und die HTTP-Antwort. Hier ist meine Middleware für die Ausnahmebehandlung:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate next;
    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        if      (ex is MyNotFoundException)     code = HttpStatusCode.NotFound;
        else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
        else if (ex is MyException)             code = HttpStatusCode.BadRequest;

        var result = JsonConvert.SerializeObject(new { error = ex.Message });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        return context.Response.WriteAsync(result);
    }
}

Registrieren Sie sie vor MVC in der Klasse Startup:

app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();

Sie können Stack-Trace, Ausnahmetyp-Namen, Fehlercodes oder was immer Sie wollen, hinzufügen. Sehr flexibel. Hier ist ein Beispiel für eine Ausnahmeantwort:

{ "error": "Authentication token is not valid." }

Ziehen Sie in Erwägung, IOptions in die Invoke-Methode zu injizieren, um es dann zu verwenden, wenn Sie das Antwortobjekt serialisieren, um die Serialisierungseinstellungen von ASP.NET MVC'in JsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings) für eine bessere Serialisierungskonsistenz über alle Endpunkte hinweg zu nutzen.

Ansatz 2

Es gibt eine weitere nicht offensichtliche API namens "UseExceptionHandler", die für einfache Szenarien funktioniert:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var feature = context.Features.Get();
    var exception = feature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

Dies ist ein nicht sehr offensichtlicher, aber einfacher Weg, die Ausnahmebehandlung einzurichten. Ich bevorzuge jedoch immer noch den Middleware-Ansatz, da ich mehr Kontrolle über die Möglichkeit habe, notwendige Abhängigkeiten zu injizieren.

Kommentare (40)

Am besten verwenden Sie Middleware, um die von Ihnen gewünschte Protokollierung zu erreichen. Sie möchten Ihre Ausnahmeprotokollierung in einer Middleware unterbringen und dann Ihre Fehlerseiten, die dem Benutzer angezeigt werden, in einer anderen Middleware behandeln. Das ermöglicht die Trennung der Logik und folgt dem Design, das Microsoft mit den 2 Middleware-Komponenten festgelegt hat. Hier ist ein guter Link zur Dokumentation von Microsoft: Fehlerbehandlung in ASP.Net Core

Für Ihr spezielles Beispiel können Sie eine der Erweiterungen in der StatusCodePage-Middleware verwenden oder Ihre eigene wie diese entwickeln.

Ein Beispiel für die Protokollierung von Ausnahmen finden Sie hier: ExceptionHandlerMiddleware.cs

public void Configure(IApplicationBuilder app)
{
    // app.UseErrorPage(ErrorPageOptions.ShowAll);
    // app.UseStatusCodePages();
    // app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
    // app.UseStatusCodePages("text/plain", "Response, status code: {0}");
    // app.UseStatusCodePagesWithRedirects("~/errors/{0}");
    // app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
    // app.UseStatusCodePages(builder => builder.UseWelcomePage());
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");  // I use this version

    // Exception handling logging below
    app.UseExceptionHandler();
}

Wenn Sie diese spezielle Implementierung nicht mögen, können Sie auch ELM Middleware verwenden, und hier sind einige Beispiele: Elm Exception Middleware

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}

Wenn das für Ihre Bedürfnisse nicht funktioniert, können Sie jederzeit Ihre eigene Middleware-Komponente entwickeln, indem Sie sich die Implementierungen der ExceptionHandlerMiddleware und der ElmMiddleware ansehen, um die Konzepte für die Erstellung Ihrer eigenen Middleware zu verstehen.

Es ist wichtig, die Middleware für die Ausnahmebehandlung unterhalb der StatusCodePages-Middleware, aber oberhalb aller anderen Middleware-Komponenten einzufügen. Auf diese Weise wird Ihre Exception-Middleware die Ausnahme erfassen, protokollieren und dann die Anfrage an die StatusCodePage-Middleware weiterleiten, die dem Benutzer die freundliche Fehlerseite anzeigt.

Kommentare (4)

Konfigurieren Sie zunächst ASP.NET Core 2 Startup so, dass bei Fehlern vom Webserver und unbehandelten Ausnahmen erneut eine Fehlerseite aufgerufen wird.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}

Als nächstes definieren Sie einen Ausnahmetyp, mit dem Sie Fehler mit HTTP-Statuscodes auslösen können.

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}

Schließlich passen Sie in Ihrem Controller für die Fehlerseite die Antwort auf der Grundlage der Fehlerursache und der Tatsache an, ob die Antwort direkt von einem Endbenutzer gesehen wird. Dieser Code geht davon aus, dass alle API-URLs mit "/api/" beginnen.

[AllowAnonymous]
public IActionResult Error()
{
    // Gets the status code from the exception or web server.
    var statusCode = HttpContext.Features.Get()?.Error is HttpException httpEx ?
        httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

    // For API errors, responds with just the status code (no page).
    if (HttpContext.Features.Get().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
        return StatusCode((int)statusCode);

    // Creates a view model for a user-friendly error page.
    string text = null;
    switch (statusCode) {
        case HttpStatusCode.NotFound: text = "Page not found."; break;
        // Add more as desired.
    }
    return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}

ASP.NET Core protokolliert die Fehlerdetails, damit Sie sie debuggen können. Daher ist ein Statuscode möglicherweise alles, was Sie einem (potenziell nicht vertrauenswürdigen) Anforderer mitteilen möchten. Wenn Sie mehr Informationen anzeigen möchten, können Sie HttpException erweitern, um diese bereitzustellen. Bei API-Fehlern können Sie JSON-kodierte Fehlerinformationen in den Nachrichtentext einfügen, indem Sie return StatusCode... durch return Json... ersetzen.

Kommentare (0)