ASP.NET MVC Framework

Магдануров Гайдар

Юнев Владимир

ГЛАВА 6

Механизмы маршрутизации

 

 

 Начиная с версии .NET Framework 3.5 SP1, ASP.NET содержит новый механизм маршрутизации пользовательских запросов. Основным предназначением механизма маршрутизации является предоставление возможности осуществлять запросы к веб-приложению не на основании файлов и файловой структуры приложения, а на основании специально определенных шаблонов, которые называются маршрутами.

Механизм маршрутизации не является составной частью MVC Framework. Представленный в .NET Framework 3.5 SP1, этот механизм является составной частью ASP.NET и может быть использован в любом проекте ASP.NET, основанном как на WebForms, так и на MVC Framework.

И хотя маршрутизация — это не часть MVC Framework, тем не менее в проектах на базе MVC Framework она играет одну из ключевых ролей. Маршрутизация помогает перейти от запросов к файлам ASPX, к запросам на основании набора контроллеров и действий. Такой переход позволяет избавиться от зависимости от строки запроса, которая в классическом ASP.NET во многом определяла структуру приложения. Кроме того, с помощью маршрутизации проекты на базе MVC Framework изначально получают возможность формирования удобочитаемых URL, что имеет большое значение в оптимизации приложения для индексации поисковыми системами.

Основное назначение маршрутизации — это обработка пользовательских запросов и представление их в удобном для разработчика виде. Но при разработке проектов на базе MVC Framework маршрутизация может стать удобным инструментом и для других целей: обеспечения безопасности доступа, валидации параметров и данных запросов, организации потоков данных.

 

Маршрутизация в ASP.NET

Механизм маршрутизации ASP.NET был представлен в .NET Framework 3.5 SP1 и представляет собой набор классов, объединенных в пространстве имен System.Web.Routing. В состав System.Web.Routing входят следующие основные классы и интерфейсы:

□ класс UrlRoutingModule реализует интерфейс iHttpModule для обработки клиентских запросов и последующего сопоставления данных запроса одному из маршрутов, определенных в приложении;

□ RouteTable представляет собой класс, хранящий таблицу маршрутизации, т. е. все маршруты, используемые в приложении;

□ класс RouteCollection предоставляет организацию и доступ к коллекции маршрутов;

□ класс Route предназначен для определения свойств маршрута и предоставления данных о нем;

□ класс RouteData содержит данные о запрошенном через механизмы RouteCollection маршруте;

□ класс RequestContext содержит информацию о контексте пользовательского запроса, его параметрах и данных, а также информацию о выбранном маршруте. Этот класс является значительным для MVC Framework механизмом, который используется на всех этапах обработки и выполнения запроса;

□ интерфейс IRouteHandler определяет обработчик маршрутов;

□ интерфейс IRouteConstraint определяет условия, налагающие ограничения на параметры маршрута.

Кроме этих классов в пространстве имен System.Web.Routing определены еще некоторые классы и перечисления, которые играют вспомогательную или базовую роль для перечисленных классов и интерфейсов.

Рассмотрим применение классов механизма маршрутизации на примере стандартного проекта MVC Framework. В базовом шаблоне проекта MVC Framework в web.config механизмом, обрабатывающим запросы по умолчанию, назначен механизм маршрутизации, представленный классом UrlRoutingModule. Именно этот модуль определен в секциях httpModules и system.webServer как модуль, обрабатывающий HTTP-запросы.

Когда веб-приложение MVC Framework запускается в первый раз, выполняется метод Application_Start, определенный в global.asax. В проекте по умолчанию MVC Framework определяет в Application_Start инициализацию таблицы маршрутизации, представленную классом RouteTable. RouteTable содержит статическое свойство Routes, которое представляет собой экземпляр коллекции RouteCollection. Routes, с помощью вспомогательного метода RegisterRoutes заполняется маршрутами, которые определяет пользователь. Каждый такой маршрут представляет собой объект типа Route. По умолчанию в проекте MVC Framework создается всего один маршрут, рассмотрим его на следующем фрагменте кода из файла global.asax.cs:

public class MvcApplication : System.Web.HttpApplication

{

  public static void RegisterRoutes(RouteCollection routes)

  {

    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(

           "Default",

           "{controller}/{action}/{id}",

           new { controller = "Home", action = "Index", id = "" }

    );

  }

  protected void Application_Start()

  {

    RegisterRoutes(RouteTable.Routes);

  }

}

Здесь для таблицы маршрутизации RouteTable.Routes выполняются два действия. Во-первых, вызывается метод IgnoreRoute, который определяет в таблице правило игнорирования маршрута для запросов, содержащих ссылки на AXD-файлы. Определяя правило игнорирования, вы исключаете запрос, который должен быть обработан другим обработчиком, из маршрутизации.

Во-вторых, вызывается метод расширения MapRoute, который регистрирует единственный маршрут в таблице маршрутизации. Для этого в данном конкретном примере он принимает три параметра: наименование маршрута, шаблон маршрута и определение значений параметров шаблона по умолчанию. Наименование маршрута, определенное как Default, может быть использовано в дальнейшем для обращения к маршруту и оперирования с ним. Более подробно разберем шаблон маршрута и параметры по умолчанию.

Шаблон маршрута, представленный строкой {controller}/{action}/{id}, описывает правило, которому будут подчинены клиентские запросы относительно веб-сайта. Правило содержит параметры шаблона: controller, action и id. Первые два из них — это стандартные параметры MVC Framework, которые позволяют задавать в шаблоне определение контроллера и действия соответственно. Параметр id — это пользовательский параметр, который будет передан действию. Например, при обращении к сайту sample.domain по следующему адресу механизм маршрутизации сопоставит параметру шаблона controller значение home, параметру action — значение index, параметру id — значение 5. На практике это будет означать следующее: механизм маршрутизации передаст механизму MVC Framework информацию о том, что в ответ на клиентский запрос необходимо вызвать действие Index у контроллера HomeController и передать действию 5 в виде значения параметра id.

Значения параметров шаблона маршрута по умолчанию, которые задаются в MapRoute в виде new { controller = "Home", action = "Index", id = "" }, представляют собой значения для параметров маршрута, которые будут использованы в том в случае, когда клиентский запрос их не содержит. Например, если обратиться к ресурсу со следующим запросом , в котором определен только первый параметр из шаблона {controller}/{action}/{id} , то механизм маршрутизации автоматически определит оставшиеся два на основании значений по умолчанию. То есть параметр controller получит значение About, а параметры action и id значения Index и пустую строку соответственно. Другими словами, на основании значений параметров по умолчанию запрос будет интерпретироваться механизмом маршрутизации как запрос . Что приведет к вызову действия Index у контроллера AboutController.

