ASP.NET Web API路由系统:路由系统的几个核心类型

虽然ASP.NET Web API框架采用与ASP.NET MVC框架类似的管道式设计,但是ASP.NET Web API管道的核心部分(定义在程序集System.Web.Http.dll中)已经移除了对System.Web.dll程序集的依赖,实现在ASP.NET Web API框架中的URL路由系统亦是如此。也就是说,ASP.NET Web API核心框架的URL路由系统与ASP.NET本身的路由系统是相对独立的。但是当我们采用基于Web Host的方式(定义在程序集System.Web.Http.WebHost.dll)将ASP.NET Web API承载于一个ASP.NET Web应用的时候,真正实现URL路由的依然是ASP.NET本身的路由系统,Web Host实际上在这种情况下起到了一个“适配”的作用,是两个相对独立的路由系统的“适配器”。我们先来讨论一下实现在ASP.NET Web API框架中这个独立的路由系统是如何设计的。[本文已经同步到《How ASP.NET Web API Works?》]

目录
一、HttpRequestMessage与HttpResponseMessage
二、HttpRouteData
三、HttpVirtualPathData
四、HttpRouteConstraint
五、HttpRoute
六、HttpRouteCollection
七、注册路由映射
八、缺省路由变量

一、HttpRequestMessage与HttpResponseMessage

ASP.NET Web API框架通过具有如下定义的类型HttpRequestMessage表示某个HTTP请求的封装。HttpRequestMessage的属性Method和RequestUri分别表示请求采用的HTTP方法和请求地址,它们可以在相应的构造函数中直接被初始化,而默认采用的HTTP方法为HTTP-GET。

   1: public class HttpRequestMessage : IDisposable
   2: {
   3:     public HttpRequestMessage();
   4:     public HttpRequestMessage(HttpMethod method, string requestUri);
   5:     public HttpRequestMessage(HttpMethod method, Uri requestUri);
   6:  
   7:     public HttpMethod                     Method { get; set; }
   8:     public Uri                            RequestUri { get; set; }
   9:     public HttpRequestHeaders             Headers { get; }
  10:     public IDictionary<string, object>    Properties { get; }    
  11:     public Version                        Version { get; set; }
  12:     public HttpContent                    Content { get; set; }
  13:  
  14:     public void Dispose();
  15: }

只读属性Headers表示的System.Net.Http.Headers.HttpRequestHeaders对象具有一个类似于字典的数据结构,用于存放HTTP请求报头。通过利用字典类型的只读属性Properties,我们可以将任意属性附加到一个HttpRequestMessage对象上。类型为System.Version的Version属性表示请求的HTTP版本,默认采用的HTTP版本为HTTP 1.1(HttpVersion.Version11)。

HttpRequestMessage具有一个Content属性封装了HTTP消息主体相关的信息,其类型为HttpContent。如下面的代码片断所示,HttpContent是一个抽象类,它定义了CopyToAsync和ReadAsByteArrayAsync两组方法进行主体内容的读写操作。HttpContent的Headers属性返回一个System.Net.Http.Headers.HttpContentHeaders对象代表HTTP消息主体内容相关的报头列表,比如表示主题内容编码和长度的“Content-Encoding”和“Content-Length”等。

   1: public abstract class HttpContent : IDisposable
   2: {
   3:     //其他成员
   4:     public Task<byte[]> ReadAsByteArrayAsync();
   5:     public Task<Stream> ReadAsStreamAsync();
   6:     public Task<string> ReadAsStringAsync();
   7:  
   8:     public Task CopyToAsync(Stream stream);
   9:     public Task CopyToAsync(Stream stream, TransportContext context);    
  10:  
  11:     public HttpContentHeaders Headers { get; }
  12: }

HTTP响应的基本信息本封装到具有如下定义的HttpResponseMessage类型中。它的RequestMessage表示与之匹配的请求。属性StatusCode和表示响应状态码以及辅助表示响应状态的文字。布尔类型的属性IsSuccessStatusCode用于判断是否属性一个成功的响应,所谓“成功的响应”指的是状态码在范围[200,299]以内的响应。类型为HttpResponseHeaders的属性Headers表示回复消息的HTTP报头列表,而Version代表HTTP消息的版本,默认采用的HTTP版本依然是HTTP 1.1(HttpVersion.Version11)。响应消息主体内容的读取和写入,以及相关内容报头的获取可以通过属性Content表示的HttpContent来完成。

   1: public class HttpResponseMessage : IDisposable
   2: {
   3:     //其他成员
   4:     public HttpRequestMessage RequestMessage { get; set; }
   5:  
   6:     public HttpStatusCode      StatusCode { get; set; }
   7:     public string              ReasonPhrase { get; set; }
   8:     public bool                IsSuccessStatusCode { get; }
   9:     public HttpResponseHeaders Headers { get; }
  10:     public Version             Version { get; set; }    
  11:     public HttpContent         Content { get; set; }
  12: }

 

二、HttpRouteData

当我们调用某个Route的GetRouteData的时候,如果指定的HTTP上下文具有一个与自身URL模板相匹配,同时满足定义的所有约束条件的情况下会返回一个RouteData对象。ASP.NET的路由系统通过RouteData对象来封装解析出来的路由数据,其核心自然是通过Values和DataTokens属性封装的路由变量。

