Фильтры исключений

94

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

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

Фильтры исключений реализуют интерфейс IExceptionFilter, который показан в примере ниже:

namespace System.Web.Mvc
{
    public interface IExceptionFilter
    {
        void OnException(ExceptionContext filterContext);
    }
}

Метод OnException() вызывается, когда возникает необработанное исключение. В качестве параметра этому методу передается объект ExceptionContext, который является производным от класса ControllerContext и предоставляет несколько полезных свойств для получения информации о запросе:

Свойства класса ControllerContext
Свойство Тип Описание
Controller ControllerBase

Возвращает объект контроллера для данного запроса

HttpContext HttpContextBase

Предоставляет доступ к деталям запроса и ответа

IsChildAction bool

Возвращает true, если это дочернее действие

RequestContext RequestContext

Предоставляет доступ к HttpContext и данным маршрутизации, которые также доступны через другие свойства

RouteData RouteData

Возвращает данные маршрутизации для этого запроса

В дополнение к свойствам, унаследованным от класса ControllerContext, в классе ExceptionContext определены дополнительные свойства, удобные для работы с исключениями:

Дополнительные свойства класса ExceptionContext
Свойство Тип Описание
ActionDescriptor ActionDescriptor

Предоставляет детали о методе действия

Result ActionResult

Результат метода действия; фильтр может отменить запрос, установив для этого свойства значение, отличное от null

Exception Exception

Необработанное исключение

ExceptionHandled bool

Возвращает true, если другой фильтр пометил исключение как обработанное

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

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

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

using System;
using System.Web.Mvc;

namespace Filter.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. Обратите внимание, что в дополнение к реализации интерфейса IExceptionFilter класс RangeExceptionAttribute унаследован от класса FilterAttribute. Чтобы класс атрибута .NET трактовался как фильтр MVC, он должен реализовывать интерфейс IMvcFilter. Это можно сделать напрямую, но фильтр гораздо проще создавать за счет наследования класса от класса FilterAttribute, который реализует требуемый интерфейс и предоставляет ряд удобных базовых средств, таких как поддержка стандартного порядка обработки фильтров.

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

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

<!DOCTYPE html>
<html>
<head>
    <title>Ошибка диапазона допустимых значений</title>
</head>
<body>
    <h2>Извините</h2>
    <p>Одно из введенных значений выходит за пределы допустимых значений</p>
</body>
</html>

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

using System;
using System.Web.Mvc;
using Filter.Infrastructure;

namespace Filter.Controllers
{
    public class HomeController : Controller
    {
        // ...

        public string RangeTest(int id)
        {
            if (id > 100)
            {
                return String.Format("Значение ID: {0}", id);
            }
            else
            {
                throw new ArgumentOutOfRangeException("id", id, "");
            }
        }
    }
}

Запустив приложение и перейдя на URL вида /Home/RangeTest/60, можно наблюдать стандартную обработку исключений. В конфигурации маршрутизации, созданной Visual Studio по умолчанию для проекта MVC, предусмотрена переменная сегмента по имени id, которая для этого URL будет установлена в 60, инициируя ответ, показанный на рисунке ниже:

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

Среда Visual Studio обнаружит исключение и остановит отладчик, предоставив вам контроль над выполнением приложения. Нажмите клавишу <F5> или щелкните на кнопке Continue (Продолжить), чтобы продолжить выполнение приложения и увидеть поведение стандартной обработки исключений.

Фильтр исключения можно применять либо к контроллерам, либо к отдельным действиям, как показано в примере ниже:

using System;
using System.Web.Mvc;
using Filter.Infrastructure;

namespace Filter.Controllers
{
    public class HomeController : Controller
    {
        // ...

        [RangeException]
        public string RangeTest(int id)
        {
            // ...
        }
    }
}

Чтобы увидеть результат, перезапустите приложение и перейдите еще раз на URL вида /Home/RangeTest/60:

Результат применения фильтра исключения

Использование представления для реагирования на исключение

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

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

using System;
using System.Web.Mvc;

namespace Filter.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. Это довольно запутанный код, т.к. мы работаем с объектом ViewResult напрямую вместо того, чтобы полагаться на метод View(), определенный в классе Controller, который используется в методах действий. Мы не собираемся подробно анализировать этот код, с помощью встроенного фильтра исключения, рассматриваемого в следующем разделе, можно достичь того же эффекта, но более элегантно. Просто необходимо продемонстрировать, как все работает "за кулисами".

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

@model int

