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上架各大商店的版本号,大概是这样的

如图,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的接口
至此,大功告成,最后看看对比图

五、结语
参考文章
源码
WebApi Swagger 接口多版本控制 适用于APP接口管理的更多相关文章
- PHP 开发 APP 接口 学习笔记与总结 - APP 接口实例 [3] 首页 APP 接口开发方案 ② 读取缓存方式
以静态缓存为例. 修改 file.php line:11 去掉 path 参数(方便),加上缓存时间参数: public function cacheData($k,$v = '',$cacheTim ...
- PHP 开发 APP 接口 学习笔记与总结 - APP 接口实例 [4] 首页 APP 接口开发方案 ③ 定时读取缓存方式
用于 linux 执行 crontab 命令生成缓存的文件 crop.php <?php //让crontab 定时执行的脚本程序 require_once 'db.php'; require_ ...
- PHP 开发 APP 接口 学习笔记与总结 - APP 接口实例 [2] 首页 APP 接口开发方案 ① 读取数据库方式
方案一:读取数据库方式 从数据库读取信息→封装→生成接口数据 应用场景: 数据时效性比较高的系统 方案二:读取缓存方式 从数据库获取信息(第一次设置缓存或缓存失效时)→封装(第一次设置缓存或缓存失效时 ...
- 关于APP接口设计(转)
最近一段时间一直在做APP接口,总结一下APP接口开发过程中的注意事项: 1.效率:接口访问速度 APP有别于WEB服务,对服务器端要求是比较严格的,在移动端有限的带宽条件下,要求接口响应速度要快,所 ...
- 关于APP接口设计
最近一段时间一直在做APP接口,总结一下APP接口开发过程中的注意事项: 1.效率:接口访问速度 APP有别于WEB服务,对服务器端要求是比较严格的,在移动端有限的带宽条件下,要求接口响应速度要快,所 ...
- app接口开发
最近一段时间一直在做APP接口,总结一下APP接口开发过程中的注意事项: 1.效率:接口访问速度 APP有别于WEB服务,对服务器端要求是比较严格的,在移动端有限的带宽条件下,要求接口响应速度要快,所 ...
- 《PHP开发APP接口》笔记
PHP开发APP接口 [TOC] 课程地址 imooc PHP开发APP接口 学习要点 APP接口简介 封装通信接口方法 核心技术 APP接口实例 服务器端 -> 数据库|缓存 -> 调用 ...
- PHP开发APP接口
第1章 APP接口简介 - 课程简介 (:) - APP接口介绍 (:) - 客户端APP通信 (:) 最近学习 - 客户端APP通信格式区别 (:) - APP接口做的哪些事儿 (:) 第2章 封装 ...
- PHP开发APP接口实现--基本篇
最近一段时间一直在做APP接口,总结一下APP接口开发以来的心得,与大家分享: 1. 客户端/服务器接口请求流程: 安卓/IOS客户端 –> PHP接口 –> 服务器端 –> ...
随机推荐
- 关于Objects类的getClass方法为什么可以得到子类的地址的思考
这一段时间,总是很纠结为什么Objects中的getClass方法可以返回包含子类地址信息的东西(我不确定返回值类型). 因为在Java中,我们定义的父类,我想破脑袋也想不出怎么可以得到子类的信息. ...
- 设计模式之委派模式(Delegate Pattern)深入浅出
学习目标:精简程序逻辑,提升代码的可读性. 内容定位:希望通过对委派模式的学习,让自己写出更加优雅的代码的人群. 委派模式定义: 委派模式(Delegate Pattern)的基本作用是负责任务的调度 ...
- 杭电OJ2007----平方和与立方和(易错题)
Problem Description 给定一段连续的整数,求出他们中所有偶数的平方和以及所有奇数的立方和. Input 输入数据包含多组测试实例,每组测试实例包含一行,由两个整数m和n组成. Out ...
- 死磕以太坊源码分析之state
死磕以太坊源码分析之state 配合以下代码进行阅读:https://github.com/blockchainGuide/ 希望读者在阅读过程中发现问题可以及时评论哦,大家一起进步. 源码目录 |- ...
- 【MySQL 高级】索引优化分析
MySQL高级 索引优化分析 SQL 的效率问题 出现性能下降,SQL 执行慢,执行时间长,等待时间长等情况,可能的原因有: 查询语句写的不好 索引失效 单值索引:在 user 表中给 name 属性 ...
- LeetCode700 二叉搜索树中搜索
给定二叉搜索树(BST)的根节点和一个值. 你需要在BST中找到节点值等于给定值的节点. 返回以该节点为根的子树. 如果节点不存在,则返回 NULL. 例如, 给定二叉搜索树: 4 / \ 2 7 / ...
- 开源:AspNetCore 应用程序热更新升级工具(全网第一份公开的解决方案)
1:下载.开源.使用教程 下载地址:Github 下载 .其它下载 开源地址:https://github.com/cyq1162/AspNetCoreUpdater 使用教程: 解压AspNetCo ...
- Docker安装MySQL,Redis,阿里云镜像加速
Docker安装 虚拟化容器技术.Docker基于镜像,可以秒级启动各种容器.每一种容器都是一个完整的环境,容器之间相互隔离. 如果之前安装的有其他版本,卸载旧的版本. $ sudo yum remo ...
- 【Oracle】regexp_substr()函数详解
环境:Oracle10.2.0.5 在SQL中尝试使用正则 可以试下regexp_substr()来进行分割 首先创建一个实验视图: SQL> create or replace view te ...
- 快速查询表中的NULL数据
正常情况下,NULL值是不会放入B-TREE索引的,因此根据IS NULL查询的时候走的通常是全表扫描,如果记录比较少还好,记录比较多,查询会非常耗时 可以通过创建一个索引来解决 CREATE IND ...