Здесь мы поверхностно рассмотрели работу механизма маршрутизации и то, как он участвует в работе с MVC Framework. В следующих разделах данной главы мы рассмотрим маршрутизацию более подробно.

 

Механизмы маршрутизации

 

В начале главы мы упомянули об основных классах, которые составляют механизм маршрутизации. В этой части главы мы рассмотрим их более подробно.

 

Маршрут и класс Route

Маршрут в понятии ASP.NET — это строка, которая представляет собой шаблон URL-строки запроса к веб-приложению. Этот шаблон определяет, какие запросы попадают под понятие данного маршрута, а какие нет. Шаблоны маршрутов содержат параметры, имена которых заключены в фигурные скобки. Обычно параметры разделены обратным слэшем (/) как разделяющие сегменты URL-строки. Например, маршрут, определенный следующим образом admin/{user}/{action}, содержит два параметра: user и action. Такому маршруту могут соответствовать следующие строки URL: admin/foo/add/ или . Где foo и bee определяются как значения параметра user, а add и edit — параметра action.

Механизм MVC Framework определяет два базовых параметра маршрутов: controller и action, которые предназначены для определения разработчиком месторасположения наименования контроллера и действия в строке запроса. Остальные параметры являются пользовательскими и определяются разработчиком. Например, следующий маршрут {controller}/{action}/{user}/{page} определяет кроме базовых параметров controller и action еще два пользовательских: user и page. Такому маршруту может соответствовать строка запроса . Механизм MVC Framework при сопоставлении данных маршрутов и строки запроса определит, что требуется вызвать действие с именем GetUserMessages в контроллере с именем UserController, которому нужно передать два параметра: user со значением user1 и page со значением 2.

Одним из базовых классов маршрутизации ASP.NET является класс Route, который позволяет разработчику определить свойства маршрута. Рассмотрим основные свойства, которые содержит класс Route:

□ Constraints — свойство типа RouteValueDictionary, которое хранит все ограничения, накладываемые разработчиком на маршрут;

□ DataTokens — свойство типа RouteValueDictionary, которое хранит набор параметров, не участвующих в разборе шаблона, но передающихся обработчику маршрута как дополнительная информация;

□ Defaults — свойство типа RouteValueDictionary, которое хранит значения параметров маршрута по умолчанию. Данные значения используются, когда определенная для маршрута строка URL не содержит необходимого параметра;

□ RouteHandler — свойство, реализующее интерфейс IRouteHandler, является обработчиком маршрута, который определяет, подходит или нет заданный URL запроса к маршруту;

□ Url — строковое свойство, которое содержит определение шаблона маршрута.

Создание экземпляра Route не вызывает трудностей, например, в следующем фрагменте представлено создание маршрута, аналогичного тому, что создается в проекте MVC Framework по умолчанию:

var defaults = new RouteValueDictionary

{

             {"controller", "Home"},

             {"action", "Index"},

             {"id", ""}

};

routes.Add(new Route(

            "{controller}/{action}/{id}",

            defaults,

            null,

            null,

            new MvcRouteHandler()

        ));

Класс Route имеет несколько конструкторов, конструктор с самым большим числом параметров определен так:

public Route(string url,

  RouteValueDictionary defaults,

  RouteValueDictionary constraints,

  RouteValueDictionary dataTokens,

  IRouteHandler routeHandler

)

Рассмотрим параметры, которые передаются в этот конструктор:

□ url — обязательный параметр, определяет строку шаблона для маршрута;

□ defaults — необязательный параметр, определяет значения по умолчанию для параметров маршрута;

□ constraints — необязательный параметр, определяет ограничения для маршрута;

□ dataTokens — необязательный параметр, определяет дополнительные данные для маршрута;

□ routeHandler — обязательный параметр, определяет обработчик, реализующий интерфейс IRouteHandler для обработки маршрута. В MVC Framework обработчиком по умолчанию является класс MVCRouteHandler.

 

Коллекция маршрутов и класс RouteCollection

Для того чтобы механизм MVC Framework начал работать с маршрутом, который определяет пользователь, необходимо, чтобы маршрут попал в таблицу маршрутизации. Такая таблица основывается на классе RouteCollection, который представляет собой коллекцию маршрутов.

По существу класс RouteCollection представляет собой класс, наследующий от класса Collection. То есть RouteCollection содержит все методы по управлению коллекций элементов, такие как Add, Clear, Remove, SetItem и пр. Кроме того, из определения понятно, что RouteCollection может оперировать только элементами типа BaseRoute и его производными, которым является класс Route.

Кроме базовых методов, унаследованных от Collection, в RouteCollection определены следующие методы и свойства:

□ Add — перегруженный метод, который позволяет не просто добавить в коллекцию элемент маршрута, но добавить его и сопоставить ему наименование;

□ GetReadLock, GetWriteLock — методы, которые позволяют организовать потокобезопасные операции с коллекцией маршрутов;

□ GetRouteData — метод, который возвращает информацию о маршруте в виде экземпляра типа RouteData;

□ GetVirtualPath — метод, который возвращает объект типа VirtualPathData, который позволяет получить URL, соответствующий заданным параметрам маршрута;

□ RouteExistingFile — булево свойство, которое позволяет определить, следует ли обрабатывать запросы к локальным файлам, таким как вебстраницы, скрипты, стили или изображения в виде запросов к механизму маршрутизации, или отдавать их, минуя этот механизм.

Работа с коллекцией маршрутов в MVC Framework происходит через таблицу маршрутизации RouteTable и статическое свойство Routes, которое является экземпляром RouteCollection.

Для упрощения работы с коллекцией RouteCollection механизм MVC Framework определяет два метода расширения:

□ MapRoute — позволяет добавлять в коллекцию маршрутов новый маршрут;

□ IgnoreRoute — позволяет добавлять в коллекцию маршрутов новый маршрут, который, однако, добавляется с обработчиком маршрутов StopRoutingHandler, что означает игнорирование механизмом маршрутизации указанного маршрута.

Рассмотрим, как работают эти методы расширения. Метод MapRoute имеет несколько вариантов, далее представлено определение для метода с самым большим числом параметров:

public static Route MapRoute(

    this RouteCollection routes,

    string name,

    string url,

    object defaults,

    object constraints,

    string[] namespaces

)

