Зачем сначала сопоставлять специальные маршруты, а не общие маршруты в asp.net mvc?

С www:

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

Почему я должен сначала нанести на карту специализированные маршруты? Кто-нибудь может дать мне пример, пожалуйста, где я могу увидеть сбой "сначала на карте общего маршрута"?

1 ответ

Решение

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

Причина этого заключается в том, что RouteTable используется как оператор switch-case. Изобразите следующее:

int caseSwitch = 1;
switch (caseSwitch)
{
    case 1:
        Console.WriteLine("Case 1");
        break;
    case 1:
        Console.WriteLine("Second Case 1");
        break;
    default:
        Console.WriteLine("Default case");
        break;
}

Если caseSwitch является 1, второй блок никогда не достигается, потому что первый блок ловит его.

Route классы следуют похожему образцу (как в GetRouteData а также GetVirtualPath методы). Они могут вернуть 2 состояния:

  1. Набор значений маршрута (или VirtualPath объект в случае GetVirtualPath). Это указывает на то, что маршрут соответствует запросу.
  2. null, Это указывает на то, что маршрут не соответствует запросу.

В первом случае MVC использует значения маршрута, которые создаются маршрутом, для поиска Action метод. В этом случае RouteTable больше не анализируется.

Во втором случае MVC проверит следующий Route в RouteTable чтобы увидеть, совпадает ли это с запросом (встроенное поведение соответствует URL и ограничениям, но технически вы можете сопоставить что угодно в HTTP-запросе). И еще раз, этот маршрут может вернуть набор RouteValues или же null в зависимости от результата.

Если вы попытаетесь использовать оператор switch-case, как описано выше, программа не скомпилируется. Однако, если вы настраиваете маршрут, который никогда не возвращается null или возвращает RouteValues объект в большем количестве случаев, чем следует, программа скомпилирует, но будет работать неправильно.

Пример неверной конфигурации

Вот классический пример, который я часто вижу в Stackru (или некотором его варианте):

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            name: "CustomRoute",
            url: "{segment1}/{action}/{id}",
            defaults: new { controller = "MyController", action = "Index", id = UrlParameter.Optional }
        );

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

В этом примере:

  1. CustomRoute будет соответствовать любому URL длиной 1, 2 или 3 сегмента (обратите внимание, что segment1 требуется, потому что не имеет значения по умолчанию).
  2. Default будет соответствовать любому URL длиной 0, 1, 2 или 3 сегмента.

Поэтому, если приложению передается URL \Home\About, CustomRoute будет соответствовать, и поставьте следующее RouteValues в MVC:

  1. segment1 = "Home"
  2. controller = "MyController"
  3. action = "About"
  4. id = {}

Это заставит MVC искать действие с именем About на контроллере по имени MyControllerController, который потерпит неудачу, если его не существует. Default В этом случае route является недоступным путем выполнения, потому что даже если он будет соответствовать двухсегментному URL, каркас не даст ему такой возможности, потому что первое совпадение выиграет.

Исправление конфигурации

Есть несколько вариантов, как приступить к исправлению конфигурации. Но все они зависят от поведения, которое выигрывает первое совпадение, а затем маршрутизация не будет смотреться дальше.

Вариант 1: добавить один или несколько буквенных сегментов

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            name: "CustomRoute",
            url: "Custom/{action}/{id}",

            // Note, leaving `action` and `id` out of the defaults
            // makes them required, so the URL will only match if 3
            // segments are supplied begining with Custom or custom.
            // Example: Custom/Details/343
            defaults: new { controller = "MyController" }
        );

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

Вариант 2. Добавьте 1 или более ограничений RegEx.

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            name: "CustomRoute",
            url: "{segment1}/{action}/{id}",
            defaults: new { controller = "MyController", action = "Index", id = UrlParameter.Optional },
            constraints: new { segment1 = @"house|car|bus" }
        );

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

Вариант 3: Добавить 1 или несколько пользовательских ограничений

public class CorrectDateConstraint : IRouteConstraint
{
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        var year = values["year"] as string;
        var month = values["month"] as string;
        var day = values["day"] as string;

        DateTime theDate;
        return DateTime.TryParse(year + "-" + month + "-" + day, System.Globalization.CultureInfo.InvariantCulture, DateTimeStyles.None, out theDate);
    }
}

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            name: "CustomRoute",
            url: "{year}/{month}/{day}/{article}",
            defaults: new { controller = "News", action = "ArticleDetails" },
            constraints: new { year = new CorrectDateConstraint() }
        );

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

Вариант 4: Сделать обязательные сегменты + Сделать количество сегментов не совпадает с существующими маршрутами

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            name: "CustomRoute",
            url: "{segment1}/{segment2}/{action}/{id}",
            defaults: new { controller = "MyController" }
        );

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

В приведенном выше случае CustomRoute будет соответствовать только URL с 4 сегментами (обратите внимание, что это могут быть любые значения). Default Маршрут, как и прежде, соответствует только URL с 0, 1, 2 или 3 сегментами. Поэтому нет недоступного пути выполнения.

Вариант 5: реализовать RouteBase (или Route) для пользовательского поведения

Все, что маршрутизация не поддерживает "из коробки" (например, сопоставление в определенном домене или поддомене), может быть выполнено путем реализации вашего собственного RouteBase подкласс или маршрут подкласс. Это также лучший способ понять, как / почему маршрутизация работает так, как она работает.

public class SubdomainRoute : Route
{
    public SubdomainRoute(string url) : base(url, new MvcRouteHandler()) {}

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        var routeData = base.GetRouteData(httpContext);
        if (routeData == null) return null; // Only look at the subdomain if this route matches in the first place.
        string subdomain = httpContext.Request.Params["subdomain"]; // A subdomain specified as a query parameter takes precedence over the hostname.
        if (subdomain == null) {
            string host = httpContext.Request.Headers["Host"];
            int index = host.IndexOf('.');
            if (index >= 0)
                subdomain = host.Substring(0, index);
        }
        if (subdomain != null)
            routeData.Values["subdomain"] = subdomain;
        return routeData;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        object subdomainParam = requestContext.HttpContext.Request.Params["subdomain"];
        if (subdomainParam != null)
            values["subdomain"] = subdomainParam;
        return base.GetVirtualPath(requestContext, values);
    }
}

Этот класс был заимствован из: Можно ли сделать маршрут ASP.NET MVC на основе поддомен?

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.Add(new SubdomainRoute(url: "somewhere/unique"));

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

ПРИМЕЧАНИЕ: настоящая ошибка здесь заключается в том, что большинство людей предполагают, что их маршруты должны выглядеть так: Default маршрут. Копировать, вставить, готово, верно? Неправильно.

При таком подходе обычно возникают 2 проблемы:

  1. Практически на каждом другом маршруте должен быть хотя бы один буквальный сегмент (или ограничение, если вы любите подобные вещи).
  2. Наиболее логичное поведение обычно состоит в том, чтобы остальные маршруты имели требуемые сегменты.

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

Microsoft удалось сделать маршрутизацию на основе соглашений, расширяемой и мощной. Им не удалось сделать это интуитивно понятным для понимания. Практически все терпят неудачу в первый раз, когда они пробуют это (я знаю, я сделал!). К счастью, раз вы понимаете, как это работает, это не очень сложно.

Другие вопросы по тегам