Настройка системы маршрутизации

144

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

Создание специальной реализации RouteBase

Если способ, которым стандартные объекты Route сопоставляются с URL, не устраивает или нужно реализовать что-то необычное, можно создать альтернативный класс, производный от RouteBase. Это обеспечит контроль над тем, как происходят сопоставления с URL, каким образом извлекаются параметры и как генерируются URL. При наследовании класса от RouteBase понадобится реализовать два метода:

GetRouteData

Это механизм, посредством которого работает сопоставление входящих URL. Инфраструктура вызывает этот метод для каждого элемента RouteTable.Routes по очереди, пока какой-то из них не возвратит отличное от null значение.

GetVirtualPath

Это механизм, посредством которого работает генерация исходящих URL.

Для демонстрации такой настройки мы создадим класс RouteBase, который будет обрабатывать запросы к унаследованным URL. Предположим, что какое-то существующее приложение переехало на MVC Framework, но некоторые пользователи в прошлом создали закладки на URL из предыдущей версии или жестко закодировали их в сценариях. Мы по-прежнему хотим поддерживать эти старые URL. Данную ситуацию можно было бы решить с помощью обычной системы маршрутизации, но сейчас нам интересно применить подход с использованием RouteBase.

Первым делом понадобится создать контроллер, который будет получать запросы к унаследованным URL. Этот контроллер называется LegacyController.cs, а его код показан в примере ниже:

using System.Web.Mvc;

namespace UrlsAndRoutes.Controllers
{
    public class LegacyController : Controller
    {
        public ActionResult GetLegacyURL(string legacyURL)
        {
            return View((object)legacyURL);
        }
	}
}

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

Обратите внимание в примере выше на приведение параметра к object в вызове метода View(). Одна из перегруженных версий метода View() принимает параметр типа string, указывающий имя представления для визуализации, и без такого приведения компилятор C# выберет именно эту перегруженную версию. Во избежание этого мы выполняем приведение к object, чтобы однозначно вызывалась перегруженная версия, которая принимает модель представления и использует стандартное представление. Это также можно было бы решить с помощью перегруженной версии, принимающей как имя представления, так и модель представления, но предпочтительнее не делать явных ассоциаций между методами действий и представлениями.

Создайте в папке Views/Legacy файл представления по имени GetLegacyURL.cshtml с содержимым, показанным в примере ниже:

@model string
@{
    ViewBag.Title = "Представление GetLegacyURL";
    Layout = null;
}

<h2>Представление GetLegacyURL</h2>
Значение URL из запроса: @Model

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

Маршрутизация входящих URL

В папку Infrastructure (где размещаются классы поддержки, не принадлежащие чему-либо еще) был добавлен файл класса по имени LegacyRoute.cs, содержимое которого приведено в примере ниже:

using System;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace UrlsAndRoutes.Infrastructure
{
    public class LegacyRoute : RouteBase
    {
        private string[] urls;

        public LegacyRoute(params string[] targetUrls)
        {
            urls = targetUrls;
        }

        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            RouteData result = null;

            string requestedURL =
                httpContext.Request.AppRelativeCurrentExecutionFilePath;
            if (urls.Contains(requestedURL, StringComparer.OrdinalIgnoreCase))
            {
                result = new RouteData(this, new MvcRouteHandler());
                result.Values.Add("controller", "Legacy");
                result.Values.Add("action", "GetLegacyURL");
                result.Values.Add("legacyURL", requestedURL);
            }
            return result;
        }

        public override VirtualPathData GetVirtualPath(RequestContext requestContext,
            RouteValueDictionary values)
        {
            return null;
        }
    }
}

Конструктор этого класса принимает строковый массив, представляющий индивидуальные URL, которые этот класс маршрутизации будет поддерживать. Мы укажем их позже при регистрации маршрута. В примере следует особо отметить метод GetRouteData(), в котором производится обращение к системе маршрутизации для определения, может ли экземпляр класса LegacyRoute соответствовать входящему URL.

Если экземпляр класса LegacyRoute не соответствует запросу, то просто возвращается значение null, и система маршрутизации переходит на следующий маршрут в списке, после чего процесс повторяется. Если же экземпляр класса может соответствовать запросу, возвращается экземпляр класса RouteData, содержащий значения для переменных controller и action, а также всю остальную информацию, которая должна быть передана методу действия.

При создании объекта RouteData необходимо передать обработчик, который будет иметь дело с генерируемыми значениями. Мы будем использовать стандартный класс MvcRouteHandler, привносящий смысл значениям controller и action:

result = new RouteData(this, new MvcRouteHandler());

В подавляющем большинстве приложений MVC этот класс необходим, т.к. он подключает систему маршрутизации к модели "контроллер/действие" приложения MVC. Однако, можно реализовать замену для MvcRouteHandler.

В этой специальной реализации RouteBase мы будем сопоставлять любой запрос к URL, переданный конструктору. При получении такого URL мы добавляем к объекту RouteValueDictionary жестко закодированные значения для контроллера и метода действия. Мы также передаем запрошенный URL в свойстве legacyURL. Обратите внимание, что имя этого свойства соответствует имени параметра в методе действия контроллера Legacy, гарантируя, что генерируемое значение будет передано методу действия через этот параметр.