Все варианты метода расширения MapRoute, определенные в MVC Framework, для добавления маршрута требуют указания наименования маршрута, которое задается через параметр name. Параметр url определяет шаблон маршрута, а параметры defaults и constraints — словари с набором параметров по умолчанию и ограничений. Методы расширения MapRoute не содержат возможности задавать значения для свойства DataTokens, как это делает настоящий конструктор маршрута, и на это есть причина. Поскольку MVC Framework имеет работающий по умолчанию обработчик для маршрутов в виде класса MvcRouteHandler, надобность в пользовательских параметрах, которые передаются для обработчика через DataTokens, отпадает. Однако такие параметры и DataTokens обработчик MvcRouteHandler все же использует. Через параметр namespaces метода расширения MapRoute разработчик может задать массив наименований пространств имен. Этот массив передается в DataTokens и в дальнейшем используется механизмом MVC Framework для поиска классов контроллеров только в тех пространствах имен, которые в нем определены.

Метод IgnoreRoute, в своем самом большом варианте, определен следующим образом:

public static void IgnoreRoute(

  this RouteCollection routes,

  string url,

  object constraints

)

Параметров у метода всего два. Первый — url — определяет маршрут, который подлежит игнорировать. Второй — constraints — содержит словарь ограничений для параметров маршрута, что позволяет более гибко настраивать игнорирование маршрутов. На самом деле IgnoreRoute создает маршрут, но сопоставляет ему не стандартный для MVC Framework обработчик MvcRouteHandler, а обработчик StopRoutingHandler, определенный в механизме маршрутизации ASP.NET, который предназначен для пропуска маршрутов механизмом маршрутизации.

 

Таблица маршрутизации и класс RouteTable

Как уже сообщалось, одним из основных классов механизма маршрутизации является класс RouteTable. Вместе с этим RouteTable — один из самых простых классов, он содержит только одно свойство Routes. Это статическое свойство, которое содержит коллекцию маршрутов в виде экземпляра класса RouteCollection.

Для работы MVC Framework необходимо, чтобы свойство Routes содержало хотя бы один маршрут. RouteTable можно считать контейнером для маршрутов всего приложения. Так как коллекция маршрутов типа RouteCollection определена в нем в виде статического свойства Routes, доступ ко всем маршрутам будет иметь любая часть приложения.

 

Ограничения и интерфейс IRouteConstraint

Интерфейс IRouteConstraint предназначен для определения объекта ограничения, который, основываясь на некоторой логике, определяет, подходит или нет значение параметра URL маршруту, которому присваивается это ограничение. Проще говоря, IRouteConstraint позволяет определить объект и сопоставить его имени параметра маршрута. Задачей этого объекта является проверка значений параметра на соответствие неким условиям. MVC Framework при сопоставлении URL запроса маршруту последовательно вызывает все зарегистрированные объекты, реализующие IRouteConstraint, и таким образом проверяет, подходит ли данный набор параметров URL рассматриваемому маршруту.

Интерфейс IRouteConstraint задает всего один метод Match, который имеет следующее определение:

bool Match(

  HttpContextBase httpContext,

  Route route, string parameterName,

  RouteValueDictionary values,

  RouteDirection routeDirection

)

Метод Match должен содержать логику проверки параметра строки URL запроса на соответствие некоторым условиям. Match должен вернуть true в случае, когда значение параметра соответствует условиям, и false в обратном случае. При вызове методу Match передаются следующие параметры:

□ httpContext — контекст запроса в виде экземпляра класса HttpContextBase;

□ route — маршрут, к которому применяется данное ограничение;

□ parameterName — имя параметра, значение которого следует проверить на условия ограничения;

□ values — коллекция значений параметров запроса;

□ routeDirection — параметр, определяющий текущее поведение механизма маршрутизации: обработку клиентского запроса или создание строки URL на базе маршрутов.

Основываясь на перечисленных параметрах, метод Match должен определить, подходит ли значение параметра из строки URL для параметра маршрута.

Рассмотрим работу интерфейса на примере:

public class SampleConstraint : IRouteConstraint

{

  public bool Match(HttpContextBase httpContext,

       Route route,

       string parameterName,

       RouteValueDictionary values,

       RouteDirection routeDirection)

  {

    bool result = false;

    if (parameterName == "user")

    {

      if (values["user"].ToString().Length >= 4) result = true;

    }

    return result;

  }

}

Данный класс SampleConstraint реализует интерфейс IRouteConstraint и определяет метод Match. В этом методе происходит проверка значения параметра под именем user. Если длина значения параметра менее 4 символов, то возвращается false, который указывает, что параметр не прошел проверку, в ином случае возвращается true, что говорит о прохождении параметров проверки на условия. Образно говоря, этот класс проверяет длину строки имени пользователя, которая была передана в строке URL запроса.

Для того чтобы использовать этот класс ограничения, необходимо сопоставить его маршруту следующим образом:

routes.MapRoute("Default",

    "{controller}/{action}/{user}",

    new {controller = "Home", action = "Index", id = ""},

    new {user = new SampleConstraint() }

);

По этому определению маршрута будет следовать, что запрос со строкой URL обработается, а запрос будет отклонен на этапе работы ограничения SampleConstraint, т. к. значение foo, определяющее параметр маршрута user, содержит менее 4 символов.

 

Обработчик маршрутов и интерфейс IRouteHandler

Интерфейс IRouteHandler предназначен для определения обработчика маршрута. Такой обработчик нужен для того, чтобы обработать запрос после определения маршрута. Иными словами, после клиентского запроса и определения подходящего для него маршрута механизм маршрутизации создает сопоставленный найденному маршруту обработчик и вызывает его. Обработчик должен содержать некую логику, которая исполняется в ответ на клиентский запрос. Для MVC Framework такая логика реализуется с помощью двух классов: MvcRouteHandler и MvcHandler.

Задача класса, реализующего iRouteHandler, состоит в том, чтобы вернуть подходящий экземпляр класса HTTP-обработчика, реализующего iHttpHandler. Для этого интерфейс IRouteHandler определяет всего один метод GetHttpHandler:

IHttpHandler GetHttpHandler(RequestContext requestContext)

Любой класс, реализующий IRouteHandler, должен реализовать метод GetHttpHandler, который должен возвращать инстанцированный объект HTTP-обработчика. В механизме MVC Framework за это отвечает класс MvcRouteHandler, который через GetHttpHandler возвращает экземпляр другого класса MvcHandler. Класс MvcRouteHandler сопоставляется через метод расширения MapRoute всем маршрутам по умолчанию, поэтому разработчику нет нужды напрямую его использовать.

Таким образом, механизм MVC Framework уже содержит предопределенные элементы в виде реализации интерфейса IRouteHandler и HTTP-обработчика, и в общем случае разработчик использует их. Но вы можете определить свой вариант IRouteHandler, который будет возвращать ваш собственный HTTP-обработчик, отличающийся от MvcHandler. Так вы сможете переопределить поведение механизма MVC Framework на этапе связывания маршрута и объектов модели MVC-приложения.

 

