Расширенные возможности фильтров

69

В предыдущих статьях была предоставлена исчерпывающая информация, необходимая для эффективной работы с фильтрами. В нескольких последующих разделах рассматриваются расширенные средства фильтрации ASP.NET MVC Framework, которые интересны, но используются менее широко.

Фильтрация без атрибутов

Обычный способ работы с фильтрами предусматривает применение атрибутов, как неоднократно демонстрировалось в предшествующих статьях. Однако существует альтернатива. Класс Controller реализует интерфейсы IAuthenticationFilter, IAuthorizationFilter, IActionFilter, IResultFilter и IExceptionFilter. Он также предоставляет пустые виртуальные реализации всех ранее показанных методов этих интерфейсов, таких как OnAuthorization() и OnException().

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

// ...
using System.Diagnostics;

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

        private Stopwatch timer;
        public string FilterTest()
        {
            return "Это метод действия FilterTest в контроллере Home";
        }

        protected override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            timer = Stopwatch.StartNew();
        }

        protected override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            timer.Stop();
            filterContext.HttpContext.Response.Write(
                    string.Format("<div>Общее время: {0}</div>",
                        timer.Elapsed.TotalSeconds));
        }
    }
}

Фильтры из метода действия FilterTest() были удалены, поскольку они больше не требуются. Контроллер Home будет добавлять информацию профилирования к ответу для любого метода действия. На рисунке ниже показан результат запуска приложения и перехода на URL вида /Home/RangeTest/150, который направляет на метод действия RangeTest() без генерации исключения (которую мы специально добавили при рассмотрении фильтра HandleError):

Эффект от реализации методов фильтрации непосредственно в контроллере

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

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

Использование глобальных фильтров

Глобальные фильтры применяются ко всем методам действий во всех контроллерах в рамках приложения. Существует соглашение по настройке глобальных фильтров, которые создаются Visual Studio автоматически, когда используется шаблон проекта MVC, но в случае выбора шаблона Empty (Пустой) их придется настраивать вручную.

Конфигурация, охватывающая все приложение, задается в классах, добавляемых в папку App_Start. Именно по этой причине в статьях, посвященных маршрутизации, маршруты определялись в файле App_Start/RouteConfig.cs. Чтобы создать эквивалент для фильтров, в папку App_Start добавляется новый файл класса по имени FilterConfig.cs с содержимым, представленным в примере ниже:

using System.Web;
using System.Web.Mvc;

namespace Filter
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
        }
    }
}

Такое же содержимое среда Visual Studio создала бы для шаблона MVC. В классе определен статический метод по имени RegisterGlobalFilters(), который получает коллекцию глобальных фильтров, выраженную в виде объекта GlobalFilterCollection, куда можно добавлять новые фильтры.

В этом файле необходимо отметить два соглашения. Первое соглашение заключается в том, что класс FilterConfig определен в пространстве имен Filters, а не в Filters.App_Start, которое Visual Studio будет автоматически использовать при создании файла в подпапке приложения. Второе соглашение касается того, что фильтр HandleError, описанный ранее, всегда определяется как глобальный фильтр посредством вызова метода Add() на объекте GlobalFilterCollection.

Вы не обязаны настраивать фильтр HandleError глобально, однако он определяет стандартную политику обработки исключений MVC. Это приводит к тому, что в случае возникновения необработанного исключения будет визуализироваться представление /Views/Shared/Error.cshtml. Во время разработки такая стандартная политика обработки исключений отключена.

В примере ниже демонстрируется применение фильтра ProfileAll глобально с использованием того же самого вызова метода, который настраивает фильтр HandleError:

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

namespace Filter
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new ProfileAllAttribute());
        }
    }
}

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

Следующий шаг состоит в обеспечении вызова метода FilterConfig.RegisterGlobalFilters() в файле Global.asax при запуске приложения. В примере ниже показан код, добавленный в этот файл:

using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace Filter
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        }
    }
}

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

using System.Web.Mvc;

namespace Filter.Controllers
{
    public class CustomerController : Controller
    {
        public string Index()
        {
            return "Это контроллер Customer";
        }
    }
}

Это очень простой контроллер, метод действия Index() которого возвращает строку. Запустив приложение и перейдя на URL вида /Customer, можно увидеть результат применения глобального фильтра:

Эффект от применения глобального фильтра

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

Порядок выполнения фильтров

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

В примере ниже приведено содержимое файла класса по имени SimpleMessageAttribute.cs, добавленного в папку Infrastructure, в котором определен простой фильтр, предназначенный для демонстрации порядка выполнения фильтров:

using System;
using System.Web.Mvc;

namespace Filter.Infrastructure
{
    [AttributeUsageAttribute(AttributeTargets.Method, AllowMultiple = true)]
    public class SimpleMessageAttribute : FilterAttribute, IActionFilter
    {
        public string Message { get; set; }

        public void OnActionExecuting(ActionExecutingContext filterContext)
        {
            filterContext.HttpContext.Response.Write(
                string.Format("<div>[До действия: {0}]<div>", Message));
        }

        public void OnActionExecuted(ActionExecutedContext filterContext)
        {
            filterContext.HttpContext.Response.Write(
                string.Format("<div>[После действия: {0}]<div>", Message));
        }
    }
}

Этот фильтр выводит сообщение в ответ, когда вызываются методы OnActionExecuting() и OnActionExecuted(), причем часть сообщения указана с использованием свойства Message (которое устанавливается во время применения фильтра). К методу действия можно применять сразу несколько экземпляров этого фильтра, как показано в примере ниже. (Обратите внимание, что в предыдущем примере свойство AllowMultiple атрибута AttributeUsage установлено в true.)

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

