ASP.NET MVC: История одного проекта "Обработка ошибок" (часть 8)

ru-RU | создано: 03.06.2012 | опубликовано: 03.06.2012 | обновлено: 02.01.2018 | просмотров за всё время: 18985

ASP.NET MVC Framework умеет многое, и более того может прекрасно расширяться и дополняться. В этой статье поговорим об обработке ошибок. Будут показаны несколько способов.

Содержание

ASP.NET MVC: История одного проекта "Готовимся к старту" (часть 1)
ASP.NET MVC: История одного проекта "Всё ради данных" (часть 2)
ASP.NET MVC: История одного проекта "Шаблоны и внешний вид" (часть 3)
ASP.NET MVC: История одного проекта "Еще немного классов" (часть 4)
ASP.NET MVC: История одного проекта "UI - всё для пользователя" (часть 5)
ASP.NET MVC: История одного проекта "UI - Добавление экспоната" (часть 6)
ASP.NET MVC: История одного проекта "UI - Редактирование экспоната" (часть 7)
ASP.NET MVC: История одного проекта "Обработка ошибок" (часть 8)
ASP.NET MVC: История одного проекта "Фильтрация" (часть 9)
ASP.NET MVC: История одного проекта "Поиск" (часть 10)
ASP.NET MVC: История одного проекта "Облако тегов" (часть 11)
ASP.NET MVC: История одного проекта "Главная страница" (часть 12)

Палки в колеса

ASP.NET MVC Framework умеет многое. Если вам что-то не нравится, или вы просто хотите реализовать что-либо по-другому, MVC Framework не станет "вставлять палки в колеса", а наоборот предоставит дружелюбный интерфейс. Сегодня поговорим про обработку ошибок. Будут описаны четыре с половиной способа реализации, от простого до продвинутого. Выбирать вам предстоит вам, основываясь на конкретном проекте, или на личных предпочтениях.

Вариант первый "Как два пальца об асфальт"

Самый наверное простой способ использовать HandleErrorAttribute, который любезно предоставили разработчики фреймворка. Но для начала я хочу показать, какая разница и для его нужна обработка ошибок. Никто не застрахован от ошибок, и даже программисты. :) Для этого давайте подготовим проект к выдачи ошибки при открытие главной страницы. Специально вызовим ошибку:

public ActionResult Index() {
  throw new ArgumentException("Специальная ошибка для теста");
  //return View();
}

Запустим сайт и увидим ошибку ("некрасивая" форма отображения):

Не очень приятная картина, не правда ли? Но всё меняется когда приходят мы поставим вышеуказанный атрибут (можно над методом, а можно охватить все методы поставив его на контроллером). Атрибут имеет параметр ViewName, который, как вы понимаете принимает название представления для перенаправления при возникновении ошибки. Если вы поставите атрибут над контролером, то перенаправление будет всегда на одну страницу. А если над методом, то у каждого из них может быть своя страница с ошибкой. Я поставил над контролером, а значит по умолчанию перенаправление будет на представление Error:

[HandleError() ]
public class SiteController : Controller {
  /// много букв
}

Теперь запустим... Опаньки! не сработал! Ах, чёрт побери, совсем забыл включить обработку ошибок в файле конфигурации (web.config). Итак, чтобы заработал атрибут, надо включить в секцию System.Web строку CustomErrors:

<system.web>
  <!-- много букв-->
  
  <customErrors mode="On" />
  
  <!-- много букв-->
</system.web>

Запускаем еще раз! Ура! Вот что я увидел:

Пожалуй немного поясню. По умолчанию в проекте MVCApplication в папке Views/Shared создается представление Error.cshtml. Это как раз то представление (View), куда делает перенаправление при ошибке атрибут HandleError. Я это представление немного изменил:

@model System.Web.Mvc.HandleErrorInfo
@{
  ViewBag.Title = "Error";
}
<h2>
  Ошибка