ASP.NET Web API用于封装路由数据的对象被称为HttpRouteData,其类型实现了具有如下定义的接口IHttpRouteData。IHttpRouteData接口的定义可比RouteData要简单很多,它只有两个只读的属性。Route属性表示生成该HttpRouteData的Route,而字典类型的属性Values表示解析出来的路由变量,变量名和变量值对应着该字典对象的Key和Value。

   1: public interface IHttpRouteData
   2: {
   3:     IHttpRoute                      Route { get; }
   4:     IDictionary<string, object>     Values { get; }
   5: }

在ASP.NET Web API路由系统中唯一实现了IHttpRouteData接口的公有类型为HttpRouteData,具体的定义如下所示。HttpRouteData实现的两个只读属性直接在构造函数中初始化,用于初始化Values属性的参数values的类型为HttpRouteValueDictionary,通过如下的代码片断可以看到它直接继承了Dictionary<string, object>,也就是说HttpRouteData对象具体返回的是一个类型为HttpRouteValueDictionary的对象。如果调用另一个构造函数(只包含一个唯一的参数route),其Values属性会初始化成一个不包含任何元素的空HttpRouteValueDictionary对象。

   1: public class HttpRouteData : IHttpRouteData
   2: {    
   3:     public HttpRouteData(IHttpRoute route);
   4:     public HttpRouteData(IHttpRoute route, HttpRouteValueDictionary values);
   5:  
   6:     public IHttpRoute                      Route { get; }
   7:     public IDictionary<string, object>     Values { get; }
   8: }
   9:  
  10: public class HttpRouteValueDictionary : Dictionary<string, object>
  11: {
  12:     public HttpRouteValueDictionary();
  13:     public HttpRouteValueDictionary(IDictionary<string, object> dictionary);
  14:     public HttpRouteValueDictionary(object values);
  15: }

 

三、HttpVirtualPathData

在ASP.NET 路由系统中,当我们调用Route的GetVirtualPath方法根据定义在路由本身的URL模板和指定的路由变量生成一个完整的URL的时候,在URL模板与提供的路由变量相匹配的情况下会返回一个VirtualPathData对象,我们可以通过其VirtualPath属性得到生成的URL。

ASP.NET Web API路由系统中与VirtualPathData对应的对象被称为HttpVirtualPathData,它实现了具有如下定义的接口IHttpVirtualPathData。对于定义在IHttpVirtualPathData接口中的两个属性,只读属性自然返回的是生成该HttpVirtualPathData对象的Route,另一个属性VirtualPath(改属性是可读可写的)返回生成的URL字符串。

   1: public interface IHttpVirtualPathData
   2: {
   3:     IHttpRoute     Route { get; }
   4:     string         VirtualPath { get; set; }
   5: }

在ASP.NET Web API的应用编程接口中定义了如下一个类型HttpVirtualPathData,它是实现了接口IHttpVirtualPathData的唯一公有类型。

   1: public class HttpVirtualPathData : IHttpVirtualPathData
   2: {
   3:     public HttpVirtualPathData(IHttpRoute route, string virtualPath);
   4:  
   5:     public IHttpRoute     Route { get; }
   6:     public string         VirtualPath { get; set; }
   7: }

 

四、HttpRouteConstraint

一个Route能够与HTTP请求相匹配,必须同时满足两个条件:其一,请求的URL必须与Route自身的URL的模式相匹配;其二,当前请求必须通过定义在当前Route上的所有约束。ASP.NET Web API路由系统通过HttpRouteContraint表示路由约束,具体类型实现了具有如下定义的接口IHttpRouteConstraint。

   1: public interface IHttpRouteConstraint
   2: {
   3:     bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection);
   4: }

HTTP请求是否满足HttpRouteContraint的约束通过调用定义在IHttpRouteConstraint的唯一的方法Match来决定,在这里被验证的请求(参数request)通过HttpRequestMessage对象来表示。 参数route代表当前HttpRouteContraint所在的Route。

基于HttpRouteContraint的约束是针对某个路由变量的,参数parameterName实际上代表的就是变量的名称。由于大部分路由变量会映射为定义在HttpController中某个Action方法的参数,所以这里的参数名为parameterName。当ASP.NET Web API框架实施约束检验的时候,已经通过URL模板匹配得到了所有的路由变量值,参数values表示的字典对象存放了这些路由变量,其Key和Value分别代表路由变量的名称和值。

通过对ASP.NET 路由系统的介绍我们知道URL路由具有两个“方向”上的应用,分别是匹配“入栈”请求并得到相应的路由数据,以及根据定义的路由规则和提供的路由变量生成“出栈”URL。ASP.NET路由系统通过枚举RouteDirection表示这两种“路由方向”,而ASP.NET Web API路由系统中的“路由方向”则通过具有如下定义的HttpRouteDirection枚举来表示。Match方法的参数routeDirection就是这么一个枚举对象。

   1: public enum HttpRouteDirection
   2: {
   3:     UriResolution,
   4:     UriGeneration
   5: }

