前言
本文描述ASP.NET Web API如何把一個HTTP請求路由到控制器的一個特定的Action上。關於路由的總體概述可以參見上一篇教程 http://www.cnblogs.com/aehyok/p/3442051.html。這篇文章主要來學習路由過程的細節。如果你創建了一個Web API項目,發現有一些請求沒有按照你期望的方式被路由,希望這篇文章將對你有所幫助。
本文主要分為三個階段:
1.匹配URI到一個Route Template。
2.選擇一個Controller。
3.選擇一個Action。
你可以用自己的自定義行為來替換這一過程中的某些部分。在本文中,我將來描述默認的行為。在文章結尾,我會注明可以在什么地方自定義行為。
Route Templates
路由模版看上去類似於一個URI路徑,但它可以具有占位符,並用花括號來指示:
"api/{controller}/public/{category}/{id}"
當創建一個路由的時候,你可以為某些或所有占位符提供默認值:
defaults: new { category = "all" }
你也可以提供約束,它限制URI片段如何與占位符匹配:
constraints: new { id = @"\d+" } // Only matches if "id" is one or more digits.
上面語句是通過正則表達式來限制片段的取值,上面的注釋說明 id片段只匹配一個或多個數字,因此URI中的id片段必須是數字才能與這個路由進行匹配。
這個框架試圖把URI路徑中的片段與這個模板進行匹配。模板中的文字必須嚴格匹配。一個占位符可以匹配任何值,除非你指定了約束。這個框架不會匹配URI另外的部分,例如主機名或者一個查詢字符串。這個框架會選擇路由表中第一個匹配的路由。
這里有兩個特殊的占位符:“{controller}”和“{action}”。
- “{controller}”提供控制器名。
- “{action}”提供動作名。在Web API中,通常的約定是忽略“{action}”的。
Defaults(默認值)
如果你提供默認值,那么這個路由將匹配缺少這些片段的URI。例如:
routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{category}", defaults: new { category = "all" } );
這個URI“http://localhost/api/products”與這個路由是匹配的。“{category}”片段被賦成了默認值“all”。
Route Dictionary(路由字典)
如果這個框架發現了一個匹配的URI,它會創建包含每個占位符值的一個字典。這個鍵值是不帶花括號的的占位符名稱。這個值取自於URI路徑或者是默認值中的。這個字段被存在IHttpRouteData對象中。在匹配路由階段,這個特殊的"{controller}" and "{action}"占位符的處理和其他占位符是一樣的。它們用另外的值被簡單的存儲在字典中。
在默認值中可以使用特殊的RouteParameter.Optional值。如果一個占位符被賦予了這個值,那么這個值將不會被添加到路由字典中,例如:
routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{category}/{id}", defaults: new { category = "all", id = RouteParameter.Optional } );
對於URI路徑“api/products”,路由字典將含有:controller:"products"、category:"all"。
然而,對於“api/products/toys/123”,路由字典將含有:controller:"products"、category:"toys"、id:"123"。
這個默認值也可以包含未出現在路由模板中的值。若這條路由匹配,則該值會被存儲在路由字典中。例如:
routes.MapHttpRoute( name: "Root", routeTemplate: "api/root/{id}", defaults: new { controller = "customers", id = RouteParameter.Optional } );
如果URI路徑是“api/root/8”,字典將含有兩個值:controller:“customers”,id:"8"。
Selecting a Controller
控制器選擇是由IHttpControllerSelector.SelectController方法來處理的。這個方法以HttpRequestMessage實例為參數,並返回HttpControllerDescriptor。
其默認實現是由DefaultHttpControllerSelector類提供的。這個類使用了一種很直接的算法:
1.查找路由字典的“controller”鍵。
2.取得這個鍵的值,並附加字符串“Controller”,以得到控制器的類型名。
3.用這個類型名查找Web API控制器。
例如,如果路由字典中的鍵-值對為“controller”=“products”,那么控制器類型便為“ProductsController”。如果沒有匹配類型,或有多個匹配,這個框架會給客戶端返回一條錯誤。
對於步驟3,DefaultHttpControllerSelector使用IHttpControllerTypeResolver接口以獲得Web API控制器類型的列表。 IHttpControllerTypeResolver的默認實現會返回所有符合以下條件的public類:
a:實現IHttpController的類。
b:是非抽象類。
c:名稱以“Controller”結尾的類。
Action Selection
選擇了控制器之后,這個框架會通過調用IHttpActionSelector.SelectAction方法來選擇動作。這個方法以HttpControllerContext為參數,並返回HttpActionDescriptor。
這個默認實現是由ApiControllerActionSelector類提供的。為了選擇一個動作,會查找以下方面:
1.HTTP請求的方法。
2.這個路由模板中的“action”占位符。
3.控制器中動作的參數。
在查找選擇算法之前,我們需要理解控制器動作的一些事情。
控制器中的哪些方法被看成為是“動作”?當選擇一個動作時,這個框架只考察控制器的public實例方法。而且,它會排除特殊名稱的方法(構造器、事件、操作符、重載等等),以及集成自ApiController的類方法。
HTTP Methods
這個框架只會選擇與請求的HTTP方法匹配的動作,確定如下:
1.你可以用注解屬性AcceptVerbs、HttpDelete、HttpGet、HttpHead、HttpOptions、HttpPatch、HttpPost、或HttpPut來指定HTTP方法。
2.否則,如果控制器方法名稱以“Get”、“Post”、“Put”、“Delete”、“Head”、“Options”、或“Patch”開頭,那么根據這個約定,該Action將支持相應的HTTP方法。
3.如果以上都不是,那么這個方法將支持Post。
Parameter Bindings.
參數綁定是指Web API如何創建參數值。以下是參數綁定的默認規則:1.簡單類型取自URI。2.復雜類型取自請求正文。
簡單類型包括所有“.NET框架簡單類型”,另外還有,DateTime、Decimal、Guid、String和TimeSpan。對於每一個動作,最多只有一個參數可以讀取請求正文。
它也可以重寫這種默認的綁定規則。See WebAPI Parameter binding under the hood。
在這種背景下,動作選擇算法如下:
1.創建該控制器中與HTTP請求方法匹配的所有動作的列表。
2.如果路由字典有“action”條目,移除與該條目值不匹配的動作。
3.試圖將動作參數與該URI匹配,如下:
a:針對每個動作,獲得簡單類型的參數列表,這是綁定得到URI參數的地方。該列表不包括可選參數。
b:從這個列表中,試着在路由字典或是在URI查詢字符串中,找到每個參數的匹配。匹配是與大小寫無關的,且與參數順序無關。
c:選擇這樣的一個action,在列表中的每個參數在URI中有一個匹配。
d:如果滿足這些條件的動作不止一個,選用參數匹配最多的一個。
4.忽略用[NonAction]注解屬性標注的動作。
第3步可能會讓人困擾。其基本思想是,可以從URI、或請求體、或一個自定義綁定來獲取參數值。對於來自URI的參數,我們希望確保URI在其路徑(通過路由字典)或查詢字符串中實際包含了一個用於此參數的值。
例如,考慮以下動作:
public void Get(int id)
其id參數綁定到URI。因此,這個動作只能匹配在路由字典或查詢字符串中包含了“id”值的URI。
可選參數是一個例外,因為它們是可選的。對於可選參數,如果綁定不能通過URI獲取它的值,是沒關系的。
復雜類型是另一種原因的例外。一個復雜類型只能通過自定義綁定來綁定到URI。但是在這種情況下,這個框架不能提前知道是否這個參數被綁定到一個特殊的URI。為了查明情況,這個框架需要調用這個綁定。選擇算法的目的是在調用綁定之前根據靜態描述來選擇一個動作。因此,復雜類型是屬於匹配算法之外的。
動作選擇之后,會調用所有參數綁定。
Summary:
1.動作必須匹配請求的HTTP方法。
2.動作名必須匹配路由字典中的“action”條目,如果有。
3.對於動作的各個參數,如果參數取自URI,那么該參數名必須在路由字典或URI查詢字符串中能夠被找到。(可選參數和復雜類型除外)。
4.試圖匹配最多數目的參數。最佳匹配可能是一個無參數的方法。
Extended Example
看如下路由:
routes.MapHttpRoute( name: "ApiRoot", routeTemplate: "api/root/{id}", defaults: new { controller = "products", id = RouteParameter.Optional } ); routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } );
再看如下Contoller下的內容:
public class ProductsController : ApiController { public IEnumerable<Product> GetAll() {} public Product GetById(int id, double version = 1.0) {} [HttpGet] public void FindProductsByName(string name) {} public void Post(Product value) {} public void Put(int id, Product value) {} }
HTTP請求:
http://localhost:34701/api/products/1?version=1.5&details=1
路由匹配:
該URI與名為“DefaultApi”路由匹配。路由字典包含以下條目:controller:"products",id:"1"。該路由字典並未包含查詢字符串參數“version”和“details”,但這些將在動作選擇期間考慮。
控制器選擇:
根據路由字典中的“controller”條目,控制器類型是ProductsController。
動作選擇:
這個HTTP請求是一個GET請求。支持Get的控制器動作是GetALL、GetById、FindProductsByName。這個路由字典不包含”action“條目,因此不需要匹配動作名稱。
下一步,會試圖匹配這些動作的參數名,只考查GET動作。
注意,不會考慮GetById的version參數,因為它是一個可選參數。
GetAll方法非常匹配。GetById方法也匹配,因為路由字典包含了“id”。FindProductsByName方法不匹配。
GetById方法是贏家,因為它匹配了一個參數,而GetAll無參數。該方法將以以下參數值被調用:id=1,version=1.5
注意,雖然version未被用於選擇算法,但該參數值會取自URI查詢字符串。
Extension Points
Web API為路由過程的某些部分提供了擴展點。
要為以上任一接口提供自己的實現,可使用HttpConfiguration對象的Services集合:
var config = GlobalConfiguration.Configuration; config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config));
總結