Как бы смешным не казалось, но эту статью можно начать философским вбросом про смысл жизни, куда мы движемся, и то, что движемся по спирали, и все события повторяются. Ведь посудите сами – у всего есть жизненный цикл, многие процессы имеют некую "циклическую" составляющую. И я хочу вам, мои друзья, рассказать про тот самый смысл жизни и типичную жизнь тривиального запроса в ASP.NET MVC приложении. Основой классического ASP.NET MVC приложения является IIS он и является той самой отправной точкой на длинном пути запроса "от появления на сервере до отправки ответа". Картина прихожей для запросов выглядит следующим образом:
Подробное описание устройства и взаимодействия всех частей цепочки описано в статье на хабрахабре. Отдельно хочу отметить список источников, приведенный в статье. Цель данной статьи рассказать, что именно происходит между 6м и 7м шагом на данной схеме, рассказать про устройство и всю цепочку работы ASP.NET MVC приложения. Путешествие с запросом происходит по следующей цепочке действий:
Здесь видно два "острова", которые соединены мостами. Один из них — это платформа HTTP приложения, если предметнее: открыв Global.asax, мы увидим, что ключевой класс, где происходит конфигурация нашего приложения, наследуется от HttpApllication (System.Web). HttpApplication и дает базу для нашего приложения, в том числе и события из списка выше, которые можно использовать из Global.asax.cs, либо подключаться к событиям, реализуя свой HTTP модуль (IHttpModule). Пример всех этих точек расширения HttpApplication можно посмотреть тут - исходный код. Используя данный пример, можно в режиме отладки пройти все шаги один за одним и проверить правильность порядка и существование точки расширения в принципе. Уровень HTTP приложения и интеграция его с MVC приложением будет рассмотрен в отдельной статье. В данной статье будет рассмотрен исключительно второй "остров" нашей карты, с его остановками в виде маршрутизации (routing), инициализации контроллера (controller initialization), выполнением действия контроллера (action excecution) и выполнением результата действия (result execution).
MVC цикл
Ранее был представлен общий план цикла запроса. Более детальный цикл уровня MVC приложения выглядит следующим образом:
Routing (Маршрутизация)
Первой остановкой запроса является подсистема маршрутизации, ведь необходимо определить, какой цели должен достигнуть наш запрос, какое действие и какого контроллера является для него желанным, а может быть запросу вовсе нужен статический файл, а не контроллер, в любом случае необходимо получить некую пару "ключ – значение" для маршрута и единицы, которая будет его обрабатывать дальше. Эта единица называется "обработчик запроса" (route handler) и ее задача вернуть объект HttpHandler, который будет выполнять всю дальнейшую цепочку обработки запроса. Неопытный программист задастся вопросом "какой route handler? Ведь мы при старте приложения просто указываем общий шаблон в текстовом варианте и все. Там нет указания каких-либо route handler, только имя контроллера и действия по умолчанию". И здесь тот момент, когда оптимизации и улучшения сыграли злую шутку с программистом и он не знает внутренних тонкостей из-за того, что их скрыли от него. Пример объявления маршрутизации по умолчанию:
// пример регистрации маршрута по умолчанию
routes.MapRoute(name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });
Все красиво, все просто: передаем имя маршрута, шаблон url запроса, анонимный объект, который указывает названия контроллера и действия по умолчанию. Но MapRoute – это расширяющий метод, который скрывает за собой реальную логику добавления маршрута в таблицу маршрутизации.
//метод расширения для добавления маршрута в таблицу
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces)
{
if (routes == null)
{
throw new ArgumentNullException("routes");
}
if (url == null)
{
throw new ArgumentNullException("url");
}
Route route = new Route(url, new MvcRouteHandler())
{
Defaults = CreateRouteValueDictionaryUncached(defaults),
Constraints = CreateRouteValueDictionaryUncached(constraints),
DataTokens = new RouteValueDictionary()
};
ConstraintValidation.Validate(route);
if ((namespaces != null) && (namespaces.Length > 0))
{
route.DataTokens[RouteDataTokenKeys.Namespaces] = namespaces;
}
routes.Add(name, route);
return route;
}
Пример добавления маршрута в таблицу маршрутизации без использования расширяющих методов:
//Добавление маршрута в таблицу маршрутизации без расширяющих методов
var myRoute = new Route("{controller}/{action}/{id}",
new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" }, { "id", "1" } },
new MvcRouteHandler());
routes.Add(myRoute);
Вот тут уже более наглядно будет видно, что за route handler и как они вообще попадают в маршрутизацию. Сам класс обработчика маршрута реализует интерфейс IRouteHandler у которого есть единственный метод GetHttpHandler, возвращающий HttpHandler. HttpHandler – это тот объект который берет на себя ответственность за генерацию ответа на HTTP запрос, класс этого объекта реализует интерфейс IHttpHandler. Этот интерфейс содержит свойство IsReusable и метод ProcessRequest. IsReusable - флаг, указывающий может ли другой запрос использовать этот же обработчик, иными словами нужно ли ему создавать каждый раз новый экземпляр или можно создать один раз и использовать его для всех последующих запросов. ProcessRequest – метод, который выполняет обработку запроса. По умолчанию используется MvcRouteHandler и MvcHandler. Исходный код открывает все тонкости и оптимизации, которые заложили архитекторы. В реальной жизни очень мала вероятность , что придется создавать собственный HttpHandler, так как стандартные средства ASP.NET MVC покрывают большинство задач. Приведу пример маленького HttpHandler`а, который поздоровается с вами:
//Пример обработчика
public class HelloHandler : IHttpHandler
{
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
context.Response.Write("<p>Hello world</p>");
}
}
Получить данный обработчик мы можем только из RouteHandler, поэтому сделаем его и зарегистрируем по адресу "home/hello".
//Обработчик маршрута
public class HelloRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new HelloHandler();
}
}
//Регистрация обработчика маршрута на определенный урл
routes.Add(new Route("home/hello", new HelloRouteHandler()));
Таким образом мы получим приветствие, которого ждали, но это лирическое отступление с демонстрацией точки расширения в ASP.NET MVC, где вы можете управлять процессом обработки запроса. В стандартном приложении мы получаем MvcRouteHandler, который отдает MvcHandler, а тот в свою очередь запускает нужный контроллер, нужное действие и отдает им нужные данные.
Controller initialization (Инициализация контроллера)
Как было уже сказано ранее, по умолчанию мы получаем MvcRouteHandler, он в свою очередь отдает MvcHandler. MvcHandler в методе ProcessRequest начинает процесс создания нужного контроллера и исполнение нужного действия.
//Запуск процеса обработки запроса
private void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory)
{
HttpContext currentContext = HttpContext.Current;
if (currentContext != null)
{
bool? isRequestValidationEnabled = ValidationUtility.IsValidationEnabled(currentContext);
if (isRequestValidationEnabled == true)
{
ValidationUtility.EnableDynamicValidation(currentContext);
}
}
AddVersionHeader(httpContext);
RemoveOptionalRoutingParameters();
//Получение имени контроллера
string controllerName = RequestContext.RouteData.GetRequiredString("controller");
//Получение фабрики контроллера
factory = ControllerBuilder.GetControllerFactory();
//Создание объекта контроллера
controller = factory.CreateController(RequestContext, controllerName);
if (controller == null)
{
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
MvcResources.ControllerBuilder_FactoryReturnedNull,
factory.GetType(),
controllerName));
}
}
В MVCHandler мы получаем объект фабрики контроллеров (controller factory), который ответственен за получение контроллера и реализует интерфейс IControllerFactory. По умолчанию ControllerBuilder отдает объект класса DefaultControllerFactory. Увидеть это мы можем, посмотрев исходный код:
//Получение фабрики контроллеров в ControllerBuilder
public IControllerFactory GetControllerFactory()
{
return _serviceResolver.Current;
}
//Конструктор ControllerBuilder
internal ControllerBuilder(IResolver<IControllerFactory> serviceResolver)
{
_serviceResolver = serviceResolver ?? new SingleServiceResolver<IControllerFactory>(
() => _factoryThunk(),
new DefaultControllerFactory { ControllerBuilder = this },
"ControllerBuilder.GetControllerFactory");
}
В методе CreateController используется объект IControllerActivator в методе GetControllerInstance, такое разграничение дополнительным уровнем дает возможность использовать разрешение зависимостей при создании контроллеров.
//Создания контроллера
public virtual IController CreateController(RequestContext requestContext, string controllerName)
{
if (requestContext == null)
{
throw new ArgumentNullException("requestContext");
}
if (String.IsNullOrEmpty(controllerName) && !requestContext.RouteData.HasDirectRouteMatch())
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName");
}
//Получение типа контроллера
Type controllerType = GetControllerType(requestContext, controllerName);
//Получение объекта контроллера
IController controller = GetControllerInstance(requestContext, controllerType);
return controller;
}
//Получение контроллера используя активатор контроллеров
protected internal virtual IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
if (controllerType == null)
{
throw new HttpException(404,
String.Format(
CultureInfo.CurrentCulture,
MvcResources.DefaultControllerFactory_NoControllerFound,
requestContext.HttpContext.Request.Path));
}
if (!typeof(IController).IsAssignableFrom(controllerType))
{
throw new ArgumentException(
String.Format(
CultureInfo.CurrentCulture,
MvcResources.DefaultControllerFactory_TypeDoesNotSubclassControllerBase,
controllerType),
"controllerType");
}
return ControllerActivator.Create(requestContext, controllerType);
}
Активатор контроллера по умолчанию реализован в классе DefaultControllerActivator и реализация получения объекта с использованием разрешения зависимостей (dependency resolver) выглядит следующим образом:
private class DefaultControllerActivator : IControllerActivator
{
private Func<IDependencyResolver> _resolverThunk;
public DefaultControllerActivator()
: this(null)
{
}
public DefaultControllerActivator(IDependencyResolver resolver)
{
if (resolver == null)
{
_resolverThunk = () => DependencyResolver.Current;
}
else
{
_resolverThunk = () => resolver;
}
}
public IController Create(RequestContext requestContext, Type controllerType)
{
try
{
return (IController)(_resolverThunk().GetService(controllerType) ?? Activator.CreateInstance(controllerType));
}
catch (Exception ex)
{
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
MvcResources.DefaultControllerFactory_ErrorCreatingController,
controllerType),
ex);
}
}
}
_resolveThunk по умолчанию получает объект класса DefaultDependencyResolver, который является оберткой для System.Activator, но если вы подключите собственный dependency resolver, то использоваться будет именно он. Используя различные контейнеры инверсии зависимостей (IoC containers) + внедрение зависимостей (DI), мы расширяем наши контроллеры и можем дополнительно управлять жизненным циклом объектов. Интеграция контейнера присходит посредством класса DependencyResolver, он статичен и имеет метод SetResolver, где и будет установлен наш IoC контейнер, который, в свою очередь, будет получен в активаторе контроллера в конструкторе (тот самый _resolveThunk). И вот, когда контроллер получен, MvcHandler может вызывать метод Execute у контроллера, передав объект RequestContext (контекст запроса) в качестве аргумента. Схематически, в сжатом виде, инициализация контроллера выглядит следующим образом:
Action execution (Выполнение действия)
При выполнении действий контроллера, так же выполняются фильтры. Фильтры — это точки расширения, которые позволяют обогатить код дополнительной логикой. Существуют следующие типы фильтров authentication filters (фильтры аутентификации), authorization filters (фильтры авторизации), action filters (фильтры действий), result filters (фильтры результата действия), exception filters (фильтры исключений). Фильтры действий реализуют интерфейс IActionFilter, в котором определены два метода OnActionExecuting и OnActionExecuted, соответственно они выполняются перед выполнением действия и после выполнения действия. При выполнении цикла запроса фильтры исполняются в следующем порядке:
- Фильтры аутентификации
- Фильтры авторизации
- Фильтры действий
- Фильтры результата действия
Фильтры исключений выполняются на любом этапе, ведь ошибки нужно отлавливать и обрабатывать по всему приложению.
В исходном коде класса Controller мы видим, что Controller наследует ControllerBase, в ControllerBase реализован метод Execute и в нем уже вызывается ExecuteCore. ExecuteCore в свою очередь реализован в классе Controller, получив имя действия для выполнения, запускает его на выполнение с исполнением дополнительного уровня в виде ActionInvoker. ActionInvocker в себе инкапсулирует выполнение дополнительных действий (привязку данных, выполнение всех фильтров, запуск на исполнение результата действия) и выполнение самого действия. Все происходит в следующем порядке внутри метода InvokeAction:
- Фильтры аутентификации (метод InvokeAuthenticationFilters)
- Фильтры авторизации (метод InvokeAuthorizationFilters)
- Получение значений параметров, если формальнее "привязка модели" (метод GetParameterValues)
- Выполнение действия и фильтров действия (метод InvokeActionMethodWithFilters)
- Выполнение результата действия (метод InvokeActionResultWithFilters)
//Выполнение действия контроллера
public virtual bool InvokeAction(ControllerContext controllerContext, string actionName)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
Contract.Assert(controllerContext.RouteData != null);
if (String.IsNullOrEmpty(actionName) && !controllerContext.RouteData.HasDirectRouteMatch())
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName");
}
ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext);
ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName);
if (actionDescriptor != null)
{
FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor);
try
{
AuthenticationContext authenticationContext = InvokeAuthenticationFilters(controllerContext, filterInfo.AuthenticationFilters, actionDescriptor);
if (authenticationContext.Result != null)
{
AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge(
controllerContext, filterInfo.AuthenticationFilters, actionDescriptor,
authenticationContext.Result);
InvokeActionResult(controllerContext, challengeContext.Result ?? authenticationContext.Result);
}
else
{
AuthorizationContext authorizationContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor);
if (authorizationContext.Result != null)
{
AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge(
controllerContext, filterInfo.AuthenticationFilters, actionDescriptor,
authorizationContext.Result);
InvokeActionResult(controllerContext, challengeContext.Result ?? authorizationContext.Result);
}
else
{
if (controllerContext.Controller.ValidateRequest)
{
ValidateRequest(controllerContext);
}
IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor);
ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters);
AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge(
controllerContext, filterInfo.AuthenticationFilters, actionDescriptor,
postActionContext.Result);
InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters,
challengeContext.Result ?? postActionContext.Result);
}
}
}
catch (ThreadAbortException)
{
throw;
}
catch (Exception ex)
{
ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex);
if (!exceptionContext.ExceptionHandled)
{
throw;
}
InvokeActionResult(controllerContext, exceptionContext.Result);
}
return true;
}
return false;
}
ActionInoker определяет, какое действие необходимо запустить, для этого берутся за ключевые фильтры: имя действия, параметры действия, атрибуты ActionMethodSelectorAtribute (если добавлены). Такими атрибутами являются HttpGet, HttpPost, к примеру, вы можете добавить свой, для этого просто необходимо создать класс, который наследует ActionMethodSelectorAtribute и переопределяет метод IsValidForRequest. Пример атрибута, который уточнит выбор действия, определив является ли клиент мобильным браузером или нет:
//Дополнительный параметр выборки действия
public class IsMobile : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
return controllerContext.HttpContext.Request.Browser.IsMobileDevice;
}
}
Если при выборке необходимого действия было найдено более одного или ни одного, то будет ошибка выполнения, ведь явное одно действие не было выбрано. Отдельно хочу отметить, что этап привязки данных к модели описан в статье ASP.NET MVC привязка данных, тут можно подчерпнуть информацию и посмотреть примеры, как мы можем влиять на привязку модели.
Result execution (Выполнение результата действия)
В результате выполнения всей цепочки мы получаем объект ActionResult, который вместе с фильтрами результата выполняется в методе InvokeActionResultWithFilters. Данный метод вызывает InvokeActionResultFilterRecursive в котором рекурсивно выполняются фильтры результата и выполняется сам результат. Что значит выполняется сам результат? Объект ActionResult записывает свои данные в ответ запросу. Если действие возвращает JsonResult, то в ответ будет записан сериализованный в json объект, который отдан в результат действия. Если же результатом нашего действия является представление или частичное представление, то будет выполнена следующая цепочка действий:
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (String.IsNullOrEmpty(ViewName))
{
ViewName = context.RouteData.GetRequiredString("action");
}
ViewEngineResult result = null;
if (View == null)
{
result = FindView(context);
View = result.View;
}
TextWriter writer = context.HttpContext.Response.Output;
ViewContext viewContext = new ViewContext(context, View, ViewData, TempData, writer);
View.Render(viewContext, writer);
if (result != null)
{
result.ViewEngine.ReleaseView(context, View);
}
}
Как видно, тут происходит следующее:
- В базовом классе ViewResultBase вызывается метод ExecuteResult
- Происходит поиск представления для рендера (реализован в классе ViewResult) по всем движкам представлений (ViewEngines)
- Найденное представление отправляется на рендер
- Результат рендера записывается в ответ на запрос
Есть в этой цепочке один нюанс, мы можем определить свой ViewEngine с путями поиска представлений и добавить его к списку стандартных. Таким образом мы сможем расширить правила поиска представлений. Ключевой момент: при поиске представления, проходит цикл по всем зарегистрированным ViewEngines и первый, который сможет найти представление возвращает результат, то есть для указания более частного случая необходимо свой ViewEngine регистрировать в списке на более ранних позициях.
//код движка представления
public class ThemeViewEngine : RazorViewEngine
{
public ThemeViewEngine()
{
ViewLocationFormats = new string[] { "~/Themes/{1}/{0}.cshtml" };
PartialViewLocationFormats = new string[] { "~/Themes/{1}/{0}.cshtml" };
}
}
//регистрация движка представления
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
ViewEngines.Engines.Insert(0, new ThemeViewEngine());
}
Данный пример объясняет цепочку вызовов, когда результатом является представление, если у нас более простой ответ, к примеру, ContentResult, то вся цепочка выглядит куда проще:
public class ContentResult : ActionResult
{
public string Content { get; set; }
public Encoding ContentEncoding { get; set; }
public string ContentType { get; set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
HttpResponseBase response = context.HttpContext.Response;
if (!String.IsNullOrEmpty(ContentType))
{
response.ContentType = ContentType;
}
if (ContentEncoding != null)
{
response.ContentEncoding = ContentEncoding;
}
if (Content != null)
{
response.Write(Content);
}
}
}
Вот так мы и подошли к концу цикла запроса в MVC, в следующей статье завершим детальный обзор цикла HTTP приложения, получив полную картину, как работает само приложение, как обрабатываются запросы от пользователей. Если вы захотите сами побродить по просторам MVC приложения, то это можно сделать, скачав код тут : https://aspnetwebstack.codeplex.com или тут : https://github.com/ASP-NET-MVC/aspnetwebstack. Сodeplex уходит в прощальную гастроль, но официальный репозиторий пока только там, на github не завезли еще, только не официальное зеркало. Так же посмотрите подробные диаграмы жизненного цикла от Microsoft Lifecycle of an ASP.NET MVC 5 Application