以Asp.Net Web Api 为例,随着业务的扩展,产品的迭代,咱们的web api也在随之变化,不少时候会出现多个版本共存的现象,这个时候咱们就须要设计一个支持版本号的web api link,好比:web
原先:http://www.test.com/api/{controller}/{id}api
现在:http://www.test.com/api/{version}/{controller}/{id}浏览器
在咱们刚设计的时候,有可能没有考虑版本的问题,我看到不少的项目都会在link后加入一个“?version=”的方式,这种方式确实可以解决问题,但对Asp.Net Web Api来讲,进入的仍是同一个Controller,咱们须要在同一个Action中进行判断版本号,例如:app
http://www.test.com/api/bolgs?version=v2[HttpGet]ide
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,放在对应的目录中(或者项目中),好比:post
为了避免影响原先的项目,咱们尽可能不要改动原Controller的Namespace,除非你有十足的把握没有影响,否则请尽可能只是移动到目录。url
ok,为了保持原接口的映射,咱们须要在WebApiConfig.Register中注册支持版本号的Route映射:spa
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,就让它走原先的映射。orm
一、项目启动的时候,先把符合条件的Controller加入到一个字典中
二、判断request,符合规则的,咱们返回咱们制定的controller。
思路有了,那改造起来也很是简单,今天咱们先作一个简单的,等有时间改为可配置的。
第一步,咱们先建立一个Selector类,继承自DefaultHttpControllerSelector,而后初始化的时候建立一个属于咱们本身的字典:
public class VersionHttpControllerSelector : DefaultHttpControllerSelector { private readonly HttpConfiguration _configuration; private readonly Lazy<Dictionary<string, 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 Lazy<Dictionary<string, 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 ,这时应该能看到正确的执行: