Шаблоны URL

171

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

Такой подход хорошо работает для инфраструктуры Web Forms, в которой каждая страница ASPX представляет собой и файл, и самодостаточный ответ на запрос. Это не имеет смысла в приложении MVC, где запросы обрабатываются методами действий из классов контроллеров, и однозначное соответствие между запросами и файлами на диске отсутствует.

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

Пример приложения

Для демонстрации работы системы маршрутизации нам нужен проект, к которому мы сможем добавлять маршруты. Мы создали новое приложение MVC с использованием шаблона Empty (Пустой) и назвали проект UrlsAndRoutes. Кроме того, в решение Visual Studio добавлен тестовый проект по имени UrlsAndRoutes.Tests за счет отметки флажка Add unit tests (Добавить модульные тесты), как показано на рисунке ниже:

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

Создание модульных тестов вручную уже демонстрировалось в статьях, посвященных интернет-магазину 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 имеются два сегмента, как показано на рисунке ниже:

Сегменты в примере 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
Переменные сегментов 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. Позже будет показано, как изменить такие стандартные аспекты поведения.

Как уже упоминалось, системе маршрутизации ничего не известно о приложении 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.

Пройди тесты
Лучший чат для C# программистов