ASP.NET Routing 101
介绍
以前的做法
先前的ASP.NET Web Form开发时期,一个请求总是对应一个特定的物理文件,在文件的路径后用?key=value;的方式来传递参数,http://www.msn.com/books/products.aspx?name=twilight,这种方式不便于用户去记忆,也对SEO不够友好。相对于http://www.msn.com/books/twilight来说后者更具有可读,可记忆性。
还可以使用 URL 模式通过编程方式来创建对应于路由的 URL。这使您能够集中逻辑用于创建 ASP.NET 应用程序中的超链接。
对比 URL Rewriter
早期开源的URL Rewriter开始利用正则表达式匹配,将一个匹配规则请求进行转换并跳转另一个对应物理文件的URL上,从而实现上面所提到例子的功能。ASP.NET Routing则是直接将请求发送到对应的文件或者对应类和其方法的,从这一点来说效率是优于URL Rewriter的。
在MVC中的应用
由于MVC中使用Routing作为默认的路由工具,致使大多数人都认为其是MVC的一部分,这里澄清一下,MVC只是依赖Routing去做URL路由,将一个请求导向到一个Controller里的Action并生成其余的参数,而Routing本身还可以将请求导向到具体的文件,用作于Web Form开发。
接下来我们以MVC为例来讲解怎么进行Routing的配置,使用和测试。
MVC中的Routing
打开VS,新建一个项目之后,在Global.asax.cs中我们能看到有默认的Routing规则:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace BlueMvc { // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801 public class MvcApplication : System.Web.HttpApplication { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterRoutes(RouteTable.Routes); } } }
分析上面代码可以看到,在应用启动时的Application_Start方法中去注册Router,所有的规则都是存放在RouteCollection这个集合的实例下面,可以通过该对象的Add方法进行追加新的Routing规则。
MVC中RouteCollectionExtensions类对ASP.NET Routing进行的扩展,新增了MapRoute方法用于对Controller和Action进行的映射,上面代码就是使用了MapRoute来进行添加路由规则的。我们再来看下MapRoute这个方法的定义。
static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints);
参数具体的祥解
string routeName, 该路由规则的名称
string url, 约定要进行匹配URL的规则,如上面代码中的"{controller}/{action}/{id}", 只能匹配“product/search/101”这样三段式的URL
object defaults, 该参数指名要传递的Controller和Action
object constraints, 该参数用来对url中的规则进行约束,例如,指定id只能为数值型,Action名字等等。
定义URl Route
路由定义 | 匹配 URL 示例 |
{controller}/{action}/{id} |
/Products/show/beverages |
blog/{action}/{entry} |
/blog/show/123 |
/report/{type}/{year}/{month}/{day} |
/report/sales/2008/1/5 |
{language}-{country}/{action} |
/zh-cn/show |
{controller}/{action}/{*id} |
/Products/show/1/0/1 |
占位符
在路由中,您可以通过用大括号{ 和 }来定义占位符(称为“URL 参数”)。在分析 URL 时将 / 字符解释为分隔符。将路由定义中将不是分隔符和不在大括号中的信息视为一个常量值。(URL中只有三种值,占位符,/分隔符)、常量)
您可以在分隔符之间定义多个占位符,但必须用一个常量值分隔开。例如,{language}-{country}/{action} 是有效的路由模式。但是,由于占位符之间没有常量或分隔符,所以 {language}{country}/{action} 不是有效的模式。
*可变数量段
有时您需要处理包含可变数量的 URL 段的 URL 请求。定义路由时,通过将参数标记星号 (*) 可以指定最后一个参数应与 URL 的其余部分匹配。因而该参数称为“全部捕捉”参数。
{controller}/{action}/{*id}
默认值
如果 URL 没有包括该参数的值,则会使用默认值。通过将字典分配给 Route 类的 Defaults 属性,可以设置路由的默认值,上面代码中也有使用默认值,如下代码:
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
约束
约束是通过使用正则表达式或使用实现 IRouteConstraint 接口的对象来定义的。将路由定义添加到 Routes 集合时,同时也通过创建一个包含验证测试的 RouteValueDictionary 对象添加了约束。然后将此对象分配给 Constraints 属性。字典中的关键字标识约束适用的参数。字典中的值可以是表示正则表达式的字符串,也可以是实现 IRouteConstraint 接口的对象。
提供字符串后,路由将视字符串为正则表达式,并通过调用 Regex 类的 IsMatch 方法检查参数值是否有效。总是将正则表达式视为不区分大小写。
new { controller = @"^\w+", action = @"^\w+", id = @"^\d+", httpMethod = “POST” });
顺序
Request进来之后按照Route的顺序来进行匹配,一量URL满足最先能匹配的规则时,则URL Routing会对该URL进行处理,提取URL信息,生成一个RouteData,使用实现IRouteHandler接口的Hanlder来执行具体映射,并且不会再去匹配处理后面的规则,所以在设置Routes顺序时有以下两条准则。
-
将子集放到前面,父集放在后面。
-
将父集加上更祥细的约束
RoutingExistFiles,该属性指定是否要在指不到对应的规则情况下去寻找该URL对应位置的物理文件。
重要的命名空间、类和接口
- System.Web.Routing - 此命名空间提供用于URL路由的类
- IRouteHandler - 路由处理程序的接口,自定义的路由处理程序都要实现这个接口
- RequestContext - 封装所请求的路由的相关信息和当前的http上下文
- RouteData - 所请求路由相关信息,如Controller和Action的值
- RouteCollection - 路由集合
- RouteValueDictionary - 字典表,用来包含各样信息,不区分大小写
- Route - 继承RouteBase,内含路由相关信息
最佳实践
很多人担心如果前期URL设置不好或有新的Features加入如何修改Routing配置的问题,就目前应用结果来说这点担心有点多余,URL请求规则是在产品发布时定义好的,在产品修改时才会作出修改,换句话说配置改变的几率很小。
当然,我们仍可以将URL Routing放在外部配置文件中。具体作法这里先不谈。
单元测试
在测试中使用Moq Mock框架,模拟一次URL请求,通过判断route执行结果返回的Controller和Action值来确认Routing的正确性。
using System; using System.Text; using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System.Web; using System.Web.Routing; using Core; namespace Tests { [TestClass] public class RoutingTest { [TestMethod] public void TestRouting() { const string url = "~/"; var mockRequest = new Mock<HttpRequestBase>(); mockRequest.Setup(p => p.AppRelativeCurrentExecutionFilePath).Returns(url); mockRequest.Setup(p => p.PathInfo).Returns(string.Empty); HttpRequestBase request = mockRequest.Object; var mockContext = new Mock<HttpContextBase>(); mockContext.Setup(p => p.Request).Returns(request); HttpContextBase context = mockContext.Object; RouteTable.Routes.Clear(); BuleLiteApplication.RegisterRoutes(RouteTable.Routes); var routeData = RouteTable.Routes.GetRouteData(context); Assert.AreEqual("Home", routeData.Values["controller"]); Assert.AreEqual("Index", routeData.Values["action"]); } } }