Ограничение маршрутизации

104

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

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

Ограничение маршрута с использованием регулярного выражения

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

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

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute(
                name: "MyRoute",
                url: "{controller}/{action}/{id}/{*catchcall}",
                defaults: new
                {
                    controller = "Home",
                    action = "Index",
                    id = UrlParameter.Optional
                },
                namespaces: new[] { "UrlsAndRoutes.Controllers" },
                constraints: new { controller = "^H.*" });
        }
    }
}

Ограничения определяются путем их передачи в качестве параметра методу MapRoute(). Подобно стандартным значениям, ограничения выражаются в виде анонимного типа, свойства которого соответствуют именам ограничиваемых переменных сегментов. В этом примере было применено ограничение посредством регулярного выражения, которое соответствует только тем URL, в которых значение переменной controller начинается с буквы "H".

Стандартные значения используются перед проверкой ограничений. Таким образом, например, при запросе URL вида / применяется стандартное значение для controller, которым является Home. Затем проверяются ограничения, и поскольку значение controller начинается с "H", стандартный URL будет соответствовать маршруту.

Ограничение маршрута набором специфических значений

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

// ...
constraints: new { controller = "^H.*", action = "^Index$|^CustomVariable$" });

Это ограничение разрешает маршруту соответствовать только URL со значением сегмента action, равным Index или CustomVariable. Ограничения применяются совместно, так что ограничения, наложенные на значение переменной action, комбинируются с ограничениями, наложенными на значение переменной controller. Это значит, что маршрут в примере будет соответствовать только таким URL, в которых значение переменной controller начинается с буквы "H", а значением переменной action является Index или CustomVariable.

Итак, теперь вы видите, что понимается под созданием очень точных маршрутов.

Ограничение маршрута с использованием методов HTTP

Маршруты можно ограничивать так, чтобы они соответствовали URL, только когда запрос производится с использованием специфичного метода HTTP, что демонстрируется в примере ниже:

// ...
constraints: new
{
    controller = "^H.*",
    action = "^Index$|^CustomVariable$",
    httpMethod = new HttpMethodConstraint("GET")
});

Формат для указания ограничения по методу HTTP выглядит несколько странно. Имя, выбираемое для свойства, роли не играет, т.к. свойству присваивается экземпляр класса HttpMethodConstraint. В примере свойство названо httpMethod, чтобы помочь отличить его от ограничений на основе значений, которые были определены ранее.

Возможность ограничения маршрутов по методу HTTP не имеет отношения к возможности ограничения методов действий с помощью таких атрибутов, как HttpGet и HttpPost. Ограничения маршрутов обрабатываются намного раньше в конвейере запросов, и они определяют имя контроллера и действие, требуемое для обработки запроса. Атрибуты методов действий используются для определения, какой конкретный метод действия будет применяться для обслуживания запроса контроллером.

Имена методов HTTP, которые должны поддерживаться, передаются в виде строковых параметров конструктору класса HttpMethodConstraint. В примере выше мы ограничили маршрут запросами GET, но очень просто добавить поддержку и других методов, например:

// ...
constraints: new
{
    controller = "^H.*",
    action = "^Index$|^CustomVariable$",
    httpMethod = new HttpMethodConstraint("GET", "POST")
});

Модульное тестирование: ограничение маршрутов

При тестировании ограниченных маршрутов важно проверить как URL, которые должны соответствовать, так и URL, которые вы попытались исключить:

// ...
[TestMethod]
public void TestIncomingRoutes()
{
    TestRouteMatch("~/", "Home", "Index");
    TestRouteMatch("~/Home", "Home", "Index");
    TestRouteMatch("~/Home/Index", "Home", "Index");
    TestRouteMatch("~/Home/CustomVariable", "Home", "CustomVariable");
    TestRouteMatch("~/Home/Index/All", "Home", "Index");
    TestRouteMatch("~/Home/Index/All/Delete", "Home", "Index",
        new { id = "All", catchcall = "Delete" });
    TestRouteMatch("~/Home/Index/All/Delete/Insert", "Home", "Index",
        new { id = "All", catchcall = "Delete/Insert" });

    TestRouteFail("~/Home/About");
    TestRouteFail("~/Admin/Index");
}
// ...

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

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

// ...
using System.Web.Mvc.Routing.Constraints;

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute(
                // ...
                constraints: new
                {
                    controller = "^H.*",
                    action = "^Index$|^CustomVariable$",
                    httpMethod = new HttpMethodConstraint("GET", "POST"),
                    id = new RangeRouteConstraint(10, 20)
                });
        }
    }
}

С помощью классов ограничений, определенных в пространстве имен System.Web.Mvc.Routing.Constraints, можно проверять, имеют ли переменные сегментов значения различных типов C#, а также выполнять другие базовые проверки. В примере выше использовался класс RangeRouteConstraint, который проверяет, что значение, предоставленное переменной сегмента, имеет тип int и находится в диапазоне между двумя границами - 10 и 20.

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

Классы ограничений маршрутов
Класс Конструктор класса Описание Атрибут ограничения
AlphaRouteConstraint AlphaRouteConstraint()

Соответствует символам английского алфавита, независимо от регистра (A-Z, a-z)

alpha
BoolRouteConstraint BoolRouteConstraint()

Соответствует значению, которое может быть разобрано в bool

bool
DateTimeRouteConstraint DateTimeRouteConstraint()

Соответствует значению, которое может быть разобрано в DateTime

