Web API 中的路由和操作选择

本文介绍 ASP.NET Web API如何将 HTTP 请求路由到控制器上的特定操作。

注意

有关路由的高级概述,请参阅 ASP.NET Web API中的路由

本文介绍路由过程的详细信息。 如果创建 Web API 项目并发现某些请求未按预期方式路由,希望本文能有所帮助。

路由有三个main阶段:

  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}”。

默认值

如果提供默认值,则路由将与缺少这些段的 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”
  • id:“123”

默认值还可以包含不会显示在路由模板中的任何位置的值。 如果路由匹配,该值将存储在字典中。 例如:

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

如果 URI 路径为“api/root/8”,则字典将包含两个值:

  • 控制器:“customers”
  • id:“8”

选择控制器

控制器选择由 IHttpControllerSelector.SelectController 方法处理。 此方法采用 HttpRequestMessage 实例并返回 HttpControllerDescriptor。 默认实现由 DefaultHttpControllerSelector 类提供。 此类使用简单的算法:

  1. 在路由字典中查找键“控制器”。
  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 方法: AcceptVerbsHttpDeleteHttpGetHttpHeadHttpOptionsHttpPatchHttpPostHttpPut
  2. 否则,如果控制器方法的名称以“Get”、“Post”、“Put”、“Delete”、“Head”、“Options”或“Patch”开头,则按照约定操作支持该 HTTP 方法。
  3. 如果上述方法均不支持 POST。

参数绑定。 参数绑定是 Web API 为参数创建值的方式。 下面是参数绑定的默认规则:

  • 简单类型取自 URI。
  • 复杂类型取自请求正文。

简单类型包括所有.NET Framework基元类型,以及 DateTimeDecimalGuidStringTimeSpan。 对于每个操作,最多一个参数可以读取请求正文。

注意

可以替代默认绑定规则。 请参阅 后台的 WebAPI 参数绑定

在此背景下,下面是操作选择算法。

  1. 创建控制器上与 HTTP 请求方法匹配的所有操作的列表。

  2. 如果路由字典具有“action”条目,请删除名称与此值不匹配的操作。

  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”
  • id:“1”

路由字典不包含查询字符串参数“version”和“details”,但在操作选择过程中仍会考虑这些参数。

控制器选择

从路由字典中的“控制器”条目中,控制器类型为 ProductsController

操作选择

HTTP 请求是 GET 请求。 支持 GET 的控制器操作为 GetAllGetByIdFindProductsByName。 路由字典不包含“action”条目,因此不需要匹配操作名称。

接下来,我们尝试匹配操作的参数名称,只查看 GET 操作。

操作 要匹配的参数
GetAll
GetById "id"
FindProductsByName “name”

请注意,不考虑 的版本GetById参数,因为它是可选参数。

方法 GetAll 与 基本匹配。 方法 GetById 也匹配,因为路由字典包含“id”。 方法 FindProductsByName 不匹配。

方法 GetById 胜出,因为它与一个参数匹配,而不是匹配 任何 GetAll参数。 使用以下参数值调用 方法:

  • id = 1
  • version = 1.5

请注意,即使选择算法中未使用 version ,参数的值也来自 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));