ASP.NET Core обработка ошибок

ASP.NET Core обработка ошибок

Единственная настоящая ошибка — не исправлять своих прошлых ошибок. Конфуций.

Ошибки в приложении неизбежны, но есть те, которые мы "поджидаем", а есть зайцы, которые проскакивают сквозь дыру в заборе. Как сделать так, чтоб незамеченным не остался ни один заяц, и чтоб пользователь был доволен всегда будет рассказано в этой статье.

Для обработки ошибки выделим три ключевых шага, которые необходимо выполнить:

  1. Перехватить ошибку.
  2. Обработать ошибку (добавить информацию в логи или установить сообщение об ошибке пользователю, выполнить обработку ошибочной операции).
  3. Отобразить приемлемый результат пользователю.

Ключевой особенностью ASP.NET Core MVC приложений, по сравнению с ASP.NET MVC, является то, что в основу архитекторы заложили принцип объединения в цепочку отдельных функциональных частей (middleware).

Middleware цепочка

Соответственно MVC является одним из элементов данной цепочки, и запрос проходит по данной цепочке до обработчика и обратно. Каждый элемент вызывает последующий, если тот существует. И Каждый элемент возвращает результат вызова следующего. А это дает следующий факт - обработать ошибку можно, используя следующие точки влияния:

  1. Try catch в своем коде.
  2. Фильтры исключений MVC.
  3. “Try catch” на уровне middleware.

Обработка на уровне своего кода предельно ясна и ее рассматривать мы не будем. Для MVC мы можем использовать фильтр исключения. Для этого нам нужно создать фильтр исключения и зарегистрировать его для всех действий контроллеров. Код фильтра:

//Класс фильтра ошибок
public class GlobalExceptionFilter : IExceptionFilter
{
    private readonly ILogger<GlobalExceptionFilter> _logger;

    public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
    {
        _logger = logger;
    }
    public void OnException(ExceptionContext context)
    {
        _logger.LogError(0, context.Exception, context.Exception.Message);

        context.ExceptionHandled = true;
        context.Result = new ViewResult {ViewName = "Error-500"};
    }
}

В фильтре мы логируем нашу ошибку и возвращаем страницу ошибки сервера "Error-500". Что бы добавить фильтр, мы должны зарегистрировать его в глобальные фильтры MVC, то есть фильтры, которые выполняются для всех действий всех контроллеров.

//Добавляем глобальный фильтр, который будет получать информацию об ошибке
services.AddMvc(options =>
{
    options.Filters.Add(typeof(GlobalExceptionFilter));
});

В данном случае мы обработаем все наши Exception и наследуемые от него, но как же отобразить пользователю красивое окно 404, к примеру, когда он пытался посмотреть ресурс, которого нет. Для этого мы добавим обработку HTTP кодов. В компоненте StatusCodePagesMiddleware есть так же методы UseStatusCodePages, UseStatusCodePagesWithRedirects и UseStatusCodePagesWithReExecute. Объясню разницу между 3мя методами.

UseStatusCodePages – использует текстовое представление HTTP кода ошибки, и все что вы можете изменить, это формат и текст сообщения, который увидете в браузере.

Пример страницы с ошибкой

UseStatusCodePagesWithRedirects – может перенаправить вас на страницу ошибки, но ключевое слово перенаправит, то есть браузер, пытаясь получить не существующий ресурс, получит HTTP код 302 (ресурс перемещен) и будет выполнено перенаправление на страницу ошибки. Но по факту браузер будет думать, что ресурс существует, просто перемещен на страницу ошибки.

UseStatusCodePagesWithReExecute – самый оптимальный вариант, так как может вернуть и HTTP код и страницу ошибки в одном флаконе. Для использования мы зарегистрируем компонент:

//Добавляем разденеие обработки ошибок
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseBrowserLink();
}
else
{
    app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");
}

В строчном параметре {0} будет передано числовое отображение HTTP кода ошибки, с которым мы можем оперировать. Код контроллера с перенаправлением на страницы ошибок:

//Контроллер и действие обработки ошибки
public class StatusCodeController : Controller
{
    private Int32[] _availableCodes = new Int32[] { 404, 500 };

    [Route("/StatusCode/{statusCode}")]
    public IActionResult Index(Int32 statusCode)
    {
        if (!_availableCodes.Contains(statusCode))
            statusCode = 500;
        return View($"Error-{statusCode}");
    }
}

И последняя точка для обработки исключений – обработчик Middleware. Для чего это может быть нужно? Если у нас не используется MVC, а мы написали свой middleware для обработки запроса, то функциональность фильтров из MVC нам не будет доступна, поэтому и используем отдельный middleware для обработки ошибок:

//Middleware для обработки ошибок
public sealed class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlerMiddleware> _logger;
    public ExceptionHandlerMiddleware(RequestDelegate next, ILogger<ExceptionHandlerMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception exception)
        {
            try
            {
                //Обработка ошибки, к примеру, просто можем логировать сообщение ошибки
            }
            catch (Exception innerException)
            {
                _logger.LogError(0, innerException, "Ошибка обработки исключения");
            }
            // Если в коде обработки ошибки мы снова получили ошибку, то пробрасываем ее выше по цепочке Middleware
            throw;
        }
    }
}

Регистрируем наш middleware:

app.UseMiddleware<ExceptionHandlerMiddleware>();

Ключевой момент – обработчик должен быть зарегистрирован раньше всех остальных элементов цепочки, потому как, если произойдет исключение в цепочке до обработчика, то он не будет выполнятся и информацию об ошибке не получит. В ASP.NET Core важен порядок регистрации элементов middleware. Ну и самый простой вариант, это, не создавая свой middleware, обернуть все регистрации в try catch и обработать ошибку. Принцип работы тот же, только менее красиво.

d2funlife | Даниил Павлов 2015-2020
Powered by ASP.NET Core 2.2, Entity Framework Core 2.2. Web + UI