Globale Fehlerbehandlung in ASP.NET-Web-API 2
von David Matson, Rick Anderson
Dieses Thema bietet eine Übersicht über die globale Fehlerbehandlung in ASP.NET-Web-API 2 für ASP.NET 4.x. Heute gibt es in der Web-API keine einfache Möglichkeit, Fehler global zu protokollieren oder zu behandeln. Einige nicht behandelte Ausnahmen können über Ausnahmefilter verarbeitet werden, aber es gibt eine Reihe von Fällen, die Ausnahmefilter nicht behandeln können. Beispiel:
- Von Controllerkonstruktoren ausgelöste Ausnahmen.
- Von Meldungshandlern ausgelöste Ausnahmen
- Während des Routings ausgelöste Ausnahmen.
- Während der Serialisierung von Antwortinhalten ausgelöste Ausnahmen.
Wir möchten eine einfache, konsistente Möglichkeit bieten, diese Ausnahmen (sofern möglich) zu protokollieren und zu behandeln.
Es gibt zwei Hauptfälle für die Behandlung von Ausnahmen: der Fall, in dem wir eine Fehlerantwort senden können, und der Fall, in dem wir nur die Ausnahme protokollieren können. Ein Beispiel für den letzteren Fall ist, wenn eine Ausnahme in der Mitte des Streamingantwortinhalts ausgelöst wird. in diesem Fall ist es zu spät, eine neue Antwortnachricht zu senden, da der status Code, Header und Teilinhalte bereits über die Leitung gegangen sind, sodass wir die Verbindung einfach abbrechen. Auch wenn die Ausnahme nicht behandelt werden kann, um eine neue Antwortnachricht zu erzeugen, wird die Protokollierung der Ausnahme weiterhin unterstützt. In Fällen, in denen ein Fehler erkannt werden kann, können wir eine entsprechende Fehlerantwort zurückgeben, wie im Folgenden gezeigt:
public IHttpActionResult GetProduct(int id)
{
var product = products.FirstOrDefault((p) => p.Id == id);
if (product == null)
{
return NotFound();
}
return Ok(product);
}
Vorhandene Optionen
Zusätzlich zu Ausnahmefiltern können nachrichtenhandler heute verwendet werden, um alle Antworten auf 500-Ebenen zu beobachten, aber das Handeln auf diese Antworten ist schwierig, da ihnen der Kontext für den ursprünglichen Fehler fehlt. Nachrichtenhandler haben auch einige der gleichen Einschränkungen wie Ausnahmefilter in Bezug auf die Fälle, die sie behandeln können. Während die Web-API über eine Ablaufverfolgungsinfrastruktur verfügt, die Fehlerbedingungen erfasst, dient die Ablaufverfolgungsinfrastruktur Diagnose Zwecken und ist nicht für die Ausführung in Produktionsumgebungen konzipiert oder geeignet. Globale Ausnahmebehandlung und -protokollierung sollten Dienste sein, die während der Produktion ausgeführt und an vorhandene Überwachungslösungen (z. B. ELMAH) angeschlossen werden können.
Übersicht über die Lösungen
Wir bieten zwei neue benutzerersetzbare Dienste, IExceptionLogger und IExceptionHandler, um nicht behandelte Ausnahmen zu protokollieren und zu behandeln. Die Dienste sind sehr ähnlich, mit zwei Standard Unterschieden:
- Wir unterstützen die Registrierung mehrerer Ausnahmeprotokollierungen, aber nur einen einzelnen Ausnahmehandler.
- Ausnahmeprotokollierungen werden immer aufgerufen, auch wenn die Verbindung abgebrochen werden soll. Ausnahmehandler werden nur aufgerufen, wenn wir noch auswählen können, welche Antwortnachricht gesendet werden soll.
Beide Dienste bieten Zugriff auf einen Ausnahmekontext, der relevante Informationen ab dem Zeitpunkt enthält, an dem die Ausnahme erkannt wurde, insbesondere httpRequestMessage, HttpRequestContext, die ausgelöste Ausnahme und die Ausnahmequelle (Details unten).
Entwurfsprinzipien
- Keine Breaking Changes Da diese Funktionalität in einer Nebenversion hinzugefügt wird, ist eine wichtige Einschränkung, die sich auf die Lösung auswirkt, darin, dass es keine breaking Änderungen gibt, entweder für die Typierung von Verträgen oder für das Verhalten. Diese Einschränkung hat einige Bereinigungen ausgeschlossen, die wir in Bezug auf vorhandene Catch-Blöcke vorgenommen hätten, um Ausnahmen in 500 Antworten zu verwandeln. Diese zusätzliche Bereinigung kann für ein nachfolgendes Hauptrelease in Betracht gezogen werden.
- Beibehalten der Konsistenz mit Web-API-Konstrukten Die Filterpipeline der Web-API ist eine hervorragende Möglichkeit, übergreifende Probleme mit der Flexibilität der Anwendung der Logik in einem aktionsspezifischen, controllerspezifischen oder globalen Bereich zu behandeln. Filter, einschließlich Ausnahmefiltern, verfügen immer über Aktions- und Controllerkontexte, auch wenn sie im globalen Bereich registriert sind. Dieser Vertrag ist für Filter sinnvoll, aber es bedeutet, dass Ausnahmefilter, auch global, nicht für einige Ausnahmebehandlungsfälle geeignet sind, z. B. Ausnahmen von Nachrichtenhandlern, in denen keine Aktion oder controllerkontext vorhanden ist. Wenn wir den flexiblen Bereich verwenden möchten, den Filter für die Ausnahmebehandlung bieten, benötigen wir weiterhin Ausnahmefilter. Wenn wir jedoch Ausnahmen außerhalb eines Controllerkontexts behandeln müssen, benötigen wir auch ein separates Konstrukt für die vollständige globale Fehlerbehandlung (etwas ohne Controllerkontext- und Aktionskontexteinschränkungen).
Einsatzgebiet
- Ausnahmeprotokollierungen sind die Lösung, um alle unbehandelten Ausnahmen zu sehen, die von der Web-API abgefangen werden.
- Ausnahmehandler sind die Lösung zum Anpassen aller möglichen Antworten auf nicht behandelte Ausnahmen, die von der Web-API abgefangen werden.
- Ausnahmefilter sind die einfachste Lösung zum Verarbeiten der nicht behandelten Ausnahmen der Teilmenge im Zusammenhang mit einer bestimmten Aktion oder einem bestimmten Controller.
Dienstdetails
Die Ausnahmeprotokollierungs- und Handlerdienstschnittstellen sind einfache asynchrone Methoden, die die jeweiligen Kontexte verwenden:
public interface IExceptionLogger
{
Task LogAsync(ExceptionLoggerContext context,
CancellationToken cancellationToken);
}
public interface IExceptionHandler
{
Task HandleAsync(ExceptionHandlerContext context,
CancellationToken cancellationToken);
}
Außerdem stellen wir Basisklassen für beide Schnittstellen bereit. Das Überschreiben der Kernmethoden (Synchronisierung oder asynchron) ist nur erforderlich, um zu den empfohlenen Zeiten zu protokollieren oder zu behandeln. Für die Protokollierung stellt die ExceptionLogger
Basisklasse sicher, dass die Kernprotokollierungsmethode für jede Ausnahme nur einmal aufgerufen wird (auch wenn sie später weiter oben im Aufrufstapel verteilt und erneut abgefangen wird). Die ExceptionHandler
Basisklasse ruft die Kernbehandlungsmethode nur für Ausnahmen am oberen Rand des Aufrufstapels auf, wobei ältere geschachtelte Catchblöcke ignoriert werden. (Vereinfachte Versionen dieser Basisklassen finden Sie im folgenden Anhang.) IExceptionHandler
Sowohl als auch IExceptionLogger
erhalten Informationen zur Ausnahme über ein ExceptionContext
.
public class ExceptionContext
{
public Exception Exception { get; set; }
public HttpRequestMessage Request { get; set; }
public HttpRequestContext RequestContext { get; set; }
public HttpControllerContext ControllerContext { get; set; }
public HttpActionContext ActionContext { get; set; }
public HttpResponseMessage Response { get; set; }
public string CatchBlock { get; set; }
public bool IsTopLevelCatchBlock { get; set; }
}
Wenn das Framework eine Ausnahmeprotokollierung oder einen Ausnahmehandler aufruft, stellt es immer eine Exception
und bereit Request
. Mit Ausnahme von Komponententests wird auch immer ein RequestContext
bereitgestellt. Es stellt selten ein ControllerContext
und ActionContext
bereit (nur beim Aufrufen aus dem Catch-Block für Ausnahmefilter). Es wird nur sehr selten ein Response
bereitgestellt (nur in bestimmten IIS-Fällen, wenn gerade versucht wird, die Antwort zu schreiben). Beachten Sie, dass der Consumer vor dem Zugriff auf Member der Ausnahmeklasse überprüfen mussnull
, da einige dieser Eigenschaften möglicherweise vorhanden sindnull
.CatchBlock
ist eine Zeichenfolge, die angibt, welcher Catch-Block die Ausnahme gesehen hat. Die Catch-Blockzeichenfolgen sind wie folgt:
HttpServer (SendAsync-Methode)
HttpControllerDispatcher (SendAsync-Methode)
HttpBatchHandler (SendAsync-Methode)
IExceptionFilter (ApiController verarbeitet die Ausnahmefilterpipeline in ExecuteAsync)
OWIN-Host:
- HttpMessageHandlerAdapter.BufferResponseContentAsync (zum Puffern der Ausgabe)
- HttpMessageHandlerAdapter.CopyResponseContentAsync (für streamingausgabe)
Webhost:
- HttpControllerHandler.WriteBufferedResponseContentAsync (zum Puffern der Ausgabe)
- HttpControllerHandler.WriteStreamedResponseContentAsync (für streamingausgabe)
- HttpControllerHandler.WriteErrorResponseContentAsync (bei Fehlern bei der Fehlerwiederherstellung im gepufferten Ausgabemodus)
Die Liste der Catch-Blockzeichenfolgen ist auch über statische Readonly-Eigenschaften verfügbar. (Die kernige Catchblockzeichenfolge befindet sich auf den statischen ExceptionCatchBlocks; der Rest wird jeweils in einer statischen Klasse für OWIN und Webhost angezeigt.)IsTopLevelCatchBlock
ist hilfreich, um das empfohlene Muster der Behandlung von Ausnahmen nur am oberen Rand des Aufrufstapels zu befolgen. Anstatt Ausnahmen in 500 Antworten zu verwandeln, wo ein geschachtelter Abfangblock auftritt, kann ein Ausnahmehandler Ausnahmen verteilen lassen, bis sie vom Host angezeigt werden.
Zusätzlich zum ExceptionContext
erhält eine Logger eine weitere Information über die vollständige ExceptionLoggerContext
:
public class ExceptionLoggerContext
{
public ExceptionContext ExceptionContext { get; set; }
public bool CanBeHandled { get; set; }
}
Die zweite Eigenschaft ermöglicht CanBeHandled
einer Protokollierung, eine Ausnahme zu identifizieren, die nicht verarbeitet werden kann. Wenn die Verbindung abgebrochen wird und keine neue Antwortnachricht gesendet werden kann, werden die Protokollierungen aufgerufen, aber der Handler wird nicht aufgerufen, und die Protokollierer können dieses Szenario anhand dieser Eigenschaft identifizieren.
Zusätzlich zu erhält ein Handler eine weitere Eigenschaft, die ExceptionContext
er für den Vollständigen ExceptionHandlerContext
festlegen kann, um die Ausnahme zu behandeln:
public class ExceptionHandlerContext
{
public ExceptionContext ExceptionContext { get; set; }
public IHttpActionResult Result { get; set; }
}
Ein Ausnahmehandler gibt an, dass er eine Ausnahme behandelt hat, indem er die Result
Eigenschaft auf ein Aktionsergebnis festlegt (z. B. ein ExceptionResult, InternalServerErrorResult, StatusCodeResult oder ein benutzerdefiniertes Ergebnis). Wenn die Result
Eigenschaft NULL ist, wird die Ausnahme nicht behandelt, und die ursprüngliche Ausnahme wird erneut ausgelöst.
Bei Ausnahmen am oberen Rand des Aufrufstapels haben wir einen zusätzlichen Schritt unternommen, um sicherzustellen, dass die Antwort für API-Aufrufer geeignet ist. Wenn die Ausnahme an den Host weitergegeben wird, würde der Aufrufer den gelben Bildschirm des Todes oder eine andere hostseitig bereitgestellte Antwort sehen, die in der Regel HTML und normalerweise keine entsprechende API-Fehlerantwort ist. In diesen Fällen startet das Ergebnis nicht NULL, und nur wenn ein benutzerdefinierter Ausnahmehandler es explizit zurücksetzt null
(nicht behandelt), wird die Ausnahme an den Host weitergegeben. Die Einstellung Result
auf null
kann in solchen Fällen für zwei Szenarien nützlich sein:
- Von OWIN gehostete Web-API mit benutzerdefinierter Ausnahmebehandlungs-Middleware, die vor/außerhalb der Web-API registriert ist.
- Lokales Debuggen über einen Browser, wobei der gelbe Bildschirm des Todes tatsächlich eine hilfreiche Antwort auf eine nicht behandelte Ausnahme ist.
Sowohl bei Ausnahmeprotokollierungen als auch bei Ausnahmehandlern wird keine Wiederherstellung ausgeführt, wenn die Protokollierung oder der Handler selbst eine Ausnahme auslöst. (Abgesehen davon, dass die Ausnahme weitergegeben wird, hinterlassen Sie Feedback am ende dieser Seite, wenn Sie einen besseren Ansatz haben.) Der Vertrag für Ausnahmeprotokollger und -handler besteht darin, dass ausnahmen nicht an ihre Aufrufer weitergegeben werden dürfen. Andernfalls wird die Ausnahme einfach an den Host weitergegeben, was zu einem HTML-Fehler (z. B. ASP. NET-Gelber Bildschirm), der an den Client zurückgesendet wird (was normalerweise nicht die bevorzugte Option für API-Aufrufer ist, die JSON oder XML erwarten).
Beispiele
Ablaufverfolgungs-Ausnahmeprotokollierung
Die folgende Ausnahmeprotokollierung sendet Ausnahmedaten an konfigurierte Ablaufverfolgungsquellen (einschließlich des Debugausgabefensters in Visual Studio).
class TraceExceptionLogger : ExceptionLogger
{
public override void LogCore(ExceptionLoggerContext context)
{
Trace.TraceError(context.ExceptionContext.Exception.ToString());
}
}
Benutzerdefinierter Ausnahmehandler für Fehlermeldungen
Der folgende Ausnahmehandler erzeugt eine benutzerdefinierte Fehlerantwort für Clients, einschließlich einer E-Mail-Adresse für die Kontaktaufnahme mit dem Support.
class OopsExceptionHandler : ExceptionHandler
{
public override void HandleCore(ExceptionHandlerContext context)
{
context.Result = new TextPlainErrorResult
{
Request = context.ExceptionContext.Request,
Content = "Oops! Sorry! Something went wrong." +
"Please contact support@contoso.com so we can try to fix it."
};
}
private class TextPlainErrorResult : IHttpActionResult
{
public HttpRequestMessage Request { get; set; }
public string Content { get; set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
HttpResponseMessage response =
new HttpResponseMessage(HttpStatusCode.InternalServerError);
response.Content = new StringContent(Content);
response.RequestMessage = Request;
return Task.FromResult(response);
}
}
}
Registrieren von Ausnahmefiltern
Wenn Sie zum Erstellen Ihres Projekts die Projektvorlage "ASP.NET MVC 4-Webanwendung" verwenden, legen Sie Den Web-API-Konfigurationscode in der WebApiConfig
Klasse im Ordner App_Start ab:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());
// Other configuration code...
}
}
Anhang: Details der Basisklasse
public class ExceptionLogger : IExceptionLogger
{
public virtual Task LogAsync(ExceptionLoggerContext context,
CancellationToken cancellationToken)
{
if (!ShouldLog(context))
{
return Task.FromResult(0);
}
return LogAsyncCore(context, cancellationToken);
}
public virtual Task LogAsyncCore(ExceptionLoggerContext context,
CancellationToken cancellationToken)
{
LogCore(context);
return Task.FromResult(0);
}
public virtual void LogCore(ExceptionLoggerContext context)
{
}
public virtual bool ShouldLog(ExceptionLoggerContext context)
{
IDictionary exceptionData = context.ExceptionContext.Exception.Data;
if (!exceptionData.Contains("MS_LoggedBy"))
{
exceptionData.Add("MS_LoggedBy", new List<object>());
}
ICollection<object> loggedBy = ((ICollection<object>)exceptionData[LoggedByKey]);
if (!loggedBy.Contains(this))
{
loggedBy.Add(this);
return true;
}
else
{
return false;
}
}
}
public class ExceptionHandler : IExceptionHandler
{
public virtual Task HandleAsync(ExceptionHandlerContext context,
CancellationToken cancellationToken)
{
if (!ShouldHandle(context))
{
return Task.FromResult(0);
}
return HandleAsyncCore(context, cancellationToken);
}
public virtual Task HandleAsyncCore(ExceptionHandlerContext context,
CancellationToken cancellationToken)
{
HandleCore(context);
return Task.FromResult(0);
}
public virtual void HandleCore(ExceptionHandlerContext context)
{
}
public virtual bool ShouldHandle(ExceptionHandlerContext context)
{
return context.ExceptionContext.IsOutermostCatchBlock;
}
}