您的位置:首页 > 编程语言 > ASP

ASP.NET Web API 是怎么选择路由和操作的

2017-05-26 00:00 417 查看
摘要: 这是一篇翻译文
原文地址:https://docs.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/routing-and-action-selection
原文名称:Routing and Action Selection in ASP.NET Web API
原文作者:Mike Wasson

这篇文章描述了ASP.NET Web API 怎么将一个HTTP请求转换成指定控制器的指定操作。

Note:有关路由的高层概述,查看Routing in ASP.NET Web API.

本文关注的是路由过程的细节。如果你创建了一个Web API 项目并且发现一些请求不能像你预期的那样被路由,希望本文可以提供一些帮助。

路由有三个主要的部分:

将URI和路由模板互相匹配

选择控制器

选择操作

你可以将其中的任意一个过程替换为自定义行为。本文主要描述默认行为。在文末,我标记了你可以自定义的地方。

##路由模板

一个路由模板看起来很像URI路径,但是它有被大括号包裹的占位符:

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

当你创建一个路由的时候,可以给占位符提供默认值:

defaults: new { category = "all" }

你也可以提供约束,来限制URI怎么匹配占位符

constraints: new { id = @"\d+" } //当id是数字是才匹配成功

框架尝试将URI和模板匹配,模板中的字面量必需一字不差,占位符可以是任何值(除非指定了约束)。框架不会匹配URI的所有内容,比如主机名、查询参数。框架会选择路由表中匹配的第一个路由。

这里有两个特殊的占位符:"{controller}" 和 "{action}"

"{controller}" 提供了控制器的名字。

"{action}" 提供了操作的名字。在Web API里,惯例会省略"{action}"。

###默认值

如果提供了默认值,那些缺省了相应部分的URI也可以匹配成功。例如:

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

"
http://localhost/api/products
"就可以匹配到这个路由。"{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 }
);

对于"api/products"来说,路由字典将会包含:

controller: "products"

category: "all"

对于"api/products/toys/123"来说,路由字典将会包含:

controller: "products"

category: "toys"

id: "123"

即使路由模板里没有出现某个占位符,依然可以为其设置默认值。如果路由匹配成功,它的值就会被存储到字典里。例如:

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

如果URI路径是"api/root/8",路由字典将会包含:

controller: "customers"

id: "8"

##选择控制器

控制器的选择,由IHttpControllerSelector.SelectController方法掌管。这个方法获取HttpRequestMessage实例作为参数,返回一个HttpControllerDescriptor对象。其默认实现由DefaultHttpControllerSelector类提供。这个类使用了一个简单的算法:

在路由字典里查找键"controller"

获取此项的值,并追加字符串"Controller"以获得控制器类型名称

查找具有此类型名称的Web API控制器

举个例子,如果路由字典包含键值对 "controller" = "products",然后控制器类型就是"ProductsController"。如果没有匹配的类型或者匹配到了多个,框架会给客户端返回一个错误。

第三步,DefaultHttpControllerSelector使用IHttpControllerTypeResolver接口获取所有的Web API控制器类型。IHttpControllerTypeResolver的默认实现,返回(a)实现了IHttpController、(b)不是抽象的、(c)名称以"Controller"结尾的所有公共类。

##操作选择

选择了控制器之后,框架就会调用IHttpActionSelector.SelectAction方法来选择操作。这个方法接收HttpControllerContext对象并返回HttpActionDescriptor对象。

其默认实现由ApiControllerActionSelector类提供。

若要选择一个操作,它将查看以下内容:

请求的Http方法

路由模板中的"{action}"占位符(如果存在的话)

控制器上所有操作的参数

在查看选择算法之前,我们需要了解一些关于控制器操作的事情。

控制器上的哪个方法会被当做“操作”?[b]当选择一个操作时,框架只看控制器上的公共方法。此外,还排除了一些具有“特殊名称”的方法(构造函数、事件、运算符重载等)和从ApiController[/b]类继承的方法。

Http 方法。框架只选择与请求的Http方法匹配的操作,如下所示:

可以使用属性指定Http方法:AcceptVerbs,HttpDelete,HttpGet,HttpHead,HttpPatch,HttpPost,或者HttpPut

另外,如果控制器方法的名字以"Get"、"Post"、"Put"、"Delete"、"Head"、"Options"或者"Patch"开头,那么按约定,操作支持对应的Http方法。

如果以上都不满足,这个方法支持Post操作

参数绑定。参数绑定是指Web API如何为参数创建值。绑定的默认规则如下:

简单类型直接从URI中获取

复杂类型从请求主体中获取

简单类型包括.NET Framework primitive types",加上DateTimeDecimalGuidStringTimeSpan。对于每个操作,最多一个参数可以从请求正文里读取。

可以重写默认绑定规则。参照WebAPI Parameter binding under the hood

以此为背景,操作的选择算法如下:

创建一个列表,其包含了控制器上与http请求方法相匹配的所有操作

如果路由字典有"action"项,则移除其名称与此值不匹配的操作

尝试将操作参数与URI匹配,如下:

对于每个操作,获取一个简单类型的参数列表,其中绑定从URI获取的参数。排除可选参数

在该列表中,尝试在路由字典或URI查询字符串中查找每个参数名称的匹配项。匹配不区分大小写,不依赖参数顺序

选择一个操作,其列表中的每个参数都在URI存在匹配

如果不止一个操作符合这些条件,选择参数匹配最多的那个

忽略带有[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 Put(int id, Product value) {}
}

HTTP 请求:

GET http://localhost:34701/api/products/1?version=1.5&details=1

###路由匹配

和这个URI匹配的路由是“DefaultApi”。路由字典包含以下内容:

controller: "products"

id: "1"

路由字典没有包含查询字符串参数(“version” 和 “details”)但是在操作选择阶段仍会参考它们。

###选择控制器

根据路由字典里的"controller"项,控制器类型是
ProductsController


###操作选择

HTTP 请求是 GET 请求。控制器操作中支持 GET 的是
GetAll
GetById
FindProductsByName
。路由字典里没有包含"action",所以我们不需要匹配操作名称。

下一步,我们尝试匹配操作的参数名,只看 GET 操作。

ActionParameters to Match
GetAllnone
GetById"id"
FindProductsByName"name"
注意 ,没有考虑
GetById
的version参数,因为它是可选的。

GetAll
方法是匹配的。
GetById
方法也是匹配的,因为路由字典里包含"id"。
FindProductsByName
方法就不匹配了。

两者相较,
GetById
方法胜出,因为它匹配了一个参数,要比没有参数的
GetAll
多一个。使用以下参数值调用方法:

id = 1

version = 1.5

注意,尽管选择算法中没有用到version,但是其参数值依然从URI查询字符串中取到。

##扩展知识点
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