</h2>
@Html.ShowAlert(@"В результате выполнения запроса 
возникла непредвиденная ситуация. Информация о 
случившемся уже направлена администраторам системы. 
Проблема будет устранена в кратчайшие сроки.
Приносим вам свои извинения за возможно доставленные неудобства.")
<p>
  С уважением, Администрация</p>

Именно это вы и увидели на предыдущей картинке. Уже не плохо, но дальше - лучше.

Вариант второй "Немного летмотивов"

Поехали дальше... Я убрал атрибут и настройку в файле конфигурации web.config для чистоты эксперимента. Контролеры в MVC - это лейтмотив фреймворка. По умолчанию контролеры, которые вы создаете или будете создавать в своем проекте наследуются от базого абстрактного класса Controller:

public abstract class Controller : 
  ControllerBase, IActionFilter, 
  IAuthorizationFilter, IDisposable, 
  IExceptionFilter, IResultFilter {
///...много букв
}

Нам интересен тот факт, что у этого базового контролера есть виртуальный метод OnException:

//
// Summary:
//     Called when an unhandled exception occurs in the action.
//
// Parameters:
//   filterContext:
//     Information about the current request and action.
protected virtual void OnException(ExceptionContext filterContext);

Из описания понятно, что предназначен отслеживать исключения в контролере. А раз он виртуальный, то его можно переопределить в своём контролере, например, следующим образом:

protected override void OnException(ExceptionContext filterContext) {
  var controllerName = filterContext.RouteData.Values["controller"] as String;
  var actionName = filterContext.RouteData.Values["action"] as String;
  var model = new HandleErrorInfo(
    filterContext.Exception, 
    controllerName, 
    actionName);
  var result = new ViewResult {
    <strong>ViewName = "error",</strong>
    MasterName = string.Empty,
    ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
    TempData = filterContext.Controller.TempData
  };
  filterContext.Result = result;

  // сконфигурируем отправляемый ответ
  filterContext.ExceptionHandled = true;
  filterContext.HttpContext.Response.Clear();
  filterContext.HttpContext.Response.StatusCode = 500;
  filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}

Результатом поделанных махинаций действий будет представление, обратите внимание на жирную строку... Правильно! Всё то же представление (Error.cshtml) об ошибке (назовем его " красивое"). Уже совсем другой компот, потому что появилась какая-то "управляемость" процессом.

Вариант третий "Место встречи изменить нельзя"

Чтобы не писать в каждом контролере один и тот же код на обработку исключений можно сделать немного интереснее. В файле Global.asax в методе Application_Error написать код один раз, например так:

private void Application_Error(Object sender, EventArgs e) {
  var exception = Server.GetLastError();
  if (exception == null)
    return;

  //  ваша реализация обработки ошибки
  // вплоть до отправки ее на электронную почту
  // администратора системы

  // очищаем ошибку
  Server.ClearError();

  // перенаправляем пользователя на другую страницу
  // созданную специльно для этого.
  <strong>Response.Redirect("site/feedback");</strong>
}

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

Примечание. Если реализуете третий вариант и второй, то приоритет на срабатывание первым будет иметь OnException и только потом уже Application_Error.

Вариант четверный "Любимый"

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

public class ErrorController : Controller
{
  public ActionResult General(Exception exception) {
    return View("Exception", exception);
  }

  public ActionResult Http404() {
    return View("404");
  }

  public ActionResult Http403() {
    return View("403");
  }

  public ActionResult ExhibitNotFound() {
    return View();
  }
}

Также создаю все указанные представления (View) для методов. ExhibitNotFound.cshtml, 404.cshtml и 403.cshtml содержат просто текст с информацией, что "экспонат не найден","страница не найдена" или "доступ ограничен" соответственно, а General ключевое представление, поэтому покажу его полностью:

@model Exception
@{
  ViewBag.Title = "Exception";
  Layout = "~/Views/Shared/_LayoutExtended.cshtml";
}
<h2>
  Ошибка сайта</h2>
<p style="font-size: 1.2em; color: Red; font-weight: bold;">
  Сообщение об ошибке уже отправлено разработчикам. Надеемся на ваше понимание.</p>
<p style="font-weight: bold;">@Model.Message</p>
<p>
  Вы можете:</p>
<ul>
  <li>Перейти на главную @Html.ActionLink("страницу", "index", "site"). </li>
  <li>Сообщить о том, что Вы искали или при каких условиях появилась этот ошибка. Напишите
    разработчикам @Html.ActionLink("сообщение", "feedback", "site")</li></ul>
<fieldset style="font-size: .85em;">
  <legend>Информация для разработчиков</legend>
  @Html.Raw(@Model.StackTrace.ToString())
</fieldset>

После этого я в Global.asax пишу метод Application_Error примерно так:

protected void Application_Error() {
#if !DEBUG
  var exception = Server.GetLastError();
  var httpException = exception as HttpException;
  Response.Clear();
  Server.ClearError();
  var routeData = new RouteData();
  routeData.Values["controller"] = "Error";
  routeData.Values["action"] = "General";
  routeData.Values["exception"] = exception;
  Response.StatusCode = 500;
  if (httpException != null) {
    Response.StatusCode = httpException.GetHttpCode();
    switch (Response.StatusCode) {
    case 403:
      routeData.Values["action"] = "Http403";
      break;
    case 404:
      routeData.Values["action"] = "Http404";
      break;
    }
  }
  Response.TrySkipIisCustomErrors = true;
  IController errorsController = new ErrorController();
  HttpContextWrapper wrapper = new HttpContextWrapper(Context);
  var rc = new RequestContext(wrapper, routeData);
  errorsController.Execute(rc);
#endif

}

Если запустить мой проект сейчас, то я увижу такою картинку:

Мне кажется, это более интересная реализация обработки ошибок. Причем стек можно отображать только при условии, если пользователь имеет права администратора. А самое главное, что теперь можно немного усовершенствовать полученный результат и записывать ошибки в базу данных, создать, своего рода, журнал изменений (Logs), в который можно писать не только ошибки (Errors), но и предупреждения (Warnings), и просто информацию (Information) о действиях пользователя или статусов системы.

Вариант четвертный с половиной или новый класс (Log)

Создаю просто класс, который будет использоваться при работе с логами:

public class Log {
  public Log() { }
  /// <summary>
  /// Создает экземпляр записи в журнале документов
  /// </summary>
  /// <param name="message">текст сообщения</param>
  public Log(string message) {
    this.Message = message;
    this.CreatedAt = DateTime.Now;
  }

  [Key]
  [Display(Name = "Идентификатор")]
  public int Id { get; set; }

  /// <summary>
  /// Текст сообщения
  /// </summary>
  [Required]
  [Display(Name = "Текст сообщения о событии")]
  [StringLength(500)]
  public string Message { get; private set; }

  /// <summary>
  /// Дата выполнения операции
  /// </summary>
  [Required]
  [Display(Name = "Выполнено")]
  public DateTime CreatedAt { get; private set; }

  /// <summary>
  /// Имя пользователя
  /// </summary>
  [Required]
  [ Display(Name = "Автор")]
  [ StringLength(50)]
  public string UserName { get; set; }
}

А теперь при помощи MvcScaffolding создаю репозиторий:

PM> Scaffold Repository Log -DbContextType MuseumContext
Added 'Logs' to database context 'Calabonga.Mvc.Humor.Engine.MuseumContext'
Added repository 'Models\LogRepository.cs'
PM>

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

public interface ILogRepository {
  IQueryable<Log> All { get; }
  Log Find(int id);
  void Log(string message, bool sendNotify);
  void Log(string messageformat, string param0);
  void Log(string messageformat, string param0, bool sendNotify);
  void Log(string messageformat, string param0, string param1);
  void Log(string messageformat, string param0, string param1, bool sendNotify);
  void Delete(int id);
  void Clear();
}

Вы можете придумать свою реализацию этого интерфейса, да и, собственно говоря, и сам интерефейс тоже можете себе придумать сами. А мне остается только добавить запись в журнал в методе Application_Error изпользуя новый LogRepository (жирным шрифтом):

protected void Application_Error() {
#if !DEBUG
  var exception = Server.GetLastError();
  var httpException = exception as HttpException;
  Response.Clear();
  Server.ClearError();
  var routeData = new RouteData();
  routeData.Values["controller"] = "Error";
  routeData.Values["action"] = "General";
  routeData.Values["exception"] = exception;
  Response.StatusCode = 500;
  if (httpException != null) {
    Response.StatusCode = httpException.GetHttpCode();
    switch (Response.StatusCode) {
      case 403:
        routeData.Values["action"] = "Http403";
        break;
      case 404:
        routeData.Values["action"] = "Http404";
        break;
    }
  }
  Response.TrySkipIisCustomErrors = true;
  IController errorsController = new ErrorController();
  <strong>LogRepository logger = new LogRepository();
  logger.Log(string.Concat("ОШИБКА: ", exception.Message));
</strong>  HttpContextWrapper wrapper = new HttpContextWrapper(Context);
  var rc = new RequestContext(wrapper, routeData);
  errorsController.Execute(rc);
#endif
}

У вас наверное возникнет вопрос, почему именно так? Потому что если возникает ошибка или "выстреливает" какое-нибудь исключение, то получить ILogRepository через Dependency Injection уже не получится, поэтому я создаю экземпляр класса и использую его напрямую. Но в контролерах я буду получать именно ILogRepository через Dependency Injection в конструкторе, как и положено.

И напоследок

Я обычно в методах, которые должны каким-то образом реагировать на ошибки и исключения писал TODO, например как этом методе:

public ActionResult Show(int id) {
  Exhibit exh = exhibitRepository.Find(id);
  if (exh != null) {
    return View(new ShowExhibitViewModel(exh));
  }
  // TODO: пока при отсутвии обработки ошибок
  // буду перекидывать на страницу списка
  return RedirectToAction("index");
}

После того как заработала система обработки ошибок, я могу все методы поправить включив обработку в методы. Например для предыдущего метода сделаю так:

public ActionResult Show(int id) {
  Exhibit exh = exhibitRepository.Find(id);
  if (exh != null) {
    return View(new ShowExhibitViewModel(exh));
  }
  return RedirectToAction("http404", "error");
}

Таким образом, пользователь получит правильное уведомление о том что такой записи в базе нет.

Заключение

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

И, как я обещал в комментариях к предыдущей статье, вот [ссылка удалена] на скачивание проекта в текущей сборке. Спасибо за внимание - пишите комментарии.

Комментарии к статье (17)

04.06.2012 8:37:08 Алекс

Спасибо большое за проект, за вашу работу, которой вы занимаетесь.

26.05.2017 4:28:54 Artyom Krivokrisenko

Довольно странное решение использовать Application_Error.

В идеологию MVC гораздо лучше ложится вариант с созданием собственного ActionFilter, реализации в нем нужной логики обработки ошибок и оповещения пользователя о них.

ActionFilter регистрируется в списке глобальных фильтров и будут применяться к каждому Action.

Также очень спорный способ оповещения об ошибках 404.

Вместо

return RedirectToAction("http404", "error");

стоит использовать

return this.HttpNotFound();

будет показываться стандартная ошибка 404, или, если это требуется, можно сделать чтоб отдавалась кастомная страница с описанием ошибки 404. Главное - не будет затираться оригинальный URL в браузере.

Замечания по реализации класса Log. В нем присутствует пустой конструктор и конструктор с параметром. Стоит привести логику их работы к однообразию, т.е. или оба инициализируют свойство CreatedAt, или ни один из них этого не делает.

04.06.2012 10:49:02 Calabonga

Уважаемый Artyom Krivokrisenko, спасибо за комментарий. Текст этот статьи это практически перевод некоторого обзаца из книги "ASP.NET MVC Programming Microsoft® Updated for ASP.NET MVC 3 2 SECOND EDITION" автор Dino Esposito:

Global Error Handling from global.asax 
Since the very first version of the ASP.NET runtime, the HttpApplication  object—the object behind
global.asax —has featured an Error event  . T he event is raised whenever an unhandled exception
reaches the outermost shell of code in the application  . H ere’s how to write such a handler:
void Application_Error(Object sender, EventArgs e)  

   ... 
}

