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