Создание маршрутов

 

В предыдущей части этой главы мы уже рассмотрели простой пример создания маршрута и класс Route, который используется для этих целей. При создании маршрута с помощью класса Route важную роль играют параметры имени маршрута, Url и три словаря параметров, определенные свойствами Defaults, Constraints и DataTokens. Рассмотрим их назначение и применение более подробно.

 

Наименование маршрута

 

В MVC Framework добавление маршрута реализуется путем вызова одного из вариантов MapRoute, методов расширения RouteCollection. Одним из отличиев MapRoute является требование к обязательному указанию имени маршрута, тогда как стандартные средства RouteCollection позволяют добавить маршрут без указания имени.

 Примечание

Следует учесть, что имена для маршрутов должны быть уникальными. При попытке добавить в таблицу маршрутизации маршрут с именем, которое уже существует в таблице, будет вызвано исключение.

********************************

Имя маршрута — это достаточно важная часть маршрутизации в MVC Framework. Есть несколько полезных вариантов использования имени маршрута, рассмотрим их по порядку.

 

RedirectToRoute

Имя маршрута используется для возврата результата действия в виде RedirectToRouteResult с помощью стандартного метода контроллера RedirectToRoute. Назначение RedirectToRoute — это перенаправление выполнения запроса с одного маршрута на другой. На самом деле, для переадресации на другой маршрут MVC Framework на основании переданного имени маршрута генерирует строку URL, которая соответствует новому маршруту и производит возврат клиенту ответа в виде требования на переадресацию на новый URL (redirect).

К примеру, допустим определен маршрут с именем AccountLogOn так, как показано далее:

routes.MapRoute(

  "AccountLogOn",

  "Account/LogOn",

  new { controller = "Account", action = "LogOn" }

);

В этом случае вызов RedirectToRoute, который переадресует пользователя на новый маршрут, будет выглядeть так:

public ActionResult SomeAction()

{

  return RedirectToRoute("AccountLogOn");

}

После выполнения действия SomeAction пользователь будет перенаправлен на URL Account/LogOn, согласно определению маршрута AccountLogOn.

 

AjaxHelper

В методах расширения класса AjaxHelper, таких как BeginRouteForm и RouteLink, имя маршрута используется для генерации строки запроса.

BeginRouteForm — это метод расширения, который позволяет упростить создание формы для отправки результатов через Ajax.BeginRouteForm в данном смысле является аналогом BeginForm другого метода расширения класса AjaxHelper, который строит форму на основании определенных значений контроллера и действия. В случае, когда вызывается BeginRouteForm, механизм класса AjaxHelper формирует на базе имени маршрута строку URL и подставляет ее атрибуту action при рендеринге формы в представлении.

RouteLink — это метод расширения, позволяющий упростить создание ссылки в представлении, которая осуществляла бы Ajax-запросы. Роль имени маршрута при работе RouteLink точно такая же, как и в BeginRouteForm — по имени маршрута строится URL-строка, которая рендерится в представлении в виде атрибута href.

 

UrlHelper

В методе расширения RouteUrl класса UrlHelper имя маршрута используется для создания строки URL, соответствующей маршруту.

 

Шаблон маршрута и свойство Url

При создании маршрута строковый параметр Url определяет шаблон маршрута, который, как правило, задает некоторую группу возможных клиентских запросов. Следует учитывать, что при наличии нескольких маршрутов механизм маршрутизации ASP.NET всегда выбирает самый первый из них — тот, что был добавлен в таблицу маршрутизации первым.

Как мы уже говорили, по умолчанию MVC Framework создает один-единственный маршрут с именем Default и шаблоном {controller}/{action}/{id}. Этот маршрут хорош тем, что он один позволяет обрабатывать большую часть возможных пользовательских запросов. По сути такой шаблон говорит о том, что данный маршрут определяет все запросы с вложениями до третьего уровня, т. е. этот маршрут обработает и и , и , где level1, level2 и level3 могут принимать любые значения. На самом деле это очень большое количество возможных запросов и на практике один маршрут Default покрывает все требования разработчиков к маршрутизации.

Однако разработчик может добавить гибкости своему проекту и улучшить представления строк URL, если определит свои маршруты. Далее представлено два подобных маршрута:

routes.MapRoute(

  "AccountLogOn",

  "LogOn",

  new { controller = "Account", action = "LogOn" }

);

routes.MapRoute(

  "Home",

  "{action}",

  new { controller = "Home", action = "Index" }

);

Первый вызов MapRoute создает маршрут под наименованием AccountLogOn, шаблон которого четко соответствует только одному возможному клиентскому запросу . Второй вызов создает маршрут Home более широкого определения. Под этот маршрут попадают все клиентские запросы вида .

Польза от создания таких маршрутов очевидна. Имея набор именованных маршрутов, которые четко определяют область клиентских запросов, а не обхватывают все запросы подряд, вы можете более гибко управлять маршрутами. Например, теперь для простого перенаправления на страницу входа в систему вам достаточно будет вызвать следующий метод:

public ActionResult SomeAction()

{

  return RedirectToRoute("AccountLogOn");

}

Другим видимым преимуществом этого маршрута является то, что согласно ему клиентский запрос не обязательно должен содержать в себе наименование контроллера. Таким образом, путь http://some.domain/Account/LogOn уменьшается до http://some.domain/LogOn, что придает ссылкам вебприложения более компактный вид. Компактность строк запросов может играть свою роль в случае, когда в большом веб-приложении используются десятки контроллеров с массой действий и параметров.

 

Значения параметров маршрута по умолчанию и свойство Defaults

Свойство Defaults определяет набор параметров, сопоставляемых параметрам маршрута по умолчанию, в случае, когда URL строки запроса их не содержит. Рассмотрим для примера маршрут {controller}/{action}/{id}, создаваемый в проектах MVC Framework по умолчанию. При его создании свойство Defaults было инициализировано с помощью метода расширения MapRoute следующим значением:

new { controller = "Home", action = "Index", id = "" }

На практике это будет означать, что механизм MVC Framework одинаково обработает запросы http://sample.domain/Home/Index/, http://sample.domain/Home/ и . Все эти запросы приведут к вызову действия Index в контроллере HomeController. Это стало возможным, поскольку были определены значения по умолчанию для параметров маршрута controller и action. В связи с этим URL при сопоставлении маршруту дополучит часть параметров из словаря Defaults, став, таким образом, равнозначным запросу http://sample.domain/Home/Index/ .

 

Ограничения параметров маршрута и свойство Constraints