You could do something useful in this event handler, such as sending an email to the site
 administrator or writing to the Microsoft Windows event log to say that the page failed to execute
properly.

Так что я просто перевел его. А что касается "затерания" страницы - дык, я это делаю намеренно! А про Log вы правы... не углядел. Спасибо поправлю.

08.06.2012 18:24:55 !ME

Великолепная работа! Очень поучительно! Как уже писали в комментах надо бы Вашу работу популяризировать, например, на хабре, чтобы было проще найти и максимальное количество пользователей смогло ознакомиться с проектом. А вообще, как уже писали, на хабре была классная идея по циклу статей о создании самого хабра на MVC, но к сожалению, и реализация и все остальное там подкачало. Автор вроде бросил, едва начав( Было бы здорово реализовать эту идею в рамках самого хабра с использованеим MVC4 силами "сообщества". Был бы рад внести и свою посильную помощь.

АВТОР -  ВЫ БОЛЬШУЩИЙ МОЛОДЕЦ! СПАСИБО!

15.06.2012 20:05:15 hazzik

Такое ощущение, что автор сам себе пишет восхваляющие комментарии

15.06.2012 23:35:27 Calabonga

А почему, собственно говоря, только восхваляющие? Я все комментарии себе сам пишу! :) Сам статью, а потом сам комментарии - а то как-то скучно, понимаете ли... :) Да так все блогеры делают... У вас же, на сколько я знаю, тоже есть блог, вы разве не делаете так же? :)

