Самая крупная проблема, с которой сталкиваются сегодня многие архитекторы ПО, заключается в том, как проектировать и реализовать приложение, способное соответствовать всем требованиям не только версии 1, но и всех последующих версий. Удобство в сопровождении было одним из фундаментальных атрибутов проектирования ПО еще со времен первого чернового варианта документа ISO/IEC 9126, появившегося аж в 1991 г. (В этом документе дано формальное описание качества ПО, определяемого как набор первичных и вторичных характеристик, одной из которых является удобство в сопровождении. PDF-версию этого документа можно получить на сайте iso.org.)
Возможность обслуживать нынешние и будущие нужды заказчика, конечно же, не является неким новым требованием для любой части ПО. Однако, что требуется сегодня от многих веб-приложений, — краткосрочная форма сопровождения и простота небольших изменений. Заказчики зачастую не хотят включения новых функций или другой реализации существующей функции. Им просто нужно, чтобы вы добавляли, заменяли, настраивали или удаляли небольшие части функциональности. Типичный пример — запуск специфических рекламных кампаний веб-сайтами с большими аудиториями. Общее поведение сайта не меняется, но наряду с существующими операциями должны выполняться дополнительные. Более того, эти изменения обычно носят временный характер. Через несколько недель их нужно убрать, а потом через несколько месяцев вновь включить, но сконфигурировать иначе и т. д. То есть требуется программировать любую функциональность комбинированием небольших блоков кода, отслеживать зависимости, не оказывая сильного влияния на исходный код, и двигаться в направлении аспектно-ориентированной архитектуры ПО. Таковы некоторые из основных причин быстрого принятия инфраструктур с инверсией управления (Inversion of Control, IoC) во многих корпоративных проектах.
Так о чем же эта статья? Она не будет скучной лекцией о том, как меняется сегодня ПО. Вместо этого мы займемся глубоким исследованием мощного механизма контроллеров в ASP.NET MVC, который может существенно помочь вам в построении аспектно-ориентированных веб-решений: фильтров операций ASP.NET MVC.
Что же такое фильтр операции?
Фильтр операции (action filter) — это атрибут, который, будучи связанным с классом или методом контроллера, позволяет декларативно подключать к запрошенной операции некое поведение. Написав фильтр операции, вы можете подключить исполняющий конвейер метода операции (action method) и адаптировать его под свои потребности. Благодаря этому вы также можете выделить из класса контроллера любую логику, которая непосредственно к нему не относится. Тем самым вы сделаете данное поведение повторно используемым и, что важнее, необязательным. Фильтры операций идеально подходят для реализации комплексов дополнительной обработки, влияющих на работу контроллеров.
ASP.NET MVC поставляется с несколькими предопределенными фильтрами, например HandleError, Authorize и OutputCache. HandleError позволяет перехватывать исключения, генерируемые при выполнении методов в целевом классе контроллера. Программный интерфейс атрибута HandleError дает возможность сопоставлять представление об ошибке с данным типом исключения.
Атрибут Authorize блокирует выполнение метода при попытках обращения к нему неавторизованных пользователей. Однако он не делает различий между пользователями, которые просто пока не вошли в систему, и зарегистрированными пользователями, которым не хватает разрешений для выполнения данной операции. В конфигурации этого атрибута можно указать любые роли, необходимые для выполнения операции.
Атрибут OutputCache обеспечивает кеширование в течение указанного времени ответа метода контроллера и нужного списка параметров.
Класс фильтра операции реализует несколько интерфейсов. Полный список интерфейсов приведен в табл. 1.
Табл. 1. Интерфейсы фильтра операции
Интерфейсы | Описание |
IActionFilter | Методы этого интерфейса вызываются до и после выполнения метода контроллера |
IAuthorizationFilter | Методы этого интерфейса вызываются до выполнения метода контроллера |
IExceptionFilter | Методы этого интерфейса вызываются всякий раз, когда при выполнении метода контроллера генерируется исключение |
IResultFilter | Методы этого интерфейса вызываются до и после обработки результата операции |
В наиболее распространенных случаях вас в основном интересуют IActionFilter и IResultFilter. Давайте рассмотрим их подробнее. Вот определение интерфейса IActionFilter:
public interface IActionFilter
{
void OnActionExecuted(ActionExecutedContext filterContext);
void OnActionExecuting(ActionExecutingContext filterContext);
}
Вы реализуете метод OnActionExecuting для выполнения своего кода перед тем, как будет запущена операция контроллера, и метод OnActionExecuted для постобработки состояния контроллера, определенного методом. Объекты контекста предоставляют массу информации периода выполнения. Вот сигнатура ActionExecutingContext:
public class ActionExecutingContext : ControllerContext
{
public ActionDescriptor ActionDescriptor { get; set; }
public ActionResult Result { get; set; }
public IDictionary<string, object> ActionParameters { get; set; }
}
В частности, дескриптор операции сообщает сведения о методе операции, такие как его имя, контроллер, параметры, атрибуты и дополнительные фильтры. Сигнатура ActionExecutedContext отличается лишь в деталях:
public class ActionExecutedContext : ControllerContext
{
public ActionDescriptor ActionDescriptor { get; set; }
public ActionResult Result { get; set; }
public bool Canceled { get; set; }
public Exception Exception { get; set; }
public bool ExceptionHandled { get; set; }
}
В дополнение к ссылке на описание и результат операции класс предоставляет информацию об исключении, которое может произойти, и содержит два булевых флага, заслуживающих более пристального внимания. Флаг ExceptionHandled указывает, что ваш фильтр операции получил шанс отметить произошедшее исключение как обработанное. Флаг Canceled относится к свойству Result класса ActionExecutingContext.
Заметьте, что свойство Result класса ActionExecutingContext существует только для того, чтобы перекладывать бремя генерации любого ответа с метода контроллера на список зарегистрированных фильтров операции. Если какой-либо фильтр операции присваивает значение свойству Result, целевой метод класса контроллера никогда не вызывается. Тем самым вы обходите целевой метод, перекладывая создание ответа исключительно на фильтры. Однако, если для метода контроллера зарегистрировано несколько фильтров операции, все они совместно используют один результат операции. Когда фильтр устанавливает результат, все последующие фильтры в цепочке будут получать объект ActionExecuteContext со свойством Canceled, равным true. Устанавливаете вы свойство Canceled программно на этапе после выполнения операции или задаете свойство Result на этапе выполнения операции, целевой метод никогда не будет вызываться.
Написание фильтров операции
Как упоминалось, когда дело доходит до написания собственных фильтров, вас в основном интересуют фильтры, которые обеспечивают предварительную и постобработку результата операции, а также фильтры, срабатывающие до и после выполнения обычного метода контроллера. Класс фильтра операции обычно наследуется от ActionFilterAttribute:
public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter
{
public virtual void OnActionExecuted(ActionExecutedContext filterContext);
public virtual void OnActionExecuting(ActionExecutingContext filterContext);
public virtual void OnResultExecuted(ResultExecutedContext filterContext);
public virtual void OnResultExecuting(ResultExecutingContext filterContext);
}
OnActionExecuted переопределяется, чтобы добавить собственный код в процесс выполнения метода, а OnActionExecuting переопределяется как предусловие выполнения целевого метода. Наконец, OnResultExecuting и OnResultExecuted переопределяются, чтобы поместить свой код во внутреннюю стадию, управляющую генерацией ответа метода.
На рис. 2 показан пример фильтра операции, сжимающего ответ метода, к которому он применяется.
Рис. 2. Пример фильтра операции для сжатия ответа метода
public class CompressAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(
ActionExecutingContext filterContext)
{
// Analyze the list of acceptable encodings
var preferred = GetPreferredEncoding(
filterContext.HttpContext.Request);
// Compress the response accordingly
var response = filterContext.HttpContext.Response;
response.AppendHeader("Content-encoding", preferred.ToString());
if (preferredEncoding == CompressionScheme.Gzip)
{
response.Filter = new GZipStream(
response.Filter, CompressionMode.Compress);
}
if (preferredEncoding == CompressionScheme.Deflate)
{
response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
}
return;
}
static CompressionScheme GetPreferredEncoding(HttpRequestBase request)
{
var acceptableEncoding = request.Headers["Accept-Encoding"];
acceptableEncoding = SortEncodings(acceptableEncoding);
// Get the preferred encoding format
if (acceptableEncoding.Contains("gzip"))
return CompressionScheme.Gzip;
if (acceptableEncoding.Contains("deflate"))
return CompressionScheme.Deflate;
return CompressionScheme.Identity;
}
static String SortEncodings(string header)
{
// Omitted for brevity
}
}
В ASP.NET сжатие обычно достигается регистрацией HTTP-модуля, который перехватывает любые запросы и сжимает ответы на них. В качестве альтернативы можно включить сжатие на уровне IIS. ASP.NET MVC поддерживает оба варианта и предлагает третий: управление сжатием индивидуально для каждого метода. Тем самым вы можете управлять уровнем сжатия для конкретного URL, обходясь без написания, регистрации и сопровождения HTTP-модуля.
Как видно на рис. 2, фильтр переопределяет метод OnActionExecuting. Это может поначалу показаться странным, так как вы могли ожидать, что о сжатии следует заботиться перед самым возвратом ответа. Сжатие реализуется через свойство Filter встроенного объекта HttpResponse. Любой ответ, создаваемый исполняющей средой, возвращается клиентскому браузеру через объект HttpResponse. Соответственно, любые собственные потоки данных, подключаемые к потоку вывода по умолчанию через свойство Filter, могут изменять передаваемый вывод. Таким образом, в методе OnActionExecuting вы просто создаете дополнительные потоки поверх потока вывода по умолчанию.
Однако, когда дело доходит до HTTP-сжатия, самое трудное — тщательно учесть предпочтения браузера. Браузер посылает свои предпочтения применительно к сжатию в заголовке Accept-Encoding. Содержимое этого заголовка указывает, что браузер может обрабатывать лишь определенные способы кодирования — обычно gzip и deflate. Чтобы соблюсти корректность, ваш фильтр должен попытаться точно выяснить, на что способен браузер. А это может оказаться хитроумной задачей. Роль заголовка Accept-Encoding полностью описывается в RFC 2616 (w3.org/Protocols/rfc2616/rfc2616-sec14.html). Но, если вкратце, содержимое заголовка Accept-Encoding может включать параметр q, который указывает приоритетное значение из допустимого набора. Например, если все из показанных ниже строк являются допустимыми значениями для кодирования, то лишь в первом случае gzip будет считаться приоритетным выбором:
gzip, deflate
gzip;q=.7,deflate
gzip;q=.5,deflate;q=.5,identity
В фильтре сжатия нужно учитывать это так, как сделано в фильтре на рис. 2. Эти детали еще раз подчеркивают ту мысль, что при написании фильтра операции вы вмешиваетесь в обработку запроса. А значит, что бы вы ни делали, это должно быть согласовано с ожиданиями клиентского браузера.
Применение фильтра операции
Как упоминалось, фильтр операции — это атрибут, применяемый к индивидуальным методам или ко всему родительскому классу. Вот пример его подготовки:
[Compress]
public ActionResult List()
{
// Some code here
...
}
Если класс атрибута содержит какие-то открытые свойства, вы можете декларативно присваивать им значения, используя знакомую нотацию атрибутов:
[Compress(Level=1)]
public ActionResult List()
{
...
}
На рис. 3 показан сжатый ответ в том виде, в каком он отображается в Firebug.
Рис. 3. Сжатый ответ, полученный через атрибут Compress
Однако атрибут — это статический способ настройки метода. А значит, вам нужен второй этап компиляции для применения дополнительных изменений. Тем не менее фильтры операций, выраженные в форме атрибутов, дают одно важное преимущество: они избавляют основной метод операции от комплекса дополнительной обработки.
Более широкий взгляд на фильтры операций
Чтобы оценить реальную мощь фильтров операций, подумайте о приложениях, где в течении срока их жизни приходится выполнять огромный объем работ по подстройке функциональности, и о приложениях, требующих адаптации при установке в разных организациях-заказчиках.
Представим, к примеру, веб-сайт, где в некий момент стартует рекламная кампания. Ее смысл в том, что каждый зарегистрированный пользователь может получить премиальные баллы, если он выполняет какую-нибудь стандартную операцию на сайте (покупает товары, отвечает на вопросы, участвует в чате, ведет блог и т. д.). Как разработчику вам, вероятно, потребуется какой-то код, запускаемый после обычного метода выполнения транзакции, публикации комментария или начала новой ветки в чате. К сожалению, этот код непостоянен и едва ли может быть частью исходной реализации основного метода операции. С помощью фильтров операций вы можете создать различные компоненты для каждого необходимого варианта и, например, подготовить фильтр операции для начисления премиальных баллов. Затем вы подключаете этот фильтр к любым методам, где требуется постобработка, и выполняете перекомпиляцию:
[Reward(Points=100)]
public ActionResult Post(String post)
{
// Core logic for posting to a blog
...
}
Как упоминалось, атрибуты являются статическими и требуют дополнительной стадии компиляции. Хотя это может оказаться нежелательным в ряде случаев (скажем, на сайтах с часто изменяемой функциональностью), лучше так, чем никак. По крайней мере вы получаете возможность быстро обновлять веб-решения с минимальным влиянием на существующие функции.
Динамическая загрузка
В этой статье фильтры операций были продемонстрированы в контексте методов операций контроллера. Я показал канонический подход к написанию фильтров как атрибутов, позволяющих статически помечать методы операций. Но возникает вопрос, а нельзя ли загружать фильтры операций динамически?
Инфраструктура ASP.NET MVC представляет собой отлично написанный (и большой) кусок кода, поэтому она предоставляет ряд интерфейсов и переопределяемых методов, с помощью которых вы можете настраивать почти все аспекты этой инфраструктуры. К счастью, эта тенденция проявится еще сильнее в предстоящей третьей версии Model-View-Controller (MVC). Согласно опубликованной «дорожной карте», одна из целей группы разработки MVC 3 — поддержка внедрения зависимостей на всех уровнях. Поэтому ответ на предыдущий вопрос насчет динамической загрузки зависит от появления в инфраструктуре MVC поддержки внедрения зависимостей. Возможная стратегия — настройка механизма запуска операций на получение доступа к списку фильтров до выполнения метода. Поскольку список фильтров выглядит как обыкновенный объект-набор для чтения и записи, трудностей с его динамическим заполнением возникнуть не должно. Но это уже тема для новой статьи.