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

Содержание

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 : 
	ControllerBaseIActionFilterIAuthorizationFilterIDisposableIExceptionFilterIResultFilter {
///...много букв
}

Нам интересен тот факт, что у этого базового контролера есть виртуальный метод 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 {
		ViewName = "error",
		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();
 
	// перенаправляем пользователя на другую страницу
	// созданную специльно для этого.
	Response.Redirect("site/feedback");
}

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

Примечание. Если реализуете третий вариант и второй, то приоритет на срабатывание первым будет иметь 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-size1.2emcolorRedfont-weightbold;">
	Сообщение об ошибке уже отправлено разработчикам. Надеемся на ваше понимание.</p>
<p style="font-weightbold;">@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 { getset; }
 
	/// <summary>
	/// Текст сообщения
	/// </summary>
	[Required]
	[Display(Name = "Текст сообщения о событии")]
	[StringLength(500)]
	public string Message { getprivate set; }
 
	/// <summary>
	/// Дата выполнения операции
	/// </summary>
	[Required]
	[Display(Name = "Выполнено")]
	public DateTime CreatedAt { getprivate set; }
 
	/// <summary>
	/// Имя пользователя
	/// </summary>
	[Required]
	[ Display(Name = "Автор")]
	[ StringLength(50)]
	public string UserName { getset; }
}

А теперь при помощи 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();
	LogRepository logger = new LogRepository();
	logger.Log(string.Concat("ОШИБКА: ", exception.Message));
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");
}

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

Заключение

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

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

Подробнее: http://feedproxy.google.com/~r/blogmusor/~3/dkUT5gO-ZAQ/83

Читать комменты и комментировать

Добавить комментарий / отзыв



Защитный код
Обновить

ASP.NET MVC: История одного проекта Обработка ошибок (часть 8) | | 2012-09-13 08:38:46 | | Программирование | | Содержание ASP.NET MVC: История одного проекта Готовимся к старту (часть 1)ASP.NET MVC: История одного проекта Всё ради данных (часть 2)ASP.NET MVC: История одного проекта Шаблоны и внешний вид | РэдЛайн, создание сайта, заказать сайт, разработка сайтов, реклама в Интернете, продвижение, маркетинговые исследования, дизайн студия, веб дизайн, раскрутка сайта, создать сайт компании, сделать сайт, создание сайтов, изготовление сайта, обслуживание сайтов, изготовление сайтов, заказать интернет сайт, создать сайт, изготовить сайт, разработка сайта, web студия, создание веб сайта, поддержка сайта, сайт на заказ, сопровождение сайта, дизайн сайта, сайт под ключ, заказ сайта, реклама сайта, хостинг, регистрация доменов, хабаровск, краснодар, москва, комсомольск |
 
Поделиться с друзьями: