Единственная настоящая ошибка — не исправлять своих прошлых ошибок. Конфуций.
Ошибки в приложении неизбежны, но есть те, которые мы "поджидаем", а есть зайцы, которые проскакивают сквозь дыру в заборе. Как сделать так, чтоб незамеченным не остался ни один заяц, и чтоб пользователь был доволен всегда будет рассказано в этой статье.
Для обработки ошибки выделим три ключевых шага, которые необходимо выполнить:
- Перехватить ошибку.
- Обработать ошибку (добавить информацию в логи или установить сообщение об ошибке пользователю, выполнить обработку ошибочной операции).
- Отобразить приемлемый результат пользователю.
Ключевой особенностью ASP.NET Core MVC приложений, по сравнению с ASP.NET MVC, является то, что в основу архитекторы заложили принцип объединения в цепочку отдельных функциональных частей (middleware).
Соответственно MVC является одним из элементов данной цепочки, и запрос проходит по данной цепочке до обработчика и обратно. Каждый элемент вызывает последующий, если тот существует. И Каждый элемент возвращает результат вызова следующего. А это дает следующий факт - обработать ошибку можно, используя следующие точки влияния:
- Try catch в своем коде.
- Фильтры исключений MVC.
- “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 и обработать ошибку. Принцип работы тот же, только менее красиво.