Главная страница   /   16.4. Использование фильтров для исключений (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

16.4. Использование фильтров для исключений

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

  • другого фильтра (фильтра авторизации, действия или результата);
  • самого метода действия;
  • при выполнении результата действия (подробная информация о результатах действия дана в главе 15).

Создаем фильтр исключения

Фильтры исключений должны реализовывать интерфейс IExceptionFilter, который показан в листинге 16-10.

Листинг 16-10: Интерфейс IExceptionFilter
namespace System.Web.Mvc
{
	public interface IExceptionFilter
	{
		void OnException(ExceptionContext filterContext);
	}
}

Когда появится необработанное исключение, будет вызван метод OnException. Параметром для этого метода является объект ExceptionContext, который наследует от ControllerContext и имеет ряд полезных свойств, с помощью которых можно получить информацию о запросе. Они приведены в таблице 16-3.

Таблица 16-3: Свойства ControllerContext
Название Тип Описание
Controller ControllerBase Возвращает объект контроллера для данного запроса
HttpContext HttpContextBase Обеспечивает доступ к информации о запросе и доступ к ответу
IsChildAction bool Возвращает true, если это дочернее действие (будет кратко обсуждаться позже в этой главе и подробно в главе 18)
RequestContext RequestContext Предоставляет доступ к объекту HttpContext и данным маршрутизации, хотя и то, и то доступно через другие свойства
RouteData RouteData Возвращает данные маршрутизации для данного запроса

В дополнение к свойствам, наследованным от класса ControllerContext, класс ExceptionContext определяет некоторые дополнительные свойства, которые также полезны при работе с исключениями. Они показаны в таблице 16-4.

Подсказка

Мы разделили эти таблицы, потому что есть и другие классы контекста фильтров, которые наследуют от ControllerContext и которые мы представим по ходу этой главы.

Таблица 16-4: Дополнительные свойства ExceptionContext
Название Тип Описание
ActionDescriptor ActionDescriptor Предоставляет подробную информацию о методе действия
Result ActionResult Результат для метода действия; фильтр может отменить запрос, установив для этого свойства иное значение, кроме null
Exception Exception Необработанное исключение
ExceptionHandled bool Возвращает true, если другой фильтр отметил это исключение как обработанное

Сгенерированное исключение доступно через свойство Exception. Фильтр исключения может сообщить, что он обработал исключение, установив для свойства ExceptionHandled значение true. Вызываются все фильтры исключений, примененные к действию, даже если ExceptionHandled уже содержит true, так что лучше всегда проверять, обработано ли исключение другим фильтром, чтобы не решать повторно уже решенную проблему.

Примечание

Если ни один из фильтров исключений не установил свойству ExceptionHandled значение true, MVC Framework будет использовать стандартную процедуру обработки исключений ASP.NET, которая приведет к самому нежелательному результату - "желтому экрану смерти".

Свойство Result используется фильтром исключений, чтобы сообщить MVC Framework дальнейшую последовательность действий. В основном это регистрация исключения и отображение соответствующего сообщения для пользователя. Чтобы продемонстрировать выполнение этих задач, мы создали новый класс под названием RangeExceptionAttribute.cs, который добавили в папку Infrastructure нашего проекта. Содержимое этого файла показано в листинге 16-11.

Листинг 16-11: Реализуем фильтр исключений
using System;
using System.Web.Mvc;
namespace Filters.Infrastructure
{
	public class RangeExceptionAttribute : FilterAttribute, IExceptionFilter
	{
		public void OnException(ExceptionContext filterContext)
		{
			if (!filterContext.ExceptionHandled &&
				filterContext.Exception is ArgumentOutOfRangeException)
			{
				filterContext.Result
					= new RedirectResult("~/Content/RangeErrorPage.html");
				filterContext.ExceptionHandled = true;
			}
		}
	}
}

Этот фильтр исключений обрабатывает экземпляры ArgumentOutOfRangeException, перенаправляя браузер пользователя к файлу RangeErrorPage.html из папки Content.

Обратите внимание, что мы наследовали класс RangeExceptionAttribute от класса FilterAttribute наряду с реализацией интерфейса IExceptionFilter. Чтобы класс атрибута .NET работал как фильтр MVC, класс должен реализовать интерфейс IMvcFilter. Это можно сделать напрямую, но самый простой способ создать фильтр – это наследовать от класса FilterAttribute, который реализует необходимый интерфейс и предоставляет некоторые полезные базовые функции, такие как изменение стандартного порядка выполнения фильтров (к чему мы вернемся позже в этой главе).

Применяем фильтр исключений

Прежде чем мы сможем протестировать наш фильтр исключений, нужно создать для этого некоторую базу. Во-первых, создадим в проекте папку Content и в ней - файл RangeErrorPage.html. Он будет использоваться для отображения простого сообщения, которые вы можете видеть в листинге 16-12.

Листинг 16-12: Содержимое файла RangeErrorPage.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<title>Range Error</title>
</head>
<body>
	<h2>Sorry</h2>
	<span>One of the arguments was out of the expected range.</span>
</body>
</html>

Далее нам нужно добавить метод действия в контроллер Home, который будет выбрасывать интересующее нас исключение. Он показан в листинге 16-13.

Листинг 16-13: Добавляем новое действие в контроллер Home
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Filters.Infrastructure;
namespace Filters.Controllers
{
	public class HomeController : Controller
	{
		[Authorize(Users = "adam, steve, jacqui", Roles = "admin")]
		public string Index()
		{
			return "This is the Index action on the Home controller";
		}
		public string RangeTest(int id)
		{
			if (id > 100)
			{
				return String.Format("The id value is: {0}", id);
			}
			else
			{
				throw new ArgumentOutOfRangeException("id", id, "");
			}
		}
	}
}

Если вы запустите приложение и перейдете по ссылке /Home/RangeTest/50, то увидите стандартную обработку исключения. В маршрутизации по умолчанию, которую для проекта создает Visual Studio, есть переменная сегмента под названием id, которой в этом URL мы установили значение 50, что и приведет к результату, показанному на рисунке 16-3. (Подробно маршрутизация и сегменты URL описаны в главах 13 и 14.)

Рисунок 16-3: Ответ стандартной обработки исключений

Мы можем применить фильтр исключений либо к контроллерам, либо к отдельным действиям, как показано в листинге 16-14.

Листинг 16-14: Применяем фильтр
[RangeException]
public string RangeTest(int id)
{
	if (id > 100)
	{
		return String.Format("The id value is: {0}", id);
	}
	else
	{
		throw new ArgumentOutOfRangeException("id");
	}
}

Если вы перезапустите приложение и снова перейдете по ссылке Home/RangeTest/50, то увидите результат, показанный на рисунке 16-4.

Рисунок 16-4: Эффект применения фильтра исключений

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

Проще и безопаснее всего для отображения ответа при выбросе исключения использовать страницу или статический контент – едва ли при таком подходе что-то пойдет не так и возникнут дополнительные проблемы. Однако такое решение будет не достаточно хорошим для пользователя, который получит общее предупреждение и будет выброшен из приложения.

Альтернативный подход - использовать представление для отображения конкретного сообщения о проблеме и предоставления пользователю некоторой контекстной информации и возможностей, с помощью которых можно данную проблему решить. Чтобы это продемонстрировать, мы внесли некоторые изменения в класс RangeExceptionAttribute, как показано в листинге 16-15.

Листинг 16-15: Возвращаем представление из фильтра исключений
using System;
using System.Web.Mvc;
namespace Filters.Infrastructure
{
	public class RangeExceptionAttribute : FilterAttribute, IExceptionFilter
	{
		public void OnException(ExceptionContext filterContext)
		{
			if (!filterContext.ExceptionHandled &&
				filterContext.Exception is ArgumentOutOfRangeException)
			{
				int val = (int)(((ArgumentOutOfRangeException) filterContext.Exception).ActualValue);
				filterContext.Result = new ViewResult
				{
					ViewName = "RangeError",
					ViewData = new ViewDataDictionary<int>(val)
				};
				filterContext.ExceptionHandled = true;
			}
		}
	}
}

Мы создали объект ViewResult и установили значения его свойств ViewName и ViewData, определяющие название представления и объект модели, который будет в него передан. Это немного запутанный код, потому что мы работаем с объектом ViewResult напрямую и не используем определенный в классе Controller метод View, который всегда применяем в методах действий. Мы не станем останавливаться на этом коде, потому что представления будут подробно рассмотрены в главе 18, и встроенный фильтр исключений, который мы опишем в следующем разделе, позволит достичь того же эффекта более понятным способом. Сейчас мы просто хотим вам показать, как все работает «под капотом».

В объекте ViewResult мы указываем представление под названием RangeError и передаем значение int вызвавшего исключение аргумента в качестве объекта модели представления. Затем мы добавим папку Views/Shared в проект Visual Studio и создадим в нем файл RangeError.cshtml, содержимое которого показано в листинге 16-16.

Листинг 16-16: Файл представления RangeError.cshtml
@model int
<!DOCTYPE html>
<html>
<head>
	<meta name="viewport" content="width=device-width" />
	<title>Range Error</title>
</head>
<body>
	<h2>Sorry</h2>
	<span>The value @Model was out of the expected range.</span>
	<div>
		@Html.ActionLink("Change value and try again", "Index")
	</div>
</body>
</html>

В файле представления мы используем стандартные теги HTML и Razor, чтобы предоставить пользователю (немного) более полезное сообщение. Наш пример приложения довольно ограничен, поэтому у нас нет каких-либо полезных страниц, на которые можно направить пользователя для решения этой проблемы. Мы создали ссылки на другой метод действия с помощью вспомогательного метода ActionLink, просто чтобы показать, что в представлении вы можете использовать какие угодно функции. Чтобы увидеть результат, перезапустите приложение и перейдите по ссылке /Home/RangeTest/50, как показано на рисунке 16-5.

Рисунок 16-5: Используем представление для отображения сообщения от фильтра исключения

Как избегать лишних исключений

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

Недостатком этого подхода является то, что вам придется тщательно протестировать представление и убедиться, что вы не просто генерируете еще одно исключение. Мы часто с этим сталкиваемся, когда разработчики уделяют основное внимание тестированию основных функций приложения и не учитывают всех потенциальных ситуаций, которые могут привести к ошибке. Чтобы продемонстрировать это, мы добавили блок кода Razor в представление RangeError.cshtml, который вызовет исключение, как показано в листинге 16-17.

Листинг 16-17: Добавляем в представление код, который вызовет исключение
@model int

@{
	var count = 0;
	var number = Model / count;
}

<!DOCTYPE html>
<html>
<head>
	<meta name="viewport" content="width=device-width" />
	<title>Range Error</title>
</head>
<body>
	<h2>Sorry</h2>
	<span>The value @Model was out of the expected range.</span>
	<div>
		@Html.ActionLink("Change value and try again", "Index")
	</div>
</body>
</html>

При визуализации представления наш код будет генерировать DivideByZeroException. Если вы запустите приложение и снова перейдите по ссылке /Home/RangeTest/50, то увидите исключение, которое возникает при попытке визуализировать представление, а не то, которое выбрасывает контроллер, как показано на рисунке 16-6.

Рисунок 16-6: Исключение, которое возникает при попытке визуализировать представление

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

Используем встроенный фильтр исключений

Мы разобрали процесс создания фильтров исключений, потому что важно понимать, что происходит «под капотом» в MVC Framework. Но в реальных проектах вам не часто понадобится создавать пользовательские фильтры, потому что Microsoft включила в MVC Framework атрибут HandleErrorAttribute, который является встроенной реализацией интерфейса IExceptionFilter. Вы можете указать исключения, имена представлений и разметку, используя свойства, описанные в таблице 16-5.

Таблица 16-5: Свойства HandleErrorAttribute
Название Тип Описание
ExceptionType Type Тип исключения, который обрабатываться данным фильтром. Это свойство также будет обрабатывать типы исключений, которые наследуют от указанного, но будет игнорировать все другие. По умолчанию для ExceptionType указано значение System.Exception, что означает, что оно будет обрабатывать все стандартные исключения.
View string Название шаблона представления, которое визуализируется данным фильтром. Если вы не указываете значение, по умолчанию устанавливается значение Error, так что будет визуализировано Views/<currentControllerName>/Error.cshtml или /Views/Shared/Error.cshtml.
Master string Имя макета, который используется при визуализации представления данного фильтра. Если вы не указываете значение, представление использует свой макет страницы по умолчанию.

Когда появляется необработанное исключение указанного типа в ExceptionType, HandleErrorAttribute визуализирует представление, указанное в свойстве View (используя макет по умолчанию или определенный в свойстве Master).

Готовимся использовать встроенный фильтр исключений

Фильтр HandleErrorAttribute работает только тогда, когда в файле Web.config включена обработка пользовательских исключений. Поэтому мы добавляем атрибут customErrors в узел <system.web>, как показано в листинге 16-18.

Листинг 16-18: Включаем обработку пользовательских исключений в файле Web.config
<system.web>
	<httpRuntime targetFramework="4.5" />
	<compilation debug="true" targetFramework="4.5" />
	<pages>
		<namespaces>
			<add namespace="System.Web.Helpers" />
			<add namespace="System.Web.Mvc" />
			<add namespace="System.Web.Mvc.Ajax" />
			<add namespace="System.Web.Mvc.Html" />
			<add namespace="System.Web.Routing" />
			<add namespace="System.Web.WebPages" />
		</namespaces>
	</pages>
	<customErrors mode="On" defaultRedirect="/Content/RangeErrorPage.html"/>
</system.web>

По умолчанию для атрибута mode установлено значение RemoteOnly, что означает, что во время разработки HandleErrorAttribute не будет перехватывать исключения, но когда вы развернете приложение на сервере и начнете делать запросы с другого компьютера, то HandleErrorAttribute вступит в силу. Чтобы понять, что увидят конечные пользователи, установите для customErrors mode значение On. В атрибуте defaultRedirect указывается страница по умолчанию, которая будет отображаться, если все остальные не работают.

Применяем встроенный фильтр исключений

В листинге 16-19 показано, как мы применили фильтр HandleError к контроллеру Home.

Листинг 16-19: Используем фильтр HandleErrorAttribute
[HandleError(ExceptionType = typeof(ArgumentOutOfRangeException), View = "RangeError")]
public string RangeTest(int id)
{
	if (id > 100)
	{
		return String.Format("The id value is: {0}", id);
	}
	else
	{
		throw new ArgumentOutOfRangeException("id", id, "");
	}
}

В этом примере мы создали ту же ситуацию, которая у нас была с пользовательским фильтром, то есть при возникновении ArgumentOutOfRangeException пользователь увидит представление RangeError.

Визуализируя представление, фильтр HandleErrorAttribute передает объект модели представления HandleErrorInfo, который содержит исключение и дополнительную информацию, которую мы будем использовать в представлении. В таблице 16-6 приведены свойства, определенные в классе HandleErrorInfo.

Таблица 16-6: Свойства HandleErrorInfo
Название Тип Описание
ActionName string Возвращает имя метода действия, который сгенерировал исключение
ControllerName string Возвращает имя контроллера, который сгенерировал исключение
Exception Exception Возвращает исключение

Обратите внимание, как мы обновили представление RangeError.cshtml, чтобы использовать этот объект модели в листинге 16-20.

Листинг 16-20: Используем объект модели HandleErrorInfo в представлении RangeError
@model HandleErrorInfo
@{
	ViewBag.Title = "Sorry, there was a problem!";
}
<!DOCTYPE html>
<html>
<head>
	<meta name="viewport" content="width=device-width" />
	<title>Range Error</title>
</head>
<body>
	<h2>Sorry</h2>
	<span>The value @(((ArgumentOutOfRangeException)Model.Exception).ActualValue)
		was out of the expected range.</span>
	<div>
		@Html.ActionLink("Change value and try again", "Index")
	</div>
	<div style="display: none">
		@Model.Exception.StackTrace
	</div>
</body>
</html>

Мы должны были привести значение свойстваModel.Exception к типу ArgumentOutOfRangeException только затем, чтобы потом иметь возможность прочитать свойство ActualValue, так как класс HandleErrorInfo является объектом модели общего назначения и используется для передачи любого исключения в представление.

Внимание

При использовании фильтра HandleError иногда возникает странное поведение, при котором представление не отображается пользователю, пока в него не включено значение свойства Model.Exception.StackTrace. Так как мы не хотим его отображать, мы заключили его вывод в элемент div, в котором свойству CSS display задали значение none, благодаря чему он стал невидимым для пользователя.