最近研究了下swagger多版本的維護,網上的文章千篇一律,無法滿足我的需求,分享下我的使用場景以及實現
演示環境:Visual Studio 2019、Asp.NET WebAPI、NET Framework 4.5.2、Swashbuckle.Core 5.6.0
本文地址:https://www.cnblogs.com/oppoic/p/14380233.html
一、背景
BS應用沒有介面版本的概念,因為網站一上線,介面和頁面都是新的,服務端不需要維護老介面
但是對於手機APP,服務端就必須要考慮老版本的介面了,因為使用者如果不更新APP,老版本的介面必須存在,這就有了介面版本的概念
二、我們的使用場景
我司APP開發調服務端介面的時候,喜歡把版本號放到請求Header裡面,這個版本號就是APP上架各大商店的版本號,大概是這樣的
如圖,1.9.9版本APP呼叫服務端介面,Header裡的Version就是1.9.9。迭代到2.0.0,調同樣的介面帶的版本號就變成了2.0.0,服務端怎麼處理呢?
常規做法是通過路由實現,但實際情況是這樣的:上架蘋果App Store順利通過,版本號為1.9.9。但是上架華為應用市場,因為軟著的問題被拒了,再次提交版本號就變成了2.0.0。其實APP內部沒有任何改變,服務端這個時候再加上2.0.0的所有介面,然後再次發版嗎?理想的狀態應該是這樣:
- 版本號可以向前相容,服務端沒有2.0.0版本的介面就自動找1.9.9版本的介面;
- 介面可以複用,例:2.0.0版本只修改了1.9.9版本的1個介面,其他介面的實現都是一樣的,那就沒必要把1.9.9版本的介面都拷貝到2.0.0;
- 計算版本號一定要快,因為隨著APP的迭代,服務端維護的版本可能特別多,計算慢的話介面訪問速度會越來越差
以上需求都實現好了,具體請參考:大家是怎麼做APP介面的版本控制的?歡迎進來看看我的方案。升級版的Versioning
接下來才是本篇文章的重點,服務端介面都寫好了,怎麼提供給前端同事檢視呢?
三、和swagger結合
每次寫完介面都錄進文件太麻煩了,以後修改還要維護,如果能自動生成文件就好了。swagger就是解決這個問題的
新建一個空的 Asp.net WebAPI 程式(非Core程式)並安裝下swagger
Asp.net WebAPI 安裝的是 Swashbuckle.Core,只要安裝一個即可,swagger頁面、js、css等檔案都打包在這個dll裡面。結合前篇文章已經實現的服務端介面多版本控制,現在專案結構如下
看下幾個控制器的程式碼
using System.Web.Http; namespace WebAPISwaggerVersioning.Controllers.v1 { public class Employee_1_0_0_Controller : ApiController { [HttpGet] public virtual string Get() { return "1.0.0"; } [HttpGet] public virtual string GetEmployee() { return "GetEmployee:1.0.0"; } } }
1.0.0 版本的 Employee 控制器有兩個虛方法:Get 和 GetEmployee,因為是虛方法,如果下一個版本同樣的介面有變化的話,直接 override 即可
接下來,看看 1.0.1 版本的 Employee 控制器
using System.Web.Http; namespace WebAPISwaggerVersioning.Controllers.v1 { public class Employee_1_0_1_Controller : Employee_1_0_0_Controller { [HttpGet] public override string Get() { return "1.0.1"; } [HttpGet] public virtual string GetEmployeeList() { return "GetEmployeeList:1.0.1"; } } }
1.0.1 版本的 Employee 控制器重寫了 1.0.0 版本的 Get 方法,並加了一個新的虛方法 GetEmployeeList,因為繼承了上一個版本,所以還有一個繼承過來的方法 GetEmployee
再看看 2.0.0 版本
using System.Web.Http; using WebAPISwaggerVersioning.Controllers.v1; namespace WebAPISwaggerVersioning.Controllers.v2 { public class Employee_2_0_0_Controller : Employee_1_0_1_Controller { [HttpGet] public override string Get() { return "2.0.0"; } [HttpGet] public override string GetEmployee() { return "GetEmployee:2.0.0"; } } }
2.0.0 版本接著繼承上一個版本,同時重寫了 Get 和 GetEmployee 方法
swagger的配置類 SwaggerConfig.cs
using System.Web.Http; using Swashbuckle.Application; namespace WebAPISwaggerVersioning { public class SwaggerConfig { public static void Register() { var thisAssembly = typeof(SwaggerConfig).Assembly; GlobalConfiguration.Configuration .EnableSwagger(c => { c.SingleApiVersion("v1", "專案名稱"); }) .EnableSwaggerUi(c => { c.DocumentTitle("WebAPISwaggerVersioning"); }); } } }
直接執行起來看看效果
真的不錯,安裝了swagger並簡單配置就有了這樣的效果,但是有幾個問題:
- 沒有區分版本:1.x 和 2.x 的介面都在一個頁面;
- 直接把 控制器名稱 和 版本號 都讀取出來了:/api/Employee_1_0_0_/Get,前端呼叫其實是這樣的:/api/Employee/Get,版本號攜帶在請求Header裡;
- 另外把 繼承的方法 也讀取出來了:Employee_1_0_1_Controller 下並沒有 GetEmployee 方法,繼承的方法不需要展示,否則太多了
現在開始改進
public static void Register() { var thisAssembly = typeof(SwaggerConfig).Assembly; var xmlPath = string.Format("{0}/bin/WebAPISwaggerVersioning.xml", AppDomain.CurrentDomain.BaseDirectory); GlobalConfiguration.Configuration .EnableSwagger(c => { c.MultipleApiVersions((apiDesc, targetApiVersion) => ResolveVersionSupportByRouteConstraint(apiDesc, targetApiVersion), (v) => { v.Version("v1", "版本1.x").Description("1.x介面文件。點選右上角下拉選單,檢視新版本介面"); v.Version("v2", "版本2.x").Description("增加了手機號找回密碼、財務報銷等功能"); }); }) .EnableSwaggerUi(c => { c.DocumentTitle("WebAPISwaggerVersioning"); c.EnableDiscoveryUrlSelector();//下拉選單列出版本資訊 }); } /// <summary> /// 返回特定版本下的介面 /// </summary> /// <param name="apiDesc"></param> /// <param name="targetApiVersion"></param> /// <returns></returns> private static bool ResolveVersionSupportByRouteConstraint(ApiDescription apiDesc, string targetApiVersion) { var controllerFullName = apiDesc.ActionDescriptor.ControllerDescriptor.ControllerType.FullName; return controllerFullName.Split('.').Contains(targetApiVersion, StringComparer.OrdinalIgnoreCase); }
通過 MultipleApiVersions 方法開啟了多版本
注:配置的 v1 和 v2 必須和資料夾名稱相同,因為 ResolveVersionSupportByRouteConstraint 方法是通過名稱空間來區分版本的,執行看下效果
2.x 的控制器已經不在這個頁面顯示了,但是醜陋的 Employee_1_0_0_ 對前端不友好
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class SwaggerControllerViewAttribute : Attribute { /// <summary> /// 控制器名稱 /// </summary> public string ControllerName { get; private set; } /// <summary> /// 版本號 /// </summary> public string Version { get; private set; } /// <summary> /// Swagger文件顯示 /// </summary> /// <param name="cName">控制器名稱</param> /// <param name="version">版本號</param> public SwaggerControllerViewAttribute(string cName, string version) { ControllerName = string.IsNullOrEmpty(cName) ? "請填寫控制器名稱" : cName; Version = string.IsNullOrEmpty(version) ? "請填寫版本號" : version; } }
建一個特性 SwaggerControllerViewAttribute ,標註到控制器上
[SwaggerControllerView("員工", "v1.0.0")] public class Employee_1_0_0_Controller : ApiController
再利用 GroupActionsBy 方法讀取特性為控制器分組
c.GroupActionsBy(apiDesc => { System.Diagnostics.Debug.WriteLine(apiDesc.ID); var attribute = apiDesc.GetControllerAndActionAttributes<SwaggerControllerViewAttribute>(); if (attribute.Any()) return attribute.First().ControllerName + " " + attribute.First().Version; else return apiDesc.ActionDescriptor.ControllerDescriptor.ControllerName; });
看下效果
標註在控制器上的名稱已經讀取出來了,再把介面後面的版本號幹掉
/// <summary> /// 自定義文件過濾器 /// </summary> internal class CustomDocumentFilter : IDocumentFilter { /// <summary> /// Apply /// </summary> /// <param name="swaggerDoc">文件</param> /// <param name="schemaRegistry">schema註冊</param> /// <param name="apiExplorer">api概覽</param> public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer) { //多版本介面名修正 var match = new Dictionary<string, PathItem>(); foreach (var path in swaggerDoc.paths) { var lsXG = path.Key.Split('/'); if (lsXG.Count() == 4) { var lsXXG = lsXG[2].Split('_'); if (lsXXG.Count() == 5) { match.Add("/" + lsXG[1] + "/" + lsXXG[0] + "/" + lsXG[3] + "?version=v" + lsXXG[1] + "." + lsXXG[2] + "." + lsXXG[3], path.Value); } } } swaggerDoc.paths = match; } }
swaggerDoc.paths 就是所有介面,繼承 IDocumentFilter 介面實現 Apply 方法,可以自定義介面名稱,想怎麼顯示就怎麼顯示
介面名稱已經修正了,但是有個遺憾,因為 swaggerDoc.paths 是字典型別的,key不能重複,所以每個介面後面都跟著 version=,稍後通過前端注入js把 ?version=xxx 去掉
四、柳暗花明
本以為大功告成了,但是注意看 /api/Employee/GetEmployee?version=v1.0.1 這個介面不應該出現,如果把每個繼承過來的方法都顯示出來了,那簡直太亂了,前端只關注本次版本新增(virtual)和變更(override)的方法
到這塊可把我難住了,試了很久,swagger沒有提供任何一個介面可以解決這個問題。距離完美就差一點了,還是不死心,最後通過判斷方法的父類解決了:父類是當前控制器就是新方法或者重寫的方法,不是肯定就是繼承過來的,直接移除不展示
foreach (var apiDesc in apiExplorer.ApiDescriptions) { var key = "/" + apiDesc.RelativePath; if (!swaggerDoc.paths.ContainsKey(key)) continue;//swaggerDoc.paths是當前選擇版本的介面,例:v1 var controllerName = apiDesc.ActionDescriptor.ControllerDescriptor.ControllerType.Name; var actionName = apiDesc.ActionDescriptor.ActionName; if (!string.IsNullOrEmpty(controllerName) && !string.IsNullOrEmpty(actionName)) { var t = Type.GetType(apiDesc.ActionDescriptor.ControllerDescriptor.ControllerType.Namespace + "." + controllerName); if (t != null) { var baseControllerName = t.GetMethod(actionName).DeclaringType.Name; if (controllerName != baseControllerName) { if (key.Contains("?")) key = key.Substring(0, key.IndexOf("?", StringComparison.Ordinal)); swaggerDoc.paths.Remove(key);//移除繼承的Action,避免文件中重複展示 } } } }
再向前端注入js解決介面後面帶 ?version=xxx 的問題。是的,swagger就是這麼靈活,後端前端都可以各種自定義
c.InjectJavaScript(thisAssembly, "WebApiSwaggerVersioning.Scripts.swagger.js");
$("#resources_container .resource").each(function (idx, item) { $.each($(item).find(".endpoints .endpoint"), function (i, v) { var path = $(v).find(".path a"); var pathTxt = path.text(); if (pathTxt) { path.text(pathTxt.substring(0, pathTxt.indexOf('?'))); } }); });
看看簡潔的介面名稱
介面已經完美了,同時注入的 swagger.js 裡面還有漢化包,現在可以顯示中文了。注:swagger.js 需要設定 右鍵 - 屬性 - 生成操作 - 嵌入的資源
文件裡 /api/Employee/Get 出現了兩次,怎麼區分調哪個版本呢?通過繼承 IOperationFilter 實現向請求Header里加自定義引數
public class AuthHeaderFilter : IOperationFilter { public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription) { if (operation.parameters == null) operation.parameters = new List<Parameter>(); var arr = new string[] { }; if (!string.IsNullOrEmpty(operation.operationId)) arr = operation.operationId.Split('_'); operation.parameters.Add(new Parameter { name = "version", @in = "header", description = "介面版本號", type = "string", @default = arr.Length > 4 ? arr[1] + "." + arr[2] + "." + arr[3] : "" }); var filterPipeline = apiDescription.ActionDescriptor.GetFilterPipeline();//是否新增許可權過濾器 var isAuthorized = filterPipeline.Select(filterInfo => filterInfo.Instance).Any(filter => filter is IAuthorizationFilter);//是否允許匿名方法 var allowAnonymous = apiDescription.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any(); if (isAuthorized && !allowAnonymous) { operation.parameters.Add(new Parameter { name = "token", @in = "header", description = "介面token", required = true, type = "string" }); } } }
為每個介面的Header裡設定了兩個引數:version 和 token,模擬APP端調介面傳遞的 版本號 和 鑑權token
終極效果如下
調下 1.0.1 版本的 Get 介面
測試一個不存在的Version
前端即便傳來了一個服務端沒有的Version 1.0.5,也能自動向前找最近一個版本1.0.1的介面
至此,大功告成,最後看看對比圖
五、結語
參考文章
原始碼