29.06.2012 13:48:54 интересующийся

День добрый.

Хотелось бы задать вопрос. Вот имеем ErrorController
Как вызвать его метод General из другого контроллера, причем передав ему Exception. Типа:

public ActionResult MyMethod() 
{
   try
   { 
      //что-то сделали
   }
   catch(Exception e)
   {
      //перейти в наш Error контроллер 
   }
}

Ведь в том же RedirectToAction надо указывать параметр маршрута. Причем укажется он и в url строке....
Или в таком случае написать свой HandleError аттррибут, в котором вызывать Error контроллер с нужным эксепшеном?

29.06.2012 13:54:23 Calabonga

интересующийся,

попробуйте так:

 public ActionResult Index() {
  try {
    return View();
  } catch (Exception ex) {
    throw new Exception("Описание ошибки", ex);
  }      
}
29.06.2012 14:25:48 интересующийся

calabonga, 

А обрабатывать эту ошибку уже в Application_Error()? Просто тогда получется, что ВСЕ эксепшены доходят до самого верха. С одной стороны хорошо: вся обработка идет в одном месте. 

Но с другой - надо ли их до туда доводить , когда обработать можно и раньше?...

А мысль еще такая, что Application_Error() должен обрабатывать только те исключения, которые в коде не отлавливались вообще. Типа как последний рубеж )