Свойство Constraints определяет набор параметров, которые служат ограничителями для параметров маршрута. Рассмотрим пример: при создании проекта вы определяете маршрут, один из параметров которого указывает логин пользователя. Согласно правилам вашего ресурса логин пользователя не может содержать менее 4 символов в виде букв и цифр. Поэтому все запросы, которые попадают под ваш маршрут и содержат параметр логина пользователя с тремя или менее символами либо с недопустимыми символами, вы рассматриваете как ошибочно сформированные. Задача по обработке такой ситуации идеально вписывается в механизм ограничений маршрутов, который представлен свойством Constraints.

Чтобы решить задачу с ограничением на длину имени логина в запросе, нужно определить следующее ограничение в свойстве Constraints при создании маршрута:

var constraint = new RouteValueDictionary {

                         {"user", "\\w{4,}"}

                     };

routes.Add(new Route(

    "{controller}/{action}/{user}/",

    defaults,

    constraint,

    null,

    new MvcRouteHandler()

));

Ограничение определяется как элемент словаря RouteValueDictionary, где ключом является имя параметра, а значением регулярное выражение, описывающее правило для параметра. В данном случае регулярное выражение \w{4,} предполагает, что строка должна содержать буквы и цифры в размере от 4 элементов.

После такого определения маршрута, если вы попытаетесь обратиться по следующему адресу , вы получите стандартное сообщение браузера о возвращенной сервером ошибке с кодом 404 "Страница не найдена".

Ранее в этой главе мы рассмотрели ограничения параметров как коллекцию регулярных выражений в свойстве Constraints класса Route. Однако существует еще один, альтернативный, способ создания маршрута на базе интерфейса iRouteConstraint. Он также описан в этой главе. Вместо того чтобы добавлять в коллекцию Constraints строки с регулярными выражениями, можно воспользоваться интерфейсом IRouteConstraint и реализовать класс, который будет выполнять проверку параметров на соответствие условиям. Добавляется такой класс похожим на обычный способом:

routes.MapRoute("Default",

                "{controller}/{action}/{user}",

                new {controller = "Home",

                     action = "Index",

                     id = ""},

                new {user = new SampleConstraint()}

);

Здесь SampleConstraint — это класс, который реализует интерфейс IRouteConstraint.

 

Параметры маршрута и свойство DataTokens

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

Однако есть исключение, делающее свойство DataTokens экземпляра маршрута полезным даже для проектов MVC Framework. Дело в том, что фабрика контроллеров по умолчанию проверяет DataTokens на наличие параметра Namespaces, от которого в дальнейшем зависит, какие контроллеры могут быть выбраны для инстанцирования.

Параметр Namespaces может содержать строки в виде перечисления типа iEnumerable. Когда фабрика контроллеров находит такой параметр в свойстве DataTokens, она предполагает, что в нем перечислены наименования пространств имен, в которых можно искать контроллеры для инстанцирования в ответ на клиентский запрос.

Наличие такого механизма, как ограничение поиска контроллеров для маршрута определенным списком пространств имен, нельзя переоценить. В крупных проектах, в которых могут создаваться десятки или сотни пространств имен, поиск типа контроллера через механизм отражений может занимать неоправданно много процессорного времени. Ограничив маршрут набором пространств имен, вы поможете фабрике контроллеров более быстро найти и инстанцировать необходимый для обработки маршрута контроллер.

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

var route = routes.MapRoute(

      "AccountLogOn",

      "LogOn",

      new { controller = "Account", action = "LogOn" }

    );

route.DataTokens = new RouteValueDictionary();

List nsList = new List();

nsList.Add("SomeNamespace");

nsList.Add("SomeNamespace2");

route.DataTokens.Add("Namespaces", nsList);

Здесь после создания собственно маршрута, у него инициализируется свойство DataTokens и заполняется ключом Namespaces со значением списка из двух строк с именами пространств имен SomeNamespace и SomeNamespace2. Такие действия с маршрутом подразумевают, что контроллер для него будет искаться в пространствах имен SomeNamespace и SomeNamespace2.

Еще одна возможность ограничить список пространств имен, в которых фабрика контроллеров ищет контроллеры для инстанцирования, — это свойство DefaultNamespaces класса ControllerBuilder. В каждом приложении MVC Framework существует экземпляр ControllerBuilder в виде статического свойства Current класса ControllerBuilder. Используя свойство DefaultNamespaces, вы можете определить пространства имен по умолчанию, в которых фабрика контроллеров будет искать контроллеры.

ControllerBuilder.Current.DefaultNamespaces.Add("SomeNamespaces3");

DataTokens и DefaultNamespaces можно использовать вместе. В целом очередность работы механизмов DataTokens и DefaultNamespaces в процессе поиска контроллера следующая:

1. Фабрика контроллеров пытается найти контроллер в пространствах имен, определенных в DataTokens маршрута, поиск прекращается, если такой контроллер найден.

2. Фабрика контроллеров пытается найти контроллер в пространствах имен, определенных через DefaultNamespaces, поиск прекращается, если контроллер найден.

3. В конце концов, фабрика контроллеров просматривает все пространства имен в поиске необходимого контроллера.

Здесь следует обратить внимание на то, что определение неверных пространств имен в DataTokens и DefaultNamespaces может привести к тому, что поиск вместо ускорения замедлится, поскольку будет проходить трижды, согласно информации каждого механизма.

 

Игнорирование маршрутов

Игнорирование маршрутов — это необходимый инструмент, позволяющий указать маршрут, т. е. группу клиентских запросов, которые не будут обрабатываться MVC Framework. Есть множество случаев, когда запрос не должен обрабатываться как запрос к MVC Framework. Например, запросы к изображениям, находящимся на сервере, или к любым другим статическим ресурсам, таким как файлы скриптов или стилей. Нет совершенно никакой необходимости в том, чтобы такие запросы проходили через многоступенчатую обработку MVC Framework. Вместо этого файлы изображений или стилей должны отдаваться клиентам напрямую.

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

Чтобы создать правило игнорирования маршрута, MVC Framework содержит IgnoreRoute — метод расширения класса RouteCollection. Этот метод имеет следующие входные параметры:

□ url — указывает шаблон маршрута, все запросы, которые подпадают под этот маршрут, будут игнорироваться MVC Framework;

□ constraints — указывает дополнительный набор параметров, позволяющий более гибко задать условия, при которых производится игнорирование.

Когда создается проект MVC Framework через шаблон в Visual Studio, то в файле global.asax автоматически будет сгенерировано определение одного маршрута игнорирования:

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

