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

157

В этой статье мы продолжим создание тестового проекта UrlsAndRoutes, созданного ранее, и рассмотрим некоторые возможности ASP.NET MVC в настройке маршрутизации URL.

Определение стандартных значений

Причина получения ошибки при запросе стандартного URL для приложения состоит в том, что этот URL не соответствует определенному ранее маршруту. Стандартный URL выражается для системы маршрутизации как "~/" и в этой строке нет никаких сегментов, которые бы соответствовали переменным controller и action, определенным нашим простым шаблоном маршрута.

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

Стандартное значение (defaults) применяется, когда URL не содержит сегмента для сопоставления со значением. В примере ниже приведен пример маршрута, содержащего стандартное значение:

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

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute("MyRoute", "{controller}/{action}",
                defaults: new { action = "Index" });
        }
    }
}

Стандартные значения задаются как свойства анонимного типа. В примере видно, что для переменной action предоставляется стандартное значение Index. Как и ранее, этот маршрут будет соответствовать всем двухсегментным URL. Например, при запрос URL вида http://localhost:64399/Home/Index маршрут извлечет Home в качестве значения для controller и Index - для action.

Теперь, когда предоставлено стандартное значение для сегмента action, маршрут будет также соответствовать и односегментным URL. При обработке односегментного URL система маршрутизации извлечет значение для переменной controller из единственного сегмента URL и будет использовать стандартное значение для переменной action. В этом случае можно запрашивать URL вида http://localhost:64399/Home и тем самым вызывать метод действия Index() контроллера Home.

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

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

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute("MyRoute", "{controller}/{action}",
                defaults: new { action = "Index", controller = "Home" });
        }
    }
}

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

Соответствие URL
Количество сегментов Пример На что отображается
0 mysite.com controller = Home, action = Index
1 mysite.com/Admin controller = Admin, action = Index
2 mysite.com/Admin/List controller = Admin, action = List
3 mysite.com/Admin/List/All Соответствия нет - сегментов слишком много

Чем меньше сегментов мы получаем во входящем URL, тем больше полагаемся на стандартные значения, вплоть до ситуации, когда URL вообще не содержит сегментов и применяются только стандартные значения. Результат использования стандартных значений можно увидеть, запустив пример приложения снова; на этот раз при запросе в браузере корневого URL приложения для переменных сегментов controller и action будут применяться стандартные значения, что приведет к вызову инфраструктурой MVC Framework метода действия Index() контроллера Home.

Модульное тестирование: стандартные значения

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

// ...
[TestMethod]
public void TestIncomingRoutes()
{
    TestRouteMatch("~/", "Home", "Index");
    TestRouteMatch("~/Home", "Home", "Index");
    TestRouteMatch("~/Home/Index", "Home", "Index");
    TestRouteFail("~/Home/Index/All");
}
// ...

Единственное, что здесь следует отметить: стандартный URL должен быть указан как "~/", поскольку именно так ASP.NET представляет URL для системы маршрутизации. Если указать пустую строку "", которая используется для определения маршрута, или просто "/", система маршрутизации сгенерирует исключение и тест не пройдет.

Использование статических сегментов URL

Не все сегменты в шаблоне URL должны быть переменными. Допускается также создавать шаблоны, включающие статические сегменты. Предположим, что требуется обеспечить соответствие для показанного ниже URL, чтобы поддерживать URL, имеющие префикс Public:

http://mysite.com/Public/Home/Index

Это можно сделать с помощью шаблона, приведенного в примере ниже:

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

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute(null, "Public/{controller}/{action}",
                defaults: new { action = "Index", controller = "Home" });

            routes.MapRoute("MyRoute", "{controller}/{action}",
                defaults: new { action = "Index", controller = "Home" });
        }
    }
}

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

Можно также создавать шаблоны URL, которые имеют сегменты, содержащие как статические, так и переменные элементы, вроде шаблона, показанного в примере ниже:

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

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute(null, "X{controller}/{action}");

            // ...
        }
    }
}

