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 的預設實作會傳回所有公用類別,即 (a) 實作 IHttpController、(b) 不是抽象的,而 (c) 的名稱結尾為 “Controller”。

動作選擇

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

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

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

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

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

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

  1. 您可以使用屬性指定 HTTP 方法: AcceptVerbsHttpDeleteHttpGetHttpHeadHttpOptionsHttpPatchHttpPostHttpPut
  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。 因此,此動作只能比對在路由字典或查詢字串中包含「id」值的 URI。

選擇性參數是例外狀況,因為它們是選擇性參數。 對於選擇性參數,如果繫結無法從 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 none
GetById "id"
FindProductsByName "name"

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

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

GetById 方法獲勝,因為它符合一個參數,而 GetAll 則沒有參數。 使用以下參數值呼叫該方法:

  • 識別碼 = 1
  • 版本 = 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));