namespace Filter.Controllers
{
    public class CustomerController : Controller
    {
        [SimpleMessage(Message="A")]
        [SimpleMessage(Message = "Б")]
        public string Index()
        {
            return "Это контроллер Customer";
        }
    }
}

Были созданы два фильтра одного типа, но с разными сообщениями: "A" и "Б". Можно было бы использовать два разных фильтра, но такой подход позволяет продемонстрировать возможность конфигурирования глобальных фильтров посредством свойств. После запуска приложения и перехода на URL вида /Customer будет получен результат, показанный на рисунке ниже:

Применение нескольких фильтров к одному методу действия

Инфраструктура MVC Framework выполняет фильтр "A" перед фильтром "Б", однако может случиться и наоборот. Инфраструктура MVC Framework не гарантирует какого-то определенного порядка выполнения фильтров. В большинстве ситуаций порядок значения не имеет. Но если порядок важен, можно воспользоваться свойством Order, как доказано в примере ниже:

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

namespace Filter.Controllers
{
    public class CustomerController : Controller
    {
        [SimpleMessage(Message="A", Order=2)]
        [SimpleMessage(Message = "Б", Order=1)]
        public string Index()
        {
            return "Это контроллер Customer";
        }
    }
}

Параметр Order принимает значение типа int, а ASP.NET MVC Framework выполняет фильтры в порядке по возрастанию. В примере выше фильтру "Б" для Order назначено меньшее значение, поэтому инфраструктура выполнит его первым:

Указание порядка выполнения фильтров

Обратите внимание, что методы OnActionExecuting() выполняются в указанном порядке, но методы OnActionExecuted() - в порядке, обратном указанному. Инфраструктура MVC Framework строит стек фильтров по мере их выполнения перед запуском метода действия, а затем впоследствии раскручивает его. Данное поведение раскручивания стека изменить нельзя.

Если значение для свойства Order не задано, ему присваивается стандартное значение -1. Это значит, что при смешивании фильтров, часть из которых имеет значения Order, а часть - нет, фильтры без значений Order будут выполняться первыми, поскольку они имеют наименьшее значение свойства Order.

Если множество фильтров одного и того же типа (скажем, фильтры действий) имеют одинаковое значение Order (например, 1), то MVC Framework определяет порядок выполнения на основе того, когда тот или иной фильтр применяется. Первыми выполняются глобальные фильтры, затем - фильтры, примененные к классу контроллера, а после этого - фильтры, примененные к методу действия.

Для фильтров исключений порядок выполнения меняется на противоположный. Если Фильтры исключений с одним и тем же значением Order применяются и к контроллеру, и к методу действия, фильтр на методе действия будет выполнен первым. Глобальные фильтры исключений с одним и тем же значением Order выполняются последними.

Переопределение фильтров

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

using System;
using System.Web.Mvc;

namespace Filter.Infrastructure
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
        AllowMultiple = true)]
    public class SimpleMessageAttribute : FilterAttribute, IActionFilter
    {
        // ...
    }
}

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

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

namespace Filter.Controllers
{
    [SimpleMessage(Message="A")]
    public class CustomerController : Controller
    {
        public string Index()
        {
            return "Это контроллер Customer";
        }

        [SimpleMessage(Message = "Б")]
        public string OtherMethod()
        {
            return "Это метод действия OtherMethod в контроллере Customer";
        }
    }
}

Фильтр SimpleMessage применяется к классу контроллера, следовательно, сообщение "A" будет добавлено в ответ, когда вызывается любой из методов действий. Кроме того, в класс добавлен новый метод OtherMethod(), к которому снова применяете фильтр SimpleMessage, но на этот раз с сообщением "Б". Проблема в том, что по умолчанию на метод OtherMethod() оказывают влияние оба случая применения фильтра: на уровне контроллера и на уровне метода. Чтобы посмотреть, как это работает, запустите приложение и перейдите на URL вида /Customer/OtherMethod:

Стандартное поведение фильтра

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

namespace System.Web.Http.Filters
{
    public interface IOverrideFilter
    {
        Type FiltersToOverride { get; }
    }
}

Метод FiltersToOverride() возвращает тип фильтра, который будет переопределен. В этом примере интересуют фильтры действий, поэтому в папке Infrastructure создается файл CustomOverrideActionFiltersAttribute.cs. Как показано в примере ниже, метод FiltersToOverride() реализован так, чтобы новый атрибут переопределял тип IActionFilter:

using System;
using System.Web.Mvc;
using System.Web.Mvc.Filters;

namespace Filter.Infrastructure
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
        Inherited = true, AllowMultiple = false)]
    public class CustomOverrideActionFiltersAttribute : FilterAttribute,
            IOverrideFilter
    {
        public Type FiltersToOverride
        {
            get { return typeof(IActionFilter); }
        }
    }
}

В инфраструктуре MVC Framework имеется несколько встроенных переопределений фильтров, находящихся в пространстве имен System.Web.Mvc.Filters: OverrideAuthenticationAttribute, OverrideActionFiltersAttribute и т.д. На момент написания этих строк они не работали. Причина в том, что эти классы унаследованы от Attribute, а не FilterAttribute. Проблема должна быть решена в более позднем выпуске, но пока придется создавать специальные атрибуты переопределения фильтров вроде продемонстрированного выше.

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

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

namespace Filter.Controllers
{
    [SimpleMessage(Message="A")]
    public class CustomerController : Controller
    {
        // ...

        [CustomOverrideActionFilters]
        [SimpleMessage(Message = "Б")]
        public string OtherMethod()
        {
            return "Это метод действия OtherMethod в контроллере Customer";
        }
    }
}

На рисунке ниже видно, что выполняется только атрибут SimpleMessage, который был применен непосредственно к методу OtherAction():

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