在目前的主流架构中,我们越来越多的看到 web Api 的存在,小巧,灵活,基于 Http 协议,使它在越来越多的微服务项目或者移动项目充当很好的 service endpoint。
以 Asp.Net Web Api 为例,随着业务的扩展,产品的迭代,我们的 web api 也在随之变化,很多时候会出现多个版本共存的现象,这个时候我们就需要设计一个支持版本号的 web api link,比如:
原先:http://www.test.com/api/{controller}/{id}
如今:http://www.test.com/api/{version}/{controller}/{id}
在我们刚设计的时候,有可能没有考虑版本的问题,我看到很多的项目都会在 link 后加入一个 "?version=" 的方式,这种方式确实能够解决问题,但对 Asp.Net Web Api 来说,进入的还是同一个 Controller,我们需要在同一个 Action 中进行判断版本号,例如:
http://www.test.com/api/bolgs?version=v2[HttpGet]
- public class BlogsController: ApiController {
- // GET api/<controller>
- public IEnumerable < string > Get([FromUri] string version = "") {
- if (!String.IsNullOrEmpty(version)) {
- return new string[] {
- $ "{version} blog1",
- $ "{version} blog2"
- };
- }
- return new string[] {
- "blog1",
- "blog2"
- };
- }
- }
我们看到我们通过判断 url 中的 version 参数进行对应的返回,为了确保原先接口的可用,我们需要对参数赋上默认值,虽然能够解决我们的版本迭代问题,但随着版本的不断更新,你会发现这个 Controller 会越来越臃肿,维护越来越困难,因为这种修改已经严重违反了 OCP(Open-Closed Principle),最好的方式是不修改原先的 Controller,而是新建新的 Controller,放在对应的目录中(或者项目中),比如:
为了不影响原先的项目,我们尽量不要改动原 Controller 的 Namespace,除非你有十足的把握没有影响,不然请尽量只是移动到目录。
ok,为了保持原接口的映射,我们需要在 WebApiConfig.Register 中注册支持版本号的 Route 映射:
- config.Routes.MapHttpRoute(name: "DefaultVersionApi", routeTemplate: "api/{version}/{controller}/{id}", defaults: new {
- id = RouteParameter.Optional
- });
打开浏览器或者 postman,输入原先的 api url,你会发现这样的错误:
那是因为 web api 查找 Controller 的时候,只会根据 ClassName 进行查找的,当出现相同 ClassName 的时候,就会报这个错误,这时候我们就需要打造自己的 Controller Selector,好在微软留了一个接口给到我们:IHttpControllerSelector。不过为了兼容原先的 api(有些不在我们权限范围内的 api,不加版本号的那种),我们还是直接集成 DefaultHttpControllerSelector 比较好,我们给定一个规则,不负责我们版本迭代的 api,就让它走原先的映射。
1、项目启动的时候,先把符合条件的 Controller 加入到一个字典中
2、判断 request,符合规则的,我们返回我们制定的 controller。
思路有了,那改造起来也非常简单,今天我们先做一个简单的,等有时间改成可配置的。
第一步,我们先创建一个 Selector 类,继承自 DefaultHttpControllerSelector,然后初始化的时候创建一个属于我们自己的字典:
- public class VersionHttpControllerSelector: DefaultHttpControllerSelector {
- private readonly HttpConfiguration _configuration;
- private readonly Lazystring,
- HttpControllerDescriptor >> _lazyMappingDictionary;
- private const string DefaultVersion = "v1"; //默认版本号,因为之前的api我们没有版本号的概念
- private const string DefaultNamespaces = "WebApiVersions.Controllers"; //为了演示方便,这里就用到一个命名空间
- private const string RouteVersionKey = "version"; //路由规则中Version的字符串
- private const string DictKeyFormat = "{0}.{1}";
- public VersionHttpControllerSelector(HttpConfiguration configuration) : base(configuration) {
- _configuration = configuration;
- _lazyMappingDictionary = new Lazystring,
- HttpControllerDescriptor >> (InitializeControllerDict);
- }
- private Dictionary < string,
- HttpControllerDescriptor > InitializeControllerDict() {
- var result = new Dictionary < string,
- HttpControllerDescriptor > (StringComparer.OrdinalIgnoreCase);
- var assemblies = _configuration.Services.GetAssembliesResolver();
- var controllerResolver = _configuration.Services.GetHttpControllerTypeResolver();
- var controllerTypes = controllerResolver.GetControllerTypes(assemblies);
- foreach(var t in controllerTypes) {
- if (t.Namespace.Contains(DefaultNamespaces)) //符合NameSpace规则
- {
- var segments = t.Namespace.Split(Type.Delimiter);
- var version = t.Namespace.Equals(DefaultNamespaces, StringComparison.OrdinalIgnoreCase) ? DefaultVersion: segments[segments.Length - 1];
- var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);
- var key = string.Format(DictKeyFormat, version, controllerName);
- if (!result.ContainsKey(key)) {
- result.Add(key, new HttpControllerDescriptor(_configuration, t.Name, t));
- }
- }
- }
- return result;
- }
- }
有了字典接下来就好办了,只需要分析 request 就好了,符合我们版本要求的,就从我们的字典中查找对应的 Descriptor,如果找不到,就走默认的,这里我们需要重写 SelectController 方法:
- public override HttpControllerDescriptor SelectController(HttpRequestMessage request) {
- IHttpRouteData routeData = request.GetRouteData();
- if (routeData == null) throw new HttpResponseException(HttpStatusCode.NotFound);
- var controllerName = GetControllerName(request);
- if (String.IsNullOrEmpty(controllerName)) throw new HttpResponseException(HttpStatusCode.NotFound);
- var version = DefaultVersion;
- if (IsVersionRoute(routeData, out version)) {
- var key = String.Format(DictKeyFormat, version, controllerName);
- if (_lazyMappingDictionary.Value.ContainsKey(key)) {
- return _lazyMappingDictionary.Value[key];
- }
- throw new HttpResponseException(HttpStatusCode.NotFound);
- }
- return base.SelectController(request);
- }
- private bool IsVersionRoute(IHttpRouteData routeData, out string version) {
- version = String.Empty;
- var prevRouteTemplate = "api/{controller}/{id}";
- object outVersion;
- if (routeData.Values.TryGetValue(RouteVersionKey, out outVersion)) //先找符合新规则的路由版本
- {
- version = outVersion.ToString();
- return true;
- }
- if (routeData.Route.RouteTemplate.Contains(prevRouteTemplate)) //不符合再比对是否符合原先的api路由
- {
- version = DefaultVersion;
- return true;
- }
- return false;
- }
完成这个类后,我们去 WebApiConfig.Register 中进行替换操作:
- config.Services.Replace(typeof(IHttpControllerSelector), new VersionHttpControllerSelector(config));
ok,再次打开浏览器,输入 http://www.xxx.com/api/blogs 和 http://www.xxx.com/api/v2/blogs , 这时应该能看到正确的执行:
今天我们打造了一个简单符合 webapi 版本号更新迭代的 ControllerSelector,不过还不是很完善,因为很多都是 hard code,后面我会做一个支持配置的 ControllerSelector 放到 github 上。
之前一直在研究 eShopOnContrainers,最近也在研究,不过工作确实有点忙,见谅见谅,如果大家. Net 有什么问题或者喜欢技术交友的,都可以加 QQ 群:376248054
来源: http://www.cnblogs.com/inday/p/custom-version-webapi-route.html