Шаблон в показанном маршруте соответствует любому двухсегментному URL, в котором первый сегмент начинается с буквы X. Значение для controller берется из первого сегмента, исключая X. Значение для action получается из второго сегмента. Чтобы увидеть эффект от добавления этого маршрута, запустите приложение и перейдите на URL вида /XHome/Index; результат показан на рисунке ниже:

Смешивание статических и переменных элементов в одном сегменте

Упорядочение маршрутов

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

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

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

// ...
   
routes.MapRoute("MyRoute", "{controller}/{action}",
    defaults: new { action = "Index", controller = "Home" });
    
routes.MapRoute(null, "X{controller}/{action}");

// ...

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

http://mysite.com/XHome/Index

будет нацелен на контроллер по имени XHome, которого не существует, и это приведет к отправке пользователю ошибки 404 Not Found (404 - не найдено).

Создание псевдонимов для переменных сегментов

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

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

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

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute("ShopSchema", "Shop/{action}",
                defaults: new { action = "Index", controller = "Home" });

            // ...
        }
    }
}

Добавленный маршрут соответствует любому двухсегментному URL, в котором первым сегментом является Shop. Значение для action берется из второго сегмента URL. Шаблон URL не содержит переменного сегмента для controller, поэтому применяется определенное нами стандартное значение. Это значит, что запрос какого-то действия из контроллера Shop транслируется в запрос, предназначенный для контроллера Home.

Эффект от добавления такого маршрута можно увидеть, запустив приложение и перейдя на URL вида /Shop/Index; как показано на рисунке ниже, новый маршрут приводит к тому, что инфраструктура MVC Framework вызывает метод действия Index() из контроллера Home:

Создание псевдонима для предохранения схемы URL

Мы можем пойти еще дальше и создать псевдонимы для методов действий, которые в результате рефакторинга также перестали существовать в контроллере. Для этого мы просто создаем статический URL и предоставляем стандартные значения controller и action, как показано в примере ниже:

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

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute("ShopSchema2", "Shop/OldMethod",
                defaults: new { action = "Index", controller = "Home" });

            // ...
        }
    }
}

Еще раз обратите внимание, что новый маршрут размещен так, чтобы определяться первым. Причина в том, что он является более специфичным, чем маршруты, следующие после него. Если запрос для Shop/OldMethod обработается, к примеру, следующим, определенным маршрутом, мы можем получить результат, отличающийся от желаемого. Запрос имел бы дело с ошибкой 404 - Not Found вместо того, чтобы транслироваться для предохранения контракта с нашими клиентами.

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

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

// ...
[TestMethod]
public void TestIncomingRoutes()
{
            TestRouteMatch("~/", "Home", "Index");
            TestRouteMatch("~/Home", "Home", "Index");
            TestRouteMatch("~/Home/Index", "Home", "Index");
            TestRouteFail("~/Home/Index/All");
            TestRouteMatch("~/Shop/OldMethod", "Home", "Index");
}
// ...

Определение специальных переменных сегментов

Переменные сегментов controller и action имеют специальное назначение в MVC Framework и, что вполне очевидно, соответствуют контроллеру и методу действия, который будет использоваться для обслуживания запроса. Вы не ограничены применением этих встроенных переменных сегментов - при желании можно определять и собственные переменные, как показано в примере ниже. (Чтобы начать все с начала, существующие маршруты, которые были определены в предыдущем разделе, удалены.)

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}",
                defaults: new { controller = "Home", action = "Index", id = "DefaultId" }
            );
        }
    }
}

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

Некоторые имена зарезервированы и не доступны для использования в качестве имен специальных переменных сегментов. К ним относятся controller, action и area. Назначение первых двух очевидно, а роль областей объясняется позже.

Получить доступ к любой переменной сегмента в методе действия можно через свойство RouteData.Values. Для демонстрации этого добавим в класс HomeController метод действия по имени CustomVariable(), как показано в примере ниже:

using System.Web.Mvc;

namespace UrlsAndRoutes.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            // ...
        }

        public ActionResult CustomVariable()
        {
            ViewBag.Controller = "Home";
            ViewBag.Action = "CustomVariable";
            ViewBag.CustomVariable = RouteData.Values["id"];
            return View();
        }
	}
}

