這篇文章描述了ASP.NET Web API如何將HTTP請求路由到控制器上的特定動作。
備注:想要了解關于路由的高層次概述,請查看Routing in ASP.NET Web API。
這篇文章側重于路由過程的細節(jié)。如果你創(chuàng)建了一個Web API項目并且發(fā)現(xiàn)一些請求并沒有按你預期得到相應的路由,希望這篇文章有所幫助。
路由有以下三個主要階段:
你可以用自己的習慣行為來替換其中一些過程。在本文中,我會描述默認行為。在結尾,我會指出你可以自定義行為的地方。
路由模板看起來和URI路徑非常相似,但是它能包含用大括號指明的占位符。
"api/{controller}/public/{category}/{id}"
當你創(chuàng)建了一個路由,你為一些或全部占位符提供默認的值:
defaults: new { category = "all" }
你也可以提供一些約束(constraints),它限制了URI字段如何才能匹配一個占位符:
constraints: new { id = @"\d+" } // Only matches if "id" is one or more digits.
框架會盡力將URI路徑中的字段匹配到模板中。模板中的文字必須準確匹配。一個占位符可以匹配多個變量,除非你指定了約束??蚣懿粫ヅ銾RI的其他部分,比如主機名或查詢參數(shù)。框架僅僅在用于匹配URI的路由表中選擇第一個路由。
這里有兩個特殊的占位符:”{controller}“和“{action}”。
如果你提供了默認的API,路由將會匹配缺少這些的URI。例如:
routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{category}",
defaults: new { category = "all" }
);
對于URI http: //localhost/api/products 將會匹配這個路由。{category} 字段會被分配默認值 all。
如果框架發(fā)現(xiàn)了URI的一個匹配,它會創(chuàng)建一個包含了每個占位符適用的值的字典集合。鍵是不包含大括號的占位符名稱。值是提取自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”,路由字典將會包含:
然而對于“api/products/toys/123”,路由字典將會包含:
對于defaults,它同樣也會包含一個沒有在路由模板中任何地方出現(xiàn)的值。如果路由匹配了,這個值會被存儲在字典中。例如:
routes.MapHttpRoute(
name: "Root",
routeTemplate: "api/root/{id}",
defaults: new { controller = "customers", id = RouteParameter.Optional }
);
如果URI路徑“api/root/8”,字典將會包含兩個值:
控制器的選擇由IHttpControllerSelector.SelectController方法來處理。這個方法需要傳入一個HttpRequestMessage實例并返回HttpControllerDescriptor對象。默認的實現(xiàn)是由DefaultHttpControllerSelector類來實現(xiàn)的。這個類使用了一個簡單的算法:
例如,如果路由字典包含鍵值對“controller”=“products”,那么控制器類型就是“ProductsController”。如果這里不存在匹配的類型,或存在多個匹配,那么框架就會向客戶端發(fā)送一個錯誤。
對于步驟3,DefaultHttpControllerSelector會使用IHttpControllerTypeResolver接口來得到Web API控制器類型的列表。IHttpControllerTypeResolver的默認實現(xiàn)會返回(a)實現(xiàn)IHttpController,(b)不是抽象的,(c)名稱以“Controller“結尾的所有公共的類。
在選擇控制器之后,框架會通過調用IHttpActionSelector.SelectAction方法來選擇動作。這個方法需要傳入一個HttpControllerContext參數(shù)以及返回一個HttpActionDescriptor對象。
默認的實現(xiàn)由ApiControllerActionSelector類來提供。為了選擇一個動作,它會按以下要求來查找: 1) 請求的HTTP方法 2) 路由模板中的“{action}“占位符(如果存在) 3) 控制器中動作的參數(shù)
在查看選擇算法之前,我們需要理解關于控制器動作的一些東西。
控制器中的哪些方法會被認為是“動作“?當選擇一個動作時,框架僅僅在控制器中查找公共的實例方法。當然了,它會排除一些”特殊“的方法(構造函數(shù),事件,操作重載等等)和繼承自ApiController類的方法。
HTTP方法。框架只會選擇匹配請求的HTTP方法的動作,它取決于以下幾點:
參數(shù)綁定。參數(shù)綁定是指Web API如何如何為參數(shù)創(chuàng)建一個值。這里是參數(shù)綁定的默認規(guī)則:
簡單類型包括所有.NET框架基本類型(.NET Framework primitive types),再加上DateTime、Decimal、Guid、String和TimeSpan。對于每個動作,最多有一個參數(shù)可以讀取請求體。
備注:重載默認綁定規(guī)則也是有可能的。查看WebAPI Parameter binding under the hood.
有了以上這些背景知識,這里是動作選擇的算法:
步驟3可能是最容易迷惑的?;镜乃枷胧菂?shù)可以從URI、請求體或綁定中獲得它的值。對于來自URI的參數(shù),我們會確保URI確實包含一個給參數(shù)的值,不論是在路徑(通過路由字典)還是在查詢字符串中。
例如,考慮如下動作:
public void Get(int id)
這個id參數(shù)綁定到URI上,因此,這個動作可以匹配到包含一個給“id“的值的URI,不論是在路由字典還是查詢字符串中。
可選參數(shù)是個例外,因為它們是可選的。對于可選參數(shù),如果這個綁定不了從URI中得到這個值也是沒關系的。
因為一些不同的原因,復雜類型也是個例外。復雜類型只能通過自定義綁定來綁定到URI上。但是在這種情況下,框架無法事先知道參數(shù)可能被綁定到一個特殊的URI。為了弄清楚它,就需要去執(zhí)行這個綁定。這個選擇算法的目標是在執(zhí)行任何綁定之前,從靜態(tài)描述中去選擇一個動作。因此,復雜類型會從這種匹配算法中執(zhí)行。
在動作被選取好了,所有的參數(shù)綁定也就被執(zhí)行了。
總結:
路由:
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
該URI會匹配到名為”DefaultApi”的路由。這個路由字典包含以下詞條:
這個路由字典不包含查詢字符串“version”和“details”,但是在動作選擇的時候這些仍然會被考慮。
根據(jù)路由字典中的“controller”詞條,控制器類型是ProductsController。
該HTTP請求是一個GET請求。相應的支持GET的控制器動作是GetAll、GetById和FindProductsByName。路由字典中不包含任何“action“詞條,所以我們不用去匹配動作名稱。
接下來,我們嘗試著匹配動作的參數(shù)名稱,現(xiàn)在僅在GET動作中查找。
| Action | Parameters to Match |
|---|---|
| GetAll | none |
| GetById | "id" |
| FindProductsByName | "name" |
注意到GetById的version參數(shù)沒有被考慮,因為它是一個可選參數(shù)。
顯而易見GetAll方法能夠匹配,GetById方法也能匹配,因為路由字典中包含“id“。FindProductsByName方法不匹配。
最后是GetById方法獲勝,因為它能夠匹配到一個參數(shù),相對應的是沒有參數(shù)能匹配GetAll。該方法伴隨以下參數(shù)的值來執(zhí)行:
注意到盡管version參數(shù)沒有在選擇算法中使用,但該參數(shù)的值也依舊是來自URI的查詢字符串中。
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. |
為任何這些接口提供自己的實現(xiàn),請使用HttpConfiguration對象上的Services集合:
var config = GlobalConfiguration.Configuration;
config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config));