Artykuł

freeimages.com freeimages.com
mar 20 2015
0

Jak najlepiej obsłużyć wyjątki w ASP.NET MVC?

Obsługa błędów nigdy nie była łatwym zadaniem, ale w większości przypadków zawsze wiedziałem z czego mogę skorzystać. Myślałem że z ASP.NET MVC będzie podobnie, a może nawet lepiej (w końcu jest to dosyć dobrze przemyślana platforma, co wielokrotnie już podkreślałem na blogu) i po części tak jest w istocie - mamy tu wręcz nadmiar możliwości:

  • Lokalna obsługa wyjątków za pomocą bloku try..catch
  • Obsługa wyjątków w metodzie Application_Error w pliku Global.asax
  • Filtry wyjątków
  • Strony custom errors (Web.config: configuration/system.web/customErrors)
  • Strony błędów web serwera (Web.config: configuration/system.webServer/httpErrors)

Jak widać, opcji jest kilka, a każda zachowuje się inaczej. Który wariant zatem wybrać? A może należy połączyć kilka opcji? Jeśli tak, jak to zrobić sprytnie, by nie duplikować kodu i nie doprowadzić do walki o wyjątek? Na te i inne pytania postaram się odpowiedzieć w dalszej części tekstu, gdzie przedstawię rozwiązanie, które zastosowałem w jednym z ostatnio tworzonych przeze mnie projektów.

Jaki jest cel?

Moim celem jest możliwie scentralizowana obsługa błędów, które mogą się objawić podczas użytkowania strony WWW. Mam tu na myśli zarówno wyjątki, rzucone przez kod kontrolerów, a także te, które mogą pojawić się na poziomie serwera, czy też samej aplikacji.

Z perspektywy użytkownika końcowego, powinny się pojawić dwie strony błędów:

  • 404 - strona dla elementów, które nie zostały odnalezione w regułach routingu, a także w sytuacji gdy sami w kodzie zgłosimy informację o tym, że określony element nie istnieje na stronie (np. recenzja książki o wybranym tytule w portalu o książkach)
  • 500 - strona wyświetlająca informację o błędzie we wszystkich pozostałych przypadkach

Ze strony technicznej, chcę zalogować możliwie jak najwięcej potencjalnych wyjątków.

Rozwiązanie - teoria

Aby zrealizować mój cel, musiałem połączyć dwa z przedstawionych wcześniej rozwiązań. Po pierwsze postanowiłem logować błędy za pomocą biblioteki NLog, a cały kod za to odpowiedzialny umieściłem w metodzie Application_Error (dla przypomnienia - znajduje się ona w pliku Global.asax).

Po drugie, postanowiłem że za wyświetlanie błędów odpowiedzialne będą dwa widoki umieszczone w lokalizacjach:

  • /Error/Error404
  • /Error/Error500

Widoki będą wyzwalane przez IIS dzięki konfiguracji umieszczonej w pliku Web.Config.

Rozwiązanie - kod

Omówienie aspektów technicznych rozpoczniemy od przestudiowania pliku Web.Config. W poniższym listingu zostawiłem elementy ważne z perspektywy niniejszego tekstu:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog"/>
  </configSections>
  <!-- -->
  <nlog>
    <extensions>
      <add assembly="NLog.Extended" />
    </extensions>
    <targets>
      <target name="errorsFile" type="File" fileName="f:\NLog\LoggingTest\errors_${shortdate}.log" />
    </targets>
    <rules>
      <logger name="*" minlevel="Warn" writeTo="errorsFile" />
    </rules>
  </nlog>
  <system.webServer>
    <httpErrors errorMode="Custom" existingResponse="Replace">
      <remove statusCode="404" />
      <error statusCode="404" responseMode="ExecuteURL" path="/Error/Error404" />
      <remove statusCode="500" />
      <error statusCode="500" responseMode="ExecuteURL" path="/Error/Error500" />
    </httpErrors>
  </system.webServer>
  <!-- -->
</configuration>

Z naszej perspektywy dzieją się tutaj dwie interesujące rzeczy. Po pierwsze - skonfigurowaliśmy NLoga do logowania wszystkiego począwszy od ostrzeżeń, do określonego katalogu na dysku (NLoga zainstalujecie za pomocą NuGeta).

Po drugie określiliśmy gdzie IIS ma szukać stron błędów (węzeł system.webServer który załatwia ten problem, dostępny jest od wersji 7.0 tego serwera).

Kolejnym krokiem naszego procesu, będzie stworzenie wskazanych wyżej widoków. Najpierw jednak utworzymy ErrorController, w którym umieścimy dwie akcje odpowiedzialne za obsługę widoków

public class ErrorController : Controller
{
    public ActionResult Error500()
    {
        Response.StatusCode = 500;
        return View();
    }

    public ActionResult Error404()
    {
        Response.StatusCode = 404;
        return View();
    }
}

Ważne jest by w obu metodach ustawić odpowiedni kod statusu, który będzie ważny również dla przeglądarki. Widoki na potrzeby przykładu mogą być jak najprostsze. Tak wygląda mój przykładowy widok dla błędu 500:

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Error500</title>
</head>
<body>
    <div> 
        500
    </div>
</body>
</html>

Ostatnim elementem układanki jest metoda Application_Error, która zostanie wywołana w pierwszej kolejności w przypadku wystąpienia jakiegokolwiek wyjątku:

protected void Application_Error(object sender, EventArgs e)
{
    Exception exc = Server.GetLastError();
    NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
    logger.Fatal(exc);
    //Server.ClearError();
}

W tym miejscu logujemy zaistniały wyjątek za pomocą NLoga. Warto zwrócić uwagę, na zakomentowaną instrukcję Server.ClearError();. Gdybyśmy ją odkomentowali, to w większości przypadków nie zadziałałby strony błędów wyzwalane przez IIS, ponieważ nie dotarłaby do niego informacja, że w aplikacji wystąpił jakiś wyjątek. W takim przypadku, nie będzie takiego problemu.

Niuanse

Na koniec kilka niuansów, które być może pozwolą na lepsze zrozumienie całego problemu.

  • Metoda Application_Error powinna wyłapać wszystkie wyjątki, które mogą pojawić się po stronie kodu naszej aplikacji
  • Błędy po stronie serwera zostaną obsłużone tylko przez widoki - ewentualne informacje o zdarzeniach powinny pojawić się w logach serwera
  • Jeśli chcemy by błędy 404 rzucone przez nas kod były logowane, powinniśmy rzucać wyjątki:
    public ActionResult Test404Exception()
    {
        throw (new HttpException(404, "Not found"));
    }
  • Jeśli nie zależy nam na 404 w logach, powinniśmy zwracać rezultat HttpNotFound:
    public ActionResult TestHttpNotFound()
    {
        return HttpNotFound();
    }

Źródła

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus