天天看點

【Web API系列教程】2.2 — ASP.NET Web API中的路由和動作選擇機制

這篇文章描述了ASP.NET Web API如何将HTTP請求路由到控制器上的特定動作。

備注:想要了解關于路由的高層次概述,請檢視Routing in ASP.NET Web API。

這篇文章側重于路由過程的細節。如果你建立了一個Web API項目并且發現一些請求并沒有按你預期得到相應的路由,希望這篇文章有所幫助。

路由有以下三個主要階段:

  1. 将URI比對到路由模闆
  2. 選擇一個控制器
  3. 選擇一個動作

你可以用自己的習慣行為來替換其中一些過程。在本文中,我會描述預設行為。在結尾,我會指出你可以自定義行為的地方。

路由模闆(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”,路由字典将會包含:

  1. controller:“products”
  2. category:“all”

然而對于“api/products/toys/123”,路由字典将會包含:

  1. 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類來實作的。這個類使用了一個簡單的算法:

  1. 在路由字典中查找鍵”controller“。
  2. 提取出這個鍵對應的值,并添加字元串“Controller”以得到控制器的類型名
  3. 用這個類型名來查找一個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方法的動作,它取決于以下幾點:

  1. 你可以用某個屬性來具體說明是HTTP方法:AcceptVerbs,HttpDelete,HttpGet,HttpHead,HttpOptions,HttpPatch,HttpPost或HttpPut。
  2. 或者,如果一個控制器方法的名稱以”Get”,“Post“,”Put“,”Delete“,”Head“,”Options“或”Patch“開始,那麼按照約定該動作就支援HTTP方法。
  3. 如果不包含以上幾點,但支援POST的方法。

參數綁定。參數綁定是指Web API如何如何為參數建立一個值。這裡是參數綁定的預設規則:

  1. 簡單類型直接從URI中提取
  2. 複雜類型從請求體重提取

簡單類型包括所有.NET架構基本類型(.NET Framework primitive types),再加上DateTime、Decimal、Guid、String和TimeSpan。對于每個動作,最多有一個參數可以讀取請求體。

備注:重載預設綁定規則也是有可能的。檢視WebAPI Parameter binding under the hood.

有了以上這些背景知識,這裡是動作選擇的算法:

  1. 基于HTTP請求方法比對到的控制器建立一個動作清單。
  2. 動作路由字典包含“action“記錄,移除其名字不比對該值的動作。
  3. 根據如下規則,盡力将動作參數比對到URI:
    • a 對于每個動作,當綁定從URI中獲得參數時得到一個簡單類型的參數清單。執行可選的參數。
    • b 從這個清單中,無論是在路由字典中還是URI查詢字元串中,都盡力找出針對每個參數名稱的比對。比對不區分大小寫并且不取決于參數順序。
    • c 當清單中的每個參數在URI中都有一個比對時,選擇一個動作。
    • d 如果多個動作符合這些标準,那麼選擇其中一個有最多參數比對的。
  4. 忽略包含[NonAction]屬性的動作。

步驟3可能是最容易迷惑的。基本的思想是參數可以從URI、請求體或綁定中獲得它的值。對于來自URI的參數,我們會確定URI确實包含一個給參數的值,不論是在路徑(通過路由字典)還是在查詢字元串中。

例如,考慮如下動作:

public void Get(int id)           

這個id參數綁定到URI上,是以,這個動作可以比對到包含一個給“id“的值的URI,不論是在路由字典還是查詢字元串中。

可選參數是個例外,因為它們是可選的。對于可選參數,如果這個綁定不了從URI中得到這個值也是沒關系的。

因為一些不同的原因,複雜類型也是個例外。複雜類型隻能通過自定義綁定來綁定到URI上。但是在這種情況下,架構無法事先知道參數可能被綁定到一個特殊的URI。為了弄清楚它,就需要去執行這個綁定。這個選擇算法的目标是在執行任何綁定之前,從靜态描述中去選擇一個動作。是以,複雜類型會從這種比對算法中執行。

在動作被選取好了,所有的參數綁定也就被執行了。

總結:

  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 }
);
           

控制器:

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));
           

繼續閱讀