datetime
DecimalRouteConstraint DecimalRouteConstraint()

Соответствует значению, которое может быть разобрано в decimal

decimal
DoubleRouteConstraint DoubleRouteConstraint()

Соответствует значению, которое может быть разобрано в double

double
FloatRouteConstraint FloatRouteConstraint()

Соответствует значению, которое может быть разобрано в float

float
IntRouteConstraint IntRouteConstraint()

Соответствует значению, которое может быть разобрано в int

int
LengthRouteConstraint LengthRouteConstraint(len)
LengthRouteConstraint(min, max)

Соответствует значению с указанным количеством символов либо имеющему длину между min и max символов

length(len), length(min, max)
MaxRouteConstraint MaxRouteConstraint(val)

Соответствует значению int, если оно меньше val

max(val)
LongRouteConstraint LongRouteConstraint()

Соответствует значению, которое может быть разобрано в long

long
MaxLengthRouteConstraint MaxLengthRouteConstraint(len)

Соответствует строке, содержащей не более len символов

maxlength(len)
MinRouteConstraint MinRouteConstraint(val)

Соответствует значению int, если оно больше val

min(val)
MinLengthRouteConstraint MinLengthRouteConstraint(len)

Соответствует строке, содержащей, по меньшей мере, len символов

minlength(len)
RangeRouteConstraint RangeRouteConstraint(min, max)

Соответствует значению int, если оно находится между min и max

range(min, max)

Ограничения можно комбинировать для одной переменной сегмента, используя класс CompoundRouteConstraint, конструктор которого принимает в своем аргументе массив ограничений. В примере ниже с помощью этого средства к переменной сегмента id применяются сразу два ограничения, AlphaRouteConstraint и MinLengthRouteConstraint, обеспечивая соответствие с маршрутом только строковых значений, которые содержат лишь алфавитные символы и имеют длину, по меньше мере, шесть символов:

// ...
constraints: new
{
    controller = "^H.*",
    action = "^Index$|^CustomVariable$",
    httpMethod = new HttpMethodConstraint("GET", "POST"),
    id = new CompoundRouteConstraint(new IRouteConstraint[] {
        new AlphaRouteConstraint(),
        new MinLengthRouteConstraint(6)
    })
});
// ...

Определение специального ограничения

Если для удовлетворения текущих потребностей стандартных ограничений не хватает, можно определить собственные ограничения, реализовав интерфейс IRouteConstraint. Для демонстрации этой возможности мы добавили в пример проекта папку Infrastructure и создали в ней новый класс UserAgentConstraint.cs, код которого приведен в примере ниже:

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

namespace UrlsAndRoutes.Infrastructure
{
    public class UserAgentConstraint : IRouteConstraint
    {
        private string requiredUserAgent;

        public UserAgentConstraint(string agentParam)
        {
            requiredUserAgent = agentParam;
        }

        public bool Match(HttpContextBase httpContext, Route route, string parameterName,
                          RouteValueDictionary values, RouteDirection routeDirection)
        {
            return httpContext.Request.UserAgent != null &&
                httpContext.Request.UserAgent.Contains(requiredUserAgent);
        }
    }
}

В интерфейсе IRouteConstraint определен метод Match(), реализацию которого можно использовать для указания системе маршрутизации, удовлетворено ли ограничение. Параметры метода Match() предоставляют доступ к запросу, поступившему от клиента, проверяемому маршруту, имени параметра ограничения, переменным сегментов, которые извлечены из URL, и признаку того, какой URL проверяет запрос - входящий или исходящий.

В рассматриваемом примере мы проверяем, содержит ли свойство UserAgent клиентского запроса значение, переданное конструктору. Использование специального ограничения в маршруте продемонстрировано в примере ниже:

// ...
using UrlsAndRoutes.Infrastructure;

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute(
                name: "GoogleChromeRoute",
                url: "{*catchcall}",
                defaults: new
                {
                    controller = "Home",
                    action = "Index"
                },
                constraints: new
                {
                    custom = new UserAgentConstraint("Chrome")
                },
                namespaces: new[] { "UrlsAndRoutes.AdditionalControllers" });

            routes.MapRoute(
                name: "MyRoute",
                // ...
        }
    }
}

Первый маршрут в примере выше ограничен так, что он будет соответствовать только запросам, поступающим от браузера, строка user-agent которого содержит Chrome. Если маршрут соответствует, запрос будет отправлен методу действия Index() контроллера Home, определенного в папке AdditionalControllers, независимо от структуры и содержимого запрошенного URL. Наш шаблон URL состоит только из переменной общего захвата, т.е. значения для переменных сегментов controller и action будут всегда браться из стандартных значений, а не из самого URL.

Второй маршрут будет соответствовать всем остальным запросам и отправляться контроллерам из папки Controllers с учетом ограничений на основе типов и значений, примененных в предыдущем разделе. Эффект, производимый этими маршрутами заключается в том, что один вид браузеров всегда попадает в одно и то же место приложения; это можно видеть на рисунке ниже, где показан результат перехода к приложению в Google Chrome:

Маршрутизация в браузере Google Chrome

На рисунке ниже можно видеть результат перехода к приложению в Internet Explorer. (Обратите внимание на добавление третьего сегмента, который содержит шесть или более алфавитных символов, чтобы обеспечить соответствие URL второму маршруту, из-за ограничений, примененных в предыдущем разделе.)

Маршрутизация в браузере Internet Explorer

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

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