01.05.2013 20:14:04 chas

Добрый вечер!

Применил Ваше решение, но

Обработка исключений   maxRequestLength

указанных в Web.config

<httpRuntime maxRequestLength="10000" />

 

Не выполняется. Завершается Не удается отобразить эту страницу!

01.05.2013 23:54:20 Calabonga

Значит не всё применилось правильно, или не все условия соблюдены. Надо смотреть конкретно сам проект, "удалённо" трудно сказать что-то определенное

07.05.2013 20:56:59 chas

Извените! Забыл указать, что все происходит при отладке в Visual Studio 2012.

08.05.2013 0:58:51 Calabonga

#if !DEBUG

для этого и предназначен, если его убрать, то будет всегда обработка ошибок.... Толька как же тогда отлаживать?!

11.05.2015 2:25:15 Михаил

Добрый день!
Я хочу обработать ошибку с кодом 404, используя ваш метод № 4. Но, если я в Url ввожу несуществующий контроллер, у меня в браузере отображается HTML-код моего "ошибочного" представления (вместо самого представления). Если же введенного пути в целом не существует, но в нем контроллер указан правильно (т. е. введенный контроллер существует), то тогда в браузере само представление отображается корректно.

Спасибо.

11.05.2015 3:30:49 Calabonga
Михаил,
Очень сложно сказать что-то определенное по этому поводу. Скорее всего происходящие похоже на то, что неверное настроены маршруты, но, опять же, это предположение навскидку. Точнее сказать не могу, надо смотреть код. А вы пробовали скачать демо-проект и посмотреть как он реализован?
11.05.2015 15:20:01 Михаил

Нет, скопипастил приведенный код в свой проект.

11.05.2015 23:30:39 Calabonga

Михаил,
Возможно, вам придется приложить немного больше усилий для решения данной проблемы, ибо "удалённо" помочь не представляется возможным. Скачайте демо-проект как образец для возможного решения. :)