После того как фабрика контроллеров создала контроллер, производится вызов его метода Execute, который с помощью специального свойства ActionInvoker определяет необходимый метод для выполнения действия и вызывает его. По умолчанию ActionInvoker создается как экземпляр класса ControllerActionInvoker, но разработчик волен переопределить его. Переопределение ActionInvoker — это еще одна точка расширения ASP.NET MVC, которой вы можете воспользоваться для самых разнообразных целей. Например, т. к. именно ActionInvoker исполняет все необходимые фильтры типа ActionFilter, то вы вольны изменить этот механизм для того, чтобы часть фильтров не могла быть использована для ваших контроллеров в некотором гипотетическом случае.
По умолчанию метод InvokeAction класса ActionInvoker вместе с самим вызовом действия реализует механизм обработки заданных через атрибуты фильтров для действия.
Всего InvokeAction формирует четыре группы фильтров:
□ ActionFilters — вызываются во время исполнения действия;
□ ResultFilters — вызываются после исполнения действия при обработке результата;
□ AuthorizationFilters — вызываются до исполнения действия, чтобы произвести проверку доступа;
□ ExceptionFilters — вызываются во время обработки возникшего исключения.
Существуют такие действия, скорость вызова которых критически важна. В связи с этим лишние операции по поиску и исполнению фильтров типа ActionFilter могут отрицательно сказаться на производительности, даже когда никаких фильтров не используется. Поэтому создание своего варианта класса ActionInvoker и метода InvokeAction имеет смысл и может быть полезно. Вы можете реализовать InvokeAction в таком виде, в котором и производительность будет на высоте, и необходимый функционал не будет потерян.
Для примера рассмотрим листинг 4.2 с реализацией такого класса ActionInvoker, который максимально быстро вызывает действие контроллера без поддержки каких-либо атрибутов и накладных расходов на такую поддержку.
Листинг 4.2
public class FastControllerActionInvoker : ControllerActionInvoker
{
public override bool InvokeAction(
ControllerContext controllerContext, string actionName)
{
if (controllerContext == null)
throw new ArgumentNullException("controllerContext");
if (String.IsNullOrEmpty(actionName))
throw new ArgumentException("actionName");
ControllerDescriptor controllerDescriptor =
GetControllerDescriptor(controllerContext);
ActionDescriptor actionDescriptor = FindAction(controllerContext,
controllerDescriptor, actionName);
if (actionDescriptor != null)
{
IDictionary<string, object> parameters =
GetParameterValues(controllerContext, actionDescriptor);
var actionResult = InvokeActionMethod(controllerContext,
actionDescriptor, parameters);
InvokeActionResult(controllerContext, actionResult);
return true;
}
return false;
}
}
Существенное достоинство этого класса в том, что уменьшение затрат на вызов действия позволило ускорить вызов в среднем на величину от 5 до 30 % по разным оценкам проведенного нами тестирования. С другой стороны, данный вариант не поддерживает многие стандартные механизмы MVC: фильтры, обработку ошибок, проверку авторизации.
Для использования нового класса FastControllerActionInvoker нужно присвоить его экземпляр свойству ActionInvoker необходимого контроллера. Например, используем наш новый класс для контроллера AccountController стандартного проекта MVC:
public AccountController()
: this(null, null)
{
ActionInvoker = new FastControllerActionInvoker();
}
Мы рассмотрели работу механизма ControllerActionInvoker, который призван найти и выполнить необходимое действие контроллера. Одной из особенностей этого поиска является поиск установленных для действий атрибутов типа ActionMethodSelectorAttribute. Эти атрибуты имеют одно-единственное предназначение — определение того, может ли быть вызвано это действие в данном контексте запроса или нет. Рассмотрим определение класса ActionMethodSelectorAttribute:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false,
Inherited = true)]
public abstract class ActionMethodSelectorAttribute : Attribute {
public abstract bool IsValidForRequest(
ControllerContext controllerContext,
MethodInfo methodInfo);
}
Как вы видите, атрибут содержит только один метод IsValidForRequest, который возвращает булево значение, определяющее, может ли быть вызвано помеченное действие. Этот атрибут очень полезен для обеспечения безопасности, т. к. позволяет "спрятать" часть методов от любой возможности неправомерного использования.
Для удобства разработчиков MVC Framework уже реализует два атрибута, наследующих от ActionMethodSelectorAttribute:
□ AcceptVerbsAttribute — позволяет задать для действия допустимые типы HTTP-запросов из следующего списка: GET, POST, PUT, DELETE, HEAD. Запросы, отличные от указанных, будут игнорироваться;
□ NonActionAttribute — позволяет пометить метод, не являющийся действием. Такой метод невозможно будет вызвать никаким внешним запросом.
Используем эти атрибуты для нашего контроллера AdminController. Так как действия Index, Select и Delete могут быть вызваны только GET-запросами, пометим их соответствующим атрибутом, как показано в следующем фрагменте:
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Index()
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Delete(Guid? userId)
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Select(Guid? userId)
Наоборот, действие Update вызывается только POST-запросами, поэтому пометим их следующим образом:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Update(Guid? userId, string email,
bool isApproved, bool isLockedOut);
Теперь, если мы попытаемся вызвать действие Update из строки запроса браузера, набрав URL вроде такого http://localhost:5434/Admin/Update, то получим ошибку с сообщением о том, что страница не была найдена. Без атрибута AcceptVerbs метод был бы вызван.
Для демонстрации действия атрибута NonActionAttribute проведем некоторые изменения в нашем коде. Обратите внимание на то, что в методах класса ActionController повторяется следующий код:
User.IsInRole("Administrators")
Вынесем его в отдельный метод UserlsAdmin:
[NonAction]
private bool UserIsAdmin()
{
return User.IsInRole(''Administrators");
}
Обратите внимание, что мы пометили этот метод атрибутом NonAction, который указывает на то, что данный метод не является действием и не может быть выбран механизмом MVC при поиске в контроллере необходимого действия.
Когда механизм MVC находит необходимое для вызова действие, производится поиск и выполнение ряда атрибутов, которые являются производными от атрибута FilterAttribute. Такие атрибуты называются фильтрами и в основном предназначены для проверки прав вызова и безопасности контекста запроса. MVC Framework содержит ряд таких атрибутов, которые вы можете использовать в своих проектах:
□ AuthorizeAttribute — позволяет указывать ограничения для имен пользователей и ролей, которые могут вызвать данное действие;
□ HandleErrorAttribute — позволяет определять действия для обработки необработанных исключений;
□ ValidateAntiForgeryTokenAttribute — проверяет контекст запроса на соответствие указанному маркеру безопасности при получении данных из форм;
□ ValidateInputAttribute — управляет механизмом проверки запроса на небезопасные значения. Позволяет отключить такого рода проверку для случаев, когда требуется получить данные разного вида, в том числе потенциально опасные.
AuthorizeAttribute — весьма полезный атрибут, который позволяет задавать группы и пользователей, имеющих доступ к заданному действию контроллера или ко всему контроллеру сразу. В нашем примере для проверки прав на доступ к действию контроллера AdminController мы создали следующий метод:
[NonAction]
private bool UserIsAdmin()
{
return User.IsInRole("Administrators");
}
С использованием атрибута AuthorizeAttribute нужда в этом методе пропадает. Чтобы продемонстрировать действие AuthorizeAttribute, перепишем контроллер AdminController по-новому, так, как показано в листинге 4.3.
Листинг 4.3
public class AdminController : Controller
{
[AcceptVerbs(HttpVerbs.Get)]
[Authorize(Roles = "Administrators")]
public ActionResult Index()
{
MembershipProvider mp = Membership.Provider;
int userCount;
var users = mp.GetAllUsers(0, Int32.MaxValue, out userCount);
ViewData.Model = users;
return View () ;
}
[AcceptVerbs(HttpVerbs.Post)]
[Authorize(Roles = "Administrators")]
public ActionResult Select(Guid? userId)
{
if (!userId.HasValue)
throw new HttpException(404, "Пользователь не найден");
MembershipProvider mp = Membership.Provider;
MembershipUser user = mp.GetUser(userId, false);
ViewData.Model = user;
return View () ;
}
[AcceptVerbs(HttpVerbs.Post)]
[Authorize(Roles = "Administrators")]
public ActionResult Update(Guid? userId, string email,
bool isApproved, bool isLockedOut)
{
if (!userId.HasValue)
throw new HttpException(404, "Пользователь не найден");
MembershipProvider mp = Membership.Provider;
MembershipUser user = mp.GetUser(userId, false);
user.Email = email; user.IsApproved = isApproved;
if (user.IsLockedOut && !isLockedOut)
user.UnlockUser();
mp.UpdateUser(user);
return RedirectToAction("Index");
}
[AcceptVerbs(HttpVerbs.Get)]
[Authorize(Roles = "Administrators")]
public ActionResult Delete(Guid? userId)
{
if (!userId.HasValue)
throw new HttpException(404, "Пользователь не найден");
MembershipProvider mp = Membership.Provider;
MembershipUser user = mp.GetUser(userId, false);
mp.DeleteUser(user.UserName, true);
return RedirectToAction("Index");
}
}
Как вы можете заметить, мы избавились от рутинной операции проверки права доступа к действию контроллера путем задания для каждого действия атрибута [Authorize(Roles = "Administrators")]. Этот атрибут предписывает механизму MVC выполнить проверку права доступа пользователя при попытке вызвать действие нашего контроллера. Важным достоинством данного атрибута является его элегантность и унификация. Вместо того чтобы самим писать такой важный код, как код проверки прав доступа, мы оперируем механизмом метаданных в виде атрибута AuthorizeAttribute, помечая нужные нам участки кода. Так снижается возможность ошибки программиста, которая в случае работы с задачей безопасности может стоить очень дорого. Другим плюсом использования атрибута AuthorizeAttribute является заметное уменьшение кода, особенно в сложных вариантах, когда требуется предоставить доступ набору групп и пользователей.
Атрибут AutorizeAttibute принимает два параметра:
□ Roles — позволяет задавать перечисление ролей, которые имеют доступ к действию, через запятую;
□ Users — позволяет задавать перечисление пользователей, которые имеют доступ к действию, через запятую.
Так, например, следующий фрагмент кода определяет, что доступ к действию могут получить только члены группы Administrators и пользователи SuperUserl и SuperUser2:
[Authorize(Roles = "Administrators", Users = "SuperUserl, SuperUser2")]
Атрибут HandleErrorAttribute предназначен для того, чтобы однообразно сформировать механизм обработки необработанных в контроллерах исключений. Атрибут HandleErrorAttribute применим как к классу контроллера, так и к любому действию. Кроме того, допустимо указывать атрибут несколько раз. По умолчанию, без параметров, механизм MVC с помощью атрибута HandleErrorAttribute при возникновении исключения произведет переадресацию на представление Error, которое должно находиться в папке -/Views/Shared. Однако это действие можно изменить под свои потребности. Для манипулирования порядком действия атрибута HandleErrorAttribute у него есть ряд параметров:
□ ExceptionType — указывает тип исключения, на возникновение которого должен реагировать данный атрибут;
□ View — указывает представление, которое нужно показать пользователю при срабатывании атрибута;
□ Master — указывает наименование Master View, которое будет использоваться при демонстрации пользователю представления;
□ Order — указывает на последовательный номер, в порядке которого атрибут будет исполняться.
Для демонстрации работы атрибута HandleErrorAttribute создадим представление AdminError, которое будет использоваться только тогда, когда произойдет ошибка при работе с контроллером AdminController. В листинге 4.3 представлен код представления.
<%@ Page Title="" Language="C#"
MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent"
runat="server">
Ошибка! Произошло необработанное исключение.
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent"
runat="server">
<% var model = (HandleErrorlnfo)ViewData.Model; %>
<h2>Внимание</h2>
<р>При работе сайта произошла исключительная
ситуация в действии <%= model.ActionName %>
контроллера <%= model.ControllerName %>.
Ниже представлена дополнительная информация об исключении:
</p>
<p><%= model.Exception.Message %></p>
<p><%= model.Exception.StackTrace %></p>
</asp:Content>
Обратите внимание, для получения доступа к расширенной информации об исключении мы используем свойство Model объекта ViewData, предварительно приведя его к типу HandleErrorInfo. Механизм атрибута HandleErrorAttribute создает для представления элемент типа HandleErrorInfo, объект которого содержит следующие данные:
□ ActionName — имя действия, в котором произошло исключение;
□ ControllerName — имя контроллера, в котором произошло исключение;
□ Exception - объект типа Exception, в котором содержится вся информация об исключении, в том числе строка сообщения и трассировка стека.
Для того чтобы проверить наше представление, создадим для тестирования новое действие TestException в контроллере AdminController:
public ActionResult TestException()
{
throw new Exception("Проверка исключения");
}
Пометим наш контроллер AdminController атрибутом HandleErrorAttribute в следующем виде, как это показано во фрагменте:
[HandleError(View = "AdminError")]
public class AdminController : Controller
Теперь, чтобы механизм атрибута HandleErrorAttribute заработал, необходимо включить механизм Custom Errors в файле web.config так, как показано во фрагменте:
<customErrors mode="On" />
После запуска приложения и попытки доступа к действию TestException мы получим сообщение об ошибке (рис. 4.8).
Кроме перечисленного, атрибут HandleErrorAttribute имеет ряд важных особенностей:
□ если механизм Custom Errors запрещен, или исключение уже было обработано другим атрибутом, то исполнение атрибута прекращается;

□ если исключение является HTTP-исключением с кодом, отличным от 500, исполнение атрибута прекращается. Другими словами, этот атрибут не обрабатывает HTTP-исключения с любыми кодами ошибок, кроме 500;
□ атрибут устанавливает Response.TrySkipIisCustomErrors = true для того, чтобы попытаться переопределить страницы веб-сервера, настроенные для отображения ошибок;
□ атрибут устанавливает код HTTP-ответа в значение 500, которое сообщает клиенту о возникшей при запросе ошибке.
Использование атрибута HandleErrorAttribte позволяет гибко настраивать реакцию вашего приложения на возникновение исключительных ситуаций.
Вы можете определять для каждого контроллера, действия или типа исключения свои представления вывода информации об ошибке.
Проблема безопасности в современном Интернете не заканчивается вместе с проверкой права доступа на базе неких правил, ролей или пользователей.
Вместе с возможностями защиты от несанкционированного доступа изменяются и способы проникновения и взлома защиты. Одним из таких способов проникнуть через защиту сайта является атака под названием Cross-site Request Forgery Attack (CSRF). Суть этой атаки заключается в следующем:
□ сайт должен иметь авторизацию по cookies, а пользователь, через которого планируется атака, должен иметь возможность автоматической авторизации;
□ на некой специальной странице в Интернете создается элемент формы со скрытыми параметрами и автоматическим отправлением этих данных по адресу сайта, на котором зарегистрирован пользователь;
□ при посещении страницы, подготовленной злоумышленником, пользователь, сам того не предполагая, авторизуется на своем сайте и выполняет некий запрос, который может делать все, что угодно, от простого принудительного "выхода" пользователя из системы до отправки неких данных на сервер в контексте авторизованного пользователя.
Опасность такой атаки очень велика, для того чтобы защититься, существует один простой, но действенный метод. К любой форме на странице добавляется скрытое поле с неким генерируемым значением, это же значение записывается в пользовательское cookie. После отправки запроса значение поля сравнивается на сервере со значением cookie, и если значения не совпадают, считается, что производится нелегальный запрос. Злоумышленник не сможет сгенерировать те же самые коды, и поэтому его попытки провести такого рода атаку будут бесполезными.
Механизм ASP.NET MVC имеет поддержку такого рода защиты в виде атрибута ValidateAntiForgeryTokenAttribute и helper-метода Html.AntiForgeryToken. В нашем примере с контроллером AdminController есть слабое и уязвимое место — это действие Delete, которое выполняется с помощью GET-запросов и может быть использовано злоумышленником для того, чтобы преднамеренно удалять данные о пользователях. Правильно сформированные формы не должны разрешать любые модификации данных по GET-запросам. Иными словами, GET-запросы должны выполнять действия "только для чтения", а все остальные действия должны происходить через POST-запросы. Перепишем наш механизм действия Delete и добавим к нему и действию Update поддержку атрибута ValidateAntiForgeryTokenAttribute, для этого изменим разметку представления так, как показано в следующем фрагменте:
<% using (Html.BeginForm("Update", "Admin")) { %>
<%= Html.Hidden("userId", (Guid)user.ProviderUserKey) %>
<%= Html.AntiForgeryToken() %>
<fieldset>
<legend>Данные</legend>
<p>
<label for="email">Email</label>
<%=Html.TextBox("email", user.Email)%>
</Р>
<p>
<label for="isApproved"><%=Html.CheckBox("isApproved",
user.IsApproved)%>подтвержден
</label>
</p>
<p>
<label for="isLockedOut"><%=Html.CheckBox("isLockedOut",
user.IsLockeCOut)%>заблокирован
</label>
</p>
</fieldset>
<input type="submit" value="Сохранить" />
<% } %>
<% using(Html.BeginForm("Delete", "Admin")) { %>
<%= Html.Hidden("userId", (Guid)user.ProviderUserKey) %>
<%= Html.AntiForgeryToken() %>
<input type="submit" va1ue="Удалить" />
<% } %>
Как вы можете заметить, к основной форме мы добавили поле Html.AntiForgeryToken(), а вместо ссылки для удаления создали еще одну форму, которая также защищена полем Html.AntiForgeryToken().
Теперь добавим поддержку защиты в наш контроллер AdminController для действий Update и Delete, как показано во фрагменте:
[AcceptVerbs(HttpVerbs.Post)]
[Authorize(Users = "Admin")]
[ValidateAntiForgeryToken]
public ActionResult Update(Guid? userId,
string email, bool isApproved, bool isLockedOut)
[AcceptVerbs(HttpVerbs.Post)]
[Authorize(Users = "Admin")]
[ValidateAntiForgeryToken]
public ActionResult Delete(Guid? userId)
Обратите внимание, что мы ограничили доступ к нашему обновленному действию Delete только для POST-запросов. Для защиты от CSRF-атак мы добавили атрибут ValidateAntiForgeryTokenAttribute. Это все, что нам нужно сделать, чтобы защитить данные формы от несанкционированного доступа.
Для более высокого уровня безопасности атрибуту ValidateAntiForgeryTokenAttribute можно передать параметр Salt ("соль"), который представляет собой числовое значение. Параметр Salt — это дополнительный секретный ключ, который используется при формировании итогового проверяемого значения, для повышения уровня защиты.
Для более гибкой настройки helper-метод Html.AntiForgeryToken имеет ряд параметров:
□ salt — задает значение Salt, которое используется в атрибуте ValidateAntiForgeryTokenAttribute;
□ domain — задает значение параметра Domain для объекта HttpCookie, указывающее конкретный домен, с которым ассоциирован cookie;
□ path — задает значение параметра Path для объекта HttpCookie, указывающее виртуальный путь, с которым ассоциирован cookie.
После наших исправлений в контроллере AdminController не осталось действий, которые манипулируют с данными по GET-запросам, а действия с POST-запросами защищены механизмом атрибута ValidateAntiForgeryTokenAttribute.
Одной из самых уязвимых частей любого веб-сайта является пользовательский ввод. Представьте ситуацию, когда после ввода пользовательских данных, они сразу же становятся видны другим пользователям. Тогда, если не существует никакой фильтрации таких данных, злоумышленник может ввести вместо данных опасный код на JavaScript, который повредит любому, кто попытается получить доступ к вашему сайту. Для предотвращения ввода таких данных в механизм ASP.NET MVC встроена защита, которая проверяет любой запрос на наличие потенциально опасных значений параметров запроса.
По умолчанию этот механизм включен для всех запросов, и ничего дополнительного делать не нужно. Но существуют случаи, когда все же требуется получить введенные пользователем данные, даже если они могут быть опасными. Например, если вы разрабатываете редактор для записей блога, то вам, возможно, будет необходимо предоставить пользователю возможность вводить HTML-разметку вместе с содержимым записи блога. Если оставить механизм защиты ASP.NET MVC включенным, то любой запрос, параметр которого содержит теги, будет вызывать исключительную ситуацию.
ASP.NET MVC предлагает разработчику гибкий механизм управления проверкой параметров запроса. Для такого управления существует атрибут ValidateInputAttribute. Для демонстрации действия этого атрибута добавим в нашу форму редактирования параметров Select.aspx возможность редактирования поля Comment, которое будет содержать любой текст, в том числе и с HTML-разметкой. Данное поле будет выводиться вместе с отображаемым именем пользователя, играя роль сообщения для пользователя.
Изменим форму ввода, добавив следующий фрагмент кода:
<p>
<label for="comment">Комментарий</label>
<%=Html.TextArea("comment", user.Comment) %>
</p>
Соответственно изменим определение действия Update контроллера AdminController:
[AcceptVerbs(HttpVerbs.Post)]
[Authorize(Users = "Admin")]
[ValidateAntiForgeryToken]
public ActionResult Update(Guid? userId, string email,
string comment, bool isApproved, bool isLockedOut)
{
if (!userId.HasValue)
throw new HttpException(404, "Пользователь не найден");
MembershipProvider mp = Membership.Provider;
MembershipUser user = mp.GetUser(userId, false);
user.Email = email;
user.Comment = comment;
user.IsApproved = isApproved;
if (user.IsLockedOut && !isLockedOut) user.UnlockUser();
mp.UpdateUser(user);
return RedirectToAction("Index");
}
Для того чтобы пользователь видел сообщение, модифицируем частичное представление LogOnUserControl.ascx так, как показано в листинге 4.4.
Листинг 4.4. Представление LogOnUserControl.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%
if (Request.IsAuthenticated) {
%>
Welcome <b><%= Html.Encode(Page.User.Identity.Name) %></b>!
[ <%= Html.ActionLink("Log Off", "LogOff", "Account") %> ]
<p><%= Membership.GetUser(Page.User.Identity.Name).Comment %></p>
}
else {
%>
[ <%= Html.ActionLink("Log On", "LogOn", "Account") %> ]
<%
}
%>
После запуска попробуем добавить в поле комментария для любого из пользователей значение <b>добрый день! </b> и сохранить изменения. Так как механизм проверки ввода включен по умолчанию, мы получим сообщение об ошибке (рис. 4.9).

Чтобы отключить проверку ввода для данного действия, для него необходимо добавить атрибут validateinputAttribute так, как показано во фрагменте:
[AcceptVerbs(HttpVerbs.Post)]
[Authorize(Users = "Admin")]
[ValidateAntiForgeryToken]
[ValidateInput(false)]
public ActionResult Update(Guid? userId, string email,
string comment, bool isApproved, bool isLockedOut)
Атрибут ValidateInput с параметром false указывает механизму ASP.NET MVC на то, что проверка параметров запроса для данного действия не требуется. После указания этого значения атрибута мы сможем задавать для значения Comment данные с HTML-тегами. Сообщение будет выведено рядом с именем пользователя, как показано на рис. 4.10.

Механизм атрибута ValidateInputAttribute позволяет в ряде случаев предоставить пользователю больший функционал, но использовать его следует с большой осторожностью. Отключая встроенный механизм проверки параметров запроса на опасное содержимое, вы таким образом берете на себя все проверки, связанные с вопросами безопасности. ValidateInputAttribute с параметром false должен использоваться только в тех местах, где он действительно необходим, в общем случае его применение не рекомендуется и его следует избегать.
Существует еще один способ расширения механизма ASP.NET MVC. Через атрибут ActionFilterAttribute разработчик может задать выполнение кода по нескольким событиям, происходящим при работе механизма MVC. Вот следующие события, на которые можно реагировать с помощью определения атрибута ActionFilterAttribute: перед выполнением действия, после выполнения действия, перед исполнением результата и после исполнения результата.
Атрибут ActionFilterAttribute представляет собой абстрактный класс с четырьмя виртуальными методами: OnActionExecuting, OnActionExecuted, onResultExecuting, onResultExecuted. Реализация атрибута ложится на плечи разработчика. Обработка этих событий может быть полезна, например, для реализации механизма логов с целью вычисления времени исполнения действий. Другим вариантом использования может быть модификация HTTP-заголовков для ответа клиенту. Таким образом работает включенный в состав ASP.NET MVC атрибут OutputCacheAttribute.
OutputCacheAttribute предназначен для управления стандартными HTTP-заголовками, влияющими на кэширование веб-страниц браузером пользователя. Разработчикам классического ASP.NET этот механизм знаком по директиве @ OutputCache для ASPX-страниц и ASCX-компонентов. OutputCacheAttribute может быть определен как для класса контроллера, так и для отдельного действия. Для управления действием у OutputCacheAttribute есть ряд параметров:
□ Duration — значение времени в секундах, на которое производится кэширование;
□ Location — значение перечисления OutputCacheLocation, которое определяет местоположение для кэшированного содержимого: на стороне клиента или сервера. По умолчанию устанавливается значение OutputCacheLocation.Any, это означает, что содержимое может кэшироваться в любом месте;
□ Shared — булево значение, которое определяет, может ли использоваться один экземпляр кэшированного значения для многих страниц. Используется, когда действие возвращает результат в виде не целой страницы, а в виде частичного результата;
□ VaryByCustom — любой текст для управления кэшированием. Если этот текст равен browser, то кэширование будет производиться условно по имени браузера и его версии (major version). Если у VaryByCustom будет указана строка, то вы обязаны переопределить метод GetVaryByCustomString в файле Global.asax для осуществления условного кэширования;
□ varyByHeader — строка с разделенными через точку с запятой значениями HTTP-заголовков, по которым будет производиться условное кэширование;
□ varyByParam — задает условное кэширование, основанное на значениях строки запроса при GET или параметрах при POST;
□ varyByContentEncodings — указывает условие кэширования в зависимости от содержимого директивы HTTP-заголовка Accept-Encoding;
□ CacheProfile — используется для указания профиля кэширования заданного через web.config и секцию caching;
□ NoStore — принимает булево значение. Если значение равно true, то добавляет в директиву HTTP-заголовка Cache-Control параметр no-store;
□ SqlDependency — строковое значение, которое содержит набор пар строк "база данных" и "таблица", от которых зависит кэшируемое содержимое. Позволяет управлять кэшированием на основе изменений определенных таблиц в базе данных.
В качестве примера рассмотрим следующий фрагмент кода, в котором устанавливается кэширование 1800 секунд (30 минут) любого результата контроллера AdminController вне зависимости от параметров запроса:
[OutputCache(Duration = 1800, VaryByParam = ="none")]
public class AdminController : Controller
В другом фрагменте, наоборот, с помощью атрибута OutputCacheAttribute отключается любое кэширование результатов контроллера AdminController:
[OutputCache(Location = OutputCacheLocation.None)]
public class AdminController : Controller
В своей работе атрибут OutputCacheAttribute переопределяет метод OnResultExecuting, который вызывается перед исполнением результата действия, когда результат типа ActionResult преобразуется в ответ на запрос пользователя, например в HTML-страницу. Вы можете создать свои варианты реализации ActionFilterAttribute, реализовав атрибут, переопределяющий ActionFilterAttribute. Для демонстрации подобной реализации создадим атрибут, который реализует сжатие результирующих страниц с помощью GZip-сжатия, которое поддерживается всеми современными браузерами.
В листинге 4.5 представлен код атрибута GZipCompressAttribute, который реализует механизм сжатия результата действия через GZip.
Листинг 4.5
using System;
using System.IO.Compression;
using System.Web.Mvc;
namespace MVCBookProject {
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
Inherited = true, AllowMultiple = false)]
public class GZipCompress : ActionFilterAttribute {
public override void OnActionExecuting(
ActionExecutingContext filterContext)
{
string acceptEncoding = filterContext.HttpContext.Request
.Headers["Accept-Encoding"];
if (string.IsNullOrEmpty(acceptEncoding)) return;
var response = filterContext.HttpContext.Response;
if (acceptEncoding.ToLower().Contains("gzip"))
{
response.AppendHeader("Content-encoding", "gzip");
response.Filter = new GZipStream(
response.Filter, CompressionMode.Compress);
}
}
}
}
Обратите внимание, что атрибут GZipCompressAttribute наследует от ActionFilterAttribute и реализует метод OnActionExecuting, в котором С Помощью класса GZipStream производится сжатие. Использование нашего атрибута тривиально, например, применяем его для контроллера AdminController так, как показано во фрагменте кода:
[GZipCompress]
public class AdminController : Controller
Реализация своего варианта ActionFilterAttribute — это очень мощное средство для расширения механизма ASP.NET MVC. Благодаря ему, мы реализовали прозрачное и простое средство для GZip-сжатия ответов для клиента. Другим стандартным средством, которое использует ActionFilterAttribute, является атрибут outputcacheAttribute, который позволяет управлять кэшированием результатов действий контроллера.
ActionResult — это базовый класс, экземпляр которого возвращает любое действие контроллера в ASP.NET MVC. В MVC существует несколько стандартных реализаций класса ActionResult: ViewResult, JsonResult, FileResult, RedirectResult, RedirectToRouteResult, ContentResult, EmptyResult. Их назначение и тип возвращаемых данных перечислены в табл. 4.1.

