为什么在 asp.net mvc 中先映射特殊路由再映射普通路由?

Why map special routes first before common routes in asp.net mvc?

来自 www:

...The routing engine will take the first route that matches the supplied URL and attempt to use the route values in that route. Therefore, less common or more specialized routes should be added to the table first, while more general routes should be added later on...

为什么要先映射专门的路线?有人可以给我一个例子,在那里我可以看到 "map common route first" ?

的失败

The routing engine will take the first route that matches the supplied URL and attempt to use the route values in that route.

之所以会出现这种情况,是因为 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;
}

如果 caseSwitch1,则永远不会到达第二个块,因为第一个块会捕获它。

Route classes 遵循类似的模式(在两种 GetRouteData and GetVirtualPath 方法中)。他们可以 return 2 个状态:

  1. 一组路由值(或 VirtualPath 对象,在 GetVirtualPath 的情况下)。这表示路由与请求匹配。
  2. null。这表明路由与请求不匹配。

在第一种情况下,MVC 使用路由生成的路由值来查找 Action 方法。在这种情况下,RouteTable 不再进一步分析。

在第二种情况下,MVC 将检查 RouteTable 中的下一个 Route 以查看它是否与请求匹配(内置行为与 URL 和约束相匹配,但从技术上讲,您可以匹配 HTTP 请求中的任何内容)。再一次,根据结果,该路线可以 return 一组 RouteValuesnull

如果您尝试使用上述的 switch-case 语句,程序将无法编译。然而,如果你配置的路由 never returns null or returns a RouteValues object in more cases than it should,该程序将编译,但会出现错误。

错误配置示例

这是我经常在 Whosebug 上看到的 classic 示例(或它的某些变体):

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 将匹配长度为 1、2 或 3 段的任何 URL(请注意 segment1 是必需的,因为它没有默认值)。
  2. Default 将匹配长度为 0、1、2 或 3 段的任何 URL。

因此,如果应用程序通过 URL \Home\AboutCustomRoute 将匹配,并向 MVC 提供以下 RouteValues

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

这将使 MVC 在名为 MyControllerController 的控制器上查找名为 About 的操作,如果不存在则失败。在这种情况下,Default 路由是无法到达的执行路径,因为即使它将匹配 2 段 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 只会匹配具有 4 个段的 URL(注意这些可以是任何值)。之前的 Default 路由仅匹配具有 0、1、2 或 3 个段的 URLs。因此不存在不可达的执行路径。

选项 5:为自定义行为实施 RouteBase(或路由)

任何路由不支持开箱即用的功能(例如在特定域或子域上的匹配)都可以通过 或 Route subclass 完成。这也是理解 how/why 路由工作方式的最佳方式。

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);
    }
}

此 class 借自:Is it possible to make an ASP.NET MVC route based on a subdomain?

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 }
        );
    }
}

NOTE: The real gotcha here is that most people assume that their routes should all look like the Default route. Copy, paste, done, right? Wrong.

There are 2 problems that commonly arise with this approach:

  1. Pretty much every other route should have at least one literal segment (or a constraint if you are into that sort of thing).
  2. The most logical behavior is usually to make the rest of the routes have required segments.

Another common misconception is that optional segments mean you can leave out any segment, but in reality you can only leave off the right-most segment or segments.

Microsoft succeeded in making routing convention-based, extensible, and powerful. They failed in making it intuitive to understand. Virtually everyone fails the first time they try it (I know I did!). Fortunately, once you understand how it works it is not very difficult.