路由过程大致分为三个阶段:
1)请求 URI 匹配已存在路由模板
2)选择控制器
3)选择操作
1 匹配已存在的路由模板
路由模板
在 webApiConfig.Register 方法中定义路由, 例如模板默认生成的路由为:
config.Routes.MapHttpRoute(
- name: "DefaultApi",
- routeTemplate: "api/{controller}/{id}",
- defaults: new { id = RouteParameter.Optional }
- );
上面使用了 public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name, string routeTemplate, object defaults)方法来配置路由. 相关参数为:
name: 路由名称.
routeTemplate: 路由模板, 与 URI 相似.
例如:
api/{controller}/{id},
api/{controller}/{action}/{id},api/{controller}/public/{category}/{id}
defaults: 路由值对象. 可为占位符设置默认值.
例如
api/{controller}/public/{category}/{id}
设置 defaults: new { category = "all" }
路由词典
如果 Web API 匹配到一个已存在的路由模板, 会创建一个路由词典, 词典的键是模板中占位符的名称, 值是占位符对应的值. 如果路由值对象被指定为 RouteParameter.Optional, 那么这个值不会被放入词典中. 路由词典会被存储到 IHttpRouteData 实例中.
匹配示例
对于 api/{controller}/{id}
首先匹配字符串 api, 然后匹配控制器(controller), 第三匹配以 HTTP 方法开头的操作(Action), 占位符 id 匹配 Action 接收的参数.
对于 api/{controller}/{action}/{id}
首先匹配字符串 api, 然后匹配控制器(controller), 最后匹配操作(Action), 占位符 id 匹配 Action 接收的参数.
对于 api/root/{id}
务必对 defaults 设置控制器 (controller) 的默认值,, 不然无法执行路由过程. 可以不设置操作 (Action). 首先匹配 api 和 root, 然后匹配默认的控制器(controller), 最后占位符 id 匹配操作(Action) 接收的参数. 若不设置操作 (Action) 那么匹配以 HTTP 方法开头的操作(Action).
2 控制器的选择
控制器 (controller) 的选择是由 IHttpControllerSelector.SelectController 完成的, IHttpControllerSelector 接口默认实现是 DefaultHttpControllerSelector.IHttpControllerSelector.SelectController 方法获取 HttpRequestMessage 实例并返回 HttpControllerDescriptor.
DefaultHttpControllerSelector 查找控制器 (controller) 的算法为:
在路由词典中查找键为 "controller" 的值, 找到键 "controller" 对应的值后, 将字符串 Controller 拼接到这个值的后边, 便可获得控制器 (Controller) 名. 根据获得的控制器 (Controller) 名查找 Web API 中的控制器 (controller). 如果没有查找到控制器(controller) 名或者匹配到了多个, 那么返回错误. DefaultHttpControllerSelector 使用 IHttpControllerTypeResolver 来获得 Web API 控制器 (controller) 类型列表. IHttpControllerTypeResolver 的默认实现返回具有如下特征的公有类:
1)实现了 IHttpController 接口.
2)不被 abstract 修饰.
3)命名以 "Controller" 结尾.
3 匹配控制器操作
IHttpActionSelector.SelectAction 方法获取 HttpControllerContext 并返回 HttpActionDescriptor,IHttpActionSelector 接口的默认实现是 ApiControllerActionSelector.ApiControllerActionSelector 会查找请求的 HTTP 方法, 路由模板中的 {action} 占位符, 控制器操作的参数列表.
Web API 框架认为控制器 (controller) 的操作 (Action) 具有如下特征:
1)公有类型的实例方法.
2)继承自 ApiController 的方法
3)非构造器, 事件, 操作符重载等特殊方法.
Web API 框架仅选择那些匹配请求的 HTTP 方法的操作, 原则为:
1)指定了相应特性的操作, 例如使用 HttpGet 特性的操作, 只能匹配 Get 请求.
2)如果控制器 (controller) 操作以 "Get", "Post", "Put", "Delete", "Head", "Options", or "Patch" 开头, 按照惯例控制器 (controller) 操作支持对应的 HTTP 请求.
3)如果不满足以上两条, 默认支持 POST 请求.
ApiControllerActionSelector 选择控制器 (controller) 操作的算法如下:
1)创建一个链表, 链表元素为所有与 HTTP 请求相匹配的操作(Action).
2)如果路由词典中包含关于操作 (Action) 的键值对, 移除链表中名称和值不匹配的操作(Action).
3)匹配操作 (Action) 参数与 URI.
l 对于每一个操作 (Action), 获得简单类型的参数列表, 参数绑定从 URI 获得操作(Action) 参数, 不包括可选的参数.
l 在参数列表中, 从路由表中或请求 URI 查询字符串中, 为每一个参数名找到一个匹配, 匹配是不区分大小写的, 并且不依赖于参数顺序.
l 选择一个操作(Action), 其参数列表中的每一个参数在请求 URI 中都对应一个值.
l 如果有多个操作 (Action) 满足以上规则, 选择有最多参数匹配的一个操作(Action).
4)忽略被标记为 [NonAction] 的方法.
补充说明:
对于步骤 3)一个参数可以从 URI, 请求消息体, 或者自定义绑定中获得它的值. 对于来自于 URI 的参数, 要确保 URI 确实包含对应参数的值, 这个值可能在路由词典中或查询字符串中.
对于可选的参数, 如果绑定不能从 URI 中获得参数的值, 对于操作 (Action) 的选择也没有影响.
对于复杂类型, 只能通过自定义绑定来匹配 URI 中的参数值. 操作 (Action) 选择算法的目的是在完成模型绑定之前选出操作 (Action), 因此操作(Action) 选择算法对复杂类型无效.
一旦操作 (Action) 被选出, 模型绑定器才会被调用.
4 路由过程的扩展
接口 | 描述 |
IHttpControllerSelector | 选择控制器 |
IHttpControllerTypeResolver | 获得控制器(controller)类型列表,DefaultHttpControllerSelector 会从这个列表中选择控制器(controller)类型。 |
IAssembliesResolver | 获得项目程序集列表,IHttpControllerTypeResolver 会从这个列表中找到控制器(controller)类型 |
IHttpControllerActivator | 创建新的控制器(controller)实例 |
IHttpActionSelector | 选择操作(Action) |
IHttpActionInvoker | 调用操作(Action) |
要想使用自定义的上述接口实现, 那么要注册服务.
- public static class WebApiConfig
- {
- public static void Register(HttpConfiguration config)
- {
- // 其他配置
- config.Services.Replace(typeof(IHttpControllerSelector), new CustomHttpControllerSelector());
- }
- }
例: 扩展 IHttpControllerSelector
实现 GetControllerMapping 和 SelectController 方法, GetControllerMapping 为发现系统所有可能的控制器(controller),SelectController 会使用这些所有可能的控制器(controller), 因此需要 CustomHttpControllerSelector 的属性存储所有可能的控制器(controller). 具体示例见 "ASP.NET Web API 编程 -- 版本控制"
- public class CustomHttpControllerSelector : IHttpControllerSelector
- {
- public IDictionary<string, System.Web.Http.Controllers.HttpControllerDescriptor> GetControllerMapping()
- {
- throw new NotImplementedException();
- }
- public System.Web.Http.Controllers.HttpControllerDescriptor SelectController(HttpRequestMessage request)
- {
- throw new NotImplementedException();
- }
- }
此外, 有时扩展 Web API 框架的 DefaultHttpControllerSelector 或许是更加合理的方式.
例: 扩展 IAssembliesResolver, 动态加载控制器(controller).
可以将控制器 (controller) 类单独编制为一个 dll, 放在指定的文件夹内, 这样无需编译整个框架, 就能修改控制器(controller).
- public class ServiceAssembliesResolver : IAssembliesResolver
- {
- private string path;
public ServiceAssembliesResolver(string path)
- {
- this.path = path;
- }
- public ICollection<System.Reflection.Assembly> GetAssemblies()
- {
- List<Assembly> assemblies = new List<Assembly>();
- try
- {
- // 初始化
- assemblies = new List<Assembly>();
- // 加载每一个服务插件
- foreach (string file in Directory.GetFiles(path, "*.dll"))
- {
var controllersAssembly = Assembly.LoadFrom(file);
- assemblies.Add(controllersAssembly);
- }
- }
- catch (Exception ex)
- {
- // 处理异常
- }
- return assemblies;
- }
- }
此外继承 Web API 框架默认的 DefaultAssembliesResolver 也是一个好办法.
- public class ServiceAssembliesResolver : DefaultAssembliesResolver
- {
- // 服务插件路径
- private string path;
- public ServiceAssembliesResolver(string path):base()
- {
- this.path = path;
- }
- public override ICollection<Assembly> GetAssemblies()
- {
- List<Assembly> assemblies = new List<Assembly>();
- try
- {
- // 获得已有的服务
- ICollection<Assembly> baseAssemblies = base.GetAssemblies();
- // 初始化
- assemblies = new List<Assembly>(baseAssemblies);
- // 加载每一个服务插件
- foreach (string file in Directory.GetFiles(path, "*.dll"))
- {
var controllersAssembly = Assembly.LoadFrom(file);
- assemblies.Add(controllersAssembly);
- }
- }
- catch (Exception ex)
- {
- // 处理异常
- }
- return assemblies;
- }
- }
5 使用特性设置路由
为了更好地支持 URI 参数, 所以使用路由特性.
5.1 使用特性
RouteAttribute
路由特性定义为:
- public sealed class RouteAttribute : Attribute, IDirectRouteFactory, IHttpRouteInfoProvider
- {
- public RouteAttribute();
- //template: 描述要匹配的 URI 模式的路由模板
- public RouteAttribute(string template);
- // 路由名称
- public string Name { get; set; }
- // 路由顺序
- public int Order { get; set; }
- // 描述要匹配的 URI 模式的路由模板
- public string Template { get; }
- }
RoutePrefix
使用 RoutePrefix 特性为整个控制器 (controller) 设置路由前缀, 路由前缀特性定义为:
- public class RoutePrefixAttribute : Attribute, IRoutePrefix
- {
- protected RoutePrefixAttribute();
- //prefix: 控制器的路由前缀.
- public RoutePrefixAttribute(string prefix);
- // 获取路由前缀.
- public virtual string Prefix { get; }
- }
例子:
[RoutePrefix("api/values")]
- public class ValuesController : ApiController
- {
- //GET api/values/getvalues
[Route("getvalues")]
- public IEnumerable<string> Get()
- {
- return new string[] { "value1", "value2" };
- }
- }
使用 "~" 可重写路由前缀, 例如:
[RoutePrefix("api/values")]
- public class ValuesController : ApiController
- {
[Route("~/api/allvalues")]
- public IEnumerable<string> Get()
- {
- return new string[] { "value1", "value2" };
- }
- }
路由前缀可以包含参数:
[RoutePrefix("api/values/{id}")]
- public class ValuesController : ApiController
- {
- //GET api/values/1/getvalues
[Route("getvalues")]
- public IEnumerable<string> Get(string id)
- {
- //
- }
- }
路由约束
限制参数的类型, 语法为:{parameter:constraint}, 可以指定多个约束, 每个约束用: 分隔. Route 和 RoutePrefix 特性均支持这种用法.
[RoutePrefix("api/values/{id:int:min(1)}")]
- public class ValuesController : ApiController
- {
[Route("GetValues")]
- public IEnumerable<string> Get(int id)
- {
- // 具体实现
- }
- }
约束规则如下:
约束 | 描述 | 例子 |
alpha | 匹配大写或小写拉丁字母(A-Z,a-z) | {x:alpha} |
bool | 匹配 Boolean 类型 | {x:bool} |
datetime | 匹配 DateTime 类型 | {x:datetime} |
decimal | 匹配 decimal 类型 | {x:decimal} |
double | 匹配 double 类型 | {x:double} |
float | 匹配 float 类型 | {x:float} |
guid | 匹配 GUID 值 | {x:guid} |
int | 匹配 int 类型 | {x:int} |
length | 匹配指定长度或指定长度范围内的字符串 | {x:length(6)} {x:length(1,20)} |
long | 匹配 long 类型 | {x:long} |
max | 匹配整型,其值不能大于设置的值 | {x:max(10)} |
maxlength | 匹配字符串,它的长度不能超过设定的值 | {x:maxlength(10)} |
min | 匹配整型,其值不能小于设定的值 | {x:min(10)} |
minlength | 匹配字符串,它的长度不能小于设置的值 | {x:minlength(10)} |
range | 指定整型的范围 | {x:range(10,50)} |
regex | 匹配正则表达式 | {x:regex(^\d{3}-\d{3}-\d{4}$)} |
可选 URI 参数与默认值
使用? 来标识路由值为可选的, 同时必须为操作参数设置默认值.
例:
[Route("api/v1/user/{id:int?}")]
[HttpGet]
public IHttpActionResult User(int id=1)
- {
- return Json("id:"+id);
- }
设置路由名称
设置路由名称后, 可以在使用控制器 (controller) 的属性 ApiController.Url 或 ApiController.Route 拼接 URL.
例: 在 GetPublicationNew 中获得路由到操作 GetPublication 的 URL
[Route("api/v1/publication",Name="V1Publication")]
public IHttpActionResult GetPublication()
- {
- return Json("api/v1/publication");
- }
[HttpGet]
[Route("api/v2/publication")]
public IHttpActionResult GetPublicationNew()
- {
- string url = Url.Link("V1Publication", null);
- return Json(url);
- }
路由顺序
RouteOrder 值较小的路由先被使用, 默认的 RouteOrder 值为 0.
比较顺序的规则为:
1)先比较 RouteOrder 的值
2)查看路由模板的 URI 参数, 对于每一个参数, 由参数决定的顺序为:
字面值顺序排第一.
含有路由约束的顺序排第二.
没有路由约束的顺序排第三.
含有通配符和路由约束的顺序排第四.
含有通配符和无路由约束的顺序排第五.
3)在上述规则无法区分的情况下, 即上述规则判定顺序相同的两个路由, 决定顺序的依据是: 不区分大小写地, 比较字符串的序号.
例: 这里引用官网文档的例子
(https://docs.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2)
[RoutePrefix("orders")]
- public class OrdersController : ApiController
- {
[Route("{id:int}")] // 设置路由约束
public HttpResponseMessage Get(int id) { ... }
[Route("details")] // 字面值
public HttpResponseMessage GetDetails() { ... }
[Route("pending", RouteOrder = 1)]// 指定路由顺序
public HttpResponseMessage GetPending() { ... }
[Route("{customerName}")] // 无路由约束
public HttpResponseMessage GetByCustomer(string customerName) { ... }
[Route("{*date:datetime}")] // 含有通配符
public HttpResponseMessage Get(DateTime date) { ... }
}
路由顺序依次为:
第一. orders/details
第二. orders/{id}
第三. orders/{customerName}
第四. orders/{*date}
第五. orders/pending
使路由特性起作用
要想使路由特性起作用, 必须在 WebApiConfig.Register 方法中加入代码: config.MapHttpAttributeRoutes();
如下完整代码:
- public static class WebApiConfig
- {
- public static void Register(HttpConfiguration config)
- {
- // 启用路由特性
config.MapHttpAttributeRoutes();
- // 其他配置
- }
- }
可以同时使用路由特性与基于协定路由:
- public static void Register(HttpConfiguration config)
- {
- // 启用路由特性
config.MapHttpAttributeRoutes();
// 基于协定的路由
config.Routes.MapHttpRoute(
- name: "DefaultApi",
- routeTemplate: "api/{controller}/{id}",
- defaults: new { id = RouteParameter.Optional }
- );
- }
自定义路由约束
实现一个继承自 IHttpRouteConstraint 接口的类, 然后注册此类.
例:
自定义 CustomHttpRouteConstraint
- public class CustomHttpRouteConstraint : IHttpRouteConstraint
- {
- public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
- {
- // 实现验证过程
- }
- }
注册 CustomHttpRouteConstraint, 为这个约束提供一个简称.
- public static class WebApiConfig
- {
- public static void Register(HttpConfiguration config)
- {
- // 其他配置
- var constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("customcons", typeof(CustomHttpRouteConstraint));
config.MapHttpAttributeRoutes(constraintResolver);
}
}
使用自定义约束
[Route("{name:customcons}")]
public IHttpActionResult SUser(string name)
- {
- return Json("name:" + name);
- }
5.2 应用场景:
支持多版本 API:
假设随着业务的扩展, 对 API 接口进行升级改造, 老的接口还要使用一段时间而不会立即停用, 这时需要版本控制机制. 如下面的例子, 使用路由特性后,
虽然 URI 片段中的指定的操作 (Action) 名称一样, 但是调用的操作 (Action) 却不一样.
例:
[Route("api/v1/publication")]
public IHttpActionResult GetPublication()
- {
- return Json("api/v1/publication");
- }
[Route("api/v2/publication")]
public IHttpActionResult GetPublicationNew()
- {
- return Json("api/v2/publication");
- }
当在浏览器中输入: http://localhost:45778/api/v1/publication 时, 显示 "api/v1/publication"
当在浏览器中输入: http://localhost:45778/api/v2/publication 时, 显示 "api/v2/publication"
由于上述操作定义在同一个控制器 (Controller) 类中, 所以方法名不能相同.
注意: 由于上述操作名称中含有 Get 字符串, 所以支持 Get 请求.
重载
为了支持重载的方法, 使用路由特性
例:
[Route("api/v1/user/{id}")]
public IHttpActionResult GetUser(int id)
- {
- return Json("id:"+id);
- }
[Route("api/v2/user/{name}")]
public IHttpActionResult GetUser(string name)
- {
- return Json("name:" + name);
- }
当在浏览器中输入 http://localhost:45778/api/v1/user/1 时, 页面显示 "id:1"
当在浏览器中输入 http://localhost:45778/api/v2/user/coding 时, 页面显示 "name:coding"
支持 URI 时间参数
例:
请求 Url:http://localhost:45778/api/user/1982-02-01
[HttpGet]
[Route("api/user/{time:datetime}")]
public IHttpActionResult User(DateTime time)
- {
- return Json("time:" + time);
- }
输出为:"time:1982/2/1 0:00:00"
请求 Url:http://localhost:45778/api/user/1982/02/01
[HttpGet]
[Route("api/user/{*time:datetime:regex(\\d{4}/\\d{2}/\\d{2})}")]
public IHttpActionResult User(DateTime time)
- {
- return Json("time:" + time);
- }
输出为:"time:1982/2/1 0:00:00"
也可以将两种约束一起使用, 这样可以同时支持两种格式了
[HttpGet]
[Route("api/user/{*time:datetime:regex(\\d{4}/\\d{2}/\\d{2})}")]
[Route("api/user/{time:datetime:regex(\\d{4}-\\d{2}-\\d{2})}")]
public IHttpActionResult User(DateTime time)
- {
- return Json("time:" + time);
- }
参考
- https://docs.microsoft.com/en-us/aspnet/web-api/
- ---------------------------------------------------------------------
来源: https://www.cnblogs.com/hdwgxz/p/8729000.html