为什么在asp.net mvc 中先映射特殊路由,然后再映射普通路由? [英] Why map special routes first before common routes in asp.net mvc?

查看:13
本文介绍了为什么在asp.net mvc 中先映射特殊路由,然后再映射普通路由?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

来自 www:

<块引用>

...路由引擎将采用与提供的 URL 匹配的第一条路由,并尝试使用该路由中的路由值.因此,应首先将不太常见或更专业的路线添加到表格中,而更一般的路线应稍后添加......

为什么要先绘制专业路线?有人可以给我一个例子,我可以在哪里看到先映射公共路线"的失败?

解决方案

路由引擎将采用与提供的 URL 匹配的第一条路由,并尝试使用该路由中的路由值.

发生这种情况的原因是因为 RouteTable 被用作 switch-case 语句.如下图:

int caseSwitch = 1;开关 (caseSwitch){情况1:Console.WriteLine("案例1");休息;情况1:Console.WriteLine("第二种情况1");休息;默认:Console.WriteLine("默认情况");休息;}

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

Route 类遵循类似的模式(在 GetRouteDataGetVirtualPath 方法).他们可以返回 2 个状态:

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

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

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

如果您尝试使用上述 switch-case 语句,程序将无法编译.但是,如果您配置的路由从不返回 null 或返回 RouteValues 对象的情况比应有的多,则程序将编译,但是会行为不端.

错误配置示例

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

公共类RouteConfig{public static void RegisterRoutes(RouteCollection 路由){route.IgnoreRoute("{resource}.axd/{*pathInfo}");路线.MapRoute(name: "自定义路由",网址:{segment1}/{action}/{id}",默认值:new { controller = "MyController", action = "Index", id = UrlParameter.Optional });路线.MapRoute(名称:默认",url: "{controller}/{action}/{id}",默认值:新 { 控制器 =主页",动作 =索引",id = UrlParameter.Optional });}}

在这个例子中:

  1. CustomRoute 将匹配长度为 1、2 或 3 段的任何 URL(请注意,segment1 是必需的,因为它没有默认值).
  2. Default 将匹配长度为 0、1、2 或 3 段的任何 URL.

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

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

这将使 MVC 在名为 MyControllerController 的控制器上查找名为 About 的操作,如果该操作不存在,则会失败.在这种情况下,Default 路由是一个无法访问的执行路径,因为即使它将匹配一个 2 段 URL,框架也不会给它机会,因为第一个匹配获胜.

修复配置

关于如何继续修复配置有多种选择.但是所有这些都取决于第一场比赛获胜然后路由不会再看下去的行为.

选项 1:添加一个或多个文字段

