Внутри ASP.NET MVC: конвейер обработки запросов, часть вторая (маршрутизация)

В продолжение данной темы, рассмотрим каждую часть конвейера обработки запросов ASP.NET MVC более подробно. В данной статье будет рассмотрена первая часть конвейера, а именно - маршрутизация. Деление на части, на самом деле, чисто условное, как было сказано тут. Но, если быть более точным, то система маршрутизации, хотя и является неотъемлемой частью процесса обработки запроса в стиле MVC, сама представляет одну из общих и необязательных частей конвейера ASP.NET. И может использоваться для разных целей, к коим относятся не только Web Forms и Web API, но и собственно написанный код для обработки запросов (позже в этой статье будет дана ссылка на пример). Сраза отмечу, что рассматривается интегрированный режим конвейера, работающий синхронно (т.е. с использованием IIS 7.x и высше). Асинхронный режим обработки запроса будет рассмотрен в отдельной статье. И так начнём с фундаментальных понятий обработки запроса в ASP.NET (детально конвейер обработки запросов ASP.NET в целом будет показан в отдельной статье, над которой уже сейчас работаю). HTTP-модуль и обработчик HTTP-данных являются таковыми. Общее схематическое представление процесса обработки запроса показано ниже.

Модули и обработчики в коде представлены как классы наследующие интерфейсы IHttpModule и IHttpHandler соответственно. Объект приложения, являющийся экземпляром класса приложения определённого в файле Global.asax, который в свою очередь наследует класс HttpApplication, генерирует события на которые подписаны HTTP-модули. Тем самым они вовлечены в процесс обработки запроса и учавствуют в его обработке. В нижнем рисунке показан кусок кода класса HttpApplication во время отладки, где модули последовательно начинают учавствовать в процессе обработки запроса.



Как видно из рисунка количество модулей подключённых к конвейеру равно 17 (это число зависит от конфигурации приложения и не является постоянным, для демонстрации используется простое приложение созданное в Visual Studio). В списке модулей виден и модуль маршрутизации, выделенный красным цветом. Как происходит подключение? В одном из внутренних методов класса HttpApplication, как показано ниже.
for (int i = 0; i < _moduleCollection.Count; i++) {
                _currentModuleCollectionKey = _moduleCollection.GetKey(i); 
                IHttpModule httpModule = _moduleCollection.Get(i); 
                ModuleConfigurationInfo moduleInfo = _moduleConfigInfo[i];
 
//................................................................... 
 
httpModule.Init(this);
 
//...................................................................
 
}
Как известно HTTP-модуль должен реализовать всего два метода: Init( ) и Dispose( ) своего базового типа IHttpModule.Собственно первый метод и вызывается у модулей коллекции нашего приложения. Остальные модули нам не интересны в данной статье. Метод Init( ) у модуля маршрутизации ничего особенного не делает на данном этапе, только подключается на обработку одного события приложения.
protected virtual void Init(HttpApplication application) {
 
            //...................................................................
            application.PostResolveRequestCache += OnApplicationPostResolveRequestCache;
        }
После того как отработают некоторые модули подписанные на определённые события приложения, возникающие до события ApplicationPostResolveRequestCache, метод OnApplicationPostResolveRequestCache нашего модуля начнёт обрабатывать данное событие (кроме модуля маршрутизации на это событие могут быть подписаны и другие модули, обработчики которых последовательно вызываются и каждый выполняет свою часть от общей работы).
private void OnApplicationPostResolveRequestCache(object sender, EventArgs e) {
            HttpApplication app = (HttpApplication)sender; 
            HttpContextBase context = new HttpContextWrapper(app.Context);
            PostResolveRequestCache(context);
        }
Особый интерес представляет метод PostResolveRequestCache(HttpContextBase context).



В нём и происходит самое интересное, но обо всём по порядку. Как мы знаем по умолчанию в обработчик события запуска приложения добавляется следующий код:
public class MvcApplication : System.Web.HttpApplication
    {
      public MvcApplication()
      {
      }
        protected void Application_Start()
        {
			//................................................................... 
            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }
    }
регистрирующий маршруты следующим образом.
namespace MvcApplication
{
  public class RouteConfig
  {
    public static void RegisterRoutes(RouteCollection routes)
    {
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
      routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}/{id}",
          defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
      );
    }
  }
}
Метод RouteConfig.RegisterRoutes(RouteTable.Routes) принимает в качестве аргумента так называемую таблицу маршрутизации. Что она из себя представляет? Ничего особенного:
public class RouteTable
  {
    public RouteTable();
    public static RouteCollection Routes { get; }
  }
