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屬性得到請求地址,一般來說這是一個包含網絡協議前綴(https://或者https://)和主機名稱的完整URL。然後HttpRoute會從該URL中提取路徑部分,並加上“/”前綴。比如說請求地址為“https://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, "https://www.artech.com/products/movies/romance/titanic/r001");
10: HttpRequestMessage request2 = new HttpRequestMessage(HttpMethod.Post, "https://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請求,它們具有相同的請求地址(“https://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>
直接運行該程序後會在瀏覽器中呈現出如右圖所示的輸出結果,針對兩個基於不同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>
直接運行該程序後會在瀏覽器中呈現出如右圖所示的輸出結果,它充分驗證了上麵我們介紹的實現在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屬性中,但是缺省路由變量則不會。
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須
最後更新:2017-10-25 15:34:19