Шаблоны URL
171ASP.NET --- ASP.NET MVC 5 --- Шаблоны URL
До появления инфраструктуры MVC Framework платформа ASP.NET предполагала наличие прямых отношений между запрашиваемыми URL и файлами на жестком диске сервера. Работа сервера заключалась в получении запроса от браузера и доставке вывода из соответствующего файла.
Такой подход хорошо работает для инфраструктуры Web Forms, в которой каждая страница ASPX представляет собой и файл, и самодостаточный ответ на запрос. Это не имеет смысла в приложении MVC, где запросы обрабатываются методами действий из классов контроллеров, и однозначное соответствие между запросами и файлами на диске отсутствует.
Для обработки URL в MVC платформа ASP.NET использует систему маршрутизации. В этой и последующих статьях мы покажем, как использовать систему маршрутизации для обеспечения мощной и гибкой обработки URL в разрабатываемых проектах. Вы увидите, что система маршрутизации позволяет создавать любой требуемый шаблон URL и выражать его в чистой и лаконичной манере. Система маршрутизации обеспечивает выполнение двух функций:
Исследование входящих URL и выяснение, для каких контроллеров и действий они предназначены. Как и следовало ожидать, это то, что система маршрутизации должна делать при получении клиентского запроса.
Генерация исходящих URL. Это URL, которые появляются в HTML-разметке, визуализированной из представлений, так что специфическое действие может быть инициировано, когда пользователь производит щелчок на ссылке (в этот момент ссылка снова становится входящим URL).
Пример приложения
Для демонстрации работы системы маршрутизации нам нужен проект, к которому мы сможем добавлять маршруты. Мы создали новое приложение MVC с использованием шаблона Empty (Пустой) и назвали проект UrlsAndRoutes. Кроме того, в решение Visual Studio добавлен тестовый проект по имени UrlsAndRoutes.Tests за счет отметки флажка Add unit tests (Добавить модульные тесты), как показано на рисунке ниже:
Создание модульных тестов вручную уже демонстрировалось в статьях, посвященных интернет-магазину GameStore, и в настоящий момент мы получаем аналогичный результат с автоматической поддержкой ссылок между проектами. Понадобится только добавить Moq, так что введите в окне консоли NuGet следующую команду:
Install-Package Moq -version 4.1.1309.1617 -projectname UrlsAndRoutes.Tests
Для демонстрации работы средства маршрутизации мы собираемся добавить в пример приложения несколько простых контроллеров. Нас интересует только способ, по которому URL интерпретируются с целью вызова методов действий, поэтому мы используем в качестве модели представлений строковые значения в объекте ViewBag, которые сообщают имена контроллера и метода действия.
Для начала создайте контроллер Home и приведите его код в соответствие с примером:
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Controller = "Home";
ViewBag.Action = "Index";
return View("ActionName");
}
}
}
Создайте контроллер Customer с кодом, показанным в примере ниже:
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers
{
public class CustomerController : Controller
{
public ActionResult List()
{
ViewBag.Controller = "Customer";
ViewBag.Action = "List";
return View("ActionName");
}
public ActionResult Index()
{
ViewBag.Controller = "Customer";
ViewBag.Action = "Index";
return View("ActionName");
}
}
}
Создайте контроллер по имени Admin и отредактируйте его согласно примеру:
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers
{
public class AdminController : Controller
{
public ActionResult Index()
{
ViewBag.Controller = "Admin";
ViewBag.Action = "Index";
return View("ActionName");
}
}
}
Во всех методах действий этих контроллеров указано представление ActionName, что позволяет определить одно представление и применять его везде в примере приложения. Создайте внутри папки Views папку под названием Shared и добавьте в нее новый файл представления по имени ActionName.cshtml с содержимым, приведенным в примере ниже:
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Представление ActionName</title>
</head>
<body>
<div>Контроллер: <strong>@ViewBag.Controller</strong></div>
<div>Метод действия: <strong>@ViewBag.Action</strong></div>
</body>
</html>
Как упоминалось ранее, среда Visual Studio пытается выяснить URL, запрашиваемый в браузере, на основе файла, который редактируется во время запуска отладчика. Это быстро начинает утомлять и данную функцию лучше отключить. Выберите пункт UrlsAndRoutes Properties (Свойства UrlsAndRoutes) в меню Project среды Visual Studio, в открывшемся диалоговом окне перейдите на вкладку Web и отметьте переключатель Specific Page (Определенная страница) в категории Start Action (Начальное действие). Вводить какое-либо значение не нужно - достаточно только выбора переключателя.
Запустив этот пример приложения, вы получите результат, показанный на рисунке ниже:
Введение в шаблоны URL
Система маршрутизации работает с использованием набора маршрутов. Эти маршруты вместе образуют схему URL, или схему для приложения, представляющую собой набор URL, которые приложение будет распознавать и реагировать на них.
Набирать вручную все индивидуальные URL, которые планируется поддерживать в приложении, не придется. Вместо этого каждый маршрут содержит шаблон URL, с которым сравниваются входящие URL. Если URL соответствует шаблону, он используется системой маршрутизации для обработки этого URL. Давайте начнем с URL для рассматриваемого примера приложения:
http://mysite.com/Admin/Index
URL могут быть разбиты на сегменты. Сегменты - это части URL за исключением имени хоста и строки запроса, которые отделяются друг от друга символом /. В приведенном выше примере URL имеются два сегмента, как показано на рисунке ниже:
Первый сегмент содержит слово Admin, а второй - слово Index. В глазах человека совершенно очевидно, что первый сегмент имеет отношение к контроллеру, а второй - к действию. Однако, естественно, это отношение должно быть выражено понятным для системы маршрутизации образом. Это делается с помощью следующего шаблона URL:
{controller}/{action}
При обработке входящего запроса задача системы маршрутизации заключается в сопоставлении запрошенного URL с шаблоном и в последующем извлечении из URL значений для переменных сегментов, определенных в шаблоне. Переменные сегментов выражаются с использованием фигурных скобок (символов { и }). В приведенном примере шаблона определены две переменные сегментов с именами controller и action, поэтому значением переменной сегмента controller будет Admin, а значением переменной сегмента action - Index.
Выше было сказано о сопоставлении с каким-то шаблоном, потому что приложение MVC обычно будет иметь несколько маршрутов, и система маршрутизации будет сравнивать входящий URL с шаблоном URL каждого маршрута до тех пор, пока не обнаружит совпадение.
Системе маршрутизации ничего не известно о контроллерах и действиях. Она просто извлекает значения для переменных сегментов. Позже в процессе обработки запросов, когда запрос достигнет собственно инфраструктуры MVC Framework, производится присваивание этих значений переменным controller и action. Именно поэтому система маршрутизации может использоваться с инфраструктурами Web Forms и Web API.
По умолчанию шаблон URL будет соответствовать любому URL, который имеет подходящее количество сегментов. Например, шаблон "{controller}/{action}" будет соответствовать любому URL с двумя сегментами, как показано в таблице ниже:
Переменные сегментов | URL запроса |
---|---|
http://localhost:64399/Admin/Index | controller = Admin, action = Index |
http://localhost:64399/Index/Admin | controller = Index, action = Admin |
http://localhost:64399/Apples/Oranges | controller = Apples, action = Oranges |
http://localhost:64399/Admin | Соответствия нет - сегментов слишком мало |
http://localhost:64399/Admin/Index/Football | Соответствия нет - сегментов слишком много |
В этой таблице отражены два ключевых аспекта поведения шаблонов URL:
Шаблоны URL являются консервативными, и будут совпадать только с теми URL, которые имеют то же самое количество сегментов, что и шаблон. Это можно наблюдать в четвертом и пятом примерах в таблице.
Шаблоны URL являются либеральными. Если URL имеет правильное количество сегментов, шаблон извлечет значение для переменной сегмента, каким бы оно ни было.
Эти два стандартных аспекта поведения являются ключевыми в понимании функционирования шаблонов URL. Позже будет показано, как изменить такие стандартные аспекты поведения.
Как уже упоминалось, системе маршрутизации ничего не известно о приложении MVC, поэтому шаблоны URL будут давать совпадение, даже если для значений, извлеченных из URL, нет соответствующих контроллеров или действий. Это демонстрируется во втором примере в таблице. Здесь в URL сегменты Admin и Index поменялись местами, и также поменялись местами извлеченные из URL значения, хотя в действительности контроллер Index в рассматриваемом примере приложения не существует.
Создание и регистрация простого маршрута
Имея в виду шаблон URL, его можно использовать для определения маршрута (Route). Маршруты определяются в файле RouteConfig.cs, который находится в папке App_Start проекта. Начальное содержимое, определяемое Visual Studio для этого файла, показано в примере:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes
{
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 }
);
}
}
}
Статический метод RegisterRoutes(), определенный в файле RouteConfig.cs, вызывается из файла Global.asax.cs, который настраивает ряд основных средств MVC при запуске приложения. Стандартное содержимое файла Global.asax.cs приведено в примере ниже:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
}
}
Метод Application_Start() вызывается лежащей в основе платформой ASP.NET, когда приложение MVC запускается в первый раз, что приводит к вызову метода RouteConfig.RegisterRoutes(). Параметром этого метода является значение статического свойства RouteTable.Routes, представляющее собой экземпляр класса RouteCollection, который вскоре будет описан.
Вызов метода AreaRegistration.RegisterAllAreas(), производимый внутри метода Application_Start(), настраивает связанное средство, называемое областями, которое рассматривается позже.
В примере ниже показано, как создать маршрут с использованием примера шаблона URL из предыдущего раздела в методе RegisterRoutes() внутри файла RouteConfig.cs (Чтобы можно было сосредоточиться только на текущем примере, все остальные операторы этого метода были удалены.)
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler());
routes.Add(myRoute);
}
}
}
Мы создаем новый объект Route, передавая его конструктору в качестве параметра шаблон URL, который выражен в виде строки. Кроме того, конструктору передается экземпляр MvcRouteHandler. Разные технологии ASP.NET предоставляют различные классы для настройки поведения маршрутизации, и MvcRouteHandler - это класс, предназначенный для приложений ASP.NET MVC. Созданный маршрут добавляется к объекту RouteCollection с помощью метода Add().
Более удобный способ регистрации маршрутов предусматривает применение метода MapRoute(), определенного в классе RouteCollection. В примере ниже показано, как зарегистрировать наш маршрут с помощью этого метода; результат будет таким же, как в предыдущем примере, но используемый синтаксис намного яснее:
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("MyRoute", "{controller}/{action}");
}
}
}
Этот подход несколько компактнее, в основном потому что не приходится создавать экземпляр класса MvcRouteHandler (это делается автоматически). Метод MapRoute() предназначен исключительно для применения с приложениями MVC Приложения ASP.NET Web Forms могут пользоваться методом MapPageRoute(), который также определен в классе RouteCollection.
Использование простого маршрута
Увидеть эффект от внесенных в маршрутизацию изменений можно, запустив пример приложения. Когда попробовать в браузере перейти на корневой URL для приложения, возникнет ошибка - но если переходить по маршруту, соответствующему шаблону {controller}/{action}, получается результат, подобный показанному на рисунке:
Наш простой маршрут не сообщает MVC Framework о том, как реагировать на запросы корневого URL, и поддерживает только единственный весьма специфический шаблон URL. Мы должны временно забыть о функциональности, которую Visual Studio добавляет в файл RouteConfig.cs при создании проекта MVC. В следующих статьях мы покажем, как строить более сложные шаблоны и маршруты.
Модульное тестирование: тестирование входящих URL
Рекомендуется проводить модульное тестирование маршрутов, чтобы удостовериться в корректной обработке ими входящих URL, даже если решено не выполнять модульное тестирование для остальной части приложения. В крупных приложениях схемы URL могут быть весьма сложными, и очень просто создать что-нибудь приводящее к неожиданным результатам.
Ранее мы избегали создания общих вспомогательных методов для совместного использования в разных тестах, стараясь сохранить каждый модульный тест самодостаточным. Здесь же мы примем другой подход. Тестирование схемы маршрутизации для приложения наиболее легко проводить, когда множество тестов упакованы в один метод, и это сделать намного проще с помощью ряда вспомогательных методов.
Для тестирования маршрутов необходимо создать имитации трех классов из MVC Framework: HttpRequestBase, HttpContextBase и HttpResponseBase. (Последний класс требуется для тестирования исходящих URL, которые будут рассматриваться позже.) Вместе эти классы воссоздают достаточную часть инфраструктуры MVC для поддержки системы маршрутизации.
Мы добавили в проект модульного тестирования UrlsAndRoutes.Tests новый файл модульных тестов по имени RouteTests.cs. Первым определением в нем является вспомогательный метод, который создает имитированные объекты HttpContextBase, как показано ниже:
using Moq;
using System;
using System.Reflection;
using System.Web;
using System.Web.Routing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UrlsAndRoutes.Tests
{
[TestClass]
public class RouteTests
{
private HttpContextBase CreateHttpContext(string targetUrl = null,
string httpMethod = "GET") {
// Создать имитированный запрос
Mock<HttpRequestBase> mockRequest = new Mock<HttpRequestBase>();
mockRequest.Setup(m => m.AppRelativeCurrentExecutionFilePath)
.Returns(targetUrl);
mockRequest.Setup(m => m.HttpMethod).Returns(httpMethod);
// Создать имитированный ответ
Mock<HttpResponseBase> mockResponse = new Mock<HttpResponseBase>();
mockResponse.Setup(m => m.ApplyAppPathModifier(
It.IsAny<string>())).Returns<string>(s => s);
// Создать имитированный контекст, используя запрос и ответ
Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
mockContext.Setup(m => m.Request).Returns(mockRequest.Object);
mockContext.Setup(m => m.Response).Returns(mockResponse.Object);
// Вернуть имитированный контекст
return mockContext.Object;
}
}
}
На самом деле код только выглядит сложным. Тестируемый URL указывается в свойстве AppRelativeCurrentExecutionFilePath класса HttpRequestBase, а HttpRequestBase - в свойстве Request имитированного класса HttpContextBase. Следующий вспомогательный метод позволяет протестировать маршрут:
// ...
private void TestRouteMatch(string url, string controller, string action,
object routeProperties = null, string httpMethod = "GET")
{
// Организация
RouteCollection routes = new RouteCollection();
RouteConfig.RegisterRoutes(routes);
// Действие - обработка маршрута
RouteData result
= routes.GetRouteData(CreateHttpContext(url, httpMethod));
// Утверждение
Assert.IsNotNull(result);
Assert.IsTrue(TestIncomingRouteResult(result, controller,
action, routeProperties));
}
Параметры этого метода позволяют указать тестируемый URL, ожидаемые значения для переменных сегментов controller и action, а также экземпляр object, содержащий ожидаемые значения для любых дополнительных переменных, которые были определены. Позже мы покажем, как создавать такие переменные. Мы также определим параметр для метода HTTP, который объясним в статье "Ограничение маршрутов".
При сравнении результата, полученного от системы маршрутизации, со значениями переменных сегментов, которые мы ожидаем, метод TestRouteMatch() полагается на другой метод - TestIncomingRouteResult(). Этот метод применяет рефлексию .NET, так что мы можем использовать анонимный тип для выражения любых дополнительных переменных сегментов. Не переживайте, если этот метод кажется бессмысленным - он предназначен только для удобства тестирования и для понимания MVC не обязателен.
Ниже приведен код метода TestIncomingRouteResult():
// ...
private bool TestIncomingRouteResult(RouteData routeResult,
string controller, string action, object propertySet = null)
{
Func<object, object, bool> valCompare = (v1, v2) =>
{
return StringComparer.InvariantCultureIgnoreCase
.Compare(v1, v2) == 0;
};
bool result = valCompare(routeResult.Values["controller"], controller)
&& valCompare(routeResult.Values["action"], action);
if (propertySet != null)
{
PropertyInfo[] propInfo = propertySet.GetType().GetProperties();
foreach (PropertyInfo pi in propInfo)
{
if (!(routeResult.Values.ContainsKey(pi.Name)
&& valCompare(routeResult.Values[pi.Name],
pi.GetValue(propertySet, null))))
{
result = false;
break;
}
}
}
return result;
}
// ...
Нам также необходим метод для проверки, что URL не работает. Как вы увидите, это может быть важной частью определения схемы URL:
// ...
private void TestRouteFail(string url)
{
// Организация
RouteCollection routes = new RouteCollection();
RouteConfig.RegisterRoutes(routes);
// Действие - обработка маршрута
RouteData result = routes.GetRouteData(CreateHttpContext(url));
// Утверждение
Assert.IsTrue(result == null || result.Route == null);
}
// ...
Методы TestRouteMatch() и TestRouteFail() содержат вызовы метода Assert(), которые генерируют исключение, если утверждение терпит неудачу. Поскольку исключения C# распространяются вверх по стеку вызовов, мы можем создать простые тестовые методы, которые будут проверять набор URL, и получить требуемое поведение тестирования.
Ниже приведен код тестового метода, проверяющего маршрут, который был определен ранее:
// ...
[TestMethod]
public void TestIncomingRoutes()
{
// Проверить URL который мы надеемся получить
TestRouteMatch("~/Admin/Index", "Admin", "Index");
// Проверить значения, получаемые из сегментов
TestRouteMatch("~/One/Two", "One", "Two");
// Удостовериться, что слишком много или слишком мало сегментов
// не приводят к совпадению
TestRouteFail("~/Admin/Index/ThirdSegment");
TestRouteFail("~/Admin");
}
// ...
Этот тест использует метод TestRouteMatch() для проверки ожидаемого URL и также проверяет URL в том же самом формате, чтобы удостовериться в корректном получении значений controller и action с применением сегментов URL. Мы также с помощью метода TestRouteFail убеждаемся, что приложение не принимает URL, которые имеют неподходящее количество сегментов. Во время тестирования URL должен снабжаться префиксом в виде тильды (~) - именно так платформа ASP.NET представляет URL системе маршрутизации.
Обратите внимание, что мы не нуждаемся в определении маршрутов в тестовых методах. Причина в том, что мы загружаем их напрямую с помощью метода RegisterRoutes() в классе RouteConfig.