這篇文章描述了ASP.NET Web API如何将HTTP請求路由到控制器上的特定動作。
備注:想要了解關于路由的高層次概述,請檢視Routing in ASP.NET Web API。
這篇文章側重于路由過程的細節。如果你建立了一個Web API項目并且發現一些請求并沒有按你預期得到相應的路由,希望這篇文章有所幫助。
路由有以下三個主要階段:
- 将URI比對到路由模闆
- 選擇一個控制器
- 選擇一個動作
你可以用自己的習慣行為來替換其中一些過程。在本文中,我會描述預設行為。在結尾,我會指出你可以自定義行為的地方。
路由模闆(Route Templates)
路由模闆看起來和URI路徑非常相似,但是它能包含用大括号指明的占位符。
"api/{controller}/public/{category}/{id}"
當你建立了一個路由,你為一些或全部占位符提供預設的值:
defaults: new { category = "all" }
你也可以提供一些限制(constraints),它限制了URI字段如何才能比對一個占位符:
constraints: new { id = @"\d+" } // Only matches if "id" is one or more digits.
架構會盡力将URI路徑中的字段比對到模闆中。模闆中的文字必須準确比對。一個占位符可以比對多個變量,除非你指定了限制。架構不會比對URI的其他部分,比如主機名或查詢參數。架構僅僅在用于比對URI的路由表中選擇第一個路由。
這裡有兩個特殊的占位符:”{controller}“和“{action}”。
- “{controller}“提供了控制器的名稱。
- “{action}“提供了動作的名稱。在Web API中,通過會忽略“{action}”。
Defaults
如果你提供了預設的API,路由将會比對缺少這些的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}“和”{action}“占位符會被像其他占位符一樣對待。它們被同其他值一起簡單地存儲在字典中。
對于defaults,它可以有一個特殊值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”,路由字典将會包含:
- id:“123“
對于defaults,它同樣也會包含一個沒有在路由模闆中任何地方出現的值。如果路由比對了,這個值會被存儲在字典中。例如:
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類來實作的。這個類使用了一個簡單的算法:
- 在路由字典中查找鍵”controller“。
- 提取出這個鍵對應的值,并添加字元串“Controller”以得到控制器的類型名
- 用這個類型名來查找一個Web API控制器
例如,如果路由字典包含鍵值對“controller”=“products”,那麼控制器類型就是“ProductsController”。如果這裡不存在比對的類型,或存在多個比對,那麼架構就會向用戶端發送一個錯誤。
對于步驟3,DefaultHttpControllerSelector會使用IHttpControllerTypeResolver接口來得到Web API控制器類型的清單。IHttpControllerTypeResolver的預設實作會傳回(a)實作IHttpController,(b)不是抽象的,(c)名稱以“Controller“結尾的所有公共的類。
動作選擇
在選擇控制器之後,架構會通過調用IHttpActionSelector.SelectAction方法來選擇動作。這個方法需要傳入一個HttpControllerContext參數以及傳回一個HttpActionDescriptor對象。
預設的實作由ApiControllerActionSelector類來提供。為了選擇一個動作,它會按以下要求來查找:
1) 請求的HTTP方法
2) 路由模闆中的“{action}“占位符(如果存在)
3) 控制器中動作的參數
在檢視選擇算法之前,我們需要了解關于控制器動作的一些東西。
控制器中的哪些方法會被認為是“動作“?當選擇一個動作時,架構僅僅在控制器中查找公共的執行個體方法。當然了,它會排除一些”特殊“的方法(構造函數,事件,操作重載等等)和繼承自ApiController類的方法。
HTTP方法。架構隻會選擇比對請求的HTTP方法的動作,它取決于以下幾點:
- 你可以用某個屬性來具體說明是HTTP方法:AcceptVerbs,HttpDelete,HttpGet,HttpHead,HttpOptions,HttpPatch,HttpPost或HttpPut。
- 或者,如果一個控制器方法的名稱以”Get”,“Post“,”Put“,”Delete“,”Head“,”Options“或”Patch“開始,那麼按照約定該動作就支援HTTP方法。
- 如果不包含以上幾點,但支援POST的方法。
參數綁定。參數綁定是指Web API如何如何為參數建立一個值。這裡是參數綁定的預設規則:
- 簡單類型直接從URI中提取
- 複雜類型從請求體重提取
簡單類型包括所有.NET架構基本類型(.NET Framework primitive types),再加上DateTime、Decimal、Guid、String和TimeSpan。對于每個動作,最多有一個參數可以讀取請求體。
備注:重載預設綁定規則也是有可能的。檢視WebAPI Parameter binding under the hood.
有了以上這些背景知識,這裡是動作選擇的算法:
- 基于HTTP請求方法比對到的控制器建立一個動作清單。
- 動作路由字典包含“action“記錄,移除其名字不比對該值的動作。
- 根據如下規則,盡力将動作參數比對到URI:
- a 對于每個動作,當綁定從URI中獲得參數時得到一個簡單類型的參數清單。執行可選的參數。
- b 從這個清單中,無論是在路由字典中還是URI查詢字元串中,都盡力找出針對每個參數名稱的比對。比對不區分大小寫并且不取決于參數順序。
- c 當清單中的每個參數在URI中都有一個比對時,選擇一個動作。
- d 如果多個動作符合這些标準,那麼選擇其中一個有最多參數比對的。
- 忽略包含[NonAction]屬性的動作。
步驟3可能是最容易迷惑的。基本的思想是參數可以從URI、請求體或綁定中獲得它的值。對于來自URI的參數,我們會確定URI确實包含一個給參數的值,不論是在路徑(通過路由字典)還是在查詢字元串中。
例如,考慮如下動作:
public void Get(int id)
這個id參數綁定到URI上,是以,這個動作可以比對到包含一個給“id“的值的URI,不論是在路由字典還是查詢字元串中。
可選參數是個例外,因為它們是可選的。對于可選參數,如果這個綁定不了從URI中得到這個值也是沒關系的。
因為一些不同的原因,複雜類型也是個例外。複雜類型隻能通過自定義綁定來綁定到URI上。但是在這種情況下,架構無法事先知道參數可能被綁定到一個特殊的URI。為了弄清楚它,就需要去執行這個綁定。這個選擇算法的目标是在執行任何綁定之前,從靜态描述中去選擇一個動作。是以,複雜類型會從這種比對算法中執行。
在動作被選取好了,所有的參數綁定也就被執行了。
總結:
- 動作必須比對請求的HTTP方法。
- 動作名(如果存在)必須比對路由字典中的“action“詞條
- 對于動作的所有參數,如果參數提取自URI,那麼參數名必須在路由字典或URI查詢字元串中被找到。(可選參數和複雜類型的參數除外。)
- 盡量去比對最多的參數數目。但最好的比對也可能是不包含任何參數的方法。
擴充示例(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 }
);
控制器:
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請求:
GET http://localhost:34701/api/products/1?version=1.5&details=1
路由比對(Route Matching)
該URI會比對到名為”DefaultApi”的路由。這個路由字典包含以下詞條:
- id:“1“
這個路由字典不包含查詢字元串“version”和“details”,但是在動作選擇的時候這些仍然會被考慮。
控制器選擇(Controller Selection)
根據路由字典中的“controller”詞條,控制器類型是ProductsController。
動作選擇(Action Selection)
該HTTP請求是一個GET請求。相應的支援GET的控制器動作是GetAll、GetById和FindProductsByName。路由字典中不包含任何“action“詞條,是以我們不用去比對動作名稱。
接下來,我們嘗試着比對動作的參數名稱,現在僅在GET動作中查找。
Action | Parameters to Match |
---|---|
GetAll | none |
GetById | “id” |
FindProductsByName | “name” |
注意到GetById的version參數沒有被考慮,因為它是一個可選參數。
顯而易見GetAll方法能夠比對,GetById方法也能比對,因為路由字典中包含“id“。FindProductsByName方法不比對。
最後是GetById方法獲勝,因為它能夠比對到一個參數,相對應的是沒有參數能比對GetAll。該方法伴随以下參數的值來執行:
- id = 1
- version = 1.5
注意到盡管version參數沒有在選擇算法中使用,但該參數的值也依舊是來自URI的查詢字元串中。
擴充點(Extension Points)
Web API為路由過程的一些部分提供了擴充點。
Interface | Description |
---|---|
IHttpControllerSelector | Selects the controller. |
IHttpControllerTypeResolver | Gets the list of controller types. The DefaultHttpControllerSelector chooses the controller type from this list. |
IAssembliesResolver | Gets the list of project assemblies. The IHttpControllerTypeResolverinterface uses this list to find the controller types. |
IHttpControllerActivator | Creates new controller instances. |
IHttpActionSelector | Selects the action. |
IHttpActionInvoker | Invokes the action. |
為任何這些接口提供自己的實作,請使用HttpConfiguration對象上的Services集合:
var config = GlobalConfiguration.Configuration;
config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config));