Web API-路由(二)
路由匹配主要有三个阶段:
1.将URI匹配到一个路由模版;
2.选择一个controller
3.选择一个action;
可以使用系统提供的拓展点,修改默认的匹配与选择逻辑规则。
路由模版:
路由模版很像一个URI但是它可以包含使用大括号包裹的占位符。
"api/{controller}/public/{category}/{id}"
我们可以定义占位符的默认值,defaults: new { category = "all" }
可以使用正则表达式定义约束条件:constraints: new { id = @"\d+" } // Only matches if "id" is one or more digits.
模版中的非占位符,文本部分必须严格匹配,占位符可以匹配任何值,除非你指定了约束条件。Web API 路由不会匹配URI的其他部分,比如主机名,和查询变量(一个uri : http://www.badiu.com/api/products/public/cat1/2?para1=a¶2=b,路由只关注红色的部分。框架会使用第一个匹配的模版(也就是说,路由模版记录是有顺序的)。
有两个特殊的占位符那就是{controller} 和 {action},很容易理解:
1.{controller}提供controller的名称;
2.{action}提供action的名称,在Web API中通常的惯例是省略"{action}";
暂时可以这样理解,除了{controller} 和 {action}两个占位符,其他的占位符一般用在匹配传递的参数(给uri中的参数命名以匹配action方法的参数,用作模型绑定用)。
路由字典(Route Dictionary)
当框架找到一个能够匹配URI的路由模版,它会创建一个字典类型对象来存储每一个占位符以及占位符匹配到的值,key值对应不包含大括号的占位符名称,vaule则为占位符在uri匹配到的值或者提供默认值。这个字典存储在一个IHttpRouteData类型的对象中。
在这个环节,特殊的占位符{controller}和{action}被当做普通的占位符对待,存储在字典中。
在给占位符指定默认值时,可以给它指定 RouteParameter.Optional值,如果给一个占位符指定了这个值,说明这个参数是可选的,如果不URI中没有指定这个参数,那么这个占位符以及他的值将不会添加到路由字典中去。比如:
routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{category}/{id}", defaults: new { category = "all", id = RouteParameter.Optional } );
如果uri为"api/products",那么路由字典包含:
controller:"products"
category:"all"
如果uri为"api/products/123",那么路由指定包含:
controller: "products"
category: "toys"
id: "123"
也就是说当指定一个占位符的默认值是:RouteParameter.Optional 时,uri没有为该占位符提供值时,字典中不包含该键值对,uri为该占位符提供值的时候,字典中包含该键值对。
还可以为没在路由模版中出现的占位符指定默认值:
routes.MapHttpRoute( name: "Root", routeTemplate: "api/root/{id}", defaults: new { controller = "customers", id = RouteParameter.Optional } );
这样匹配的controller总是为customers,路由字典包含:
controller: "customers"
id: "8"
控制器的选择(Selecting a Controller)
控制器的选择由 IHttpControllerSelector.SelectController 方法处理,这个方法接收一个HttpRequestMessage对象返回一个HttpControllerDescriptor对象。SelectController方法默认的实现由DefaultHttpControllerSelector类提供,这个类使用简单的控制器选择算法:
1.在路由字典中查找是否有键名为"controller"。
2.获取这个controller健对应的值,与“Controller”字符串组合成一个新字符串,通过这个字符串获得控制器类名称。
3.在Web API的控制器查找相同类型名称的控制器。
例如:路由字典中包含键值对"controller"="products",那么控制器类型即为"ProductsController"。如果有没有匹配的类型,获取有多个匹配的类型,框架返回一个错误给客户端。
在第三步中,DefaultHttpControllerSelector使用IHttpControllerTypeResolver接口去获取一个当前项目Web API控制器类型的列表。IHttpControllerTypeResolver默认的实现方法返回:
--所有实现了IHttpController的公开的(public)控制器。
--且.非抽象类型(are not abstract);
--且.命名以"Controller"结尾。
Action的选择(Action Selection)
在完成控制器的选择后,框架通过调用IHttpActionSelector.SelectAction方法来选择action,这个方法接收一个HttpControllerContext类型参数,返回一个HttpActionDescriptor类型对象,方法默认的实现由ApiControllerActionSelector类提供,选择Action的依据:
--HTTP请求方法(GET POST PUT ....)
--路由模版中是否有{action}占位符
--控制器中action方法的参数;
在了解action的选择算法之前,我们需要理解控制器 action的一些知识。
在控制器中什么样的方法可以作为action方法?
--控制器中的公共(public)方法,但不包括特殊名称(constructors, events, operator overloads, and so forth)的方法,和从ApiController中继承的方法。
HTTP请求方式:框架只选择能够匹配HTTP请求方式的action,由以下几个因素决定:
1.可以指定action方法可以匹配的HTTP请求方式,用 AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost, 或者 HttpPut这些标识属性。
2.action方法名称以"Get", "Post", "Put", "Delete", "Head", "Options", 或者 "Patch" 开头。
3.如果action不满足上面两个条件,action方法默认处理POST请求。
参数绑定:Web API如何给action方法选择参数值,默认规则是:
--简单类型的参数值从URI中获取
--复杂类型的值从请求主体(request body)中获取;
简单类型包括所有.net 的基本类型( .NET Framework primitive types)以及 DateTime,Decimal,Guid,String和TimeSpan类型。对于一个action,最多只有一个参数能够读取请求主体(request body)???
修改默认的参数绑定规则,看WebAPI Parameter binding under the hood.
有了以上的知识,下面来看action的选择算法:
1.创建一个匹配当前请求方式的控制器中的action列表集合;
2.如果路由字典中有"action"键,再去除名称与字段中action键值不匹配的action;
3.尝试将action方法的参数与URI中传递的参数匹配:
--循环一个action,获取简单类型的参数列表(不包括可选类型参数)
--尝试将列表中的参数名与路由字典以及URI中查询参数进行匹配,这个匹配忽略大小写,以及参数顺序。
--选择一个:action方法的每一个参数都在路由字典或者URI查询参数中匹配到值的action。
--如果有多个action方法匹配成功,选择参数(不包括可选参数)最多的那个。
4.忽略带有[NonAction]标识属性的方法。
第三步,基本的意思是,一个action方法的参数能够从URI,请求主体(request body),或者自定义绑定(custom binding)获取它的参数值,对于来自URI中的参数,我们希望确定URI中是否包含一个当前action参数的值,无论是包含在路径(路由字典)中还是在查询参数中(query string)。(也就是这里的URI参数包括路由字典中的参数和查询字符串中的参数)
比如现在有一个action
public void Get(int id)
参数id需要绑定URI传递的信息,因此这个action只能匹配一个包括"id"值的URI(id值可以在路由路径中或者查询参数中)。
可选参数除外,如果URI中有为可选参数提供值则绑定,如果没有也没有影响,可选参数使用默认值。
复杂类型参数是一个例外(action选择时不会考虑复杂类型参数),复杂类型参数必须手工绑定,这个操作在action选择之后,所以在选择action时不会考虑复杂类型参数的匹配问题。
总结:
--action必须匹配HTTP请求方式
--如果指定了{action}占位符,action的名称必须与URI为占位符提供值相同;
--对于action的每一个参数,如果参数值从URI中获取,那么参数名称必须可以在路由字典或者URI查询条件(query string)中找得到(可选参数和复杂类型参数除外),这里有一个问题,请求主体(request body)中提供的键值对信息可不可以匹配action的参数???
--尝试匹配参数最多额action,最佳匹配方法可能没有参数。
综合实例:
路由模版定义如下:
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"路由模版,路由字典中包含:
controller:"products"
id:"1"
虽然路由字典不包含"version" 和 "details" 但在action选择时还是会考虑这两个参数。
控制器选择:ProductsController。
action选择:首先看Http请求方式 ,这里是GET方式,所以满足要求的只有GetAll ,GetById 和FindProductsByName 。再看路由模版中是否包含{action}占位符,这里不包含所以不用管action的名称,最后我们看满足要求的三个action的要求URI包括的参数,GetAll 没有参数,GetById 要求URI中包含id参数,FindProductsByName要求URI中包含name参数值,因为这里URI中不包含name参数值,所以排除FindProductsByName这个action,最后看匹配参数的个数,因为GetById匹配的参数个数1大于GetAll匹配参数个数0。所以最终选择GetById。(这里因为GetById的version参数是可选参数,选择action根本不会考虑这个参数,但绑定参数时会使用URI传递的参数值)。
一个问题可选参数是否影响action的选择,比如上面的例子中如果URI还包括一个name参数值。会选择哪个action 还是会报错??
结果:不影响。比如:同时包括id和name参数时报Multiple actions were found错误
拓展点:
Web API 为路由过程控制提供拓展点:
接口 |
描述 |
IHttpControllerSelector |
选择控制器 |
IHttpControllerTypeResolver |
获取控制器类型列表,DefaultHttpControllerSelector从获取的列表中选择控制器类型 |
IAssembliesResolver |
获取项目组件的列表,IHttpControllerTypeResolver接口使用这个列表去find控制器类型 |
IHttpControllerActivator |
创建一个新的控制器对象 |
IHttpActionSelector |
选择action |
IHttpActionInvoker |
调用action |
你可以在自定义的类中实现这些接口,然后使用HttpConfiguration对象的Services集合引用自定义的类来覆盖默认的实现类
var config = GlobalConfiguration.Configuration; config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config));