公共类RouteConfig{public static void RegisterRoutes(RouteCollection 路由){route.IgnoreRoute("{resource}.axd/{*pathInfo}");路线.MapRoute(name: "自定义路由",url: "自定义/{action}/{id}",//注意,将 `action` 和 `id` 保留在默认值之外//使它们成为必需,因此 URL 仅在 3 时匹配//段以 Custom 或 custom 开头.//示例:自定义/详细信息/343默认值:new { controller = "MyController" });路线.MapRoute(名称:默认",url: "{controller}/{action}/{id}",默认值:新 { 控制器 =主页",动作 =索引",id = UrlParameter.Optional });}}

选项 2:添加 1 个或多个 RegEx 约束

公共类RouteConfig{public static void RegisterRoutes(RouteCollection 路由){route.IgnoreRoute("{resource}.axd/{*pathInfo}");路线.MapRoute(name: "自定义路由",网址:{segment1}/{action}/{id}",默认值:new { controller = "MyController", action = "Index", id = UrlParameter.Optional },约束:新{segment1 = @"house|car|bus"});路线.MapRoute(名称:默认",url: "{controller}/{action}/{id}",默认值:新 { 控制器 =主页",动作 =索引",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"] 作为字符串;var month = values["month"] 作为字符串;var day = values["day"] 作为字符串;日期时间日期;return DateTime.TryParse(year + "-" + Month + "-" + day, System.Globalization.CultureInfo.InvariantCulture, DateTimeStyles.None, out theDate);}}公共类 RouteConfig{public static void RegisterRoutes(RouteCollection 路由){route.IgnoreRoute("{resource}.axd/{*pathInfo}");路线.MapRoute(name: "自定义路由",url: "{year}/{month}/{day}/{article}",默认值:new { controller = "News", action = "ArticleDetails" },约束:新 { 年 = 新 CorrectDateConstraint() });路线.MapRoute(名称:默认",url: "{controller}/{action}/{id}",默认值:新 { 控制器 =主页",动作 =索引",id = UrlParameter.Optional });}}

选项 4:使 必需 段 + 使段数与现有路由不匹配

公共类RouteConfig{public static void RegisterRoutes(RouteCollection 路由){route.IgnoreRoute("{resource}.axd/{*pathInfo}");路线.MapRoute(name: "自定义路由",网址:{segment1}/{segment2}/{action}/{id}",默认值:new { controller = "MyController" });路线.MapRoute(名称:默认",url: "{controller}/{action}/{id}",默认值:新 { 控制器 =主页",动作 =索引",id = UrlParameter.Optional });}}

在上述情况下,CustomRoute 将仅匹配具有 4 个段的 URL(注意这些可以是任何值).前面的 Default 路由只匹配具有 0、1、2 或 3 个段的 URL.因此不存在不可达的执行路径.

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

任何路由不支持的开箱即用(例如匹配特定域或子域)都可以通过 实现您自己的 RouteBase 子类 或 Route 子类.这也是了解路由如何/为什么以这种方式工作的最佳方式.

public class SubdomainRoute : 路由{public SubdomainRoute(string url) : base(url, new MvcRouteHandler()) {}公共覆盖 RouteData GetRouteData(HttpContextBase httpContext){var routeData = base.GetRouteData(httpContext);if (routeData == null) 返回 null;//如果此路由首先匹配,则仅查看子域.string subdomain = httpContext.Request.Params["subdomain"];//指定为查询参数的子域优先于主机名.如果(子域 == 空){字符串主机 = httpContext.Request.Headers["Host"];int index = host.IndexOf('.');如果(索引 >= 0)subdomain = host.Substring(0, index);}如果(子域!= null)routeData.Values[子域"] = 子域;返回路由数据;}公共覆盖 VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values){object subdomainParam = requestContext.HttpContext.Request.Params["subdomain"];如果(子域参数!= null)值[子域"] = subdomainParam;返回 base.GetVirtualPath(requestContext, values);}}

这个类是从以下借用的:是否可以基于子域制作 ASP.NET MVC 路由?

公共类RouteConfig{public static void RegisterRoutes(RouteCollection 路由){route.IgnoreRoute("{resource}.axd/{*pathInfo}");route.Add(new SubdomainRoute(url: "somewhere/unique"));路线.MapRoute(名称:默认",url: "{controller}/{action}/{id}",默认值:新 { 控制器 =主页",动作 =索引",id = UrlParameter.Optional });}}

<块引用>

注意:这里真正的问题是大多数人认为他们的路线都应该看起来像 Default 路线.复制,粘贴,完成,对吧?错了.

这种方法通常会出现两个问题:

  1. 几乎所有其他路线都应该至少有一个文字段(或者如果您喜欢这种事情,则可以有一个限制).
  2. 最合乎逻辑的行为通常是让其余路由具有必需段.

另一个常见的误解是可选段意味着您可以省略任何段,但实际上您只能省略最右边的一个或多个段.

Microsoft 成功地使路由约定为基础、可扩展且功能强大.他们未能使其直观理解.几乎每个人在第一次尝试时都失败了(我知道我做到了!).幸运的是,一旦你理解了它的工作原理,这并不难.

From the 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...

Why should I map specialized routes first? Someone can give me an example please where I can see the failing of "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.

The reason why this happens is because the RouteTable is used like a switch-case statement. Picture the following:

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

If caseSwitch is 1, the second block is never reached because the first block catches it.

Route classes follow a similar pattern (in both the GetRouteData and GetVirtualPath methods). They can return 2 states:

  1. A set of route values (or a VirtualPath object in the case of GetVirtualPath). This indicates the route matched the request.
  2. null. This indicates the route did not match the request.

In the first case, MVC uses the route values that are produced by the route to lookup the Action method. In this case, the RouteTable is not analyzed any further.

In the second case, MVC will check the next Route in the RouteTable to see if it matches with the request (the built in behavior matches the URL and constraints, but technically you can match anything in the HTTP request). And once again, that route can return a set of RouteValues or null depending on the result.

If you try to use a switch-case statement as above, the program won't compile. However, if you configure a route that never returns null or returns a RouteValues object in more cases than it should, the program will compile, but will misbehave.

Misconfiguration Example

Here is the classic example that I frequently see posted on StackOverflow (or some variant of it):

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

In this example:

  1. CustomRoute will match any URL that is either 1, 2, or 3 segments in length (note that segment1 is required because it has no default value).
  2. Default will match any URL that is 0, 1, 2, or 3 segments in length.

Therefore, if the application is passed the URL HomeAbout, the CustomRoute will match, and supply the following RouteValues to MVC:

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

This will make MVC look for an action named About on a controller named MyControllerController, which will fail if it doesn't exist. The Default route is an unreachable execution path in this case because even though it will match a 2-segment URL, the framework will not give it the opportunity to because the first match wins.

Fixing the Configuration

There are several options on how to proceed to fix the configuration. But all of them depend on the behavior that the first match wins and then routing won't look any further.

Option 1: Add one or more Literal Segments

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

Option 2: Add 1 or more RegEx Constraints

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

Option 3: Add 1 or more Custom Constraints

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

Option 4: Make Required Segments + Make the Number of Segments not Match Existing Routes

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

In the above case, the CustomRoute will only match a URL with 4 segments (note these can be any values). The Default route as before only matches URLs with 0, 1, 2, or 3 segments. Therefore there is no unreachable execution path.

Option 5: Implement RouteBase (or Route) for Custom Behavior

Anything that routing doesn't support out of the box (such as matching on a specific domain or subdomain) can be done by implementing your own RouteBase subclass or Route subclass. It is also the best way to understand how/why routing works the way it does.

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

This class was borrowed from: 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.

这篇关于为什么在asp.net mvc 中先映射特殊路由,然后再映射普通路由?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