我们知道HTTP方法在面向资源的REST架构中具有重要的地位和作用,它体现了针对目标资源的操作类型,很多情况下我们在进行路由注册过程中指定的URL模板都是针对具体某一种或几种HTTP方法的。ASP.NET路由系统定义了一个HttpMethodConstraint类型是实现针对HTTP方法的约束,ASP.NET Web API的路由系统中则定义了如下一个同名类型实现类似的功能。

   1: public class HttpMethodConstraint : IHttpRouteConstraint
   2: {
   3:     public HttpMethodConstraint(params HttpMethod[] allowedMethods);
   4:  
   5:     protected virtual bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection);
   6:     bool IHttpRouteConstraint.Match(HttpRequestMessage request, IHttpRoute route, 
   7:     string parameterName, IDictionary<string, object> values, 
   8:     HttpRouteDirection routeDirection);
   9:  
  10:     public Collection<HttpMethod> AllowedMethods { get; }
  11: }

HttpMethodConstraint的只读属性AllowedMethods返回一个元素类型为HttpMethod的集合,它代表了允许的HTTP方法列表。Match方法从表示请求的HttpRequestMessage对象中获得当前的HTTP方法,根据它是否在允许的列表之内从而做出是否满足约束的最终判断。

除了HttpMethodConstraint,在ASP.NET Web API路由系统的应用编程接口中还定义了一系列的约束类型,比如用于验证数据类型的IntRouteConstraint、FloatRouteConstraint和BoolRouteConstraint等,用于验证字符串长度的LengthRouteConstraint、MinLengthRouteConstraint和MaxLengthRouteConstraint等。这一系列的HttpMethodConstraint类型其实是为基于特性(Attribute)的路由而设计的,但是由于它们实现了IHttpRouteConstraint接口,所以在这里它们依然是可用的。

五、HttpRoute

ASP.NET路由系统中的Route的类型均为RouteBase的子类。从前面针对HttpRouteData和HttpVirtualPathDatad的介绍,我们知道ASP.NET Web API路由系统的RouteHttpRoute的类型实现了接口IHttpRoute,其定义如下。IHttpRoute的只读属性RouteTemplate表示定义的URL模板。两个字典类型的只读属性Constraints和Defaults表示为路由变量定义的约束和默认值,字典的Key和Value分别表示变量名称和约束/默认值。另一个同样通过字典类型表示的只读属性DataTokens,我们应该不会感到陌生,至于通过制度属性Handler返回的HttpMessageHandler对象是组成ASP.NET Web API消息处理管道的核心,我们会在后续的文章中对它进行详细介绍。

   1: public interface IHttpRoute
   2: {
   3:     string                          RouteTemplate { get; }
   4:     IDictionary<string, object>     Constraints { get; }
   5:     IDictionary<string, object>     Defaults { get; }
   6:     IDictionary<string, object>     DataTokens { get; }
   7:     HttpMessageHandler              Handler { get; }
   8:  
   9:     IHttpRouteData GetRouteData(string virtualPathRoot, HttpRequestMessage request);
  10:     IHttpVirtualPathData GetVirtualPath(HttpRequestMessage request, IDictionary<string, object> values);
  11: }

HttpRoute的作用体现在两点:对请求的URL进行解析并生成封装路由数据的HttpRouteData对象,以及将提供的路由变量绑定到URL模板以生成一个完整的URL,这两个功能分别通过调用IHttpRoute的方法GetRouteData和GetVirtualPath来实现。GetRouteData方法的参数virtualPathRoot表示虚拟根路径,一般来说当通过HttpRequestMessage获取的真正请求路径后需要剔除这个根路径部分得到一个相对路径,基于URL模板的匹配应该针对这个相对路径来进行。

ASP.NET Web API路由系统中直接实现了接口IHttpRoute的唯一类型是具有如下定义的HttpRoute。HttpRoute实现的5个只读属性都可以直接通过调用相应的构造函数进行初始化,对于3个字典类型的属性(Constraints、DataTokens和Defaults),如果不曾在构造函数中通过对应的参数来指定(或者指定的对象为Null),它们会被初始化为一个空的HttpRouteValueDictionary对象。

   1: public class HttpRoute : IHttpRoute
   2: {
   3:     public HttpRoute();
   4:     public HttpRoute(string routeTemplate);
   5:     public HttpRoute(string routeTemplate, HttpRouteValueDictionary defaults);
   6:     public HttpRoute(string routeTemplate, HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints);
   7:     public HttpRoute(string routeTemplate, HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints, HttpRouteValueDictionary dataTokens);
   8:     public HttpRoute(string routeTemplate, HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints, HttpRouteValueDictionary dataTokens, HttpMessageHandler handler);
   9:  
  10:     public virtual IHttpRouteData GetRouteData(string virtualPathRoot, HttpRequestMessage request);
  11:     public virtual IHttpVirtualPathData GetVirtualPath(HttpRequestMessage request, IDictionary<string, object> values);
  12:     
  13:     public IDictionary<string, object>     Constraints { get; }
  14:     public IDictionary<string, object>     DataTokens { get; }
  15:     public IDictionary<string, object>     Defaults { get; }
  16:     public HttpMessageHandler              Handler { get; }
  17:     public string                          RouteTemplate { get; }
  18: }

