WebApi Swagger 接口多版本控制 适用于APP接口管理

最近研究了下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上架各大商店的版本号,大概是这样的

WebApi Swagger 接口多版本控制 适用于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

WebApi Swagger 接口多版本控制 适用于APP接口管理

Asp.net WebAPI 安装的是 Swashbuckle.Core,只要安装一个即可,swagger页面、js、css等文件都打包在这个dll里面。结合前篇文章已经实现的服务端接口多版本控制,现在项目结构如下

WebApi Swagger 接口多版本控制 适用于APP接口管理

看下几个控制器的代码

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");
                });
        }
    }
}

 直接运行起来看看效果

WebApi Swagger 接口多版本控制 适用于APP接口管理

真的不错,安装了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 方法是通过命名空间来区分版本的,运行看下效果

WebApi Swagger 接口多版本控制 适用于APP接口管理

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;
 });

看下效果

WebApi Swagger 接口多版本控制 适用于APP接口管理

标注在控制器上的名称已经读取出来了,再把接口后面的版本号干掉

/// <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 方法,可以自定义接口名称,想怎么显示就怎么显示

WebApi Swagger 接口多版本控制 适用于APP接口管理

接口名称已经修正了,但是有个遗憾,因为 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('?')));
        }
    });
});

看看简洁的接口名称

WebApi Swagger 接口多版本控制 适用于APP接口管理

接口已经完美了,同时注入的 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

WebApi Swagger 接口多版本控制 适用于APP接口管理

终极效果如下

WebApi Swagger 接口多版本控制 适用于APP接口管理

 调下 1.0.1 版本的 Get 接口

WebApi Swagger 接口多版本控制 适用于APP接口管理

测试一个不存在的Version

WebApi Swagger 接口多版本控制 适用于APP接口管理

前端即便传来了一个服务端没有的Version 1.0.5,也能自动向前找最近一个版本1.0.1的接口 

至此,大功告成,最后看看对比图

WebApi Swagger 接口多版本控制 适用于APP接口管理

五、结语

参考文章

源码

点我下载

上一篇:webapi使用System.Web.Http.Cors配置跨域访问的两种方式


下一篇:ASP.NET Core WebApi版本控制