ASP.NET MVC框架(第二部分): URL路径选择
【原文地址】ASP.NET MVC Framework (Part 2): URL Routing
【原文发表日期】 Monday, December 03, 2007 2:44 AM
上个月,我发表了我要撰写的系列贴子中的第一篇,这些帖子将讨论我们正在开发的新ASP.NET MVC框架。这个系列的第一个贴子建造了一个简单的电子商务产品列表/浏览场景,讨论了MVC后面的高层次的概念,示范了如何从头创建一个新ASP.NET MVC 项目,实现和测试电子商务产品列表的功能。
在今天的贴子里,我将深入讨论ASP.NET MVC框架的路径选择(routing)架构,讨论一些很酷的定制方式,你可以将其用于你应用中的一些更高级的场景。
第一部分的扼要简述
在这个系列的第一部分里,我们创建了一个电子商务网站,呈示了三类URL:
URL格式 | 行为 | URL例子 |
/Products/Categories | 浏览所有的产品分类 | /Products/Categories |
/Products/List/Category | 列出一个分类中的产品 | /Products/List/Beverages |
/Products/Detail/ProductID | 显示一个特定产品的细节 | /Products/Detail/34 |
我们通过创建象下面这样一个ProductsController类来处理这些URL:
在把上面这个类加到我们的应用中后,ASP.NET MVC框架就会把进来的URL自动导向到我们的控制器上的适当的action方法来处理请求。
在今天的贴子里,我们将深入讨论这个URL映射是如何发生的,以及探讨我们可以在ASP.NET MVC框架中利用的更高级的路径选择(routing)场景。我还将示范你如何可以轻松地单元测试URL路径选择场景。
ASP.NET MVC URL路径选择系统都做些什么?
ASP.NET MVC框架包括了一个很灵活的URL路径选择系统,它允许你在应用中定义URL映射规则。路径选择系统有2个主要目的:
- 把进来的URL映射到应用,并把它们做导向,这样,正确的Controller和Action方法执行来处理这些请求
- 构建可以用来回调Controllers/Actions的输出到客户端的URL(例如,表单提交, <a href=""> 链接, 和 AJAX 调用等等)
能够使用URL映射规则来同时处理进来的和输出的URL场景给应用代码添加了许多灵活性。这意味着,如果我们以后想改变应用的URL结构的话(譬如,把 /Products 改名为 /Catalog),我们可以修改应用层次的一套映射规则即可,而不需要改动控制器或视图模板中的任何代码。
默认的ASP.NET MVC URL路径选择规则
在默认情形下,当你使用Visual Studio用ASP.NET MVC Web Application模板来创建一个新项目时,它会往项目里添加一个ASP.NET Application类。这是在Global.asax后台代码中实现的:
ASP.NET Application类允许开发人员处理应用启动/中止以及全局性的错误处理的逻辑。
默认的ASP.NET MVC项目模板自动向该类添加一个Application_Start方法,在其中注册2条URL路径选择规则:
上面的第一条路径选择规则表示,ASP.NET MVC框架在默认情形下,在决定用哪个Controller类来生成实例,调用哪个Action方法时(以及哪些需要传入的参数时),应该使用" [controller]/[action]/[id]"的格式把URL映射到控制器上。
这个默认的路径选择规则就是为什么第一部分中我们的电子商务浏览例程中对URL /Products/Detail/3 的请求自动调用我们的ProductsController类的Detail方法,并且传入3作为id参数值的原因:
上面的第二条路径选择规则,是用来对我们应用的根URL"Default.aspx"做特例处理的(当处理一个应用的根URL的请求时,这个URL有时会被服务器代替"/"来传入)。这个规则确保对我们应用的根"/Default.aspx"或"/"的请求,都会由HomeController类(是在我们使用ASP.NET MVC Web Application项目模板生成一个新的应用时,由Visual Studio自动生成的控制器)里的Index() action方法处理。
理解Route实例
路径选择规则是通过向System.Web.Mvc.RouteTable的Routes集合添加Route实例来注册的。
Route类定义了许多你可以用以配置映射规则的属性。你可以通过“传统的” .NET 2.0属性赋值的方式来设置这些属性:
或者利用VS 2008的C#和VB编译器中的新的对象初始化器特性,更简洁地设置属性:
Route 类的Url属性定义了应该用来评估一个路径选择规则是否适用于进来的特定请求的Url匹配规则。它还定义了URL应该如何分割成(tokenized)不同的参数。URL中可替换的参数,是通过 [参数名称] 的句法来定义的。就象在后文论及的那样,我们并不限制于一套固定的“熟知”参数名称,你可以在URL使用任何数目的任意参数。例如,我可以使用一个 "/Blogs/[Username]/Archive/[Year]/[Month]/[Day]/[Title]"的URL规则把进来的一个博客贴子的URL进行分割,由MVC框架自动分析成UserName,Year,Month,Day 和 Title参数,并把它们传入我的控制器的action方法中。
Route类上的Defaults属性定义了一个默认值的字典,可以在进来的URL并不包含某个指定的参数值的情形下使用。例如,在上面的URL映射例子中,我们定义了2个默认URL参数值,一个是"[action]" ,另一个是 "[id]"。这意味着,如果应用收到的是 /Products/ 这个URL,在默认情形下,路径选择系统会默认使用“Index”作为ProductsController的action的名称来执行。同样地,如果指定了/Products/List/ ,那么就会使用null字符串作为"ID"参数的值。
Route类的RouteHandler 属性定义了在URL被分割成参数,适当的路径选择规则被确定之后,应该用来处理请求的 IRouteHandler 实例。在上面的例子中,我们表示,我们想要使用System.Web.Mvc.MvcRounteHandler类来处理我们配置好的URL。这个额外的步骤存在的原因是,我们想确保URL路径选择系统可以同时用于MVC和非MVC请求的情形。有这个IRouteHandler接口,意味着,我们也能够干净地用于非MVC的请求(例如标准的WebForms,Astoria REST支持等等)。
Route类还有一个 Validation属性,在本文的稍后我们会做讨论。这个属性允许我们指定一个路径选择规则匹配需要满足的先决条件。例如,我们可以指定一个路径选择规则应该只适用于一个特定的HTTP动词(允许我们轻松地映射REST命令),或者我们可以对参数值使用正则表达式,来过滤一个路径选择规则是否匹配。
注:在MVC框架的第一个公开预览版中,Route类是不可以扩展的(它只是个数据类),在下一个预览版中,我们正在研究把它做成可扩展的,允许开发人员添加特定场景的路径类(譬如,一个RestRoute子类)来干净利索地添加新的语义和功能。
路径规则的评估
当一个进来的URL被ASP.NET MVC Web应用收到时, MVC框架会对RouteTable.Routes集合中的路径选择规则进行评估,以决定适当的Controller来处理该请求。
MVC 框架是按RouteTable规则注册的次序做评估来选择使用哪个Controller的。将进来的URL对每条Route规则做检测,看它是否匹配,如果一个Route规则匹配的话,那么该规则(以及相关联的RouteHandler)将被用来处理进来的请求(所有后面的规则都略过不计)。这意味着你一般要按“最特殊到最不特殊(most specific to least specific,从特殊到一般)”的次序来组织你的路径选择规则。
路径选择场景:自定义查询URL
让我们使用一下现实场景中的自定义路径选择规则来对此做一流程示范,以实现我们的电子商务网站的查询功能为例。
开始,我们往我们项目中添加一个新的SearchController类:
然后,我们在SearchController类中定义2个Action方法。Index()方法用来显示一个查询网页,上有一个文本框,让用户来输入和提交查询文字。Results() action方法则用来处理相应的表单提交,对数据库做查询,然后把结果显示给用户:
使用默认的/[controller]/[action]/[id] URL路径映射规则,我们可以现成使用象下面这样的URL来调用我们的SearchController的行为:
场景 | URL | Action方法 |
查询表单: | /Search/ | Index |
查询结果: | /Search/Results?query=Beverages | Results |
/Search/Results?query=ASP.NET | Results |
注意,根URL /Search 默认映射到Index() action方法的原因是因为在Visual Studio创建一个新项目时,默认添加的 /[controller]/[action]/[id] 的路径定义将默认的action自动设置到“Index"上的(通过Defaults属性):
虽然象 /Search/Results?query=Beverages 这样的URL是完全可行的,我们也许决定对查询结果我们想要稍微好看些的URL。具体来说,我们也许想去掉URL中的“Results”action名称,把要查询的文字作为URL的一部分传入,而不是作为URL的查询字符串的值。例如:
场景 | URL | Action方法 |
查询表单: | /Search/ | Index |
查询结果: | /Search/Beverages | Results |
/Search/ASP.NET | Results |
我们可以通过在默认的 /[controller]/[action]/[id] 规则之前添加2条自定义的URL路径映射规则来启用这些比较好看的查询结果URL,象下面这样:
在前2条规则中,我们现在明确地指定了对应 /Search/ URL的控制器和Action参数。我们表明,"/Search" 应该总是由SearchController上的“Index” action来处理。而任何拥有子URL层次结构的URL (/Search/Foo, /Search/Bar等等 )则总是由SearchController上的 "Results" action 来处理。
上面的第二条路径选择规则表明,在 /Search/ 前缀之后的任何字符应该当作名为"[query]"的参数来处理,这个参数将作为方法参数来传入SearchController上的Results action方法中:
最有可能的,我们还会对查询结果启用分页(我们每次只显示10个查询结果)显示。我们可以通过查询字符串值的方法来实现(譬如, /Search/Beverages?page=2),或者我们也可以把页号嵌在URL中(譬如/Search/Beverages/2)。要支持后面这个做法的话,我们需要做的是,给我们的第二条路径选择规则再加一个额外的可省参数:
注意,上面的新URL规则现在匹配的是“Search/[query]/[page]"。我们还将默认的页号配置为1,万一页号没有包含在URL之中的话(这是通过作为“Defaults”属性值的匿名类型传入的)。
然后我们可以把我们的SearchController.Results action方法更新为接受页号参数作为一个方法参数:
这样,我们就有比较好看的查询URL了(剩下的就是实现这个查询算法,我将把它作为练习留给读者来完成 <g>)。
路径选择规则的验证先决条件
就象我在这个贴子前面提到的,Route类有个Validation属性,允许你添加为使路径选择规则匹配,必须为真的验证先决条件规则(除了URL过滤外)。ASP.NET MVC框架允许你使用正则表达式来验证URL中的参数值,也允许你对HTTP Headers进行评估(根据HTTP动词的不同进行不同的URL路径选择)。
下面是一个我们可以用到象 /Products/Detail/43 这样的URL身上的自定义的验证规则,它指定了其中的ID参数必须是数字(不允许字符串),而且它的长度必须在1到8之间:
如果我们往应用中传入象 /Products/Detail/12 这样的URL,上面的路径选择规则是合法的,但如果传入 /Products/Detail/abc 或 /Products/Detail/23232323232,它就不会匹配。
从路径选择系统构建输出的URL
在本文的前面,我说过ASP.NET MVC框架中的URL路径选择系统负责两件事情:
- 把进来的URL映射到处理的Controllers/Actions上
- 帮着构建可以在以后用来回调Controllers/Actions的输出到客户端的URL(例如,表单提交, <a href="">链接, 和 AJAX 调用等等)
URL路径选择系统有不少辅助方法和类,方便你在运行时动态查看和构建URL(你也可以直接对RouteTable的Route集合进行操作来查看URL)。
Html.ActionLink
在本博客系列的第一部分,我简单地讨论了Html.ActionLink()视图辅助方法。它可以在视图里使用,允许你动态地生成 <a href=""> 超链接。比较酷的是,它可以使用MVC路径选择系统里定义的URL映射规则来生成这些URL。例如,下面2个Html.ActionLink 调用:
automatically pick up the special Search results route rule we configured earlier in this post, and the "href" attribute they generate automatically reflect this: 会自动地使用我们在本贴子前面配置的的特殊查询结果路径规则,它们自动生成的href属性反映了这个情况:
特别地,注意上面,Html.ActionLink的第二个调用自动地把page参数映射成URL的一部分(也注意,第一个调用省略了page参数值,因为它知道服务器端会自动提供默认值)。
Url.Action
除了使用Html.ActionLink外,ASP.NET MVC还有个Url.Action()视图辅助方法。该方法生成原生的字符串URL,然后你可以任何方式来使用它们。例如,下面的代码片段:
会使用URL路径选择系统返回下面这个原生的URL(而不是包装在 <a href=""> 元素里):
Controller.RedirectToAction
ASP.NET MVC还提供了Controller.RedirectToAction()辅助方法,你可以在控制器里使用来进行转向操作(URL是使用URL路径选择系统计算出来的)。
例如,当在控制器里调用下面代码时:
在内部,它会生成一个对Response.Redirect("/Search/Beverages")的调用。
DRY (别重复自己)
上述所有的辅助方法的好处在于它们允许我们避免在我们的控制器和视图逻辑中硬写URL。如果在后来我们决定改变查询URL路径映射规则,从 "/Search/[query]/[page]" 改回到 "/Search/Results/[query]/[page]" 或者 "/Search/Results?query=[query]&page=[page]" ,我们只要在一个地方(我们的路径注册代码中)做编辑,就可以轻松搞定。我们不需要改动视图或控制器中的任何代码,就可以捡起新的URL(这就坚持了“DRY原则”)。
使用Lambda表达式从路径选择系统构建输出的URL
前面的URL辅助方法例子使用了VS 2008中VB和C#现在支持的新的匿名类型。在上面的例子中,我们使用了匿名类型来有效地传入一串名称/数值对,用以帮助映射URL(你可以把这想像为生成字典的一个比较干净的方式)。
除了使用匿名类型以动态方式传递参数外, ASP.NET MVC框架还支持使用强类型机制创建action路径的能力,这些强类型机制为URL辅助方法提供了编译时检查和intellisense。这是通过使用泛型和新的VB和C#对Lambda表达式的支持来实现的。
例如,下面这个匿名类型 ActionLink 调用:
也可以写成:
除了写起来简短外,这第二个选项还有类型安全的好处,这意味着你得到对表达式的编译时检查以及Visual Studio的代码intellisense(你还可以使用重构工具对它进行重构):
注意上面,我们是如何使用intellisense挑选出我们想用的SearchController的Action方法的,以及参数是强类型的。生成的URL都是由ASP.NET MVC URL路经选择系统驱动的。
你也许在想,这到底是怎么回事呢?如果你还记得,8个月前,我在博客里讨论Lambda表达式时,我谈到了Lambda表达式既可以编译出成代码代理(delegate),也可以编译成表达式树对象,然后在运行时可以用来分析Lambda表达式。对于 Html.ActionLink<T> 辅助方法,我们使用这个表达式树选项,然后在运行时分析对应的lambda,查出它调用的action方法以及相关的参数类型,在表达式中指定的名称和值等。然后我们可以在MVC URL路径选择系统中使用这些信息, 返回合适的URL和相关联的HTML。
重要注意事项: 当使用这Lambda表达式方法时,我们实际上从不运行对应的Controller action方法。例如,下面的代码并不调用我们的SearchController中"Results" action方法:
实际上,它只是返回这个HTML超链接:
如果这个超链接被用户点击的话,它会向服务器发回一个请求,该请求会调用SearchController的Results action方法。
单元测试路径
ASP.NET MVC框架的一个核心设计原则是促进很好的测试支持。跟MVC框架的其他部分一样,你可以轻松地单元测试路径和路径匹配规则。MVC路径选择系统可以独立于ASP.NET生成实例和运行,这意味着你可以在任何单元测试库里装载和单元测试路径模式(而不用启动web服务器),可以使用任何单元测试框架(NUnit, MBUnit, MSTest等等)。
虽然你可以在你的单元测试中直接单元测试一个ASP.NET MVC应用的全局RouteTable映射集合,但一般来说,让单元测试改变或者依赖于一个全局的状态不是一个很好的主意。一个你可以使用的较好的模式是,把你的路径注册逻辑放在一个象下面这样的RegisterRoutes()辅助方法中,对作为参数传入的RouteCollection进行操作(注:我们也许会把这个模式在下个预览版更新中做成默认的VS模板模式):
然后,你可以编写单元测试,创建自己的RouteCollection实例,调用Application的RegisterRoutes辅助方法,在其中注册应用的路径选择规则。然后,你可以向应用发出模拟请求,核实这些请求确有注册了的正确的控制器和action方法,而不用担心任何副作用:
结语
希望这个贴子提供了关于ASP.NET MVC路径选择架构工作原理的一些细节,以及你如何可以使用它来定制发布在你的ASP.NET MVC应用中的URL的结构和布局。
在默认情形下,在你创建一个新的ASP.NET MVC Web应用时,它会预先定义一个你可以使用的默认的 /[controller]/[action]/[id] 路径选择规则,而不必手工配置或启用什么。这应该允许你不用注册你自己的自定义路径选择规则,就可以建造许多应用。但希望上面的内容示范了,如果你想对你自己的URL格式做自定义结构的话,做起来并不难, MVC框架对此提供了许多的功能和灵活性。