在目前的主流架構中,我們越來越多的看到web Api的存在,小巧,靈活,基於Http協議,使它在越來越多的微服務項目或者移動項目充當很好的service endpoint。問題 以Asp.Net Web Api 為例,隨著業務的擴展,產品的迭代,我們的web api也在隨之變化,很多時候會出現多個版... ...
在目前的主流架構中,我們越來越多的看到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
思路有了,那改造起來也非常簡單,今天我們先做一個簡單的,等有時間改成可配置的。
第一步,我們先創建一個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 ,這時應該能看到正確的執行:
寫在最後
今天我們打造了一個簡單符合webapi版本號更新迭代的ControllerSelector,不過還不是很完善,因為很多都是hard code,後面我會做一個支持配置的ControllerSelector放到github上。
之前一直在研究eShopOnContrainers,最近也在研究,不過工作確實有點忙,見諒見諒,如果大家.Net有什麼問題或者喜歡技術交友的,都可以加QQ群:376248054