Это определение маршрута игнорирования достаточно просто демонстрирует, зачем вообще нужно игнорировать маршруты. Определение этого правила необходимо потому, что AXD-файлов на самом деле не существует, и запросы вида some.domain/resource.axd обрабатываются отдельным обработчиком. А так как механизмом MVC Framework игнорируются запросы только к существующим файлам, то определяется, что все запросы к AXD-ресурсам должны обрабатываться не механизмом MVC Framework, но другим обработчиком, который в данном случае определен в web.config как System.Web.Handlers.ScriptResourceHandler.

Рассмотрим простейший пример создания маршрута для игнорирования. Для этого разберем следующую ситуацию: как известно, каждый браузер при переходе на любой сайт старается найти в корне этого сайта специальную иконку, которая сопоставлена с сайтом. Эта иконка обычно представляет собой файл с названием favicon.ico и используется браузером для отображения в своем интерфейсе, например в списке избранного, если сайт туда добавлен.

Когда вы не определяете такие иконки на своем сайте либо определяете их через разметку страницы, тогда запрос к отсутствующему файлу приведет к тому, что будет вызван механизм MVC Framework. Для того чтобы такие запросы клиентских браузеров не вызывали работу механизма MVC Framework, нам следует добавить следующее правило игнорирования маршрута:

routes.IgnoreRoute("favicon.ico");

Этим определением мы указываем, что запрос вида http://some.domain/favicon.ico должен игнорироваться. Для более сложного варианта определим игнорирование маршрута к файлу favicon.ico для всех папок нашего сайта, а не только к корневой папке:

routes.IgnoreRoute("{*param}",

    new { param = @"(.*/)?favicon.ico(/.*)?" });

Здесь мы уже используем два параметра, первый со значением {*param} определяет шаблон маршрута, в данном случае его можно описать как все пути, которые завершаются со значением параметра шаблона param. Второй параметр new{param=@" (.*/) ?favicon.ico (/.*)?"} задает определение параметра шаблона param в виде регулярного выражения, т. е. определяет, на что должна заканчиваться строка URL в клиентском запросе для того, чтобы маршрут игнорирования сработал.

Рассмотренные примеры работали в режиме, когда MVC Framework и механизм маршрутизации ASP.NET игнорируют запросы к существующим файлам. Однако имеет смысл перестать игнорировать такие запросы и самому обрабатывать запросы к группе определенных файлов, разрешая к ним доступ или запрещая его. Для того чтобы регулировать поведение маршрутизации ASP.NET в плане игнорирования запросов к существующим файлам, у класса RouteCollection, определяющего коллекцию маршрутов, есть булево свойство RouteExistingFiles. По умолчанию значение RouteExistingFiles установлено в false. Установив его в значение true, вы заставите механизм маршрутизации ASP.NET обрабатывать все запросы, в том числе и те, которые ведут на существующие файлы.

 routes.RouteExistingFiles = true;

После этого любой запрос на локальный файл приведет к возникновению исключения и возврату клиенту информации об ошибке 404 "Страница не найдена". Теперь вы можете быть уверены в том, что пользователь не получит доступа ни к одному отдельному файлу на вашем сайте. Зато, определив правила игнорирования, для групп таких файлов вы сможете открыть доступ.

Рассмотрим пример использования игнорирования маршрутов для открытия доступа. Предположим, что в вашем проекте все стили, используемые на веб-страницах, расположены в папке some.domain/styles, а скрипты в some.domain/scripts. Теперь, чтобы открыть к ним доступ при включенном механизме маршуртизации для имеющихся файлов, достаточно определить следующие маршруты игнорирования:

routes.IgnoreRoute("styles/{*pathInfo}");

routes.IgnoreRoute("scripts/{*pathInfo}");

И в заключение рассмотрим последний простой пример, который часто используется на практике. Зачастую структура вашего проекта может содержать файлы с самыми разнообразными расширениями, которые вы отдаете пользователю по его запросу. Скажем, это может быть статический файл помощи с HTML-разметкой или текстовый файл с расширением txt, который может содержать информацию о лицензии. Механизм игнорирования маршрутов позволяет легко предоставить доступ к таким файлам даже при включенном механизме маршрутизации для имеющихся файлов. Например, следующий фрагмент кода разрешает загружать HTML- и TXT-файлы из корня сайта:

routes.IgnoreRoute("{file}.txt");

routes.IgnoreRoute("{file}.htm");

routes.IgnoreRoute("{file}.html");

Игнорирование маршрутов — это сильный инструмент, который по умолчанию позволяет создать правила для исключения из обработки запросов к виртуальным или несуществующим файлам. А после изменения RouteExistingFiles и включения механизма маршрутизации для всех запросов игнорирование маршрутов позволяет защитить все файлы от доступа и сформировать свои правила доступа к файлам на сайте.

 

Советы по использованию маршрутов

 

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

 

Маршруты и валидация запросов

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

Представьте себе ситуацию, когда вы создаете маршрут, одним из параметров которого является число, определяющее год. Совершенно очевидно, что такое число имеет допустимые рамки, и его валидность может быть проверена еще на этапе сопоставления маршрутов. В листинге 6.1 приведен фрагмент кода, который определяет экземпляр класса, реализующий IRouteConstraint для такого рода проверки параметра.

Листинг 6.1. Класс, реализующий IRouteConstraint

public class YearConstraint : IRouteConstraint {

  public bool Match(HttpContextBase httpContext,

      Route route,

      string parameterName,

      RouteValueDictionary values,

      RouteDirection routeDirection)

  {

    if (parameterName == "year")

    {

      try

      {

        object yearValue = values["year"];

        int year = Convert.ToInt32(yearValue);

        if (year >= 1900 && year <= 2100) return true;

      }

      catch

      {

        return false;

      }

    }

    return false;

  }

}

Для того чтобы использовать данный класс, необходимо определить маршрут с параметрами ограничения примерно так, как показано во фрагменте:

var constraints = new RouteValueDictionary

{

  {"year", new YearConstraint()}

};

routes.MapRoute("YearData",

                "{controller}/{action}/{year}",

                new { controller = "Data", action = "Index" },

                new { year = new YearConstraint() }

);

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

 

Хранение маршрутов в базе данных

Порой определение маршрутов в файле global.asax — это недостаточно гибкий механизм для решения задачи, встающей перед разработчиком. Допустим, стоит задача передать права на создание, редактирование и удаление маршрутов некоему администратору. В таком случае предоставление доступа на редактирование файла global.asax может нарушить безопасность системы. В подобных ситуациях и когда необходимо выделить механизм доступа к созданию и редактированию маршрутов, обычно создается отдельное хранилище для данных маршрутов, которое используется для инициализации механизма маршрутизации при старте приложения.