обычный статический класс, со статическим свойством только для чтения типа RouteCollection. А так как свойстово Routes у нас имеет модификатор static, то это гарантирует уникальность коллекции маршрута в пределах домена приложения, что и требуется для приложения (своего рода паттерн одиночка). А модуль моршрутизации получает эти данные очень просто в открытом одноимённом свойстве RouteCollection.
public RouteCollection RouteCollection {
            get { 
                if (_routeCollection == null) { 
                    _routeCollection = RouteTable.Routes;
                } 
                return _routeCollection;
            }
            set {
                _routeCollection = value; 
            }
        }
Теперь самое интересное - код метода PostResolveRequestCache.
public virtual void PostResolveRequestCache(HttpContextBase context)
{    
  RouteData routeData = RouteCollection.GetRouteData(context);    
 
  if (routeData == null)    
  {    
    return;    
  }    
 
  IRouteHandler routeHandler = routeData.RouteHandler;    
  if (routeHandler == null)    
  {    
    throw new InvalidOperationException(    
        String.Format(    
            CultureInfo.CurrentCulture,    
            SR.GetString(SR.UrlRoutingModule_NoRouteHandler)));    
  }    
 
  if (routeHandler is StopRoutingHandler)    
  {    
    return;    
  }    
 
  RequestContext requestContext = new RequestContext(context, routeData);    
 
  context.Request.RequestContext = requestContext;    
 
  IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);    
  if (httpHandler == null)    
  {    
    throw new InvalidOperationException(    
        String.Format(    
            CultureInfo.CurrentUICulture,    
            SR.GetString(SR.UrlRoutingModule_NoHttpHandler),    
            routeHandler.GetType()));    
  }    
 
  if (httpHandler is UrlAuthFailureHandler)    
  {    
    if (FormsAuthenticationModule.FormsAuthRequired)    
    {    
      UrlAuthorizationModule.ReportUrlAuthorizationFailure(HttpContext.Current, this);    
      return;    
    }    
    else    
    {    
      throw new HttpException(401, SR.GetString(SR.Assess_Denied_Description3));    
    }    
  }    
 
  context.RemapHandler(httpHandler);    
}
Метод RouteCollection.GetRouteData( ) извлекает данные маршрута из контекста приложения в виде экземпляра routeData класса RouteData. Болеее подробно описание класса RouteData можно посмотретьв MSDN. У него есть одно важное для данного этапа свойство - RouteHandler. Только не надо путать его с обработчиком HTTP-данных реализующим интерфейс IHttpHandler. Обработчик маршрута, на который возлагается условие обязательной реализации интерфейса IRouteHandler, это намного узкое понятие, нежели обработчик HTTP-данных. Его основной задачей является назначить текущему маршруту обработчик HTTP-данных. Назначение происходит в методе context.RemapHandler(httpHandler). В MVC в роли такого выступает MvcRouteHandler, который и сопоставляет запрос обработчику HTTP-данных которым является MvcHandler. В Web API в роли обработчика маршрута выступает HttpControllerRouteHandler, а обработчиком HTTP-данных является HttpControllerHandler. Поэтому ничего удивительного, в том, что с лёгкостью можно подключить Web API к приложению Web Forms, нет (как это сделать написано тут). Если маршрутизация используется в Web Forms, в качестве обработчика маршрута назначается класс PageRouteHandler, а обработчиком - обычная страница aspx. Если обработчиком маршрута назначается StopRoutingHandler, а это мы делаем в методе регестрации маршрутов,
public static void RegisterRoutes(RouteCollection routes)
{    
  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");        
}  
то совпавший маршрут пропускается системой маршрутизации  и дальше обрабатывается без её участия.
if (routeHandler is StopRoutingHandler) {
                return; 
            } 
Если метод RouteCollection.GetRouteData( ) ничего не вернёт, то есть routeData будет null (например когда запросы идут к физическим файлам aspx и никие ограничения на маршруты не поставлены), обработка продолжится без участия модуля маршрутизации. Дальнейшее рассмотрение обработки запросов обработчиком HTTP-данных MvcHandler (и не только им) - тема третьей части и следующий условный этап конвейера обработки запросов. Пример того, как мы можем подключить собственную логику обработки запроса для определённого нами маршрута приведён здесь.  И ещё замечу что, такие важные понятия как ограничения и токены не были рассмотрены тут. Для этого нужна отдельная статья.


AS
07.04.2016 13:43
Скажите, а данная информация по-прежнему актуальна на сегодняшний день?
07.04.2016 14:51
Да, данная статья, актуальна вплоть до текущей версии тоже.