@{
    Layout = null;
    ViewBag.Title = "Извините, возникли проблемы!";
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Ошибка диапазона допустимых значений</title>
</head>
<body>
    <h2>Извините</h2>
    <span>
        Значение <b>@Model</b>
        не входит в допустимые пределы.
    </span>
    <div>
        @Html.ActionLink("Измените это значение и попробуйте снова", "Index")
    </div>
</body>
</html>

В файле представления применяются стандартные дескрипторы HTML и Razor для отображения более полезного сообщения пользователю. Рассматриваемый пример приложения довольно ограничен, поэтому здесь нет никакой важной информации, которая помогла бы пользователю решить проблему, но мы используем вспомогательный метод ActionLink() для создания ссылки, направляющей на другой метод действия, просто для демонстрации доступности полного набора функций, связанных с представлениями.

На рисунке ниже показан результат перезапуска приложения и перехода на URL вида /Home/RangeTest/60:

Использование представления для отображения сообщения об ошибке из фильтра исключения

Избегание неправильной ловушки исключений

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

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

@model int

@{
    double count = 0, number = Model / count;
    Layout = null;
    ViewBag.Title = "Извините, возникли проблемы!";
}

...

Во время визуализации представления добавленный код сгенерирует исключение DivideByZeroException (деление на 0). Запустив приложение и перейдя на URL вида /Home/RangeTest/60, вы столкнетесь с исключением, сгенерированным при попытке визуализации представления, но не тем, которое генерирует контроллер:

Исключение, сгенерированное во время визуализации представления

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

Использование встроенного фильтра исключения

Выше было показано, каким образом создавать фильтр исключения, поскольку понимание происходящего "за кулисами" MVC Framework принесет немалую пользу. Однако создавать собственные фильтры в реальных проектах придется нечасто, т.к. в инфраструктуру MVC Framework включен класс HandleErrorAttribute, который является встроенной реализацией интерфейса IExceptionFilter. С его помощью можно указывать исключение, а также имена представления и компоновки, используя следующие свойства:

ExceptionType

Тип исключения, обрабатываемого данным фильтром. Он также будет обрабатывать типы исключений, унаследованные от указанного, но игнорировать все остальные. Стандартным значением является System.Exception, означающее обработку всех стандартных исключений.

View

Имя шаблона представления, визуализируемого данным фильтром. Если значение не задано, принимается стандартное значение Error, так что по умолчанию будет визуализироваться представление /Views/<Контроллер>/Error.cshtml или /Views/Shared/Error.cshtml

Master

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

Подготовка к использованию встроенного фильтра исключения

Фильтр HandleErrorAttribute работает, только если в файле Web.config разрешены специальные ошибки, что делается путем добавления в узел <system.web> файла Web.config элемента customErrors, как показано в примере ниже:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  ...
  <system.web>
    ...
    <customErrors mode="On" defaultRedirect="/Content/RangeErrorPage.html"></customErrors>
  </system.web>
</configuration>

Стандартным значением для атрибута mode является RemoteOnly, которое означает, что подключения, выполняемые из локальной машины, будут всегда приводить к получению ошибки типа "желтого экрана смерти". Это порождает проблему, т.к. IIS Express разрешает только локальные подключения. За счет установки атрибута mode в On указывается, что политика обработки ошибок должна применяться всегда вне зависимости от того, откуда происходит подключение. Атрибут defaultRedirect задает стандартную страницу содержимого, которая будет отображаться, если все остальное потерпело неудачу.

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

В примере ниже демонстрируется применение фильтра HandleErrorAttribute к контроллеру Home:

using System;
using System.Web.Mvc;
using Filter.Infrastructure;

namespace Filter.Controllers
{
    public class HomeController : Controller
    {
        // ...

        [HandleError(ExceptionType=typeof(ArgumentOutOfRangeException),
            View="RangeError")]
        public string RangeTest(int id)
        {
            // ...
        }
    }
}

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

При визуализации представления фильтр HandleErrorAttribute передает объект модели представления HandleErrorInfo, который является оболочкой вокруг исключения, предоставляющей дополнительную информацию для использования внутри представления. Свойства класса HandleErrorInfo описаны в таблице ниже:

Свойства класса HandleErrorInfo
Свойство Тип Описание
ActionName string

Возвращает имя метода действия, который сгенерировал исключение

ControllerName string

Возвращает имя контроллера, который сгенерировал исключение

Exception Exception

Возвращает объект исключения

В примере ниже демонстрируется модифицированное представление RangeError.cshtml для применения указанного объекта модели:

@model HandleErrorInfo

@{
    Layout = null;
    ViewBag.Title = "Извините, возникли проблемы!";
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Ошибка диапазона допустимых значений</title>
</head>
<body>
    <h2>Извините</h2>
    <span>
        Значение <b>@(((ArgumentOutOfRangeException)Model.Exception).ActualValue)</b>
        не входит в допустимые пределы.
    </span>
    <div>
        @Html.ActionLink("Измените это значение и попробуйте снова", "Index")
    </div>
</body>
</html>

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

Пройди тесты
Лучший чат для C# программистов