Когда действие завершает свое выполнение, оно возвращает результат в виде базового класса ActionResult или его наследников. После этого механизм MVC вызывает у возвращенного результата стандартный метод ExecuteResult, который и формирует результат, получаемый клиентом.
ViewResult — это стандартный и самый используемый на практике результат, наследующий тип ActionResult, который возвращается действиями контроллеров. Назначение ViewResult — это определение представления, которое будет использовано механизмом MVC для представления состояния модели.
У ViewResult и базового класса ViewResultBase, от которого ViewResult унаследован, есть ряд параметров:
□ ViewData — хранилище данных модели, которые используются представлением для отображения результата работы действия;
□ TempData — аналогичное viewData хранилище данных модели, но с существенным отличием, которое позволяет данным храниться после перенаправления запроса на другое действие;
□ viewName — имя представления, которое должно отреагировать на изменение модели контроллером. Иными словами, этот параметр указывает механизму MVC, какое представление нужно использовать для отображения результата работы действия;
□ MasterName — имя master-представления, которое должно быть использовано для отображения результата работы действия;
□ view — экземпляр представления, которое должно быть использовано для отображения результата работы действия. Может быть использовано вместо параметра viewName для определения представления.
Обычно для возвращения результата типа viewResult из действия используется стандартный метод контроллера view, который принимает те же параметры, что и viewResult. Рассмотрим пример вызова метода view:
public ActionResult Select(Guid? userid)
{
MembershipProvider mp = Membership.Provider;
MembershipUser user = mp.GetUser(userId, false);
return view("Select", "Site", user);
}
В приведенном фрагменте действие Select возвращает результат типа viewResult, который формируется стандартным методом контроллера view. В данном случае метод view принимает три параметра: имя представления Select, имя master-представления Site и модель данных user.
JsonResult — это стандартная возможность механизма MVC возвращать результат на запрос пользователя в виде JSON-данных. JSON — это формат данных, название которого расшифровывается как JavaScript Object Notation или объектная нотация JavaScript. Хотя в названии присутствует слово JavaScript, формат данных JSON языконезависимый и может быть использован при разработке на любом языке.
JSON является альтернативой другому формату данных — XML, но по сравнению с XML JSON более короткий, поэтому JSON получил распространение при совместном использовании с механизмом Ajax, когда размер передаваемых данных может иметь большое значение.
Класс JsonResult содержит несколько свойств, для более гибкой настройки возвращаемого результата:
□ ContentEncoding — устанавливает значение HTTP-параметра ContentEncoding, который определяет кодировку возвращаемого результата;
□ ContentType — устанавливает значение HTTP-параметра ContentType, если не указано, то по умолчанию устанавливается в application/json;
□ Data — любые данные, которые могут быть сериализованы в формат JSON с помощью класса JavaScriptSerializer.
Использовать JsonResult для возвращения результата в виде JSON-данных очень просто, для этого в контроллере существует стандартный метод Json, который принимает все параметры JsonResult и возвращает готовый результат. Рассмотрим пример действия для контроллера AdminController, которое возвращает JSON-данные по запросу с параметром имени пользователя:
public JsonResult SelectUserData(string userName)
{
if (string.IsNullOrEmpty(userName))
throw new HttpException(404, "Пользователь не найден");
MembershipProvider mp = Membership.Provider;
MembershipUser user = mp.GetUser(userName, false);
UserData userData = new UserData()
{
Comment = user.Comment,
Email = user.Email,
IsApproved = user.IsApproved,
IsLockedOut = user.IsLockedOut
};
return Json(userData, null, Encoding.UTF8);
}
В представленном фрагменте кода для передачи набора данных о пользователе в виде JSON-данных используется единственный метод Json, которому передается набор данных. Результатом, который получит пользователь в ответ, например, на такой запрос http://localhost:5434/Admin/SelectUserData?userName=admin будет текст в следующем формате:
{"UserId":null,"Email":"vyunev@live.ru","Comment":"","IsApproved":true, "IsLockedOut":false,"CurrentMembershipUser":null}
Очень часто в ответ на запрос пользователя требуется вернуть не HTML-страницу или данные в формате JSON, а какой-нибудь бинарный файл. FileResult — это механизм, который как раз и позволяет возвратить файл как результат работы действия контроллера.
У FileResult есть два важных свойства, которые требуется указывать при возвращении результата действия:
□ contentType — свойство, которое задается через конструктор класса FileResult и не может быть изменено напрямую. ContentType указывает MIME-тип содержимого передаваемого файла;
□ FileDownloadName — свойство, указывающее на файл, который требуется передать в ответ на запрос.
Рассмотрим использование FileResult на следующем примере. Пусть нам требуется на пользовательский запрос возвращать сопоставленный с пользователем рисунок. Реализуем эту возможность с помощью файловой системы. Для этого создадим в корне проекта папку Admin, в которой будем хранить рисунки пользователей в формате PNG с именем вида: GUID пользователя.рng. Действие GetUserImage контроллера AdminController, которое будет возвращать изображение с помощью FileResult, представлено в следующем фрагменте:
public ActionResult GetUserImage(string userName)
{
if (string.IsNullOrEmpty(userName))
throw new HttpException(404, "Пользователь не найден");
MembershipProvider mp = Membership.Provider;
MembershipUser user = mp.GetUser(userName, false);
if (user == null)
throw new HttpException(404, "Пользователь не найден");
string userGuidString = ((Guid) user.ProviderUserKey).ToString();
string fileName = userGuidString + ".png";
return File(fileName, "image/png");
}
Обратите внимание, что для возвращения результата типа FileResult в примере используется стандартный метод контроллера File, который упрощает возврат результата в виде FileResult. Методу File передается два параметра: путь к возвращаемому файлу и его MIME-тип, который в данном случае для PNG-файла равен image/png.
В MVC Framework существует еще один класс для работы с файлами — класс FileContentResult, который наследует от FileResult и позволяет возвращать данные не на основании пути к файлу, а с помощью существующего потока данных, который может генерироваться в самом действии.
Важным свойством MVC Framework является возможность перенаправлять запрос на другие действия контроллеров либо другие URL-адреса. Для этого в MVC встроены механизмы RedirectResult и RedirectToRouteResult, которые наследуют от ActionResult и являются допустимыми результатами работы любого действия.
RedirectResult предназначен для того, чтобы возвратить результат пользователю в виде перенаправления на заданный адрес URL. У RedirectResult есть только одно свойство, которое инициализируется через конструктор, — Url, оно указывает строку адреса, на которую будет перенаправлен пользователь в ответ на запрос. Контроллеры MVC содержат стандартный метод Redirect, который формирует ответ в виде RedirectResult. В следующем фрагменте приведено действие, результатом которого является перенаправление пользователя на сайт http://www.asp.net/mvc/:
public ActionResult GetAspNetSite()
{
return Redirect("http://www.asp.net/mvc/");
}
RedirectToRouteResult выполняет схожую по смыслу с RedirectResult логику, но перенаправление вызова RedirectToRouteResult производится только на основании маршрутов таблицы маршрутизации. RedirectToRouteResult имеет два конструктора, с разным числом параметров, всего параметров два:
□ routeName — указывает наименование маршрута, на который нужно выполнить перенаправление запроса;
□ routeValues — указывает набор значений параметров маршрута типа RouteValueDictionary, с помощью которых производится поиск маршрута и выполняется перенаправление.
Для упрощения работы с RedirectToRouteResult механизм MVC реализует для контроллеров, наряду с методами RedirectToRoute, набор стандартных методов RedirectToAction, которые призваны облегчить формирование перенаправления вызова на другие действия или контроллеры. Например, следующий фрагмент кода перенаправляет вызов из текущего действия в действие Index текущего контроллера:
return RedirectToAction("Index");
При использовании RedirectToAction можно указывать и контроллер, в который требуется перенаправить вызов, кроме того, можно указать набор значений параметров маршрута типа RouteValueDictionary. Следующий пример кода перенаправит вызов на действие Index контроллера AccountController:
return RedirectToAction("Index", "Account");
ContentResult — это весьма простая реализация ActionResult, которая предназначена для того, чтобы в ответ на запрос передавать любой пользовательский строковый набор данных. Для реализации логики у ContentResult есть три свойства:
□ ContentType — MIME-тип передаваемых в ответ на запрос данных;
□ ContentEncoding — кодировка данных;
□ Content — строка данных для передачи в ответ на запрос.
Благодаря ContentResult разработчик получает возможность генерировать ответы на запросы в любом виде, который можно представить в виде строки текста. Этот тип ActionResult может быть полезен при работе с механизмом RenderAction. RenderAction — это часть библиотеки MVCContrib, которая содержит расширения MVC Framework, не вошедшие в основной функционал. RenderAction позволяет представлению вывести в месте вызова результат выполнения действия. При таком применении результат типа ContentResult подходит более всего. Для упрощения контроллеры содержат специальный метод Content, который возвращает значение типа ContentResult.
Последний из рассмотренных стандартных вариантов ActionResult — это EmptyResult. Этот механизм предназначен для того, чтобы в ответ на запрос не возвращать ничего. Переопределенный в EmptyResult метод ExecuteResult не содержит ни строчки кода.
Важной особенностью механизма ActionResult является то, что вы можете создать свой собственный вариант, который будет формировать результат в том виде, который вам нужен. Например, вы можете разработать класс, наследующий ActionResult, который будет возвращать клиентам результаты запроса в виде XML-разметки. Классическим примером создания своего варианта ActionResult является реализация класса, который на запрос пользователя создает ответ в виде RSS-ленты. Продемонстрируем реализацию такого класса, добавив к нашему контроллеру AdminController действие Rss, которое будет возвращать пользователю RSS-ленту со списком зарегистрированных пользователей.
Первым делом создадим класс RssResult, который наследует ActionResult, как показано в листинге 4.6.
Листинг 4.6. Класс RssResult
namespace MVCBookProject {
using System.Web.Mvc; using System.Xml;
using System.ServiceModel.Syndication;
public class RssResult : ActionResult {
public SyndicationFeed Feed { get; set; }
public RssResult(SyndicationFeed feed)
{
Feed = feed;
}
public override void ExecuteResult(ControllerContext context)
{
context.HttpContext.Response.ContentType =
"application/rss+xml";
Rss20FeedFormatter formatter = new Rss20FeedFormatter(Feed);
using (XmlWriter writer =
XmlWriter.Create(context.HttpContext.Response.Output))
{
if (writer != null)
formatter.WriteTo(writer);
}
}
}
}
Обратите внимание, для реализации своего варианта ActionResult в классе RssResult мы перегружаем метод ExecuteResult, который и выполняет всю необходимую логику по формированию того результата, который получит клиент в своем браузере в ответ на запрос.
Использование класса RssResult ничем не отличается от применения других вариантов классов ActionResult. Добавим действие Rss в контроллер AdminControiier так, как показано во фрагменте:
[AcceptVerbs(HttpVerbs.Get)]
public RssResult Rss()
{
MembershipProvider mp = Membership.Provider;
int userCount;
var users = mp.GetAllUsers(0, Int32.MaxValue, out userCount);
List<SyndicationItem> items = new List<SyndicationItem>();
if (userCount > 0)
{
string bodyTemplate = @"email: {0}, comment: {1},
last activity: {2}, is locked: {3}, is approved: {4}";
foreach (MembershipUser item in users)
{
string body = String.Format(bodyTemplate, item.Email,
item. Comment, item. LastActivityDate,
item.IsLockedOut, item.IsApproved);
items.Add(new SyndicationItem(item.UserName, body, null));
}
}
SyndicationFeed feed = new SyndicationFeed("Cписок пользователей",
"http://localhost/rss", Request.Url, items);
return new RssResult(feed);
}
Обратите внимание, что это действие возвращает результат в виде экземпляра класса RssResult, которому передается сгенерированный RSS-поток. После того как мы реализовали RssResult и действие Rss, можно попытаться запросить результат этого действия через браузер, перейдя по относительной ссылке /Admin/Rss. В итоге вы должны получить результат в виде RSS-потока, похожий на тот, который изображен на рис. 4.11.
Создание своих вариантов ActionResult — это исключительно мощное средство для расширения базовой функциональности MVC Framework. Реализуя свои экземпляры ActionResult, вы сможете генерировать ответ на клиентский запрос в любой форме с любой структурой данных.