Этот метод получает значение специальной переменной в шаблоне URL маршрута и передает его представлению с помощью объекта ViewBag. Чтобы создать представление для этого метода действия, создайте папку Views/Home, щелкните на ней правой кнопкой мыши, выберите в контекстном меню пункт Add --> MVC 5 View Page (Razor) (Добавить --> Страница представления MVC 5 (Razor)) и установите имя в CustomVariable.cshtml. Щелкните на кнопке ОК, чтобы создать файл представления и приведите его содержимое в соответствие с примером:


@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>CustomVariable</title>
</head>
<body>
    <div>Контроллер: <strong>@ViewBag.Controller</strong></div>
    <div>Метод действия: <strong>@ViewBag.Action</strong></div>
    <div>Id: <strong>@ViewBag.CustomVariable</strong></div>
</body>
</html>

Эффект от внесенных изменений можно увидеть, запустив приложение и перейдя на URL вида /Home/CustomVariable/Привет. В результате вызывается метод действия CustomVariable() контроллера Home, а из ViewBag извлекается значение специальной переменной сегмента, которое передается представлению. Результаты показаны на рисунке ниже:

Отображение значения специальной переменной сегмента

Поскольку в маршруте для переменной сегмента id было предоставлено стандартное значение, при переходе на URL вида /Home/CustomVariable будут получены результаты, приведенные на рисунке ниже:

Стандартное значение для специальной переменной сегмента

Модульное тестирование: тестирование специальных переменных сегментов

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

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

Использование специальных переменных в качестве параметров метода действия

Использование свойства RouteData.Values - это лишь один способ доступа к специальным переменным маршрута. Другой способ намного более элегантен.

Если мы определим для метода действия параметры с именами, которые совпадают с именами переменных шаблона URL, инфраструктура MVC Framework будет передавать методу действия значения, извлеченные из URL, в виде параметров. Например, в ранее определенном маршруте, была определена специальная переменная по имени id. Мы можем модифицировать метод действия CustomVariable() контроллера Home, добавив к нему соответствующий параметр, как показано в примере ниже:

using System.Web.Mvc;

namespace UrlsAndRoutes.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            // ...
        }

        public ActionResult CustomVariable(string id)
        {
            ViewBag.Controller = "Home";
            ViewBag.Action = "CustomVariable";
            ViewBag.CustomVariable = id;
            return View();
        }
	}
}

Когда система маршрутизации обнаруживает соответствие какого-то URL маршруту, который был определен в методе RegisterRoutes(), значение третьего сегмента URL присваивается специальной переменной id. Инфраструктура MVC Framework сравнивает список переменных сегментов со списком параметров метода действия, и в случае совпадения имен передает значения из URL этому методу.

Параметр id определен как имеющий тип string, однако MVC Framework будет пытаться преобразовать значение, полученное из URL, в любой тип, указанный для параметра. Если мы определим параметр id как int или DateTime, то будем получать значение из URL, представленное в виде экземпляра этого типа. Это элегантное и полезное средство, которое избавляет от необходимости обрабатывать преобразования вручную.

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

Определение необязательных сегментов URL

Необязательный сегмент URL - это такой сегмент который пользователь может не указывать, но для которого не предусмотрено стандартного значения. В примере ниже приведен пример, и вы заметите, что переменная сегмента указывается как необязательная за счет установки стандартного значения в UrlParameter.Optional:

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}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}

Данный маршрут будет соответствовать URL, которые имеют или не имеют сегмент id. В таблице ниже показано, как он работает с различными URL:

Соответствие URL с необязательной переменной сегмента
Количество сегментов Пример URL На что отображается
0 mysite.com controller = Home, action = Index
1 mysite.com/Admin controller = Admin, action = Index
2 mysite.com/Admin/List controller = Admin, action = List
3 mysite.com/Admin/List/All controller = Admin, action = List, id = All
4 mysite.com/Admin/List/All/Delete Соответствия нет - сегментов слишком много

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

В примере ниже приведено обновление контроллера с учетом ситуации, когда для переменной сегмента id значение не указано:

using System.Web.Mvc;

namespace UrlsAndRoutes.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            // ...
        }

        public ActionResult CustomVariable(string id)
        {
            ViewBag.Controller = "Home";
            ViewBag.Action = "CustomVariable";
            ViewBag.CustomVariable = id ?? "<нет значения id>";
            return View();
        }
	}
}

На рисунке ниже показан результат запуска приложения и перехода по URL контроллера /Home/CustomVariable (в котором не определено значение для переменной сегмента id):

Обнаружение ситуации, когда URL не содержит значение для необязательной переменной сегмента

Использование необязательных сегментов URL для обеспечения разделения ответственности

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

В качестве примера ниже показано, как модифицировать метод действия CustomVariable(), чтобы определить стандартное значение для параметра id, которое будет выбрано, если URL не содержит значения:

// ...
public ActionResult CustomVariable(string id = "DefaultId")
{
    ViewBag.Controller = "Home";
    ViewBag.Action = "CustomVariable";
    ViewBag.CustomVariable = id;
    return View();
}
// ...

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

// ...
routes.MapRoute(
    name: "MyRoute",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = "DefaultId" }
);
// ...

Разница в том, что стандартное значение для переменной сегмента id определяется в коде контроллера, а не в определении маршрута.

Модульное тестирование: необязательные сегменты URL

Единственная проблема, о которой следует помнить при тестировании необязательных сегментов URL, заключается в том, что переменная сегмента не будет добавляться в коллекцию RouteData.Values, если значение не найдено в URL. Это означает, что вы не должны включать такую переменную в анонимный тип, если только не тестируете URL, который содержит необязательный сегмент.

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

// ...
[TestMethod]
public void TestIncomingRoutes()
{
    TestRouteMatch("~/", "Home", "Index");
    TestRouteMatch("~/Home", "Home", "Index");
    TestRouteMatch("~/Home/Index", "Home", "Index");
    TestRouteFail("~/Home/Index/All/Delete");
    TestRouteMatch("~/Home/Index/All", "Home", "Index");
}
// ...

Определение маршрутов переменной длины

Еще один способ изменения консервативного стандартного поведения шаблонов 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 }
            );
        }
    }
}

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

Сопоставление URL с переменной общего захвата
Количество сегментов Пример URL На что отображается
0 / controller = Home, action = Index
1 /Admin controller = Admin, action = Index
2 /Admin/List controller = Admin, action = List
3 /Admin/List/All controller = Admin, action = List, id = All
4 Admin/List/All/Delete controller = Admin, action = List, id = All, catchcall= Delete
5 /Admin/List/All/Delete/Temp controller = Admin, action = List, id = All, catchcall= Delete/Temp

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

Модульное тестирование: тестирование переменных общего захвата

Переменную общего захвата можно трактовать в точности как специальную переменную. Единственное отличие в том, что в ней следует ожидать множества сегментов, соединенных в одну строку, такую как "сегмент/сегмент/сегмент". Обратите внимание, что мы не получаем ведущий или завершающий символ /.

Ниже показаны изменения, внесенные в метод TestIncomingRoutes(), демонстрирующие тестирование сегмента общего захвата:

// ...
[TestMethod]
public void TestIncomingRoutes()
{
    TestRouteMatch("~/", "Home", "Index");
    TestRouteMatch("~/Home", "Home", "Index");
    TestRouteMatch("~/Home/Index", "Home", "Index");
    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" });
}
// ...

Назначение приоритетов контроллерам с помощью пространств имен

Когда входящий URL соответствует маршруту, инфраструктура MVC Framework берет значение переменной controller и выполняет поиск подходящего имени. Например, если значением controller является Home, MVC Framework ищет контроллер по имени HomeController. Это не полностью определенное имя класса, поэтому если есть два или более классов HomeController в разных пространствах имен, MVC Framework не будет знать, какой из них выбрать.

Чтобы продемонстрировать проблему, создайте в корне примера проекта новую папку по имени AdditionalControllers и добавьте в нее новый контроллер Home с кодом, приведенным в примере ниже:

using System.Web.Mvc;

