ASP.NET Web API 中的路由以及Action的选择

ASP.NET Web API 中的路由以及Action的选择

原文更新日期:2017.11.28

导航页面

http://blog.csdn.net/wf824284257/article/details/79475115

上一步

ASP.NET Web API 中的路由
http://blog.csdn.net/wf824284257/article/details/79477634

开始

这篇教程讲了ASP.NET Web API是如何将HTTP请求路由到特定controller的特定action的。

注意:若你想更粗略的了解路由,可以看这篇教程:http://blog.csdn.net/wf824284257/article/details/79477634

这篇教程主要讲路由过程的细节。如果你创建了一个Web API 项目,并发现有些请求并没有按照你期望的方式被路由的话,那么这篇教程可能会对你有所帮助。

路由有3个主要环节:

  1. 与一个路由模板匹配。
  2. 选择一个controller
  3. 选择一个action

你可以通过自己的一些定制操作来替换其中的一些环节。这篇教程会讲述默认的路由环节。在教程结尾的时候,我也列出了可以进行定制操作的地方。

路由模板

路由模板,看起来与URI类似,但它可以包含参数占位符,参数占位符用大括号来标识:

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

当你创建一个路由模板时,你可以为其中的一个或多个参数占位符指定默认值:

defaults: new { category = "all" }

你也可以提供一些约束,来限制URI对参数占位符的匹配。

constraints: new { id = @"\d+" }   // Only matches if "id" is one or more digits.

Web API framework 会尝试使用URI中的字段来匹配路由模板。路由模板中的字面量必须被URI相同的字面量匹配。一个参数占位符可以匹配任何未被明确约束的值。framework不会匹配URI中的其他部分,比如主机名或查询参数(query parameters)。framework 会选择路由表中第一个与URI匹配的路由。

这里有两个特殊的参数占位符:”{controller}” 和 “{action}”.

  1. “{controller}” 提供了controller的名字。
  2. “{action}” 提供了action的名字。在Web API中,一般情况下会省略”{action}”.

路由defaults字段

如果你提供了一个defaults,那么这个路由将会匹配那些不含有其他字段的URI。比如:

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

http://localhost/api/products” 会匹配上面的路由。”{category}” 字段被填充了默认值”all”.

路由字典

如果framework找到了一个匹配的URI,它会创建一个包含 每个参数占位符的值 的字典。Key是参数占位符的名字(不包含大括号),value是从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. controller: “products”
  2. category: “toys”
  3. id: “123”

defaults中也可以包含一个路由模板中未出现的值。这种情况下,如果路由匹配成功,该值也会被存储到路由字典。例如:

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

如果URI路径是”api/root/8”,路由字典会包含以下两个值:

  1. controller: “customers”
  2. id: “8”

选择一个controller

controller的选择是被IHttpControllerSelector.SelectController方法处理的。该方法读入一个HttpRequestMessage 实例,并返回一个HttpControllerDescriptor。该方法的默认实现是由DefaultHttpControllerSelector类提供的。这个类用了一个简单的算法:

  1. 在路由字典中查找Key值“controller”。
  2. 取得该key对应的value,并追加字符串“Controller”来取得Controller名字。
  3. 根据该Controller的名字来查找对应的 Web API controller

比如,如果路由字典中包含了键值对”controller” = “products”,那么Controller的名字是“ProductsController”。如果根据这个名字没有找到对应的Controller,那么framework将会返回一个错误给客户端。

对于步骤3,DefaultHttpControllerSelector使用了IHttpControllerTypeResolver接口来取得所有Web API controller的名字。IHttpControllerTypeResolver 的默认实现会返回所有的满足以下3个条件的public的类:
1. 实现了IHttpController接口
2. 不是abstract的
3. 名字是以“Controller”结尾的

Action的选择

在选择完Controller以后,framework将会调用IHttpActionSelector.SelectAction方法来选择一个action。该方法读入一个HttpControllerContext 并返回一个HttpActionDescriptor。

该接口的默认实现是由ApiControllerActionSelector类提供的。为了选择一个action,它会检查以下几项:

  1. 请求的HTTP方法类型
  2. 路由模板中的”{action}”参数占位符,如果有出现的话。
  3. Controller中action的参数。

在看 action选择算法 之前,我们需要先了解controller的action。

Controller中的哪些方法才能被认为是action:framework在选择action时,仅会在Controller中的public的方法中选择。当然,特殊命名的方法(constructors, events, operator overloads, 或者其他这种的)以及继承自ApiController的方法会被除外。

