以下文字摘译自《Pro ASP.NET MVC 3 Framework》
在引入MVC前,ASP.NET设定URL和硬盘上的文件是有直接的关系的,服务器的职责就是接受请求,从对应的文件中获取输出并且发送到客户端。
如下所示:
这个方法对于WebForm来说,很不错。但是对MVC应用程序来说,就不合适。因为MVC的请求是通过controller类中的action方法处理的,并且和硬盘上的文件没有一对一的关系。要处理MVC URL,ASP.NET平台使用路由系统。
路由系统简介
路由系统有2个功能
检测URL,找出所要请求的controller和vaction。
生成URL,生成出现在HTML页面上的URL,这样用户可以点击链接,调用指定的action。
尽管ASP.NET MVC Framework需要路由系统,但是路由系统也可以在其他的ASP.NET技术中使用,包括Web Form。因为这点,路由系统的类在System.Web程序集中而不是在System.Web.Mvc.当你创建一个MVC应用程序,你会发现Visual Studio已经将System.Web.Routing加入引用了。这是为了支持老版本上一版本,NET 3.5。用4.0的话,而已删除该引用。
建立路由项目
为了演示路由系统,这里创建一个新的MVC应用程序,并使用Internet Application模板,项目命名为为UrlsAndRoutes.我们使用这一模板是因为它能给我们一些现成的。
从Global.ascx中可以看到,程序首次启动时,Application_Start方法由ASP.NET平台调用,从而调用了RegisterRoutes方法,而该方法的参数 则是静态属性 RouteTable.Routes的值,该值是RouteCollection类的一个实例。
URL模式简介
路由系统有一组路由类组成,这些路由相应的组成URL结构,这些URL可以被应用程序所识别。我们不需要手动的列出所有支持的URL,每一个路由包含了一个URL模式,这个模式会和URL请求比较,如果模式匹配了,那么就会由路由系统处理该URL。因为MVC应用程序通常会有好几个路由,路由系统会逐一比较接收的URL,直到找到一个可匹配的路由。
路由系统不知道controller和action的存在,它只是从URL中抽取值,将他们传递给请求管道。然后,请求到达MVC框架,意味着该请求被分配到指定的controller和action。这就是为什么路由系统可以在Web Form中也能使用和我们能创建自定义变量的原因。
默认的,只要URL有正确个数的片段,URL模式就能匹配。例如下表
URL模式是保守的,它只会匹配有着相同片段数的URL,可以从第四第五个例子中看到。
URL模式也是自由的,如果URL有正确的片段数,模式将会抽取这些值,不管是什么值。正如之前提到的,路由系统不知道MVC的任何概念,URL模式也是,即使当没有对应的controller和action是,这些值也会从URL中抽取。
建立和注册路由
Registering a Route
public static void RegisterRoutes(RouteCollection routes) {
Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler());
routes.Add("MyRoute", myRoute);
}
一旦你头脑中有了URL模式,你可以使用自定义路由了。下面示例了一个简单
public static void RegisterRoutes(RouteCollection routes) {
Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler());
routes.Add("MyRoute", myRoute);
}
我们创建一个新的路由对象,把URL模式作为构造参数,一个MvcRouteHandler的实例也同样作为参数,不同的ASP.NET技术采用不同的类修改路由行为。建立好路由之后,把它加入RouteCollection对象。
为路由命名不是必须的,有一些争论认为这样会模糊了关注分离...我们很随意的使用命名,但是我们在后面会解释为什么这可能会产生问题。
注册路由更便利的方法是使用RouteCollection类中的MapRoute方法,如下面的例子:
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}");
}
此方法更紧凑,因为我们不需要创建MvcRouteHandler类实例。Maproute方法可单独使用,ASP.NET Web Form应用程序也可以使用MapPageRoute。
定义默认值
当URL不包含合法的片段时,可以设置成默认值。
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}", new { action = "Index" });
}
默认值由匿名类型的属性支持,上面的例子,我们为action提供了默认值Index,路由会匹配2个url片段,比如i, URL http://mydomain.com/Home/Index 请求时, 路由会抽取Home作为Controller,Index作为action。
现在设置了action片段的默认值,路由也会匹配单片段的URL。当处理这种URL的时候,路由系统会从URL中抽取controller,并使用默认的action值。使用这种方法,我们可以请求URL地址http://mydomain.com/Home,调用Home controller中的Index action方法。更进一步,我们可以定义URL不包含任何片段,仅仅依靠默认值识别出action和controller。我们可以用默认值同时映射默认的URL。
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
}
通过为controller何action设置默认值,没有片段的URL也可以匹配路由。
URL中片段越少,需要的默认值就越多。
使用静态URL片段
并不是所有的URL中的片段必须是变量,你可以创建拥有静态片段的URL模式,假设我们需要创建URL支持前缀是Public的:http://mydomain.com/Public/Home/Index
我们就可以如下的定义:
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
routes.MapRoute("", "Public/{controller}/{action}",
new { controller = "Home", action = "Index" });
}
这个URL模式仅匹配含有3个片段的URL,第一个值必须是自定义的那个值。其他2个值可以是任何值。
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("", "X{controller}/{action}");
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
routes.MapRoute("", "Public/{controller}/{action}",
new { controller = "Home", action = "Index" });
}
上面的模式,匹配含有2个片段的URL,其中第一个片段必须以字母X开始。controller的值由第一个片段中获得,不包括X,action的值由第二个片段中获得,比如,该模式匹配http://mydomain.com/XHome/Index,此URL会被导向到Index action 方法和Home controller。
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
routes.MapRoute("", "Public/{controller}/{action}",
new { controller = "Home", action = "Index" });
}
在上面的代码中,我们定义了一个新的路由并且将它置于最前面。我们这样做是因为路由会安照他们在RouteCollection中出现的顺序工作。MapRoute方法将把路由加在collection的最末端,意味着这个路由根据我们add他们的顺序工作。当然,我们也可以在指定位置插入路由。我们不建议使用这种方法,因为直接根据定义的顺序来工作,看起来更加直观简单。路由系统根据最先定义的路由中的URL模式匹配URL,如果不匹配,则继续下一个路由。路由会顺序的匹配下去直到找到一个匹配,或者匹配完。
对于如下的代码:
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("", "X{controller}/{action}");
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
routes.MapRoute("", "Public/{controller}/{action}",
new { controller = "Home", action = "Index" });
}
假设我们反转一下顺序
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
routes.MapRoute("", "X{controller}/{action}");
那么第一个路由会匹配URL,无论是有0个,1个或者2个片段。第二个路由将不会被匹配到。这个新的路由排除了X开头的URL。因此,URL比如http://mydomain.com/XHome/Index会被指向XHome的controller,可能这个controller不存在,因此会出现404错误。
我们可以通过合并静态URL片段和默认值来创建URL的别名,这对于你发布很有用。如果你需要重构一个应用程序,你需要保存先前的URL格式。设想,我们使用一个控制器叫Shop,现在被替换成Home,下面的例子演示了如何保存旧的URL结构。
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("ShopSchema", "Shop/{action}", new { controller = "Home" });
...other routes...
}
这个路由匹配2个片段的URL,其中第一个是shop。action值从第二个片段中获得。URL模式不包含controller的变量片段,所以会使用默认值。这意味着一个对Shop controller的请求会被转到对Home controller的请求。我们可以更进一步为action创建别名:
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("ShopSchema2", "Shop/OldAction",
new { controller = "Home", action = "Index" });
routes.MapRoute("ShopSchema", "Shop/{action}", new { controller = "Home" });
... other routes...
}
再次注意,我们把新建的路由放在最前面,因为此路由比后续的更加具体,如果请求Shop/OldAction,由下一个路由处理。如果反转顺序,可能会得到404错误。
自定义片段值
接下来,不再限制于controller和action变量。我们可以定义我们自己的变量,如下:
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "DefaultId" });
}
路由的URL模式定义了典型的controller,action变量和自定义变量:id。这个路由会匹配含有0-3个片段的URL。第三个片段值被分配给id,如果没有第三个片段,那么就会使用默认值。注意,一些名字是保留字,不能作为自定义变量名,controller, action,和 area。
我们可以通过RouteData.Values属性访问action方法中的片段变量值。为了演示这个,我们在HomeController类中加一个方法,CustomVariable,如下:
public ViewResult CustomVariable() {
ViewBag.CustomVariable = RouteData.Values["id"];
return View();
}
此方法包含URL中自定义变量的值,并且通过ViewBag传递到view,下面对应的view在CustomVariable.cshtml中的写法:
@{
ViewBag.Title = "CustomVariable";
}
<h2>Variable: @ViewBag.CustomVariable</h2>
如果你运行程序,导航到URL /Home/CustomVariable/Hello, CustomVariable action方法会被调用,自定义变量值会被获取到,并且显示出来。
使用自定义变量可以作为action方法参数,使用RouteData.Values属性是访问自定义路由变量的一种方法。还有更优雅的方法。如果我们为action方法定义了的变量匹配URL模式中的变量,那么MVC Framework会从URL中获得这个值,并传递这个值到action方法。比如,上例中我们在路由中自定义的变量id,我们可以修改CustomVariable 的action方法,让其含有一个匹配的参数,如下:
public ViewResult CustomVariable(string id) {
ViewBag.CustomVariable = index;
return View();
}
当路由系统匹配URL,第三个片段的值会被赋值给自定义变量index,MVC Framework比较了片段变量列表和actionn方法参数列表,如果名字匹配,则从URL中传递值给该方法。
我们定义了id参数为string,但是MVC Framework会尝试转换URL值的类型。如果我们把id定义为int或者datetime,那么MVC把这个值转换成我们需要的类型。这是非常有用的特征,避免了我们自己还要做处理。MVC Framework使用model绑定系统,转换URL中包含的值为.NET类型,这样可以处理很多更复杂的情况,后续会再说到。
定义可选的URL片段
可选的URL片段是指用户不需要给出,而不是没有默认值。下面是例子。我们通过指定片段变量的默认值为UrlParameter.Optional,把一个片段变量为可选的。
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}
这个路由会匹配URLs,无论id片段是否给出了。
从上表中可以看出,仅仅当URL中存在符合的片段,id变量才会被加到变量集中。清楚的说,当没有符合的片段存在是,并不是说id值为null,而是id变量没有定义。
这个特征很有用,比如当你需要知道用户是否提供了某些值的情况下。如果我们为id参数提供了默认值,并且在action方法中接收了值,我们就能知道是否默认值使用了,或者用户提交了一个包含默认值的url。
通常,用可选片段来加强关注分离,因此action方法的默认值不能在路由中定义了,如果一定要实现有默认值的功能,可以使用c#的可选参数方法来定义action方法的参数,如下:
public ViewResult CustomVariable(string id = "DefaultId") {
ViewBag.CustomVariable = id;
return View();
}
定义可变长度的路由
另一个可以改变URL模式保守性的方法是接受可变数量的URL片段。这允许你在一个路由中能处理任意长度的URL。通过指定一个片段变量为容器,增加一个前缀*,以此定义可变长度的片段。
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}
我们从先前的例子中扩展了路由,增加了一个catchall片段变量,可以把该变量想象成一个杂物箱。这个路由现在可以匹配任何URL,前3个片段用来设置controller,action,和id变量的值。如果URL包含可选片段,他们都会被分配到catchall变量。
这不限制URL模式中的片段的个数,注意,这些片段呈现的形式为segment/segment/segment。我们要自行处理这些string,把他们分割成单独的片段。
通过命名空间给予Controller优先
当请求的URL匹配了一个路由,MVC Framework得到controller变量的值,查询适合的名字。比如当controller变量是Home,然后MVC Framework去查找命名为HomeController的controller。如果当在不同的命名空间这种都有相同的叫做HomeController的类,那么MVC Framework就不知道怎么做了,然后就会报错。
这个问题经常出现,尤其是你在做一个大型的MVC项目,使用的Controller类库来自于不同的开发团队。要解决这个问题,我们可以告诉MVC Framework给出优先权,如下:
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "URLsAndRoutes.Controllers"});
}
我们将命名空间以字符串数组的形式给出,告诉MVC Framework优先在URLsAndRoutes.Controllers中查询,如果找不到,再去查找其他的。
加到路由中的命名空间都有同等的优先权,MVC Framework不会先处理第一个再处理第二个,比如,我们增加了2个命名空间:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "URLsAndRoutes.Controllers", "AdditionalControllers"});
我们就会看到之前的错误又出现了,因为MVC试着在我们增加的命名空间中处理controller。
如果我们需要在一个命名空间中给单个的controller优先权,但这其他的controller在另一个命名空间,我们就需要创建多个路由,如下:
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "AdditionalControllers" });
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "URLsAndRoutes.Controllers"});
}
在后面,我们会演示如何在整个application中设置命名空间的优先权,而不是就单个路由。这会是个更加简洁的解决方案。
我们可以让MVC Framework仅仅查看我们指定的命名空间。如果查找不到,则不查其他的地方了。如下:
public static void RegisterRoutes(RouteCollection routes) {
Route myRoute = routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "AdditionalControllers" });
myRoute.DataTokens["UseNamespaceFallback"] = false;
}
MapRoute方法返回一个Route对象,在之前的例子中,我们都是忽略了这个对象。因为我们不需要设置它。要禁止在其他命名空间查找controller,就必须获得Route对象,设置 DataTokens中的UseNamespaceFallback的值为false。
路由约束
使用路由约束可以让URL结构表现的更为精确。
使用正则表达式约束路由
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = "^H.*"},
new[] { "URLsAndRoutes.Controllers"});
}
通过给MapRoute方法传递约束参数,可以定义约束,和默认值类似,约束参数也是匿名类型,其中的属性和URL片段变量一致。
上例中,我们使用正则表达式匹配URL,仅当controller变量以H开头才能匹配。
默认值在约束之前使用,所以,如果我们请求URL /,默认的controller也是Home,接下来检查,发现Home能符合约束规则。
我们可以约束路由,使之仅仅匹配按指定的HTTP方法提交过来的URL。
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = "^H.*", action = "Index|About",httpMethod = new HttpMethodConstraint("GET") },
new[] { "URLsAndRoutes.Controllers" });
}
设定HTTP方法的约束格式看上去有的奇怪,它并不关心我们给定的属性名字。只要我们指定一个 HttpMethodConstraint类的实例。在上例我们把约束属性命名为httpMethod,以此可以在我们基于值的约束中区分出来。事实上httpMethod可以叫任何名字。
通过HTTP方式约束路由的功能和通过特性(比如HttpGet和HttpPost)限制action方法的功能无关。在请求管道中,路由约束处理的时间早多了,他们决定了controller和action的名字。而action方法的特性是用来决定指定的哪个action方法会被controller请求调用。在后续还会说到。
我们传递可以传递多个HTTP方式的名字到HttpMethodConstraint的构造函数,因为它的构造函数如下:
public HttpMethodConstraint(params string[] allowedMethods);
是接受一个参数数组。因此可以如下写法:
httpMethod = new HttpMethodConstraint("GET", "POST") },
如果自带的约束功能满足不了你的需求,你可以自定义约束,通过实现接口IRouteConstraint,下面的例子演示了自定义约束,request请求中有一部分是浏览器提供user-agent信息,自定义约束就对此信息做了约束。
using System.Web;
using System.Web.Routing;
namespace URLsAndRoutes.Infrastructure {
public class UserAgentConstraint : IRouteConstraint {
private string requiredUserAgent;
public UserAgentConstraint(string agentParam) {
requiredUserAgent = agentParam;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection) {
return httpContext.Request.UserAgent != null &&
httpContext.Request.UserAgent.Contains(requiredUserAgent);
}
}
}
IRouteConstraint 接口定义了Match方法,以此判断路由系统的约束条件是否被匹配了。下面显示了如何使用自定义约束。
public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new {
controller = "^H.*", action = "Index|About",
httpMethod = new HttpMethodConstraint("GET", "POST"),
customConstraint = new UserAgentConstraint("IE")
},
new[] { "URLsAndRoutes.Controllers" });
}
上面的例子,我们约束了路由,使之仅匹配浏览器中user-agent字符串中包含IE的。
路由请求磁盘文件
并不是所有的MVC应用程序请求都是需要controller和action的。我们仍然需要一个方法请求文件,比如images,静态html文件,javascript文件等等。作为延时,我们在Content文件夹中创建了一个文件StaticContent.html。如下:
<html>
<head><title>Static HTML Content</title></head>
<body>This is the static html file (~/Content/StaticContent.html)</body>
</html>
路由系统对于这种内容的服务提供了完整的支持,如果你启动应用程序请求/Content/StaticContent.html,你会看到这个HTML文件在浏览器中显示。
默认的,路由系统在处理路由之前,会先检查URL是否匹配了磁盘文件,如果匹配的话,会处理磁盘文件,而路由则不再应用了。我们可以翻转这个行为,能让路由系统先匹配路由,然后再匹配磁盘文件,只要设置RouteCollection的RouteExistingFiles属性为true即可。
public static void RegisterRoutes(RouteCollection routes) {
routes.RouteExistingFiles = true;
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new {
controller = "^H.*", action = "Index|About",
httpMethod = new HttpMethodConstraint("GET", "POST"),
customConstraint = new UserAgentConstraint("IE")
},
new[] { "URLsAndRoutes.Controllers" });
}
通常习惯上,把这句话放在RegisterRoutes方法的最上面,尽管即使你放在路由定义语句的下面也是有效的。一旦属性设置成true,我们就可以定义路由匹配指向磁盘文件的URL。如下:
public static void RegisterRoutes(RouteCollection routes) {
routes.RouteExistingFiles = true;
routes.MapRoute("DiskFile", "Content/StaticContent.html",
new {
controller = "Account", action = "LogOn",
},
new {
customConstraint = new UserAgentConstraint("IE")
});
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new {
controller = "^H.*", action = "Index|About",
httpMethod = new HttpMethodConstraint("GET", "POST"),
customConstraint = new UserAgentConstraint("IE")
},
new[] { "URLsAndRoutes.Controllers" });
}
当请求到Content/StaticContent.html时,路由会被映射到Account controller的LogOn action。
当RouteExistingFiles属性启用后,当没有路由能匹配时,磁盘文件会被发送到客户端。之前的例子,意味着IE用户会获得 Account controller的响应,而其他用户会看到静态内容。如下图:
要路由到磁盘文件需要仔细考虑,因为URL模式会匹配这类URL,就像匹配其它的一样。比如,一个请求/Content/StaticContent.html。会匹配诸如{controller}/{action},除非你非常小心,否则你可能会得到非常奇怪的结果,或者削减性能。所以,使用不轻易使用这种方式,为了演示,我们在Content目录创建第二个HTML文件OtherStaticContent.html,增加一个新的路由,如下:
public static void RegisterRoutes(RouteCollection routes) {
routes.RouteExistingFiles = true;
routes.MapRoute("DiskFile", "Content/StaticContent.html",
new {
controller = "Account", action = "LogOn",
},
new {
customConstraint = new UserAgentConstraint("IE")
});
routes.MapRoute("MyNewRoute", "{controller}/{action}");
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new {
controller = "^H.*", action = "Index|About",
httpMethod = new HttpMethodConstraint("GET", "POST"),
customConstraint = new UserAgentConstraint("IE")
},
new[] { "URLsAndRoutes.Controllers" });
}
当我们请求URL Content/OtherStaticContent.html,改请求会匹配,目标controller是Content,action方法是OtherStaticContent.html。事实上,任何含有2个片段的URL请求都会引起这种行为的发生。当然,事实上并没有Content controller和OtherStaticContent.html 方法。用户会收到一个404错误。
绕过路由系统
设置RouteExistingFiles属性,可以是路由系统更包容。但有时也需要排除一些路由。我们通过设置RouteCollection 的IgnoreRoute方法实现。
public static void RegisterRoutes(RouteCollection routes) {
routes.RouteExistingFiles = true;
routes.MapRoute("DiskFile", "Content/StaticContent.html",
new {
controller = "Account", action = "LogOn",
},
new {
customConstraint = new UserAgentConstraint("IE")
});
routes.IgnoreRoute("Content/{filename}.html");
routes.MapRoute("", "{controller}/{action}");
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new {
controller = "^H.*", action = "Index|About",
httpMethod = new HttpMethodConstraint("GET", "POST"),
customConstraint = new UserAgentConstraint("IE")
},
new[] { "URLsAndRoutes.Controllers" });
}
我们使用片段变量,诸如 {filename}来匹配URL,这种情况下,URL模式会匹配2个片段的URL,其中一个片段是Content,第二个片段是.html的扩展文件名。
IgnoreRoute 方法在RouteCollection中创建一个通道,这个通道的handler是StopRoutingHandler类的实例,而不是MvcRouteHandler。这个路由系统是硬编码的。如果URL传递到IgnoreRoute 方法,并且被匹配,那么该路由就被忽略,然后直接按照传统的HTTP Handler去匹配磁盘文件。上面的例子,我们使用这个特点,通过不路由任何HTML文件的请求,来最小化RouteExistingFiles属性的影响。