namespace UrlsAndRoutes.AdditionalController
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Controller = "AdditionalController.Home";
            ViewBag.Action = "Index";
            return View("ActionName");
        }
	}
}

При запуске приложения выдается сообщение об ошибке, показанное на рисунке ниже:

Сообщение об ошибке, отображаемое при наличии двух контроллеров с одинаковыми именами

Инфраструктура MVC Framework ищет класс по имени HomeController и находит два таких класса: один в исходном пространстве имен RoutesAndUrls.Controllers и еще один в новом пространстве имен RoutesAndUrls.AdditionalControllers. Если вы внимательно прочитаете текст сообщения об ошибке на рисунке выше, то увидите, что MVC Framework сообщает о том, какие классы были обнаружены.

Эта проблема случается гораздо чаще, чем можно было ожидать, особенно при работе над крупным проектом MVC, в котором используются библиотеки контроллеров от сторонних разработчиков. Например, вполне естественно назначить контроллеру, относящемуся к пользовательским учетным записям, имя AccountController, поэтому возникновение конфликта имен - вопрос только времени.

Для решения описанной проблемы необходимо заставить MVC Framework отдавать предпочтение определенным пространствам имен при попытке распознавания имени класса контроллера; это демонстрируется в примере ниже:

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.AdditionalControllers" });
        }
    }
}

Пространства имен выражаются в виде массива строк, и в примере мы сообщаем инфраструктуре MVC Framework о необходимости просмотра пространства имен URLsAndRoutes.AdditionalControllers раньше всех остальных.

Если в этом пространстве имен подходящий контроллер найти не удалось, MVC Framework восстанавливает свое стандартное поведение и просматривает все доступные пространства имен. Если запустить приложение после такого добавления к маршруту, отобразится результат, показанный на рисунке ниже, который говорит о том, что запрос корневого URL, транслируемый в обращение к методу действия Index() контроллера Home, отправлен контроллеру, определенному в пространстве имен AdditionalControllers:

Назначение приоритетов контроллерам в указанных пространствах имен

Пространства имен, добавленные к маршруту, получили одинаковый приоритет. Инфраструктура MVC Framework не проверяет первое пространство имен перед переходом ко второму и т.д. Например, предположим, что к маршруту добавлены оба наших пространства имен:

// ...
namespaces: new[] { "UrlsAndRoutes.AdditionalControllers", "UrlsAndRoutes.Controllers" });
// ...

Мы получим ту же самую ошибку, что и ранее, потому что MVC Framework пытается распознать имя класса контроллера во всех пространствах имен, добавленных к маршруту. Если необходимо отдать предпочтение какому-то конкретному контроллеру в одном пространстве имен, но обеспечить распознавание всех остальных контроллеров в другом пространстве имен, понадобится создать несколько маршрутов, как показано в примере ниже:

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

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

            routes.MapRoute(
                name: "MyRoute",
                url: "{controller}/{action}/{id}/{*catchcall}",
                defaults: new
                {
                    controller = "Home",
                    action = "Index",
                    id = UrlParameter.Optional
                },
                namespaces: new[] { "UrlsAndRoutes.Controllers" });
        }
    }
}

Первый маршрут применяется, когда пользователь явно запрашивает URL с первым сегментом Home, и направляет на контроллер Home в папке AdditionalControllers. Все прочие запросы, в том числе не имеющие указанного первого сегмента, будут обрабатываться контроллерами из папки Controllers.

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

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

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

            myRoute.DataTokens["UseNamespaceFallback"] = false;
        }
    }
}

Метод MapRoute() возвращает объект Route. В предшествующих примерах мы игнорировали этот факт, поскольку не нуждались в каких-либо корректировках созданных ранее маршрутов. Чтобы отключить поиск контроллеров в других пространствах имен, мы должны получить объект Route и установить значение false для ключа UseNamespaceFallback в свойстве типа коллекции DataTokens.

Эта установка будет передана компоненту, отвечающему за поиск контроллеров, который называется фабрикой контроллеров. Эффект от такого добавления заключается в том, что запросы, которые не могут быть удовлетворены контроллером Home из папки AdditionalControllers, дадут сбой.

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