Размер шрифта:
ASP.NET MVC жизненный цикл запроса
Play

ASP.NET MVC жизненный цикл запроса

Как бы смешным не казалось, но эту статью можно начать философским вбросом про смысл жизни, куда мы движемся, и то, что движемся по спирали, и все события повторяются. Ведь посудите сами – у всего есть жизненный цикл, многие процессы имеют некую "циклическую" составляющую. И я хочу вам, мои друзья, рассказать про тот самый смысл жизни и типичную жизнь тривиального запроса в 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, только имя контроллера и действия по умолчанию". И здесь тот момент, когда оптимизации и улучшения сыграли злую шутку с программистом и он не знает внутренних тонкостей из-за того, что их скрыли от него. Пример объявления маршрутизации по умолчанию:

Все красиво, все просто: передаем имя маршрута, шаблон url запроса, анонимный объект, который указывает названия контроллера и действия по умолчанию. Но MapRoute – это расширяющий метод, который скрывает за собой реальную логику добавления маршрута в таблицу маршрутизации.

Пример добавления маршрута в таблицу маршрутизации без использования расширяющих методов:

Вот тут уже более наглядно будет видно, что за route handler и как они вообще попадают в маршрутизацию. Сам класс обработчика маршрута реализует интерфейс IRouteHandler у которого есть единственный метод GetHttpHandler, возвращающий HttpHandler. HttpHandler – это тот объект который берет на себя ответственность за генерацию ответа на HTTP запрос, класс этого объекта реализует интерфейс IHttpHandler. Этот интерфейс содержит свойство IsReusable и метод ProcessRequest. IsReusable - флаг, указывающий может ли другой запрос использовать этот же обработчик, иными словами нужно ли ему создавать каждый раз новый экземпляр или можно создать один раз и использовать его для всех последующих запросов. ProcessRequest – метод, который выполняет обработку запроса. По умолчанию используется MvcRouteHandler и MvcHandler. Исходный код открывает все тонкости и оптимизации, которые заложили архитекторы. В реальной жизни очень мала вероятность , что придется создавать собственный HttpHandler, так как стандартные средства ASP.NET MVC покрывают большинство задач. Приведу пример маленького HttpHandler`а, который поздоровается с вами:

Получить данный обработчик мы можем только из RouteHandler, поэтому сделаем его и зарегистрируем по адресу "home/hello".

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

Controller initialization (Инициализация контроллера)

Как было уже сказано ранее, по умолчанию мы получаем MvcRouteHandler, он в свою очередь отдает MvcHandler. MvcHandler в методе ProcessRequest начинает процесс создания нужного контроллера и исполнение нужного действия.

В MVCHandler мы получаем объект фабрики контроллеров (controller factory), который ответственен за получение контроллера и реализует интерфейс IControllerFactory. По умолчанию ControllerBuilder отдает объект класса DefaultControllerFactory. Увидеть это мы можем, посмотрев исходный код:

В методе CreateController используется объект IControllerActivator в методе GetControllerInstance, такое разграничение дополнительным уровнем дает возможность использовать разрешение зависимостей при создании контроллеров.

Активатор контроллера по умолчанию реализован в классе DefaultControllerActivator и реализация получения объекта с использованием разрешения зависимостей (dependency resolver) выглядит следующим образом:

_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, соответственно они выполняются перед выполнением действия и после выполнения действия. При выполнении цикла запроса фильтры исполняются в следующем порядке:

  1. Фильтры аутентификации
  2. Фильтры авторизации
  3. Фильтры действий
  4. Фильтры результата действия

Фильтры исключений выполняются на любом этапе, ведь ошибки нужно отлавливать и обрабатывать по всему приложению. В исходном коде класса Controller мы видим, что Controller наследует ControllerBase, в ControllerBase реализован метод Execute и в нем уже вызывается ExecuteCore. ExecuteCore в свою очередь реализован в классе Controller, получив имя действия для выполнения, запускает его на выполнение с исполнением дополнительного уровня в виде ActionInvoker. ActionInvocker в себе инкапсулирует выполнение дополнительных действий (привязку данных, выполнение всех фильтров, запуск на исполнение результата действия) и выполнение самого действия. Все происходит в следующем порядке внутри метода InvokeAction:

  1. Фильтры аутентификации (метод InvokeAuthenticationFilters)
  2. Фильтры авторизации (метод InvokeAuthorizationFilters)
  3. Получение значений параметров, если формальнее "привязка модели" (метод GetParameterValues)
  4. Выполнение действия и фильтров действия (метод InvokeActionMethodWithFilters)
  5. Выполнение результата действия (метод InvokeActionResultWithFilters)

ActionInoker определяет, какое действие необходимо запустить, для этого берутся за ключевые фильтры: имя действия, параметры действия, атрибуты ActionMethodSelectorAtribute (если добавлены). Такими атрибутами являются HttpGet, HttpPost, к примеру, вы можете добавить свой, для этого просто необходимо создать класс, который наследует ActionMethodSelectorAtribute и переопределяет метод IsValidForRequest. Пример атрибута, который уточнит выбор действия, определив является ли клиент мобильным браузером или нет:

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

Result execution (Выполнение результата действия)

В результате выполнения всей цепочки мы получаем объект ActionResult, который вместе с фильтрами результата выполняется в методе InvokeActionResultWithFilters. Данный метод вызывает InvokeActionResultFilterRecursive в котором рекурсивно выполняются фильтры результата и выполняется сам результат. Что значит выполняется сам результат? Объект ActionResult записывает свои данные в ответ запросу. Если действие возвращает JsonResult, то в ответ будет записан сериализованный в json объект, который отдан в результат действия. Если же результатом нашего действия является представление или частичное представление, то будет выполнена следующая цепочка действий:

Как видно, тут происходит следующее:

  1. В базовом классе ViewResultBase вызывается метод ExecuteResult
  2. Происходит поиск представления для рендера (реализован в классе ViewResult) по всем движкам представлений (ViewEngines)
  3. Найденное представление отправляется на рендер
  4. Результат рендера записывается в ответ на запрос

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

Данный пример объясняет цепочку вызовов, когда результатом является представление, если у нас более простой ответ, к примеру, ContentResult, то вся цепочка выглядит куда проще:

📎📎📎📎📎📎📎📎📎📎