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版本,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 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用于封装路由数据的对象被称为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,也就是说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模板与提供的路由变量相匹配的情况下会返回一个

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

HttpRoute的GetRouteData方法解决了针对“入栈”请求的检验,接下来我们来讨论HttpRoute在另一个“路由方向”上的应用,即根据定义的路由规则和给定的路由变量生成一个完整的URL。针对生成URL的路由解析实现在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的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模板中的路由变量对应的约束才有意义。

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, 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属性中,但是缺省路由变量则不会。