Последний шаг заключается в регистрации нового маршрута, который использует производный от RouteBase класс. Регистрация показана в примере ниже, где приведено содержимое файла RouteConfig.cs:

using System.Web.Mvc;
using System.Web.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapMvcAttributeRoutes();

            routes.Add(new LegacyRoute(
                "~/articles/About_ASPNET_MVC",
                "~/old/NET_Framework_4"
                ));

            routes.MapRoute("MyRoute", "{controller}/{action}");
            routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" });
        }
    }
}

Мы создаем новый экземпляр класса LegacyRoute и передаем URL, которые он должен маршрутизировать. Затем мы добавляем объект в коллекцию RouteCollection, используя метод Add(). Если теперь запустить приложение и запросить один из унаследованных URL, запрос маршрутизируется классом LegacyRoute и направляется контроллеру Legacy:

Маршрутизация запросов с использованием специальной реализации RouteBase

Генерация исходящих URL

Для поддержки генерации исходящих URL мы должны реализовать метод GetVirtualPath() в классе LegacyRoute. Если этот метод не может сгенерировать конкретный URL, мы уведомляем об этом систему маршрутизации, возвращая null. В противном случае мы возвращаем экземпляр класса VirtualPathData. Реализация этого метода показана в примере ниже:

using System;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace UrlsAndRoutes.Infrastructure
{
    public class LegacyRoute : RouteBase
    {
        // ...

        public override VirtualPathData GetVirtualPath(RequestContext requestContext,
            RouteValueDictionary values)
        {
            VirtualPathData result = null;

            if (values.ContainsKey("legacyURL") &&
                urls.Contains((string)values["legacyURL"], StringComparer.OrdinalIgnoreCase))
            {
                result = new VirtualPathData(this,
                    new UrlHelper(requestContext)
                        .Content((string)values["legacyURL"]).Substring(1));
            }
            return result;
        }
    }
}

В предыдущих статьях переменные сегментов и другие детали передавались с использованием анонимных типов. Однако "за кулисами" система маршрутизации преобразует их в объекты RouteValueDictionary, так что они могут быть обработаны реализациями RouteBase. В примере ниже показано дополнение файла представления ActionName.cshtml, которое генерирует исходящий URL с применением специального класса маршрута:

@{
    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>
    <div>
        @Html.ActionLink("Ссылка", "GetLegacyURL",
            new { legacyURL = "~/old/NET_Framework_4" })
    </div>
</body>
</html>

Как и можно было ожидать, при визуализации этого представления вспомогательный метод ActionLink() генерирует следующую HTML-разметку:

<a href="/old/NET_Framework_4">Ссылка</a>

Созданный объект анонимного типа со свойством legacyURL преобразуется в экземпляр класса RouteValueDictionary, который содержит ключ с таким именем. В данном примере мы решили иметь дело с запросом исходящего URL, если существует ключ по имени legacyURL со значением одного из URL, переданных конструктору. Можно было бы также дополнительно проверить значения controller и action, но для простого примера этого вполне достаточно.

Если совпадение получено, мы создаем новый экземпляр VirtualPathData, передавая ссылку на текущий объект и исходящий URL. С помощью метода Content() класса UrlHelper выполняется преобразование URL, относящегося к приложению, в URL-который может быть передан браузерам. Система маршрутизации добавляет в начало URL дополнительный символ /, так что мы должны позаботиться об удалении ведущего символа из сгенерированного URL.

Создание специального обработчика маршрутов

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

В примере ниже показано содержимое файла CustomRouteHandler.cs, добавленного в папку Infrastructure примера проекта:

using System.Web;
using System.Web.Routing;

namespace UrlsAndRoutes.Infrastructure
{

    public class CustomRouteHandler : IRouteHandler
    {
        public IHttpHandler GetHttpHandler(RequestContext requestContext)
        {
            return new CustomHttpHandler();
        }
    }

    public class CustomHttpHandler : IHttpHandler
    {
        public bool IsReusable
        {
            get { return false; }
        }

        public void ProcessRequest(HttpContext context)
        {
            context.Response.Write("Привет");
        }
    }
}

Назначение интерфейса IRouteHandler заключается в том, чтобы предоставить средства для генерирования реализаций интерфейса IHttpHandler, который отвечает за обработку запросов. В реализации MVC этих интерфейсов контроллеры обнаруживаются, методы действий вызываются, представления визуализируются, а результаты записываются в ответ. Наша реализация намного проще: она лишь выводит слово "Привет" для клиента (не HTML-документ, содержащий это слово, а только сам текст).

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

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

using System.Web.Mvc;
using System.Web.Routing;
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapMvcAttributeRoutes();

            routes.Add(new Route("SayHello", new CustomRouteHandler()));

            routes.Add(new LegacyRoute(
                "~/articles/About_ASPNET_MVC",
                "~/old/NET_Framework_4"
                ));

            routes.MapRoute("MyRoute", "{controller}/{action}");
            routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" });
        }
    }
}

Когда поступает запрос URL вида /SayHello, для его обработки используется специальный обработчик маршрутов. Результат показан на рисунке ниже:

Использование специального обработчика запросов

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

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