主頁 > .NET開發 > WebApi Swagger 介面多版本控制 適用于APP介面管理

WebApi Swagger 介面多版本控制 適用于APP介面管理

2021-02-08 06:10:11 .NET開發

最近研究了下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 控制器有兩個虛方法:GetGetEmployee,因為是虛方法,如果下一個版本同樣的介面有變化的話,直接 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 版本接著繼承上一個版本,同時重寫了 GetGetEmployee 方法

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里設定了兩個引數:versiontoken,模擬APP端調介面傳遞的 版本號鑒權token

終極效果如下

 調下 1.0.1 版本的 Get 介面

測驗一個不存在的Version

前端即便傳來了一個服務端沒有的Version 1.0.5,也能自動向前找最近一個版本1.0.1的介面 

至此,大功告成,最后看看對比圖

五、結語

參考文章

  • WebApi的Swagger多版本控制實作
  • How to change controller's name in swagger-ui?

原始碼

點我下載

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/257623.html

標籤:.NET技术

上一篇:記一次dump檔案定位鎖

下一篇:自定義 ocelot 中間件輸出自定義錯誤資訊

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more