问题描述
来自 www:
...路由引擎将采用与提供的 URL 匹配的第一条路由,并尝试使用该路由中的路由值.因此,应首先将不太常见或更专业的路线添加到表格中,而更一般的路线应稍后添加......
为什么要先绘制专业路线?有人可以给我一个例子,我可以在哪里看到先映射公共路线"的失败?
发生这种情况的原因是因为 RouteTable
被用作 switch-case 语句.如下图:
int caseSwitch = 1;开关 (caseSwitch){情况1:Console.WriteLine("案例1");休息;情况1:Console.WriteLine("第二种情况1");休息;默认:Console.WriteLine("默认情况");休息;}
如果 caseSwitch
是 1
,则永远不会到达第二个块,因为第一个块捕获了它.
Route
类遵循类似的模式(在 GetRouteData
和 GetVirtualPath
方法).他们可以返回 2 个状态:
- 一组路由值(或者在
GetVirtualPath
的情况下是一个VirtualPath
对象).这表明路由与请求匹配. null
.这表明路由与请求不匹配.
在第一种情况下,MVC 使用路由生成的路由值来查找 Action
方法.在这种情况下,不会进一步分析 RouteTable
.
在第二种情况下,MVC 将检查 RouteTable
中的下一个 Route
以查看它是否与请求匹配(内置行为匹配 URL 和约束,但从技术上讲,您可以匹配 HTTP 请求中的任何内容).再一次,该路由可以根据结果返回一组 RouteValues
或 null
.
如果您尝试使用上述 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 });}}
在这个例子中:
CustomRoute
将匹配长度为 1、2 或 3 段的任何 URL(请注意,segment1
是必需的,因为它没有默认值).莉>Default
将匹配长度为 0、1、2 或 3 段的任何 URL.
因此,如果应用程序通过 URL HomeAbout
,CustomRoute
将匹配,并向 MVC 提供以下 RouteValues
:
segment1 = "Home"
controller = "MyController"
action = "关于"
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
路线.复制,粘贴,完成,对吧?错了.
这种方法通常会出现两个问题:
- 几乎所有其他路线都应该至少有一个文字段(或者如果您喜欢这种事情,则可以有一个限制).
- 最合乎逻辑的行为通常是让其余路由具有必需段.
另一个常见的误解是可选段意味着您可以省略任何段,但实际上您只能省略最右边的一个或多个段.
Microsoft 成功地使路由约定为基础、可扩展且功能强大.他们未能使其直观理解.几乎每个人在第一次尝试时都失败了(我知道我做到了!).幸运的是,一旦你理解了它的工作原理,这并不难.
From the www:
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 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:
- A set of route values (or a
VirtualPath
object in the case ofGetVirtualPath
). This indicates the route matched the request. 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:
CustomRoute
will match any URL that is either 1, 2, or 3 segments in length (note thatsegment1
is required because it has no default value).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:
segment1 = "Home"
controller = "MyController"
action = "About"
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 }
);
}
}
这篇关于为什么在asp.net mvc 中先映射特殊路由,然后再映射普通路由?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!