ASP.NET Web API中的路由和動作選取

本文說明 ASP.NET Web API如何將 HTTP 要求路由傳送至控制器上的特定動作。

注意

如需路由的高階概觀,請參閱ASP.NET Web API中的路由

本文將探討路由程式的詳細資料。 如果您建立 Web API 專案,併發現某些要求不會以您預期的方式路由傳送,希望本文將有所説明。

路由有三個主要階段:

  1. 比對 URI 與路由範本。
  2. 選取控制器。
  3. 選取動作。

您可以將程式的某些部分取代為您自己的自訂行為。 在本文中,我描述預設行為。 最後,我注意到您可以自訂行為的位置。

路由範本

路由範本看起來類似于 URI 路徑,但可以有預留位置值,以大括弧表示:

"api/{controller}/public/{category}/{id}"

當您建立路由時,您可以為部分或所有預留位置提供預設值:

defaults: new { category = "all" }

您也可以提供限制 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

如果您提供預設值,路由會比對遺漏這些區段的 URI。 例如:

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{category}",
    defaults: new { category = "all" }
);

URI http://localhost/api/products/allhttp://localhost/api/products 符合上述路由。 在後者的 URI 中,遺漏 {category} 的區段會指派預設值 all

路由字典

如果架構找到 URI 的相符專案,它會建立包含每個預留位置值的字典。 索引鍵是預留位置名稱,不包括大括弧。 這些值取自 URI 路徑或預設值。 字典會儲存在 IHttpRouteData 物件中。

在此路由比對階段期間,特殊 「{controller}」 和 「{action}」 預留位置會被視為其他預留位置。 它們只會與其他值一起儲存在字典中。

預設值可以有特殊值的 RouteParameter.Optional。 如果預留位置被指派這個值,則值不會新增至路由字典。 例如:

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{category}/{id}",
    defaults: new { category = "all", id = RouteParameter.Optional }
);

針對 URI 路徑 「api/products」,路由字典將包含:

  • 控制器:「products」
  • 類別:「all」

不過,對於 「api/products/toys/123」,路由字典將會包含:

  • 控制器:「products」
  • 類別:「toys」
  • 識別碼:「123」

預設值也可以包含未出現在路由範本中任何位置的值。 如果路由相符,該值會儲存在字典中。 例如:

routes.MapHttpRoute(
    name: "Root",
    routeTemplate: "api/root/{id}",
    defaults: new { controller = "customers", id = RouteParameter.Optional }
);

如果 URI 路徑為 「api/root/8」,字典將包含兩個值:

  • 控制器:「customers」
  • 識別碼:「8」

選取控制器

控制器選取是由 IHttpControllerSelector.SelectController 方法處理。 這個方法會採用 HttpRequestMessage 實例,並傳回 HttpControllerDescriptor。 預設實作是由 DefaultHttpControllerSelector 類別所提供。 此類別會使用直接的演算法:

  1. 查看金鑰 「controller」 的路由字典。
  2. 取得此機碼的值,並附加字串 「Controller」 以取得控制器類型名稱。
  3. 尋找具有此類型名稱的 Web API 控制器。

例如,如果路由字典包含機碼/值組 「controller」 = 「products」,則控制器類型為 「ProductsController」。 如果沒有相符的類型或多個相符專案,架構會將錯誤傳回給用戶端。

針對步驟 3, DefaultHttpControllerSelector 會使用 IHttpControllerTypeResolver 介面來取得 Web API 控制器類型清單。 IHttpControllerTypeResolver的預設實作會傳回 () 實作IHttpController的所有公用類別, (b) 不是抽象的, (c) 的名稱結尾為 「Controller」。

動作選取

選取控制器之後,架構會呼叫 IHttpActionSelector.SelectAction 方法來選取動作。 此方法會採用 HttpControllerCoNtext 並傳回 HttpActionDescriptor

預設實作是由 ApiControllerActionSelector 類別所提供。 若要選取動作,它會查看下列內容:

  • 要求的 HTTP 方法。
  • 如果有的話,路由範本中的 「{action}」 預留位置。
  • 控制器上動作的參數。

在查看選取演算法之前,我們需要瞭解控制器動作的一些事項。

控制器上的哪些方法會被視為「動作」? 選取動作時,架構只會查看控制器上的公用實例方法。 此外,它會排除 「特殊名稱」 方法 (建構函式、事件、運算子多載等) ,以及繼承自 ApiController 類別的方法。

HTTP 方法。 架構只會選擇符合要求的 HTTP 方法的動作,如下所示:

  1. 您可以使用屬性來指定 HTTP 方法:AcceptVerbsHttpDeleteHttpGetHttpHeadHttpOptionsHttpPatchHttpPost 或 HttpPut
  2. 否則,如果控制器方法的名稱以 「Get」、「Post」、「Put」、「Delete」、「Head」、「Options」 或 「Patch」 開頭,則依照慣例,動作支援該 HTTP 方法。
  3. 如果沒有上述任何專案,此方法就支援 POST。

