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个主要环节:
- 与一个路由模板匹配。
- 选择一个controller
- 选择一个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}”.
- “{controller}” 提供了controller的名字。
- “{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”来说,路由字典中包含两项:
- controller: “products”
- category: “all”
然而,对于”api/products/toys/123”来说,路由字典中包含了三项:
- controller: “products”
- category: “toys”
- 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”
选择一个controller
controller的选择是被IHttpControllerSelector.SelectController方法处理的。该方法读入一个HttpRequestMessage 实例,并返回一个HttpControllerDescriptor。该方法的默认实现是由DefaultHttpControllerSelector类提供的。这个类用了一个简单的算法:
- 在路由字典中查找Key值“controller”。
- 取得该key对应的value,并追加字符串“Controller”来取得Controller名字。
- 根据该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,它会检查以下几项:
- 请求的HTTP方法类型
- 路由模板中的”{action}”参数占位符,如果有出现的话。
- Controller中action的参数。
在看 action选择算法 之前,我们需要先了解controller的action。
Controller中的哪些方法才能被认为是action:framework在选择action时,仅会在Controller中的public的方法中选择。当然,特殊命名的方法(constructors, events, operator overloads, 或者其他这种的)以及继承自ApiController的方法会被除外。
HTTP方法:framework仅会选择匹配与HTTP请求的方法的action,由以下几条决定:
你可以使用这些特性来指定HTTP方法:AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost, 或者 HttpPut.
另外,如果Controller中的方法是以”Get”, “Post”, “Put”, “Delete”, “Head”, “Options”, 或者 “Patch”开头来命名的,那么依据惯例,action会支持该HTTP方法。
如果以上两条都不符合,那么该action支持POST。
参数绑定:参数绑定是Web API为参数创建值得方式。下面是参数绑定的默认规则:
- 简单类型从URI中读取
- 复杂类型从请求的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的选择算法了:
- 创建一个列表,存放所有的可以匹配HTTP请求的action的名字。
如果路由字典中含有 “action”,就从列表中去除所有不匹配于路由字典中action对应value的action。
依照一下步骤来尝试将URI匹配到action的参数:
a. 对于每个action,取得一个参数列表,该列表包含了从URI的参数绑定中得到的简单类型的参数(可选参数除外)。
b. 对于该列表,尝试从路由字典或者URI的query stirng中找到所有参数的匹配值。这些匹配值是不区分大小写和参数顺序的。
c. 选定action,使参数列表中的每个参数都可以从URI中找到一个匹配值。
d. 如果有多个action符合这些标准,就从里面选一个匹配参数最多的。忽略具有[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选定之后,所有的参数绑定被调用。
总结:
- action必须匹配HTTP方法。
- action名字必须匹配路由字典中的action值,如果出现的话。
- 对于action的每个参数来说,如果参数是从URI中获取的,那么这个参数必须可从路由字典或者URI的query string中取得。(可选参数以及复杂类型参数除外)
- 尝试匹配尽可能多的参数。最好的匹配项是一个没有参数的方法。
扩展例子
路由:
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,路由字典中包含以下元素:
- controller: “products”
- id: “1”
路由字典中不包含query string参数(”version” 和 “details”),但是在action的选择阶段这些仍会被考虑到。
Controller的选择:
从路由字典中的controller元素得到controller名字是ProductsController。
Action选择:
HTTP请求是一个GET请求。controller中支持GET的action有GetAll, GetById, 以及 FindProductsByName。路由字典中不含有action元素,所以我们不需要对action名字进行匹配。
接下来,我们尝试为这些支持GET的action匹配参数名字:
注意GetById方法的version参数没有被考虑,因为它是一个可选参数。
GetAll方法匹配情况很一般。GetById方法也是匹配的,因为路由字典中包含了“id”。这个方法被调用,参数值为:
- id = 1
- version = 1.5
注意,尽管version参数在选择算法中没被用到,但该参数的值来自URI的 query string。
扩展接口
Web API为路由过程中的某些部分 提供了扩展接口:
若需要自己实现以上接口,需要使用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