Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Autor : David Matson, Rick Anderson
Ten temat zawiera omówienie globalnej obsługi błędów w interfejsie ASP.NET Web API 2 dla ASP.NET 4.x. Obecnie w internetowym interfejsie API nie ma łatwego sposobu rejestrowania lub obsługi błędów globalnie. Niektóre nieobsługiwane wyjątki można przetworzyć za pomocą filtrów wyjątków, ale istnieje wiele przypadków, w których filtry wyjątków nie mogą obsłużyć. Przykład:
- Wyjątki rzucane przez konstruktory kontrolera.
- Wyjątki zgłaszane przez programy obsługi komunikatów.
- Wyjątki zgłaszane podczas routingu.
- Wyjątki rzucane podczas serializacji zawartości odpowiedzi.
Chcemy zapewnić prosty, spójny sposób rejestrowania i obsługi (tam, gdzie to możliwe) tych wyjątków.
Istnieją dwa główne przypadki obsługi wyjątków— przypadek, w którym możemy wysłać odpowiedź o błędzie i przypadek, w którym wszystko, co możemy zrobić, to zarejestrować wyjątek. Przykładem ostatniego przypadku jest rzucenie wyjątku w środku zawartości transmisji strumieniowej; wówczas jest za późno na wysłanie nowej wiadomości zwrotnej, ponieważ kod stanu, nagłówki i częściowa zawartość zostały już przesłane, więc po prostu przerywamy połączenie. Mimo że nie można obsłużyć wyjątku w celu utworzenia nowego komunikatu odpowiedzi, nadal obsługujemy rejestrowanie wyjątku. W przypadkach, gdy możemy wykryć błąd, możemy zwrócić odpowiednią odpowiedź o błędzie, jak pokazano poniżej:
public IHttpActionResult GetProduct(int id)
{
var product = products.FirstOrDefault((p) => p.Id == id);
if (product == null)
{
return NotFound();
}
return Ok(product);
}
Istniejące opcje
Oprócz filtrów wyjątków obsługiwacze komunikatów mogą być obecnie używane do monitorowania wszystkich odpowiedzi z kodem 500, ale trudne jest podejmowanie działań na ich podstawie, gdyż brakuje im kontekstu oryginalnego błędu. Programy obsługi komunikatów mają również niektóre z tych samych ograniczeń co filtry wyjątków dotyczące przypadków, które mogą obsłużyć. Chociaż internetowy interfejs API ma infrastrukturę śledzenia, która przechwytuje warunki błędów, infrastruktura śledzenia jest przeznaczona do celów diagnostycznych i nie jest przeznaczona do uruchamiania w środowiskach produkcyjnych. Globalna obsługa wyjątków i rejestrowanie powinny być usługami, które mogą być uruchamiane podczas produkcji i być podłączone do istniejących rozwiązań monitorowania (na przykład ELMAH).
Omówienie rozwiązania
Udostępniamy dwie nowe usługi z możliwością wymiany użytkowników, IExceptionLogger i IExceptionHandler , aby rejestrować i obsługiwać nieobsługiwane wyjątki. Usługi są bardzo podobne, z dwoma głównymi różnicami:
- Obsługujemy rejestrowanie wielu rejestratorów wyjątków, ale tylko jedną procedurę obsługi wyjątków.
- Rejestratory wyjątków zawsze są wywoływane, nawet jeśli planujemy przerwać połączenie. Programy obsługi wyjątków są wywoływane tylko wtedy, gdy nadal możemy wybrać komunikat odpowiedzi do wysłania.
Obie usługi zapewniają dostęp do kontekstu wyjątku zawierającego odpowiednie informacje z punktu, w którym wykryto wyjątek, w szczególności HttpRequestMessage, HttpRequestContext, zgłoszony wyjątek i źródło wyjątku (szczegóły poniżej).
Zasady projektowania
- Brak zmian powodujących niezgodność Ponieważ ta funkcja jest dodawana w wersji pomocniczej, jednym z ważnych ograniczeń wpływających na rozwiązanie jest brak zmian powodujących niezgodność, typ kontraktów lub zachowanie. To ograniczenie wykluczyło pewne oczyszczanie, które chcielibyśmy zrobić w odniesieniu do istniejących bloków catch zamieniając wyjątki w 500 odpowiedzi. To dodatkowe czyszczenie jest czymś, co możemy rozważyć w przypadku kolejnej wersji głównej.
- Utrzymywanie spójności przy użyciu konstrukcji Web API Potok filtrów Web API to doskonały sposób obsługi aspektów przekrojowych z elastycznością stosowania logiki w zakresie specyficznym dla akcji, kontrolera lub globalnym. Filtry, w tym filtry wyjątków, zawsze mają konteksty akcji i kontrolera, nawet gdy są zarejestrowane w globalnym zakresie. Ten kontrakt ma sens w przypadku filtrów, ale oznacza to, że filtry wyjątków, nawet w skali globalnej, nie są dobrym rozwiązaniem dla niektórych przypadków obsługi wyjątków, takich jak wyjątki z procedur obsługi komunikatów, w których nie istnieje żaden kontekst akcji lub kontrolera. Jeśli chcemy użyć elastycznego zakresu zapewnianego przez filtry do obsługi wyjątków, nadal potrzebujemy filtrów wyjątków. Jeśli jednak musimy obsłużyć wyjątek poza kontekstem kontrolera, potrzebujemy również oddzielnej konstrukcji do pełnej globalnej obsługi błędów (coś bez ograniczeń kontekstu kontrolera i kontekstu akcji).
Kiedy należy używać
- Rejestratory wyjątków są rozwiązaniem pozwalającym na wyświetlanie wszystkich nieobsługiwanych wyjątków przechwyconych przez Web API.
- Procedury obsługi wyjątków to rozwiązanie do dostosowywania wszystkich możliwych odpowiedzi na nieobsługiwane wyjątki przechwycone przez internetowy interfejs API.
- Filtry wyjątków są najprostszym rozwiązaniem do przetwarzania podzbioru nieobsługiwanych wyjątków powiązanych z określoną akcją lub kontrolerem.
Szczegóły usługi
Interfejsy serwisu rejestrowania wyjątków i obsługi to proste metody asynchroniczne obsługujące odpowiednie konteksty.
public interface IExceptionLogger
{
Task LogAsync(ExceptionLoggerContext context,
CancellationToken cancellationToken);
}
public interface IExceptionHandler
{
Task HandleAsync(ExceptionHandlerContext context,
CancellationToken cancellationToken);
}
Udostępniamy również klasy podstawowe dla obu tych interfejsów. Zastępowanie podstawowych metod (synchronizowanych lub asynchronicznych) jest wymagane do rejestrowania lub obsługi w zalecanych momentach. W przypadku rejestrowania klasa bazowa ExceptionLogger zapewni, że podstawowa metoda rejestrowania jest wywoływana tylko raz dla każdego wyjątku (nawet jeśli później przechodzi dalej w górę stosu wywołań i zostanie ponownie złapany). Klasa ExceptionHandler bazowa wywoła główną metodę obsługi tylko dla wyjątków na szczycie stosu wywołań, ignorując starsze zagnieżdżone bloki przechwytywania. (Uproszczone wersje tych klas bazowych znajdują się w dodatku poniżej). Zarówno IExceptionLogger , jak i IExceptionHandler odbierają informacje o wyjątku za pośrednictwem elementu 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; }
}
Gdy platforma wywołuje rejestrator wyjątków lub obsługę wyjątków, zawsze udostępnia Exception i Request. Z wyjątkiem testów jednostkowych, zawsze będzie również dostarczać element RequestContext. Rzadko udostępnia element ControllerContext i ActionContext (tylko podczas wywoływania z bloku catch dla filtrów wyjątków). Bardzo rzadko udostępnia element Response(tylko w niektórych przypadkach serwera IIS, podczas próby zapisywania odpowiedzi). Należy pamiętać, że ponieważ niektóre z tych właściwości mogą być null, na konsumencie spoczywa odpowiedzialność za sprawdzenie null przed uzyskaniem dostępu do elementów klasy wyjątków.
CatchBlock to ciąg wskazujący, który blok catch zarejestrował wyjątek. Tekst bloków catch jest następujący:
HttpServer (metoda SendAsync)
HttpControllerDispatcher (metoda SendAsync)
HttpBatchHandler (metoda SendAsync)
IExceptionFilter (przetwarzanie przez ApiController potoku filtru wyjątków w metodzie ExecuteAsync)
Host OWIN:
- HttpMessageHandlerAdapter.BufferResponseContentAsync (na potrzeby buforowania danych wyjściowych)
- HttpMessageHandlerAdapter.CopyResponseContentAsync (dla przesyłania strumieniowego zawartości odpowiedzi)
Host internetowy:
- HttpControllerHandler.WriteBufferedResponseContentAsync (na potrzeby buforowania danych wyjściowych)
- HttpControllerHandler.WriteStreamedResponseContentAsync (do przesyłania strumieniowego danych wyjściowych)
- HttpControllerHandler.WriteErrorResponseContentAsync (w przypadku błędów odzyskiwania w trybie buforowanych danych wyjściowych)
Lista ciągów blokowych catch jest również dostępna za pośrednictwem statycznych właściwości tylko do odczytu. (Podstawowy blok obsługi wyjątków znajduje się w statycznej klasie ExceptionCatchBlocks; pozostałe są umieszczone w jednej klasie statycznej dla OWIN i hostingu webowego).
IsTopLevelCatchBlock jest pomocny w przypadku przestrzegania zalecanego wzorca obsługi wyjątków tylko na szczycie stosu wywołań. Zamiast zamieniać wyjątki w odpowiedzi 500 gdziekolwiek występuje zagnieżdżony blok przechwytywania, mechanizm obsługi wyjątków może pozwolić na ich propagację, dopóki nie będą widoczne przez hosta.
Oprócz ExceptionContext rejestrator programu uzyskuje dodatkową informację dzięki pełnemu dostępowi do ExceptionLoggerContext.
public class ExceptionLoggerContext
{
public ExceptionContext ExceptionContext { get; set; }
public bool CanBeHandled { get; set; }
}
Druga właściwość , CanBeHandledumożliwia rejestratorowi zidentyfikowanie wyjątku, którego nie można obsłużyć. Gdy połączenie ma zostać przerwane i nie można wysłać nowego komunikatu odpowiedzi, rejestratory będą wywoływane, ale program obsługi nie zostanie wywołany, a rejestratory mogą rozpoznać tę sytuację na podstawie tej właściwości.
W dodatku do ExceptionContext programu, obsługujący otrzymuje jeszcze jedną właściwość, która może być ustawiona na pełnym ExceptionHandlerContext, aby obsłużyć wyjątek.
public class ExceptionHandlerContext
{
public ExceptionContext ExceptionContext { get; set; }
public IHttpActionResult Result { get; set; }
}
Procedura obsługi wyjątków wskazuje, że obsłużyła wyjątek, ustawiając właściwość Result na rezultat akcji (na przykład WynikWyjątku, InternalServerErrorResult, StatusCodeResult lub wynik niestandardowy).
Result Jeśli właściwość ma wartość null, wyjątek jest nieobsługiwany, a oryginalny wyjątek zostanie rzucony ponownie.
W przypadku wyjątków w górnej części stosu wywołań wykonaliśmy dodatkowy krok, aby upewnić się, że odpowiedź jest odpowiednia dla wywołań interfejsu API. Jeśli wyjątek zostanie przekazany do hosta, wywołujący zobaczy żółty ekran śmierci lub inną odpowiedź dostarczoną przez hosta, która jest zazwyczaj w formacie HTML, a zwykle nie jest odpowiednią odpowiedzią błędu interfejsu API. W takich przypadkach Wynik jest zainicjalizowany wartością inną niż null i tylko jeśli niestandardowa procedura obsługi wyjątków jawnie ustawi ją z powrotem na null (nieobsługiwane), dopiero wtedy wyjątek zostanie przekazany do hosta. Ustawienie Result na null w takich przypadkach może być przydatne w dwóch scenariuszach.
- OWIN hostowany Web API z niestandardowym oprogramowaniem pośredniczącym odpowiedzialnym za obsługę wyjątków, zarejestrowanym przed lub poza Web API.
- Lokalne debugowanie za pośrednictwem przeglądarki, gdzie żółty ekran śmierci jest faktycznie pomocną odpowiedzią na nieobsługiwany wyjątek.
W przypadku zarówno rejestratorów wyjątków, jak i procedur obsługi wyjątków, nie robimy nic, aby naprawić sytuację, jeśli rejestrator lub sama procedura obsługi zgłasza wyjątek. (Oprócz pozwolenia na propagację wyjątków, zostaw opinię na dole tej strony, jeśli masz lepsze podejście.) Umowa dotycząca rejestratorów i obsługi wyjątków polega na tym, że nie powinny one pozwalać na propagację wyjątków do swoich wywołujących; w przeciwnym razie wyjątek będzie propagowany, często aż do hosta, co skutkuje odesłaniem błędu HTML (na przykład żółtego ekranu ASP.NET) z powrotem do klienta (co zazwyczaj nie jest preferowaną opcją dla użytkowników API oczekujących JSON lub XML).
Examples
Rejestrator logów wyjątków
Poniższy rejestrator wyjątków wysyła dane dotyczące wyjątków do skonfigurowanych źródeł śledzenia (w tym do okna wyjściowego debugowania w programie Visual Studio).
class TraceExceptionLogger : ExceptionLogger
{
public override void LogCore(ExceptionLoggerContext context)
{
Trace.TraceError(context.ExceptionContext.Exception.ToString());
}
}
Niestandardowa procedura obsługi wyjątków komunikatu o błędzie
Poniższa procedura obsługi wyjątków generuje niestandardową odpowiedź na błędy dla klientów, w tym adres e-mail na potrzeby kontaktowania się z pomocą techniczną.
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);
}
}
}
Rejestrowanie filtrów wyjątków
Jeśli do utworzenia projektu używasz szablonu projektu "ASP.NET MVC 4 Web Application", umieść kod konfiguracji internetowego interfejsu API wewnątrz WebApiConfig klasy w folderze App_Start :
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());
// Other configuration code...
}
}
Dodatek: Szczegóły klasy bazowej
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;
}
}