Globalna obsługa wyjątków w C#

Może się zdarzyć tak, że nasza aplikacja rzuci wyjątek, którego się nie spodziewaliśmy. Nie wygląda to dobrze, jeśli taki nieobsłużony wyjątek trafi do użytkownika. Dodatkowo, jeśli nie otrzymamy informacji o tym, że taki wyjątek wystąpił, trudno będzie nam go poprawić i nie dopuścić do jego powtórzenia.

W związku z tym warto w naszej aplikacji dodać globalną obsługę wyjątków i ich logowanie. O tym, jak skonfigurować logowanie, pisałem tutaj i tutaj. W tym poście pokażę kilka sposobów na złapanie takich wyjątków.

Aplikacje konsolowe

Try {} catch {}

Najprostszym sposobem jest umieszczenie całego kodu w bloku try {} catch {}. Jeśli piszemy aplikację konsolową, to w bloku try {} powinniśmy umieścić wszystko, co znajduje się w metodzie Main w klasie Program. W bloku catch {} warto dodać logowanie wyjątku. Dzięki temu będziemy mieli zapisaną informację o tym, że on wystąpił. Pomoże to nam nie dopuścić do jego ponownego wystąpienia.

Taki kod może wyglądać następująco:

static async Task Main(string[] args)
{
    try
    {
        Log.Logger.Information("Starting an app");
        ...
    }
    catch (Exception e)
    {
        Log.Logger.Error(e, "App terminated unexpectedly");
    }
}

AppDomain.UnhandledException

Czy to wystarczy? Niestety nie. To dobry początek, ale jeśli np. nasza aplikacja korzysta z wielu wątków, to nie złapiemy wyjątku, który wystąpi poza wątkiem głównym. Czasem też nie wszystko jesteśmy w stanie umieścić w bloku try {} catch {} – min. wyjątek, który może wystąpić w bloku catch {}.
Tu z pomocą przychodzi nam AppDomain.UnhandledException. Dzięki podpięciu się pod zdarzenie UnhandledException będziemy w stanie zalogować wszystkie wyjątki, które nie zostały złapane w bloku catch {}.

AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
{
    var exception = eventArgs.ExceptionObject as Exception;
    Log.Logger.Fatal(exception, "Unhandled Exception occured.");
};

Dobrą praktyką jest umieszczenie tego na samym początku metody Main, abyśmy byli w stanie złapać jak najwięcej nieprzewidzianych wyjątków.

Aplikacje webowe

A co, gdy mamy aplikację webową? Dzięki blokowi try {} catch {} złapiemy w zasadzie jedynie wyjątki, które wystąpią podczas uruchamiania aplikacji. Gdy aplikacja już będzie działać, to najczęściej pojawią się nieprzewidziane wyjątki dopiero podczas przetwarzania żądań (requests).

GlobalExceptionFilter

Gdy mamy WebAPI, to możemy stworzyć globalny filtr, który będzie przechwytywał wszystkie nieobsłużone wyjątki. Taki filtr może wyglądać następująco:

public class GlobalExceptionFilter : IExceptionFilter
{
    private readonly ILogger<GlobalExceptionFilter> _logger;

    public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
    {
        _logger = logger;
    }

    public void OnException(ExceptionContext context)
    {
        var ex = context.Exception;
        _logger.LogError(ex, "Unexpected error occurred");

        switch (ex)
        {
            default:
                context.Result = new BadRequestObjectResult(ex.Message);
                break;
        }
    }
}

Jeśli chodzi o filtry wyjątków to mamy do dyspozycji dwa interfejsy: IExceptionFilter oraz IAsyncExceptionFilter. Jeśli mamy potrzebę użycia asynchronicznych metod w naszym filtrze (albo po prostu chcemy zawsze tworzyć asynchroniczne metody), to możemy użyć tego drugiego.

Musimy jeszcze pamiętać o dodaniu tego filtra w konfiguracji naszej aplikacji. W tym celu powinniśmy w metodzie AddMvc (z reguły jej użycie znajduje się w metodzie ConfigureServices w klasie Startup) dodać nasz filtr do listy filtrów:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddMvc(options => 
    {
        options.Filters.Add(typeof(GlobalExceptionFilter));
    });
    ...
}

Strona błędu w MVC

W aplikacji MVC możemy z kolei stworzyć specjalną stronę, która wyświetli się użytkownikowi jeśli poleci, jakiś nieoczekiwany wyjątek. Taka strona może wyglądać następująco:

Aby taka strona została wyświetlona musimy dodać trochę kodu.

Po pierwsze będziemy potrzebowali widok (plik .cshtml), który będzie zawierał nasz kod html. W związku z tym do katalogu View należy dodać podkatalog Error, a w nim utworzyć plik Index.cshtml.

W tym pliku możemy umieścić następujący kod:

@{
    ViewData["Title"] = "Error";
}

<h1 class="text-danger">Error :(</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>

Kolejnym krokiem jest stworzenie odpowiedniego kontrolera o nazwie ErrorController:

public class ErrorController : Controller
{
    ...
}

Dodajemy w nim metodę Index, w której możemy zalogować nasz wyjątek. Ta metoda nie powinna zawierać żadnego atrybutu z serii HttpGet, HttpPost, itd.

public IActionResult Index()
{
    var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
    _logger.LogError(exceptionHandlerPathFeature.Error,
        $"Error occurred processing: {exceptionHandlerPathFeature.Path}");
    return View();
}

Jeśli nie potrzebujemy znać ścieżki, w której wystąpił wyjątek, to możemy zamiast interfejsu IExceptionHandlerPathFeature użyć interfejsu IExceptionHandlerFeature.

Na koniec musimy jeszcze dodać obsługę wyjątków w konfiguracji naszej aplikacji. W klasie Startup należy zmodyfikować metodę Configure:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    app.UseExceptionHandler("/Error");
    ...
}

3 myśli na “Globalna obsługa wyjątków w C#”

  1. Pingback: dotnetomaniak.pl

Dodaj komentarz