从设计角度出发,一个 ASP.NET 应用程序并非强制地依赖物理页面。在 ASP.NET MVC 中,用户发出请求并在某个资源上做相应处理。然而,就整个框架而言,并非授权这种语法来描述资源和行为。我深信这样的表述使你很可能地想到了 REST(Representational State Transfer)。
虽然你可以在一个 ASP.NET MVC 应用中使用纯粹的 REST 方案, 但是我想说的是ASP.NET 是松散的面向 REST,它承认例如资源和行为的概念。举个例子,在一个纯 REST 解决方案中,你会使用 HTTP 动词来表示对应的行为 - GET,POST,PUT 和 DELETE,并使用 URL 来定位资源。在 ASP.NET 中实现一个纯 REST 解决方案是可行的,但是你需要做额外的工作。
ASP.NET MVC 的默认行为是使用自定义的 URL,并且路由到哪些行为和资源,这都是由你负责的。这些语法形式一系列 URL 模式集合表达的,也被称为路由。
URL 模式和路由
一个路由就是一个模式匹配字符串,换句话说就是通过它来匹配一个 URL 的绝对路径,URL 字符串是不包含协议,服务器,和端口信息的。一个路由是一个常量字符串,但是它往往也包含一些占位符。底下就是一个例子:
/home/test
这个路由是一个常量字符串,而且仅能匹配 URL 为 /home/test 的路由。绝大多数的情况下,我们更多的是处理那些包含一个或多个占位符的路由。这里有两个例子:
/{resource}/{action}
/Customer/{action}
这两个路由仅会匹配那些包含两个片段的 URL。后者仅会匹配首个片段字符串为“Customer”的 URL。二前者,并没有对片段设置相应的要求。
占位符通常被称作 URL 参数,它是由{}包裹的。你可以在一个路由中添加多个占位符,只要它们之间被常量或者分隔符分开。斜杠/在路由的多个片段之间充当分隔符。占位符的名称(比如 action)就是键,通过它,你可以在代码中通过编程的方式获取对应片段中的值。
这就是 ASP.NET MVC 应用程序的默认路由:
{controller}/{action}/{id}
在这个路由中,包含了三个占位符,并且被斜杠所分割开。而底下的 URL 就可以匹配这个路由:
/Customers/Edit/ALFKI
你可以添加任意多的路由,在路由中也可以添加任意多的占位符。同样,你也可以删除程序默认的路由。
定义应用路由
一个应用程序的路由通常定义在 global.asax 文件中,而且他们是在程序启动的时候被处理。现在,就看一下 global.asax 处理路由的代码片段:
1 public class MvcApplication : HttpApplication 2 { 3 proteted void Application_Start() 4 { 5 RouteConfig.RegisterRoutes(RouteTable.Routes); 6 7 // Other code 8 ... 9 } 10 }
RegisterRoutes 是 RouteConfig 类中的一个方法,这个类被定义在项目中其他的文件夹下。通常这个文件夹叫 App_Start (你也可以随意定义这个文件夹的名称)。底下就是这个类的定义:
1 public class RouteConfig 2 { 3 public static void RegisterRoutes(RouteCollection routes) 4 { 5 // Other code 6 ... 7 8 // Listing routes 9 routes.MapRoute( 10 "Default", 11 "{controller}/{action}/{id}", 12 new { 13 controller = "Home", 14 action = "Index", 15 id = UrlParameter.Optional 16 }); 17 } 18 }
通过上面代码可以看到,Application_Start 事件处理器通过调用 RegisterRoutes 公共静态方法来注册所有路由。注意到 RegisterRoutes 方法的名称和原型是随意的,所以如果你有充分的理由,你就可以修改它。
应用所支持的路由必须注册到 Route 对象的一个静态集合中,它是由 ASP.NET MVC 管理,这个集合就是 RouteTable.Routes。你一般通过 MapRoute 方法来创建路由集合。MapRoute 方法提供了许多重载,并且多数情况上都已够用。但是,这个方法并不会让你配置一个路由对象的各个方面。如果你想在一个路由对象上设置一些内容但是 MapRoute 方法并不提供,你可能就会求助于底下的代码了:
1 // Create a new route and add it to the system collection 2 var route = new Route(...); 3 RouteTable.Routes.Add("NameOfTheRoute", route);
一个路由通常包含一些属性,比如名称,URL 模式,默认值,限制,数据标记,和路由处理器。而最常设置的属性包括名称,URL 模式,和默认值。现在让我们再来回顾一下默认路由:
1 routes.MapRoute( 2 "Default", 3 "{controller}/{action}/{id}", 4 new { 5 controller = "Home", 6 action = "Index", 7 id = UrlParameter.Optional 8 });
第一个参数是路由的名称,每一个路由都应该有一个独有的名称。第二个参数是 URL 模式。第三个参数是一个对象,用来设置默认值的。
注意,一个不完整的 URL 也可能匹配这个路由。让我们看一下根 URL - http://yourserver.com。第一眼看下去,这样的路由应该不会匹配默认路由,但是,如果路由参数有默认值提供,那么这些参数片段是可以为空的。因此,在上面的例子中,当你请求了根 URL,这个请求就会被解析,并调用 Home 控制器的 Index 方法。
处理路由
ASP.NET URL routing module 应用了一系列规则将入站请求 URL 与一个定义好的路由进行匹配。最重要的一个规则就是,程序会按照路由在 global.asax 注册的顺序进行检查匹配。
为了确保路由在正确的顺序下被处理,你必须将他们按最可能的情况到最不可能的情况进行排序。但是不管在什么情况下,搜寻一个匹配路由的过程总是在找到第一个匹配的路由时结束。这就表示,如果在路由列表的最后添加一个新的路由可能并不奏效,或者将会产生一些错误。另外,要清楚在列表顶部放置的 catch-all 模式将会使特定格式的路由被忽略掉。
除了外观上显而易见的排序外,也有其他的因素干扰匹配路由的过程。正如之前提到的,为路由提供的默认值。默认值会被自动赋值到定义好的占位符上,如果当前 URL 没有提供相应的值。考虑以下的两个路由:
{Orders}/{Year}/{Month}
{Orders}/{Year}
在第一个路由上,你为{Year}和{Month}两个占位符设置了默认值,那么第二个路由将永远不会被匹配到,这是由于设置了默认值而导致的。第一个路由将会永远被匹配到不管 URL 是否已经指定了一个年份或者月份。
斜杠也是一个陷阱。{Orders}/{Year}和{Orders}/{Year}/是两个完全不同的路由,其中一个永远不会匹配另一个。
另一个影响路由匹配的因素就是可选属性 - 约束。一个路由约束就强制路由参数必须满足约束要求,不然就无法匹配。URL 不仅需要与路由模式相适应,而且也要包含满足约束要求的数据。一个路由限制可以通过多种方法来定义,包括通过使用正则表达式。这里就有一个约束的例子:
1 routes.MapRoute( 2 "ProductInfo", 3 "{controller}/{productId}/{locale}", 4 new { controller = "Product", action = "Index", locale = "en-us" }, 5 new { productId = @"\d{8}", locale = "[a-z]{2}-[a-z]{2}" } 6 );
因此,这个路由的 productId 占位符必须是八位数的数字,而 locale 占位符必须是一对由连字符连接而成的两个字符的字符串。约束并不能保证将所有无效的产品编号和本地代码拦截在门外,但是它至少削减了许多额外的工作。
路由处理器
根据 routing module 决定入站请求 URL 是否被应用所接受,路由仅定义了一小部分规则。而最终决定怎么重新映射请求进来的 URL 的组件就是 route handle。路由处理器就是一个处理所有匹配过的请求的对象。它的唯一目标就是返回真正处理请求的 HTTP handler。
技术上讲,一个路由处理器就是一个实现了 IRouteHandler 接口的类。这个接口定义如下:
1 public interface IRouteHandler 2 { 3 IHttpHandler GetHttpHandler(RequestContext requestContext); 4 }
RequestContext 类定义在 System.Web.Routing 命名空间下,它封装了请求的 HTTP 上下文和一些与路由相关的可用信息,比如 Route 对象本身, URL 参数,和约束。这些数据被封装在一个 RouteData 对象里。底下就是 RequestContext 类的声明:
1 public class RequestContext
2 {
3 public RequestContext(HttpContextBase httpContext, RouteData routeData);
4
5 // Properties
6 public HttpContextBase HttpContext { get; set; }
7 public RouteData RouteData { get; set; }
8 }
ASP.NET MVC 框架并没有提供太多内置的路由处理器,这也就暗示了需要自定义的路由处理器并不常见。但是,如果需要,你可以利用它,过会,我们还会回到自定义路由处理器上并在后面章节上介绍一个例子。
为物理文件处理请求
在路由系统中,另一个可配置的方面并且可能会影响到是否能够成功匹配的方面就是路由系统是否需要处理一个能顾匹配物理文件的路由请求。
默认情况下,ASP.NET 路由系统会忽略那些可以匹配到物理文件的请求。注意,如果这个服务器文件存在,路由系统会忽略当前请求即使这个请求可以匹配一个路由。
如果你需要的话,你可以强制路由系统处理所有请求,只要设置 RouteCollection 对象的 RouteExistingFiles 属性为 true 即可。代码如下:
1 // In global.asax.cs 2 public static void RegisterRoutes(RouteCollection routes) 3 { 4 routes.RouteExistingFiles = true; 5 ... 6 }
注意,在一个 ASP.NET MVC 应用程序中,如果所有请求都通过路由处理可能会产生一些问题。比如,如果你在一个简单的 ASP.NET MVC 应用中的 global.asax.cs 文件中添加之前的代码,并且运行程序,如果你访问 default.aspx 页面,就会立即收到 HTTP 404 错误。
阻止路由定义好的 URL
ASP.NET URL routing module 并没有限制你维护一堆可接受的 URL 模式。你也可以将某些特定的 URL 排除在路由机制之外。你可以通过两部方法来阻止路由系统来处理特定的 URL。第一,你为这些 URL 定义一个模式,并将它保存在一个路由对象中。第二,你将这个路由对象与一个特殊的路由处理器相连,这个路由处理器就是 StopRoutingHandler 类。它所做的就是当它的 GetHttpHandler 方法被调用的时候抛出一个 NotSupported 异常。
比如,底下的代码指导路由系统忽略所有 .axd 请求:
1 // In global.asax.cs 2 public static void RegisterRoutes(RouteCollection routes) 3 { 4 routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 5 }
所有的 IgnoreRoute 方法都与 StopRoutingHandler 路由处理器相关联。
最后,为 URL 中的 {*pathInfo} 占位符做一点解释是有必要的。pathInfo 记号表示任意跟着 .axd 后面的内容。而星号表示最后的参数应该匹配 URL 的剩余部分。换句话说,任何紧跟着 .axd 扩展的内容都将进入 pathInfo 参数中。这个参数也被认为是 catch-all 参数。
属性路由
在 ASP.NET MVC 5 中内置了一个流行的 NuGet 包,就是AttributeRouting(参见 http://attributerouting.net)。属性路由就是通过属性直接在控制器的行为上定义路由。正如前面所说,传统的路由是基于在程序启动时建立在 global.asax 中的协定。
任何时候如果有请求进来,URL 会与注册的路由模板进行匹配。如果一个路由匹配成功,那么处理这个请求的控制器和当中的方法就被确定下来了。如果没有匹配成功,那么请求就会被拒绝,并且会产生一个 404 错误消息。但是现在,在大型应用中,或者在一个具有浓浓 REST 风味的中型项目中,路由的数量可能会非常多,并且这样的路由记录可能就有上百条。为此,属性路由应运而生,并且现在已经集成到 ASP.NET MVC 5 中,甚至在 Web API 中。我们将会在后面详细讨论。
1 [HttpGet("orders/{orderId}/show")] 2 public ActionResult GetOrderById(int orderId) 3 { 4 ... 5 }
这段代码设置了 GetOrderById 方法仅可以通过 HTTP GET 请求,并且 URL 模板必须匹配所指定的模式的时候才可以被访问。路由参数 - orderId 指令 - 必须与定义在方法签名中的参数所匹配。这里也有更多的属性可用。但是属性路由的主旨全都在这里。想要获取更多的信息(比如,配置)你可以参阅 http://attributerouting.net ,因为 ASP.NET MVC 中集成的路由属性就是直接来源于现有的 NuGet 包。
【声明:本文是个人翻译而来。当中可能会存在许多不当之处,万望指出,谢谢。文章会持续更新】