现在我们来简单讨论一下实现在GetRouteData方法中的路由匹配规则以及最终路由数据如何生成。HttpRoute首先根据表示请求的HttpRequestMessage对象的RequestUri属性得到请求地址,一般来说这是一个包含网络协议前缀(http://或者https://)和主机名称的完整URL。然后HttpRoute会从该URL中提取路径部分,并加上“/”前缀。比如说请求地址为“http://www.artech.com/webapi/products/001”,最终得到的相对URL为“/webapi/products/001”。

如果GetRouteData方法中通过virtualPathRoot指定了一个根路径,如果这个路径不是上面得到的相对URL的前缀(比如“/webservice”),那么匹配失败并直接返回Null。HttpRoute会从这个相对URL中将这个根路径部分剔除掉,最终得到的URL与自身定义的URL模板进行模式匹配。比如说,如果指定的根路径为“/webapi”,那么最终与URL模板进行匹配的相对URL为“products/001”。如果请求URL不符合URL模板的模式,HttpRoute会直接返回Null。

基于URL模板的模式匹配成功之后,解析出来的路由变量会保存到一个字典对象中。HttpRoute接下来需要检验通过URL模板验证的请求是否满足自身定义的所有约束。从上面给出的关于接口IHttpRoute的定义我们知道表示针对路由变量约束的列表的属性Constraints不是IDictionary<string, IHttpRouteConstraint>,而是IDictionary<string, object>。字典对象的Key代表路由变量的名称,其Value可以是一个真正的HttpRouteContraint对象,也可以是针对某种类型HttpRouteContraint的字符串表达式。

如果保存在Constraints属性中的一个真正的HttpRouteContraint,HttpRoute会直接调用它的Match方法对请求进行相应的约束检验,作为参数parameterName和values传入的分别是对应的Key和通过URL模板匹配解析出来的路由变量。如果保存在Constraints中的是针对某种HttpRouteContraint类型的字符串表达式,HttpRoute会据此创建对应的HttpRouteContraint对象对请求予以验证。由于这些HttpRouteContraint主要是针对“特性路由”而设计的,对于每个HttpRouteContraint的表达式各自具有怎样的格式,我们会在本书第3章“基于标注特性的路由”中进行详细介绍。

如果指定的表示请求的HttpRequestMessage通过了所有HttpRouteContraint的检验,HttpRoute会根据解析出来的以字典形式表示的路由变量生成一个HttpRouteData并作为GetRouteData的返回值,该HttpRouteData对象的Route属性就是对它自身的引用。

我们可以通过一个简单的实例来演示HttpRoute对请求的路由匹配与检验规则。我们在一个空的ASP.NET MVC应用中定义了如下一个HomeController。在默认的Action方法中我们创建了一个HttpRoute对象,它的URL模板为“movies/{genre}/{title}/{id}”(针对某一个电影,定义其中的三个变量分别表示电影的类型、片名和ID),而HTTP方法被设置为HTTP-POST。我们为此HttpRoute添加了一个HttpMethodConstraint类型的约束,并将允许的HTTP方法限定为HTTP-POST。

   1: public class HomeController : Controller
   2: {
   3:     public ActionResult Index()
   4:     {
   5:         string routeTemplate = "movies/{genre}/{title}/{id}";
   6:         IHttpRoute route = new HttpRoute(routeTemplate);
   7:         route.Constraints.Add("httpMethod", new HttpMethodConstraint(HttpMethod.Post));
   8:  
   9:         HttpRequestMessage request1 = new HttpRequestMessage(HttpMethod.Get, "http://www.artech.com/products/movies/romance/titanic/r001");
  10:         HttpRequestMessage request2 = new HttpRequestMessage(HttpMethod.Post, "http://www.artech.com/products/movies/romance/titanic/r001");
  11:  
  12:         string virtualPathRoot1 = "/";
  13:         string virtualPathRoot2 = "/products/";
  14:  
  15:         IHttpRouteData routeData1 = route.GetRouteData(virtualPathRoot1, request1);
  16:         IHttpRouteData routeData2 = route.GetRouteData(virtualPathRoot1, request2);
  17:         IHttpRouteData routeData3 = route.GetRouteData(virtualPathRoot2, request1);
  18:         IHttpRouteData routeData4 = route.GetRouteData(virtualPathRoot2, request2);
  19:  
  20:         return View(new bool[] { routeData1 != null, routeData2 != null, routeData3 != null, routeData4 != null });
  21:     }
  22: }

我们创建了两个HttpRequestMessage对象作为被检验的HTTP请求,它们具有相同的请求地址(“http://www.artech.com/products/movies/romance/titanic/r001”)不同的HTTP方法(HTTP-GET和HTTP-POST)。为了验证指定不同的虚拟根路径对HttpRoute路由解析的影响,我们分别定义了两个不同的根路径(“/”和“/products/”)。针对两个不同的请求和根路径的组合,我们4次调用了HttpRoute的GetRouteData方法,通过判断返回的HttpRouteData是否为Null来判断对应的请求针对给定的根路径是否与定义在HttpRoute中的路由规则相匹配。

Action方法Index最终将默认的View呈现出来,指定的Model是一个布尔类型元素的数组,每个一个布尔值代表对应的请求与根路径组合是否通过了HttpRoute的检验。如下所示的就是对应View的定义,这是一个Model类型为bool[]的强类型View,我们将代表检验结果的布尔值以表格的形式呈现出来。

   1: @model bool[]
   2: <html>
   3: <head>
   4:     <title>路由解析</title>
   5: </head>
   6: <body>
   7:     <table>
   8:         <tr>
   9:             <th></th>
  10:             <th>HTTP-GET</th>
  11:             <th>HTTP-POST</th>
  12:         </tr>
  13:         <tr>
  14:             <td>/</td>
  15:             <td>@Model[0]</td>
  16:             <td>@Model[1]</td>
  17:         </tr>
  18:         <tr>
  19:             <td>/products/</td>
  20:             <td>@Model[2]</td>
  21:             <td>@Model[3]</td>
  22:         </tr>
  23:     </table>
  24: </body>
  25:     </html>

image

直接运行该程序后会在浏览器中呈现出如右图所示的输出结果,针对两个基于不同HTTP方法的请求和两个不同虚拟根路径的组合,只有最后一组能够完全符合定义在HttpRoute中的路由规则,由此可以看出上面我们介绍的URL模板、约束以及指定的虚拟根路径对HttpRoute路由解析的影响。

HttpRoute的GetRouteData方法解决了针对“入栈”请求的检验,接下来我们来讨论HttpRoute在另一个“路由方向”上的应用,即根据定义的路由规则和给定的路由变量生成一个完整的URL。针对生成URL的路由解析实现在GetVirtualPath方法中,我们现在来详细介绍用于封装生成URL的HttpVirtualPathData是如何生成出来的。

   1: public class HttpRoute : IHttpRoute
   2: {
   3:     //其他成员
   4:     public virtual IHttpVirtualPathData GetVirtualPath(HttpRequestMessage request, IDictionary<string, object> values);
   5: }

如上面的代码片断所示,HttpRoute的GetVirtualPath方法具有两个参数,分别是表示请求的HttpRequestMessage对象和用于替换掉定义在URL模板中路由变量占位符的“值”。HttpRoute能够根据模板生成一个完整的URL取决于是否能够提供定义在URL模板中所有路由变量占位符的值,而这个路由变量值具有如下三个来源。

  • 调用GetVirtualPath参数传入的字典类型的参数values。
  • 附加到HttpRequestMessage对象属性列表(对应于它的Properties属性)中的HttpRouteData对象的Values属性表示字典。
  • HttpRoute定义的默认值。

上述的这个列表顺序也体现了HttpRoute对象在提取路由变量值过程中的选择优先级。换句话说,如果同名变量值同时存在于上述的三个或者两个数据源,排在前面的会被优先选择。

至于如何将封装路由数据的HttpRoute对象附加到某个HttpRequestMessage对象上,实际上就是将对象添加到HttpRequestMessage的Properties属性表示的字典对象中,ASP.NET Web API的路由系统为它限定了一个固定的Key值为“MS_HttpRouteData”,我们可以通过如下所示的定义在静态类型HttpPropertyKeys中的只读字段HttpRouteDataKey得到这个值。除此之外,我们还可以调用针对HttpRequestMessage类型的两个扩展方法GetRouteData/SetRouteData来提取和设置HttpRouteData。

   1: public static class HttpPropertyKeys
   2: {
   3:     //其他成员
   4:     public static readonly string HttpRouteDataKey;
   5: }
   6:  
   7: public static class HttpRequestMessageExtensions
   8: {
   9:     //其他成员
  10:     public static IHttpRouteData GetRouteData(this HttpRequestMessage request);
  11:     public static void SetRouteData(this HttpRequestMessage request, IHttpRouteData routeData);
  12: }

如果HttpRoute在上述三个来源中不能完全获取用于替换定义在URL模板中的所有路由变量占位符,它会直接返回Null。即使能够完全获得这些变量值,它还有一个很“隐晦”的条件:要求参数values表示的字典对象中必须包含一个Key值为“httproute”的元素,否则会认为提供的对象并非一个有效的能够提供“路由变量值”的字典。至于这个特殊的Key值,我们可以通过定义在类型HttpRoute中如下一个静态只读字段HttpRouteKey来获得。

   1: public class HttpRoute : IHttpRoute
   2: {
   3:     //其他成员
   4:     public static readonly string HttpRouteKey = "httproute";
   5: }

HttpRoute根据优先级从上述三个数据源中获取到以字典对象表示的所有路由变量值之后,还需要检验它们是否能够满足自身定义的所有约束,如果不满足任何一个约束,HttpRoute依然会直接返回Null。当得到的路由变量值得到了所有约束的检验,这些值会绑定到URL模板生成一个完整的URL,最终被封装成类型为HttpVirtualPathData的对象返回。

为了使读者能够对定义在HttpRoute的GetVirtualPath方法中的路由解析逻辑具有更加深刻的印象,我们来做一个简单的实例演示。我们在一个空的ASP.NET MVC应用中定义了如下一个HomeController,在默认的Action方法Index中将5次调用HttpRoute对象的GetVirtualPath方法返回的HttpVirtualPathData对象呈现在默认的View中。

   1: public class HomeController : Controller
   2: {
   3:     public ActionResult Index()
   4:     {
   5:         string routeTemplate = "weather/{areacode}/{days}";
   6:         IHttpRoute route = new HttpRoute(routeTemplate);
   7:         route.Constraints.Add("httpMethod", 
   8:             new HttpMethodConstraint(HttpMethod.Post));
   9:         route.Defaults.Add("days", 2);
  10:  
  11:         List<IHttpVirtualPathData> virutualPathList = 
  12:             new List<IHttpVirtualPathData>();
  13:         HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "/");
  14:  
  15:         //1. 不能提供路由变量areacode的值
  16:         Dictionary<string, object> values = new Dictionary<string, object>();
  17:         virutualPathList.Add(route.GetVirtualPath(request, values));
  18:  
  19:         //2. values无Key为"httproute"的元素
  20:         values.Add("areaCode", "028");
  21:         virutualPathList.Add(route.GetVirtualPath(request, values));
  22:  
  23:         //3. 所有的路由变量值通过values提供
  24:         values.Add("httproute", true);
  25:         values.Add("days", 3);
  26:         IHttpRouteData routeData = new HttpRouteData(route);
  27:         routeData.Values.Add("areacode", "0512");
  28:         routeData.Values.Add("days", 4);
  29:         request.SetRouteData(routeData);
  30:         virutualPathList.Add(route.GetVirtualPath(request, values));
  31:  
  32:         //4. 所有的路由变量值通过request提供
  33:         values.Clear();
  34:         values.Add("httproute", true);           
  35:         virutualPathList.Add(route.GetVirtualPath(request, values));
  36:  
  37:         //5. 采用定义在HttpRoute上的默认值(days = 2)
  38:         routeData.Values.Remove("days");
  39:         virutualPathList.Add(route.GetVirtualPath(request, values));
  40:  
  41:         return View(virutualPathList.ToArray());
  42:     }
  43: }

如上面的代码片断所示,我们针对URL模板“weather/{areacode}/{days}”创建了一个HttpRoute对象,其中路由变量days具有默认值2。除此之外,我们为创建的HttpRoute添加了一个HttpMethodConstraint类型的约束将允许的HTTP方法限定为HTTP-POST。我们随后创建了基于HTTP-GET的HttpRequestMessage对象,其请求地址为“/”。

第一次调用GetVirtualPath方法传入的参数分别是上面创建的HttpRequestMessage和一个空的字典对象values,很显然在此情况下HttpRoute不能为路由变量areaCode获取相应的替换值。对于第二次调用,传入的字典对象为路由变量areaCode指定了相应的值。

在第三次调用中,变量values表示的字典对象不仅仅同时包含了路由变量areaCode和days的值,还添加了一个Key和Value分别为“httproute”和True的元素。对于提供的HttpRequestMessage对象,我们通过调用扩展方法SetRouteData为它设置了一个HttpRouteData对象,该对象的Values属性表示的字典中同样具有areaCode和days这两个路由变量的值。

我们在第四次调用GetVirtualPath方法之前将values变量保存的路由变量areaCode和days的值清除,但保留了Key为“httproute”的元素。对于最后一次GetVirtualPath方法调用,我们清楚了附加在HttpRequestMessage上HttpRouteData对象针对路由变量days的值。

如下所示的是Action方法Index对应View的定义,这是一个Model类型为IHttpVirtualPathData数组的强类型View。在该View中,我们将每个HttpVirtualPathData对象的VirtualPath属性表示的URL以表格的形式呈现出来。如果HttpVirtualPathData为Null,直接显示“N/A”字样。

   1: @model System.Web.Http.Routing.IHttpVirtualPathData[]
   2: <html>
   3:     <head>
   4:         <title>路由解析</title>
   5:     </head>
   6:     <body>
   7:         <table>
   8:             @for (int i = 0; i < Model.Length; i++)
   9:             { 
  10:                 <tr>
  11:                     <td>@(i+1)</td>
  12:                     <td>@(Model[i] == null ? "N/A" : Model[i].VirtualPath)</td>
  13:                 </tr>
  14:             }
  15:         </table>
  16:     </body>
  17: </html>

image直接运行该程序后会在浏览器中呈现出如右图所示的输出结果,它充分验证了上面我们介绍的实现在HttpRoute的GetVirtualPath方法中的路由解析逻辑。对于第一、二次针对HttpRoute的GetVirtualPath方法的调用,由于不满足“必须提供定义在URL模板中所有路由变量值”和“提供路由变量值的字典必须包含一个Key为httproute的元素”的条件,所以直接返回Null。最后三次针对GetVirtualPath方法的调用印证了上面我们介绍的“路由变量数据源选择优先级”的论述。

其实这个实例还说明了另一个问题:HttpRoute的GetVirtualPath方法只会进行针对定义在URL模板中路由变量的约束检验。对于这个演示实例来说,我们创建的HttpRoute具有一个基于HTTP-POST的HttpMethodConstraint类型的约束(对应的名称为“httpMethod”),但是调用GetVirtualPath方法传入的确是一个针对HTTP-GET的HttpRequestMessage对象,依然是可以生成相应HttpVirtualPathData的。这也很好理解,因为HttpRoute的GetVirtualPath方法的目的在于生成一个合法的URL,定义在URL模板中的路由变量对应的约束才有意义。

六、HttpRouteCollection

故名思义HttpRouteCollection就是一个元素类型为IHttpRoute的集合,如下面的代码片断所示,它实现了接口ICollection<IHttpRoute>。ASP.NET Web API路由系统中的路由表实际上就是一个HttpRouteCollection对象。HttpRouteCollection具有一个只读属性VirtualPathRoot表示进行路由解析时默认使用的虚拟跟路径,该属性可以直接在调用构造函数是通过参数指定,其默认值为“/”。

   1: public class HttpRouteCollection : ICollection<IHttpRoute>, IDisposable
   2: {
   3:     //其他成员
   4:     public HttpRouteCollection();
   5:     public HttpRouteCollection(string virtualPathRoot);    
   6:    
   7:     public IHttpRoute CreateRoute(string routeTemplate, object defaults, object constraints);
   8:     public IHttpRoute CreateRoute(string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, IDictionary<string, object> dataTokens);
   9:     public virtual IHttpRoute CreateRoute(string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, IDictionary<string, object> dataTokens, HttpMessageHandler handler);
  10:     
  11:     public virtual IHttpRouteData GetRouteData(HttpRequestMessage request);
  12:     public virtual IHttpVirtualPathData GetVirtualPath(HttpRequestMessage request, string name, IDictionary<string, object> values);
  13:  
  14:     public virtual string VirtualPathRoot { get; }
  15: }

除了实现定义在接口ICollection<IHttpRoute>中的众多方法之外,HttpRouteCollection还定义了两个CreateRoute方法重载帮助我们根据指定的URL模板、路由变量默认值、约束和DateToken列表以及HttpMessageHandler对象创建HttpRoute对象。

HttpRouteCollection同样定义了GetRouteData和GetVirtualPath方法,它们的逻辑与ASP.NET路由系统中的RouteCollection类型中的同名方法一致:按照先后顺利调用每个HttpRoute对象的同名方法直到返回一个具体的HttpRouteData或者HttpVirtualPathData对象。

HttpRouteCollection的GetRouteData方法中并没有表示虚拟根路径的参数,那么当它在调用具体HttpRoute对象的同名方法的时候如何指定这个参数呢?具体的逻辑是这样的:它先判断虚拟根路径是否已经被添加到表示请求的HttpRequestMessage的属性字典(Properties属性)中,对应的Key为“MS_VirtualPathRoot”,如果这样的属性存在并且是一个字符串,那么这将直接被用作调用HttpRoute的GetRouteData方法的参数。否则直接使用通过属性VirtualPathRoot表示的默认根路径

HttpRequestMessage属性字典中表示虚拟根路径的Key可以直接通过类型HttpPropertyKeys的静态只读字段VirtualPathRoot获取。我们可以直接调用HttpRequestMessage如下两个扩展方法GetVirtualPathRoot和SetVirtualPathRoot获取或者设置虚拟根路径。

   1: public static class HttpPropertyKeys
   2: {
   3:     //其他成员
   4:     public static readonly string VirtualPathRoot;
   5: }
   6:  
   7: public static class HttpRequestMessageExtensions
   8: {
   9:     //其他成员
  10:     public static string GetVirtualPathRoot(this HttpRequestMessage request);
  11:     public static void SetVirtualPathRoot(this HttpRequestMessage request, string virtualPathRoot);
  12: }

关于HttpRouteCollection,值得一提的是它对于添加的HttpRoute对象的保存方式。如下面的代码片断所示,HttpRouteCollection具有_collection和_dictionary两个只读字段,类型分别是List<IHttpRoute>和IDictionary<string, IHttpRoute>,前者单纯地保存添加的HttpRoute对象,后者给每个添加的HttpRoute对象匹配一个具有唯一性的名称。

   1: public class HttpRouteCollection : ICollection<IHttpRoute>, IDisposable
   2: {
   3:     // 其他成员
   4:     private readonly List<IHttpRoute>                 _collection;
   5:     private readonly IDictionary<string, IHttpRoute>     _dictionary;
   6:    
   7:     public virtual void Insert(int index, string name, IHttpRoute value);
   8:     public virtual void Add(string name, IHttpRoute route);
   9:     void ICollection<IHttpRoute>.Add(IHttpRoute route);
  10:     
  11:     public virtual bool Remove(string name);    
  12:     bool ICollection<IHttpRoute>.Remove(IHttpRoute route);
  13:  
  14:     public virtual IHttpRoute this[int index] { get; }
  15:     public virtual IHttpRoute this[string name] { get; }
  16: }

HttpRouteCollection采用“接口显式实现”的方式实现了定义在ICollection<T>中的Add和Remove方法,所以这两个方法我们基本上不用使用。取而代之的是额外的Add和Remove方法,通过调用Add方法可以为添加的HttpRoute对象指定一个“注册名称”,而根据这个注册名称可以调用Remove方法将对应的HttpRoute移除。

调用Add方法添加的HttpRoute会同时被添加到通过字段_collection和_dictionary表示的集合和字典之中。不论是调用HttpRouteCollection的GetRouteData方法还是GetVirtualPath方法,它总是按照HttpRoute在集合_collection中的顺序进行便利,第一个匹配的HttpRoute会被选用,所以HttpRoute在集合中的顺序显得尤为重要。由于通过Add方法添加的HttpRoute对象总是被添加到集合的最后,所以另一个Insert方法被定义在HttpRouteCollection中使我们可以同时决定被添加HttpRoute的名称和次序。除了上述这些方法外,我们还可以通过索引的方式得到存在于HttpRouteCollection对象中的HttpRoute对象。

七、注册路由映射

与ASP.NET路由系统下的路由映射类似,ASP.NET Web API下的路由映射就是为针对应用的路由表添加相应HttpRoute对象的过程。整个ASP.NET Web API框架是一个请求处理的管道,我们可以在程序启动的时候对其进行相应的配置是整个管道按照我们希望的方式来工作,我们所做的扩张也是通过相应的配置应用到管道之上。

我们对ASP.NET Web API的请求处理管道所做的所有配置基本上都是通过一个类型为HttpConfiguration的对象来完成,而路由注册自然也不例外。如下面的代码片断所示,HttpConfiguration具有一个类型为HttpRouteCollection的只读属性Routes,我们进行路由映射注册的HttpRoute正是被添加于此。

   1: public class HttpConfiguration : IDisposable
   2: {
   3:     //其他成员
   4:     public HttpRouteCollection                      Routes { get; }
   5:     public string                                   VirtualPathRoot { get; }
   6:     public ConcurrentDictionary<object, object>     Properties { get; }
   7: }

HttpConfiguration的另一个与路由相关的属性VirtualPathRoot表示默认使用的虚拟根路径,它直接返回通过Routes属性表示的HttpRouteCollection对象的同名属性。我们可以通过字典类型的只读属性Properties将相应的对象附加到HttpConfiguration,这与我们使用HttpRequestMessage的Properties属性的方式一致。在具体的运行环境中,我们使用HttpConfiguration都是针对整个应用的全局对象,所以我们添加到Properties属性中的对象也是全局,我们在整个应用的任何地方都可以提取它们。

我们可以直接根据指定的URL模板,以及针对路由变量的默认值和约束来创建相应的HttpRoute,并最终将其添加到通过HttpConfiguration的Routes对象表示的路由表中从而到达注册路由映射的目的。除此之外,我们还可以直接调用HttpRouteCollection如下一系列重载的扩展方法MapHttpRoute实现相同的目的。实际上这些扩展方法最终还是调用HttpRouteCollection的Add方法将创建的HttpRoute添加到路由表中的。

   1: public static class HttpRouteCollectionExtensions
   2: {
   3:     //其他成员
   4:     public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name, string routeTemplate);
   5:     public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name, string routeTemplate, object defaults);
   6:     public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name, string routeTemplate, object defaults, object constraints);
   7:     public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name, string routeTemplate, object defaults, object constraints, HttpMessageHandler handler);
   8: }