Таким хранилищем может быть любой источник данных: от текстовых файлов и XML до отдельной базы данных, в таблицах которой хранятся определения маршрутов. Рассмотрим создание такой базы данных для хранения простейших маршрутов.

Для начала определим две таблицы, Routeitem и Param, для хранения данных о маршрутах так, как продемонстрировано на рис. 6.1 и 6.2.

Таблица Routeitem будет содержать информацию о маршрутах, а Param — соответственно, о параметрах для каждого маршрута.

Чтобы добавить в проект поддержку этих таблиц, создадим с помощью мастера Linq To Sql классы для работы. Получившийся DBML-файл будет представлять схему, показанную на рис. 6.3.

После создания классов Linq To Sql настало время создать логику по регистрации маршрутов в механизме маршрутизации. Для этого создадим класс DatabaseRoutes, представленный в листинге 6.2.

Листинг 6.2. Класс DatabaseRoutes

using System.Linq;

using System.Web.Mvc;

using System.Web.Routing;

namespace Routing {

  public class DatabaseRoutes

  {

    readonly RouteDbDataContext db = new RouteDbDataContext();

    private readonly RouteCollection Routes;

    public DatabaseRoutes(RouteCollection routes)

    {

      Routes = routes;

    }

    public void Register()

    {

      var routes = db.RouteItems.Where(x => x.State).OrderBy(x => x.LoadOrder);

      foreach (var route in routes)

      {

        RouteValueDictionary defaults = new RouteValueDictionary();

        foreach (var param in route.Params)

          defaults.Add(param.ParamKey, param.ParamValue);

        Routes.Add(route.Name, new Route(

            route.Template,

            defaults,

            new MvcRouteHandler()

        ));

      }

    }

  }

}

Обратите внимание, конструктор-класс DatabaseRoute принимает параметр типа RouteColection, для того чтобы произвести регистрацию маршрутов. Единственный метод класса Register предназначен для выборки данных из базы данных и инициализации механизма маршрутизации в виде полученного экземпляра RouteColection.

Для того чтобы использовать наш класс, необходимо заполнить таблицы значениями. Сделаем это для определения стандартного маршрута Default так, как представлено на рис. 6.4 и 6.5.

После того как данные внесены в базу данных, можно интегрировать механизм в проект. Для этого следует в global.asax удалить старое определение маршрута Default и добавить инициализацию класса DatabaseRotes так, как показано в следующем фрагменте кода:

public static void RegisterRoutes(RouteCollection routes)

{

  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

  DatabaseRoutes dbRoutes = new DatabaseRoutes(routes);

  dbRoutes.Register();

}

Если вы сделали все правильно, то результатом будет работа вашего проекта на основе маршрута, полученного из базы данных. Теперь, реализовав каким-либо образом доступ к базе данных маршрутов, вы получите возможность предоставить функции редактирования маршрутов администратору без модификации исходных кодов в виде global.asax.

Представленный пример предполагает реализацию хранения только простейших маршрутов. Для хранения данных об ограничениях, данных DataTokens или данных игнорирования маршрутов механизм придется расширить, что не должно составить труда.

 

Маршрутизация и тестирование

 

Когда дело касается модульного тестирования механизмов маршрутизации, то определение области тестирования не выглядит очевидным. Что необходимо тестировать? В простейшем случае, когда ваш проект на MVC Framework содержит всего один маршрут Default, определенный по умолчанию, модульное тестирование теряет большую часть смысла. Но в случаях, когда маршрутов в проекте много, повсеместно используется ограничение и игнорирование маршрутов, модульное тестирование обретает широкую область для применения.

Здесь мы на нескольких примерах рассмотрим, как включить в свой проект тесты маршрутов, ограничений и маршрутов игнорирования.

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

Для тестирования маршрутов воспользуемся тремя популярными средствами:

□ NUnit () — альтернативное средство тестирования;

□ RhinoMocks () — позволяет создавать фальшивые, так называемые, "мок-объекты";

□ MvcContrib () — содержит функционал, расширяющий возможности MVC Framework, в том числе и в сфере модульного тестирования.

 

Подготовка инструментов

Для того чтобы использовать инструменты тестирования в своем проекте, необходимо проделать следующие действия. Во-первых, установите пакет тестирования NUnit. Во-вторых, добавьте в проект ссылки на сборки:

MvcContrib.TestHelper.nunit.framework и Rhino.Mocks так, как показано на рис. 6.6.

После этих двух шагов вы готовы для создания модульных тестов механизма маршрутизации. Для тестирования в MvcContrib существует набор специальных методов расширения, которые позволяют значительно упростить создание тестов. Это метод ShouldMapTo, который позволяет протестировать строку определения маршрута, и метод ShouldBeIgnored, который позволяет протестировать правило игнорирования маршрута.

 

Создание тестов

Для демонстрации создания модульных тестов первым делом определим набор маршрутов, которые будут подвергнуты тестам. Далее представлен фрагмент кода с определением ряда маршрутов:

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(

       "Product",

       "Product/{id}",

       new { controller = "Product", action = "GetById" }

);

routes.MapRoute("ProductList",

       "ProductList/{year}",

       new { controller = "Product", action = "List" },

       new { year = new YearConstraint() }

);

routes.MapRoute(

       "Default",

       "{controller}/{action}/{id}",

       new { controller = "Home", action = "Index", id = "" }

);

Как вы можете видеть, в коде определяется три маршрута: Product, ProductList и маршрут по умолчанию Default. Кроме того, определено стандартное правило игнорирования маршрута для игнорирования запросов к

AXD-ресурсам. Обратите внимание, что маршрут с названием ProductList содержит ограничение для параметра year (ранее в этой главе мы рассматривали это ограничение).

Для определения тестов необходимо создать простой класс, например, представленный во фрагменте:

namespace Routing {

  using System.Web.Routing;

  using MvcContrib.TestHelper;

  using NUnit.Framework;

  using Routing.Controllers;

  [TestFixture]

  public class TestRoutes

  {

  }

}

Обратите внимание на использование атрибута TestFixture. Это атрибут инструмента NUnit, который позволяет определить класс с набором тестов. Добавим в класс простой тест:

[Test]

public void TestSimpleRoute()

{

  "~/".Route().ShouldMapTo(x => x.Index());

}

Этот тест направлен на проверку определения маршрута при доступе к сайту по URL без каких-либо параметров или относительных путей. Здесь ожидается, что по умолчанию должен сработать маршрут Default и выполниться действие Index контроллера HomeController.

Для того чтобы запустить наш тест, необходимо скомпилировать проект с тестом и запустить среду NUnit, главное окно которой представлено на рис. 6.7.

