我们在开发 webapi 项目时如果遇到 api 接口需要同时支持多个版本的时候,比如接口修改了入参之后但是又希望支持老版本的前端(这里的前端可能是网页,可能是App,小程序 等等)进行调用,这种情况常见于 app,毕竟网页前端我们可以主动控制发布,只要统一发布后所有人的浏览器下一次访问网页时都会重新加载到最新版的代码,但是像 app 则无法保证用户一定会第一时间升级更新最新版的app,所以往往需要 api接口能够同时保持多个版本的逻辑,同支持新老版本的调用端app进行调用。
针对上面的描述举一个例子:
比如一个创建用户的接口,api/user/createuser
如果我们这个时候对该接口的入参和返回参数修改之后,但是又希望原本的 api/user/createuser 接口逻辑也可以正常运行,常见的做法有以下几种:
第一种方式的缺陷很明显,当接口版本多了之后接口的地址会定义很乱,本文主要讲解后面两种方法,如何在 asp.NET webapi 项目中优雅的使用 header 或者 query 传入 版本标记,用来支持api的多个版本逻辑共存,并且扩展 Swagger 来实现 SwaggerUI 对于 api-version 的支持。
截至本文撰写时间,最新的 .net 版本为 .net6 ,本文中的所有示例也是基于 .net 6 来构建的。
首先创建一个 asp.net webapi 项目,本文使用 vs2022 直接创建 asp.net webapi 项目
项目创建好之后安装如下几个nuget包:
Swashbuckle.AspNetCore
Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
注册 api 版本控制服务
#region 注册 api 版本控制
builder.Services.AddApiVersioning(options =>
{
//通过Header向客户端通报支持的版本
options.ReportApiVersions = true;
//允许不加版本标记直接调用接口
options.AssumeDefaultVersionWhenUnspecified = true;
//接口默认版本
//options.DefaultApiVersion = new ApiVersion(1, 0);
//如果未加版本标记默认以当前最高版本进行处理
options.ApiVersionSelector = new CurrentImplementationApiVersionSelector(options);
//配置为从 Header 传入 api-version
options.ApiVersionReader = new HeaderApiVersionReader("api-version");
//配置为从 Query 传入 api-version
//options.ApiVersionReader = new QueryStringApiVersionReader("api-version");
});
builder.Services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
#endregion
这里我们可以选择 api-version 版本标记的传入方式是从 url query 传递还是从 http header 传递。
移除项目默认的 swagger 配置
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
采用如下 swagger 配置
#region 注册 Swagger
builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, SwaggerConfigureOptions>();
builder.Services.AddSwaggerGen(options =>
{
options.OperationFilter<SwaggerOperationFilter>();
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, $"{typeof(Program).Assembly.GetName().Name}.xml"), true);
});
#endregion
其中用到了两个自定义的类 SwaggerConfigureOptions 和 SwaggerOperationFilter ,
SwaggerConfigureOptions 是一个自定义的 Swagger 配置方法,主要用于根据 api 控制器上的描述用来循环添加不同版本的 SwaggerDoc;
SwaggerOperationFilter 是一个自定义过滤器主要实现SwaggerUI 的版本参数 api-version 必填验证和标记过期的 api 的功能,具体内容如下
SwaggerConfigureOptions .cs
/// <summary>
/// 配置swagger生成选项。
/// </summary>
public class SwaggerConfigureOptions : IConfigureOptions<SwaggerGenOptions>
{
readonly IApiVersionDescriptionProvider provider;
public SwaggerConfigureOptions(IApiVersionDescriptionProvider provider) => this.provider = provider;
public void Configure(SwaggerGenOptions options)
{
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
var modelPrefix = Assembly.GetEntryAssembly()?.GetName().Name + ".Models.";
var versionPrefix = description.GroupName + ".";
options.SchemaGeneratorOptions = new SchemaGeneratorOptions { SchemaIdSelector = type => (type.ToString()[(type.ToString().IndexOf("Models.") + 7)..]).Replace(modelPrefix, "").Replace(versionPrefix, "").Replace("`1", "").Replace("+", ".") };
}
}
static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
var info = new OpenApiInfo()
{
Title = Assembly.GetEntryAssembly()?.GetName().Name,
Version = "v" + description.ApiVersion.ToString(),
//Description = "",
//Contact = new OpenApiContact() { Name = "", Email = "" }
};
if (description.IsDeprecated)
{
info.Description += "此 Api " + info.Version + " 版本已弃用,请尽快升级新版";
}
return info;
}
}
SwaggerOperationFilter.cs
/// <summary>
/// swagger 集成多版本api自定义设置
/// </summary>
public class SwaggerOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var apiDescription = context.ApiDescription;
//判断接口遗弃状态,对接口进行标记调整
operation.Deprecated |= apiDescription.IsDeprecated();
if (operation.Parameters == null)
{
return;
}
//为 api-version 参数添加必填验证
foreach (var parameter in operation.Parameters)
{
var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
if (parameter.Description == null)
{
parameter.Description = description.ModelMetadata?.Description;
}
if (parameter.Schema.Default == null && description.DefaultValue != null)
{
parameter.Schema.Default = new OpenApiString(description.DefaultValue.ToString());
}
parameter.Required |= description.IsRequired;
}
}
}
这些都配置完成之后,开始对 控制模块进行调整
为了方便代码的版本区分,所以我这里在 Controllers 下按照版本建立的独立的文件夹 v1 和 v2
然后在 v1 和 v2 的文件夹下防止了对于的 Controllers,如下图的结构
然后只要在对应文件夹下的控制器头部加入版本标记
[ApiVersion("1")] [ApiVersion("2")] [ApiVersion("......")]
如下图的两个控制器
这样就配置好了两个版本的 UserController 具体控制器内部的代码可以不同,然后运行 项目观察 Swagger UI 就会发现如下图:
可以通过 SwaggerUI 右上角去切换各个版本的 SwaggerDoc
点击单个接口的 Try it out 时接口这边也同样会出现一个 api-version 的字段,因为我们这边是配置的从 Header 传入该参数所以从界面中可以看出该字段是从 Header 传递的,如果想要从 url 传递,主要调整上面 注册 api 版本控制服务 那边的设置为从 Query 传入即可。
至此基础的 api 版本控制逻辑就算完成了。
下面衍生讲解一下如果 项目中有部分 api 控制器并不需要版本控制,是全局通用的如何处理,有时候我们一个项目中总会存在一些基础的 api 是基本不会变的,如果每次 api 版本升级都把所有的 控制器都全部升级显然太过繁琐了,所以我们可以把一些全局通用的控制器单独标记出来。
只要在这些控制器头部添加 [ApiVersionNeutral] 标记即可,添加了 [ApiVersionNeutral] 标记的控制器则表明该控制器退出了版本控制逻辑,无论 app 前端传入的版本号的是多少,都可以正常进入该控制的逻辑。如下
[ApiVersionNeutral]
[ApiController]
[Route("api/[controller]")]
public class FileController : ControllerBase
{
}
还有一种就是当我们的 api 版本升级之后,我们希望标记某个 api 已经是弃用的,则可以使用 Deprecated 来表示该版本的 api 已经淘汰。
[ApiVersion("1", Deprecated = true)]
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
[HttpPost("CreateUser")]
public void CreateUser(DtoCreateUser createUser)
{
//内部注册逻辑此处省略
}
}
添加淘汰标记之后运行 SwaggerUI 就会出现下图的样式
通过 SwaggerDoc 就可以很明确的看出 v1 版本的 api 已经被淘汰了。