对于上面定义的这些MapHttpRoute方法重载,最终根据指定的URL模板、默认值、约束、DataToken以及HttpMessageHandler对具体HttpRoute的创建是通过调用HttpRouteCollection具有如下定义的CreateRoute方法实现的。这是一个虚方法,所以如何我们希望调用这些扩展方法注册自定义的HttpRoute,可以自定义一个HttpRouteCollection类型并重写这个CreateRoute方法即可。

   1: public class HttpRouteCollection : ICollection<IHttpRoute>, IDisposable
   2: {
   3:     //其他成员
   4:     public virtual IHttpRoute CreateRoute(string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, 
   5:         IDictionary<string, object> dataTokens, HttpMessageHandler handler);
   6: }

至于如果获取用于配置ASP.NET Web API管道的HttpConfiguration对象,这依赖于我们对Web API的寄宿方式,这并没有定义在ASP.NET Web API的核心框架之中。

八、缺省路由变量

我们在进行路由注册的时候可以为某个路由变量设置一个默认值,这个默认值可以是一个具体的变量值,也可以是通过RouteParameter具有如下定义的静态只读字段Optional返回的一个RouteParameter对象,我们具有这种默认值的路由变量成为缺省路由变量。

   1: public sealed class RouteParameter
   2: {
   3:     public static readonly RouteParameter Optional;
   4: }

实际上当我们利用Visual Studio的ASP.NET Web API向导新建一个Web应用的时候,在生成的用于注册路由的RouteConfig.cs中会默认注册如下一个HttpRoute,其路由变量id就是一个具有默认值为RouteParameter.Optional的缺省路由变量。

   1: public class RouteConfig
   2: {
   3:     public static void RegisterRoutes(RouteCollection routes)
   4:     {
   5:     //其他操作
   6:         routes.MapHttpRoute(
   7:             name            : "DefaultApi",
   8:             routeTemplate   : "api/{controller}/{id}",
   9:             defaults        : new { id = 
RouteParameter.Optional
 }
  10:         );
  11:     }
  12: }

虽然同是具有默认值的路由变量,但是缺省路由变量具有不同之处:如果请求URL中没有提供对应变量的值,普通具有默认值的路由变量依然会出现在最终HttpRouteData的Values属性中,但是缺省路由变量则不会。

posted @ 2013-07-30 11:54  Artech  阅读(10997)  评论(29编辑  收藏  举报