C помощью пункта меню File | Open Project добавим в среду сборку нашего проекта тестирования. В моем случае это сборка Routing.dll. После того как сборка загрузится, нажмите кнопку Run для запуска тестов. Как можно убедиться, наш тест не пройден (рис. 6.8).

Для того чтобы тест был пройден, в наш класс модульного тестирования нужно добавить следующий код, который инициализирует механизм

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

[TestFixtureSetUp]

public void SetUp()

{

  MvcApplication.RegisterRoutes(RouteTable.Routes);

}

[TestFixtureTearDown]

public void TearDown()

{

  RouteTable.Routes.Clear();

}

Обратите внимание на атрибуты TestFixtureSetUp и TestFixtureTearDown. Эти атрибуты необходимы для определения кода, который будет вызван перед тестом и сразу после него. В нашем случае перед тестами создаются маршруты, а после теста — разрушаются.

Скомпилируем проект и запустим тест еще раз. На этот раз тест пройден (рис. 6.9).

Для интереса вы можете теперь попробовать модифицировать маршрут Default так, чтобы наш тест перестал работать, например, можно переименовать название действия по умолчанию с Index на Index2. После этого тест будет провален.

Добавим тест для проверки маршрута под названием Product, в нем маршрут определяется простым шаблоном без участия имени действия:

[Test]

public void TestProduct()

{

  "~/Product/750".Route()

         .ShouldMapTo(x => x.GetById(750));

}

Добавим еще один тест, на этот раз для проверки игнорирования маршрута для AXD-запросов:

[Test]

public void TestIgnoreAxd()

{

  "~/someroutetoigonre.axd".ShouldBeIgnored();

}

Запустите тесты и убедитесь, что они пройдены. Обратите внимание на используемый метод расширения ShouldBeIgnored, который предназначен как раз для тестирования игнорирования маршрутов.

Последний наш тест будет направлен на проверку ограничений маршрута. Для тестирования маршрута ProductList определим два следующих теста:

[Test]

public void TestProductListValidYear()

{

"~/ProductList/2009".Route()

         .ShouldMapTo(x => x.List(2009));

}

[Test]

public void TestProductListInvalidYear()

{

           Assert.AreNotEqual("~/ProductList/1800".Route().Values["controller"],

                    "Product");

}

Первый тест TestProductListValidYear производит знакомые нам действия, при правильном значении параметра year, ограничение не должно действовать и маршрут должен быть сопоставлен контроллеру Product и действию List. Второй тест TestProductListInvalidYear, наоборот, проверяет поведение маршрута при неверном с точки зрения ограничения параметре year (он должен быть более или равен 1900). Для того чтобы протестировать этот момент, сравниваются имя сопоставленного контроллера и имя контроллера Product. Для успешного прохождения теста они не должны быть равны. Теперь, если убрать ограничение из маршрута, тест будет провален.

После написания всех тестов и запуска NUnit мы должны получить следующую картину успешного прохождения всех тестов (рис. 6.10).

В листинге 6.3 приведен код перечисленных ранее тестов для тех, кто использует тестирование на базе встроенного в Visual Studio инструмента MSTest. Различий минимум и все они касаются именования атрибутов, которое в NUnit отличается от MSTest.

Листинг 6.3. Тестирование с помощью MSTest

namespace Routing

{

  using System.Web.Routing;

  using Microsoft.VisualStudio.TestTools.UnitTesting;

  using MvcContrib.TestHelper; using Routing.Controllers;

  [TestClass]

  public class TestRoutesMSTest

  {

    [TestMethod]

    public void TestSimpleRoute()

    {

      "~/".Route().ShouldMapTo(x => x.Index());

    }

    [TestInitialize]

    public void SetUp()

    {

      MvcApplication.RegisterRoutes(RouteTable.Routes);

    }

    [TestCleanup]

    public void TearDown()

    {

      RouteTable.Routes.Clear();

    }

    [TestMethod]

    public void TestProduct()

    {

      "~/Product/750".Route()

           .ShouldMapTo(x => x.GetById(750));

    }

    [TestMethod]

    public void TestIgnoreAxd()

    {

      "~/someroutetoigonre.axd".ShouldBeIgnored();

    }

    [TestMethod]

    public void TestProductListValidYear()

    {

      "~/ProductList/2009".Route()

             .ShouldMapTo(x => x.List(2009));

    [TestMethod]

    public void TestProductListInvalidYear()

    {

      Assert.AreNotEqual("~/ProductList/1800".Route()

                  .Values["controller"], "Product");

    }

  }

}

 

Утилита ASP.NET Routing Debugger

Для тестирования и проверки правильности создания маршрутов в вашем приложении может пригодиться небольшой инструмент от разработчиков MVC Framework. Данный инструмент позволяет в режиме отладки проверить все зарегистрированные маршруты на работоспособность. Кроме того, этот инструмент поможет посмотреть на то, как и в каком порядке реагируют ваши маршруты на запросы с различными адресами URL.

Примечание

Инструмент ASP.NET Routing Debugger доступен для скачивания по адресу rl-routing-debugger.aspx.

*****************************

Для работы с Routing Debugger необходимо добавить в проект ссылку на сборку RouteDebug.dll и в файле Global.asax добавить одну строку кода в методе Application_Start так, как показано в следующем фрагменте:

protected void Application_Start()

{

  RegisterRoutes(RouteTable.Routes);

  RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);

}

После этого, запущенный проект вместо ожидаемой страницы выведет специальную страницу с отчетом о зарегистрированных в проекте маршрутах и их параметрах (рис. 6.11).

ASP.NET Routing Debugger может быть полезен, когда ваш проект содержит массу определенных маршрутов. В таком случае вы получаете наглядный инструмент, который покажет вам порядок и параметры маршрутов, и их реакцию на любой запрос. Используя Routing Debugger, вам не придется долго ломать голову над вопросом: "Почему на этот запрос вызывается этот маршрут?"

 

Заключение

Маршрутизация — это значительный элемент ASP.NET и базовый механизм для работы MVC Framework. Маршрутизация позволяет организовать обработку пользовательских запросов в виде, удобном для построения приложения, согласно паттерну MVC. С использованием маршрутизации в MVC Framework можно отойти от привязки запроса к конкретной странице ASPX и использовать строку запроса как описатель необходимого для вызова действия и контроллера.

С помощью механизмов маршрутизации можно управлять тем, как обрабатываются клиентские запросы, создавать ограничения на запросы или создать список запросов, которые вовсе будут игнорироваться. Таким образом, понимание работы маршрутизации — это важное требование при разработке приложения на базе MVC Framework.

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