HTTP方法:framework仅会选择匹配与HTTP请求的方法的action,由以下几条决定:

  1. 你可以使用这些特性来指定HTTP方法:AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost, 或者 HttpPut.

  2. 另外,如果Controller中的方法是以”Get”, “Post”, “Put”, “Delete”, “Head”, “Options”, 或者 “Patch”开头来命名的,那么依据惯例,action会支持该HTTP方法。

  3. 如果以上两条都不符合,那么该action支持POST。

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

  1. 简单类型从URI中读取
  2. 复杂类型从请求的body中读取

简单类型包含了所有的.NET Framework原语类型,再加上 DateTime, Decimal, Guid, String, 以及 TimeSpan 。 对于每个action来说,最多只有一个参数可以从请求的body中读取。

注意:默认的绑定规则是可以重写的,具体可以看这篇教程:https://blogs.msdn.microsoft.com/jmstall/2012/05/10/webapi-parameter-binding-under-the-hood/

有了以上的背景知识,我们可以看一下action的选择算法了:

  1. 创建一个列表,存放所有的可以匹配HTTP请求的action的名字。
  2. 如果路由字典中含有 “action”,就从列表中去除所有不匹配于路由字典中action对应value的action。

  3. 依照一下步骤来尝试将URI匹配到action的参数:

    a. 对于每个action,取得一个参数列表,该列表包含了从URI的参数绑定中得到的简单类型的参数(可选参数除外)。
    b. 对于该列表,尝试从路由字典或者URI的query stirng中找到所有参数的匹配值。这些匹配值是不区分大小写和参数顺序的。
    c. 选定action,使参数列表中的每个参数都可以从URI中找到一个匹配值。
    d. 如果有多个action符合这些标准,就从里面选一个匹配参数最多的。

  4. 忽略具有[NonAction]特性的action。

第3步可能是最让人不理解的。最根本的想法就是,参数的获取方式有3种:从URI中;从请求的body中;从定制的参数绑定中。对于从URI中获取的参数,我们想确认该URI确实包含了该参数的值,不论是从URI路径(通过路由字典)中还是在query string中。

举个例子,考虑下面的action:

public void Get(int id)

这个id参数绑定在URI中。所以,这个action只能匹配一个含有id值的URI,不论是在路由字典中还是在 query string 中。

由于复杂的原因,复杂类型参数是例外。复杂类型只能通过定制的参数绑定来绑定到URI中。但是在这个例子中,framework不能进一步知道该参数是否能绑定到一个具体的URI。为了查明这个问题,它需要调用这个绑定。选择算法的目的是在使用任何绑定之前,从静态描述中选择一个action。因此,复杂类型被排除在匹配算法之外。

在action选定之后,所有的参数绑定被调用。

总结:

  1. action必须匹配HTTP方法。
  2. action名字必须匹配路由字典中的action值,如果出现的话。
  3. 对于action的每个参数来说,如果参数是从URI中获取的,那么这个参数必须可从路由字典或者URI的query string中取得。(可选参数以及复杂类型参数除外)
  4. 尝试匹配尽可能多的参数。最好的匹配项是一个没有参数的方法。

扩展例子

路由:

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

Controller:

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,路由字典中包含以下元素:

  1. controller: “products”
  2. id: “1”

路由字典中不包含query string参数(”version” 和 “details”),但是在action的选择阶段这些仍会被考虑到。

Controller的选择:

从路由字典中的controller元素得到controller名字是ProductsController。

Action选择:

HTTP请求是一个GET请求。controller中支持GET的action有GetAll, GetById, 以及 FindProductsByName。路由字典中不含有action元素,所以我们不需要对action名字进行匹配。

接下来,我们尝试为这些支持GET的action匹配参数名字:

############ 1

注意GetById方法的version参数没有被考虑,因为它是一个可选参数。

GetAll方法匹配情况很一般。GetById方法也是匹配的,因为路由字典中包含了“id”。这个方法被调用,参数值为:

  1. id = 1
  2. version = 1.5

注意,尽管version参数在选择算法中没被用到,但该参数的值来自URI的 query string。

扩展接口

Web API为路由过程中的某些部分 提供了扩展接口:

################# 2

若需要自己实现以上接口,需要使用HttpConfiguration对象的Services collection 。

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

下一步

结束

本文为微软官方文档的个人译文。本译文的非商用转载请注明地址及作者,感谢。禁止商用转载。若经发现,将依法追究责任。
英文原文地址:https://docs.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/routing-and-action-selection
原文作者: MikeWasson and Other 5 Contributors
主作者链接:https://github.com/MikeWasson
作者尊重微软公司的知识产权,若贵公司认为该博客有损贵公司利益,请联系作者删除
译者:大吴凡 http://dawufan.cn

猜你喜欢

转载自blog.csdn.net/wf824284257/article/details/79491961