好吧,我承认这个标题有些八股。
在之前的文章中,有一些朋友会问我一些关于ASP.NET Routing的内容。这个组件的重要性越来越大,ASP.NET MVC,ASP.NET Dynamic Data都用到了ASP.NET Routing。事实上,在ASP.NET 4.0中还会出现对ASP.NET WebForms的支持。可惜的是,目前对于ASP.NET Routing的文档和描述内容都很少。因此,有的时候一些朋友可能无法理解我一些扩展的设计思路。现在我打算详细解释一下有关ASP.NET Routing中最常见的几个问题。
ASP.NET Routing的作用究竟是什么
ASP.NET Routing的作用有两个:
- 从请求中捕获数据。
- 从数据生成虚拟路径。
可见,ASP.NET Routing的功能是双向的。值得注意的是,这两个操作严格说来并不对称。因为第1点是从“请求”中捕获数据,而数据源并不一定是一个URL,可能是Server Variables,可能是Header;与此对应,从数据生成的虚拟路径,则一定是生成一个URL,一个字符串。
平时使用ASP.NET Routing的时候,我们会大量利用ASP.NET Routing组件中的Route类,它的作用是从Virtual Path中获取数据,并提供一些如默认值,约束等高级功能。但是,对于ASP.NET Routing组件,或者说它的“引擎”使用的是一个抽象的类型“RouteBase”。而Route只是RouteBase的一个实现罢了。
在之前的文章中,我曾经提出过其他一些扩展,如FormatRoute或DomainRoute。
为什么Route不使用正则表达式
ASP.NET Routing中自带的Route类会根据我们指定的“路径模板”,从请求的虚拟路径中捕获数据。而这个“模板”是类似这种形式的:
{controller}/{action}/{id}
于是根据当前请求的虚拟路径,便可从中捕获出controller,action和id三个值了。不过有朋友说,为什么不使用正则表达式来捕获数据呢?例如:
(?\w+)/(? \w+)/(? \d+)
使用命名捕获组的正则表达式来虚拟路径,自然也可以得到controller,action和id的值了。此外,使用正则表达式的另一个好处是可以严格约束路径中的字符,例如上面的正则表达式将controller和action限制为单词字符,而将id限制为数字,这样不满足这种形式的路径便无法获得匹配了。不过问题出在哪里呢?
这里的问题便是:ASP.NET Routing的工作是双向的,而如果使用正则表达式,捕获数据自然没话说,但是从正则表达式来生成一个字符串就不容易了,由于正则表达式的匹配形式非常广,很多时候甚至根本无法逆向得到一个字符串。因此现在,如果您要对某个值进行约束,只能为Route对象提供一个约束条件了:
new { controller = "\w+", action="\w+", id="\d+" }
当然,如果您需要的话,可以实现一个正则表达式匹配规则的子集,使生成字符串的工作变得可行,那么也是没有问题的。
ASP.NET Routing工作流程
ASP.NET Routing的工作有两方面,处理请求和生成虚拟路径。处理请求时的流程已经在之前的文章里详细谈过了,这里再谈一下ASP.NET Routing生成虚拟路径的做法。
ASP.NET Routing在生成虚拟路径时还是使用RouteCollection类型上的方法,自然我们是像RouteTable.Routes属性中注册的Routing规则,那么自然也是从RouteTable.Routes属性中获取虚拟路径。RouteCollection类型中有一个GetVirtualPath方法,返回一个VirtualPathData对象,其中包含一个VirtualPath属性,便是最终得到的虚拟路径。
GetVirtualPath方法有两个重载,第一个是:
public VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
RequestContext对象是当前请求的上下文,而RouteValueDictionary包含的则是用于构造虚拟路径的数据。与处理请求的方式相同,RouteCollection类型的GetVirtualPath方法也会依次调用每个RouteBase对象的GetVirtualPath方法,并返回第一个不为null的结果(事实上还会经过处理,且看下文)。如果一个RouteBase对象都无法匹配当前的提供的数据,则RouteCollection的GetVirtualPath返回null。
GetVirtualPath方法的另一个重载则会提供一个name参数:
public VirtualPathData GetVirtualPath(RequestContext requestContext, string name, RouteValueDictionary values)
name参数的作用是“指定”一个Routing规则,也就是一个特定的RouteBase对象。如果这个RouteBase对象返回null,则GetVirtualPath方法直接返回null。指定name的优势在于代码可读性高(开发人员可以直接找到对应的Routing策略)、性能有一定优势(无需遍历每个Routing规则),以及Routing规则之间不会产生冲突。
ASP.NET Routing支持域名吗?
不支持,从设计上就不支持。
这一点从RouteCollection的GetVirtualPath方法实现中可以看到,这个方法在遍历每个RouteBase对象并得到第一个不为null的结果后,它不是立即返回,还要使用GetUrlWithApplicationPath方法进行处理:
private static string GetUrlWithApplicationPath(RequestContext requestContext, string url) { string str = requestContext.HttpContext.Request.ApplicationPath ?? string.Empty; if (!str.EndsWith("/", StringComparison.OrdinalIgnoreCase)) { str = str + "/"; } return requestContext.HttpContext.Response.ApplyAppPathModifier(str + url); }
url参数的含义是“虚拟路径”,GetUrlWithApplicationPath方法的作用则是为其加上“应用程序目录”。例如,我们一个站点可能不是部署在“/”下,而是部署在“/MvcApp”这个“子站点”中。这样,虽然RouteBase的GetVirtualPath返回的是虚拟路径“Home/Index/5”,但是RouteCollection的GetVirtualPath方法在使用GetUrlWithApplicationPath处理之后,最终返回的路径便是/MvcApp/Home/Index/5这样的字符串了。
因此可以这么说,ASP.NET Routing从设计上就不支持域名的概念,虽然我们可以扩展RouteBase对象,但是我们无法扩展RouteCollection的GetVirtualPath方法。换句话说,即使我们的DomainRoute可以返回类似“http://www.cnblogs.com/Home”这样的URL,被RouteCollection的GetVirtualPath方法一鼓捣又会在之前加一条斜杠——因此,在MvcPatch的MvcPatch.Routing项目中,我为RouteCollection提供了一个名为GetVirtualPathEx的扩展方法,如果它发现URL以http或https开头,则不会拼接上应用程序目录。至于其他情况下,则和原来的GetVirtualPath方法行为一致。
如果您要使用DomainRoute,那么请在需要的使用GetVirtualPathEx而不是原来的GetVirtualPath方法。