为了 顺利迭代升级,web api 在维护过程是不断升级的,但用户是不能强迫他们每次都跟随你去升级,这样会让用户不胜其烦。为了保证不同版本的客户端能同时兼容,在web api接口上加入版本控制就很有必要了。
当然,对于我们开发的代码进行版本控制也有利,不至于陷入混乱。版本参数可以放置在请求的url 作为路由参数的一部分,也可以放在header里。实现的办法是 实现 IHttpControllerSelector 并在WebApiConfig的注册方法里进行替换。
public class VersionHttpControllerSelector : IHttpControllerSelector { private const string VersionKey = "version"; private const string ControllerKey = "controller"; private readonly HttpConfiguration _configuration; private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers; private readonly HashSet<string> _duplicates; public VersionHttpControllerSelector(HttpConfiguration config) { _configuration = config; _duplicates = new HashSet<string>(StringComparer.OrdinalIgnoreCase); _controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary); } private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary() { var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase); IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver(); IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver(); ICollection<Type> controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver); foreach (Type t in controllerTypes) { var segments = t.Namespace.Split(Type.Delimiter); var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length); string version = segments[segments.Length - 1]; var key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", version, controllerName); if (version == "Controllers") { key = String.Format(CultureInfo.InvariantCulture, "{0}", controllerName); } // Check for duplicate keys. if (dictionary.Keys.Contains(key)) { _duplicates.Add(key); } else { dictionary[key] = new HttpControllerDescriptor(_configuration, t.Name, t); } } foreach (string s in _duplicates) { dictionary.Remove(s); } return dictionary; } // Get a value from the route data, if present. private static T GetRouteVariable<T>(IHttpRouteData routeData, string name) { object result = null; if (routeData.Values.TryGetValue(name, out result)) { return (T)result; } return default(T); } public HttpControllerDescriptor SelectController(HttpRequestMessage request) { IHttpRouteData routeData = request.GetRouteData(); if (routeData == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } // Get the version and controller variables from the route data. string version = GetRouteVariable<string>(routeData, VersionKey); if (string.IsNullOrEmpty(version)) { version = GetVersionFromHTTPHeaderAndAcceptHeader(request); } string controllerName = GetRouteVariable<string>(routeData, ControllerKey); if (controllerName == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } // Find a matching controller. string key = String.Format(CultureInfo.InvariantCulture, "{0}", controllerName); if (!string.IsNullOrEmpty(version)) { key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", version, controllerName); } HttpControllerDescriptor controllerDescriptor; if (_controllers.Value.TryGetValue(key, out controllerDescriptor)) { return controllerDescriptor; } else if (_duplicates.Contains(key)) { throw new HttpResponseException( request.CreateErrorResponse(HttpStatusCode.InternalServerError, "Multiple controllers were found that match this request.")); } else { throw new HttpResponseException(HttpStatusCode.NotFound); } } public IDictionary<string, HttpControllerDescriptor> GetControllerMapping() { return _controllers.Value; } private string GetVersionFromHTTPHeaderAndAcceptHeader(HttpRequestMessage request) { if (request.Headers.Contains(VersionKey)) { var versionHeader = request.Headers.GetValues(VersionKey).FirstOrDefault(); if (versionHeader != null) { return versionHeader; } } var acceptHeader = request.Headers.Accept; foreach (var mime in acceptHeader) { if (mime.MediaType == "application/json" || mime.MediaType == "text/html") { var version = mime.Parameters .Where(v => v.Name.Equals(VersionKey, StringComparison.OrdinalIgnoreCase)) .FirstOrDefault(); if (version != null) { return version.Value; } return string.Empty; } } return string.Empty; } }
重点是SelectController方法,从http请求里找出合适版本的controller。我这里兼容了从路由和header里获取版本,先从路由里获取,没有再从header里获取。
IHttpRouteData routeData = request.GetRouteData(); if (routeData == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } // Get the version and controller variables from the route data. string version = GetRouteVariable<string>(routeData, VersionKey); if (string.IsNullOrEmpty(version)) { version = GetVersionFromHTTPHeaderAndAcceptHeader(request); }
private string GetVersionFromHTTPHeaderAndAcceptHeader(HttpRequestMessage request) { if (request.Headers.Contains(VersionKey)) { var versionHeader = request.Headers.GetValues(VersionKey).FirstOrDefault(); if (versionHeader != null) { return versionHeader; } } var acceptHeader = request.Headers.Accept; foreach (var mime in acceptHeader) { if (mime.MediaType == "application/json" || mime.MediaType == "text/html") { var version = mime.Parameters .Where(v => v.Name.Equals(VersionKey, StringComparison.OrdinalIgnoreCase)) .FirstOrDefault(); if (version != null) { return version.Value; } return string.Empty; } } return string.Empty; }
WebApiConfig文件调用代码如下:
public static void Register(HttpConfiguration config) { 。。。 config.Services.Replace(typeof(IHttpControllerSelector), new VersionHttpControllerSelector((config))); }
web api的定义呢,则从命名空间上区分就可以了。 比如版本号为V1的 LoginApiController 的命名空间 为定义为 xxx.WebAPI.Controllers.V1,版本号为V2的 LoginApiController 的命名空间 为定义为 xxx.WebAPI.Controllers.V2,如此类推,
客户端在header里加上参数 versoin=v1/v2... 就可以指定使用不同版本的api了。