參數系結。 參數系結是 Web API 為參數建立值的方式。 以下是參數系結的預設規則:

  • 簡單類型取自 URI。
  • 複雜類型取自要求本文。

簡單類型包括所有.NET Framework基本類型,加上DateTimeDecimalGuidStringTimeSpan。 針對每個動作,最多一個參數可以讀取要求本文。

注意

可以覆寫預設系結規則。 請參閱WebAPI 參數系結。

在此背景中,以下是動作選取演算法。

  1. 在符合 HTTP 要求方法的控制器上建立所有動作的清單。

  2. 如果路由字典有「動作」專案,請移除名稱不符合此值的動作。

  3. 嘗試比對動作參數與 URI,如下所示:

    1. 針對每個動作,取得簡單類型的參數清單,其中系結會從 URI 取得參數。 排除選擇性參數。
    2. 從此清單中,嘗試尋找路由字典或 URI 查詢字串中每個參數名稱的相符專案。 相符專案不區分大小寫,且不相依于參數順序。
    3. 選取清單中的每個參數在 URI 中都有相符的動作。
    4. 如果一個動作符合這些準則,請挑選具有最多參數相符專案的動作。
  4. 忽略具有 [NonAction] 屬性的動作。

步驟 #3 可能是最令人困惑的。 基本概念是,參數可以從 URI、要求本文或自訂系結取得其值。 對於來自 URI 的參數,我們想要確保 URI 實際包含該參數的值,不論是透過路由字典 (路徑) 或查詢字串。

例如,請考慮下列動作:

public void Get(int id)

id參數會系結至 URI。 因此,此動作只能比對 URI,其中包含路由字典或查詢字串中的 「id」 值。

選擇性參數是例外狀況,因為它們是選擇性的。 針對選擇性參數,如果系結無法從 URI 取得值,則為正常。

複雜類型是不同原因的例外狀況。 複雜類型只能透過自訂系結系結至 URI。 但在此情況下,架構無法事先知道參數是否會系結至特定的 URI。 若要瞭解,它必須叫用系結。 選取演算法的目標是在叫用任何系結之前,先從靜態描述中選取動作。 因此,複雜類型會從比對演算法中排除。

選取動作之後,會叫用所有參數系結。

摘要:

  • 動作必須符合要求的 HTTP 方法。
  • 如果存在,動作名稱必須符合路由字典中的 「action」 專案。
  • 對於動作的每個參數,如果參數取自 URI,則必須在路由字典或 URI 查詢字串中找到參數名稱。 (排除具有複雜類型的選擇性參數和參數。)
  • 嘗試比對最多數目的參數。 最佳比對可能是沒有參數的方法。

擴充範例

路線:

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」 的路由。 路由字典包含下列專案:

  • 控制器:「products」
  • 識別碼:「1」

路由字典不包含查詢字串參數 「version」 和 「details」,但在選取動作期間仍會考慮這些參數。

控制器選取

從路由字典中的 「controller」 專案,控制器類型為 ProductsController

動作選取

HTTP 要求是 GET 要求。 支援 GET 的控制器動作為 GetAllGetByIdFindProductsByName 。 路由字典不包含 「action」 的專案,因此我們不需要比對動作名稱。

接下來,我們會嘗試比對動作的參數名稱,只查看 GET 動作。

動作 要比對的參數
GetAll
GetById "id"
FindProductsByName 「name」

請注意,不會考慮 的 GetByIdversion參數,因為它是選擇性參數。

方法 GetAll 會以簡單方式比對。 方法 GetById 也會比對,因為路由字典包含 「id」。 方法 FindProductsByName 不符。

方法 GetById 會優先使用,因為它符合一個參數,而不是 的無參數 GetAll 。 使用下列參數值叫用 方法:

  • id = 1
  • version = 1.5

請注意,即使 版本 未用於選取演算法,參數的值也來自 URI 查詢字串。

擴充點

Web API 會為路由程式的某些部分提供擴充點。

介面 描述
IHttpControllerSelector 選取控制器。
IHttpControllerTypeResolver 取得控制器類型的清單。 DefaultHttpControllerSelector會從此清單中選擇控制器類型。
IAssembliesResolver 取得專案元件的清單。 IHttpControllerTypeResolver介面會使用此清單來尋找控制器類型。
IHttpControllerActivator 建立新的控制器實例。
IHttpActionSelector 選取動作。
IHttpActionInvoker 叫用動作。

若要為上述任何介面提供您自己的實作,請在HttpConfiguration物件上使用Services集合:

var config = GlobalConfiguration.Configuration;
config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config));