我們?cè)陂_(kāi)發(fā) webapi 項(xiàng)目時(shí)如果遇到 api 接口需要同時(shí)支持多個(gè)版本的時(shí)候,比如接口修改了入?yún)⒅蟮怯窒MС掷习姹镜那岸耍ㄟ@里的前端可能是網(wǎng)頁(yè),可能是App,小程序 等等)進(jìn)行調(diào)用,這種情況常見(jiàn)于 app,畢竟網(wǎng)頁(yè)前端我們可以主動(dòng)控制發(fā)布,只要統(tǒng)一發(fā)布后所有人的瀏覽器下一次訪問(wèn)網(wǎng)頁(yè)時(shí)都會(huì)重新加載到最新版的代碼,但是像 app 則無(wú)法保證用戶一定會(huì)第一時(shí)間升級(jí)更新最新版的app,所以往往需要 api接口能夠同時(shí)保持多個(gè)版本的邏輯,同支持新老版本的調(diào)用端app進(jìn)行調(diào)用。
針對(duì)上面的描述舉一個(gè)例子:
比如一個(gè)創(chuàng)建用戶的接口,api/user/createuser
如果我們這個(gè)時(shí)候?qū)υ摻涌诘娜雲(yún)⒑头祷貐?shù)修改之后,但是又希望原本的 api/user/createuser 接口邏輯也可以正常運(yùn)行,常見(jiàn)的做法有以下幾種:
- 修改接口名稱,將新的創(chuàng)建用戶接口地址定義為 api/user/newcreateuser
- url傳入版本標(biāo)記,將新的創(chuàng)建用戶接口地址定義為 api/user/createuser?api-version=2
- header傳入版本標(biāo)記,通過(guò)校驗(yàn) header 中的 api-version 字段的值,用來(lái)區(qū)分調(diào)用不同版本的api
第一種方式的缺陷很明顯,當(dāng)接口版本多了之后接口的地址會(huì)定義很亂,本文主要講解后面兩種方法,如何在 asp.NET webapi 項(xiàng)目中優(yōu)雅的使用 header 或者 query 傳入 版本標(biāo)記,用來(lái)支持api的多個(gè)版本邏輯共存,并且擴(kuò)展 Swagger 來(lái)實(shí)現(xiàn) SwaggerUI 對(duì)于 api-version 的支持。
截至本文撰寫(xiě)時(shí)間,最新的 .net 版本為 .net6 ,本文中的所有示例也是基于 .net 6 來(lái)構(gòu)建的。
首先創(chuàng)建一個(gè) asp.net webapi 項(xiàng)目,本文使用 vs2022 直接創(chuàng)建 asp.net webapi 項(xiàng)目
項(xiàng)目創(chuàng)建好之后安裝如下幾個(gè)nuget包:
Swashbuckle.AspNetCore
Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
注冊(cè) api 版本控制服務(wù)
#region 注冊(cè) api 版本控制
builder.Services.AddApiVersioning(options =>
{
//通過(guò)Header向客戶端通報(bào)支持的版本
options.ReportApiVersions = true;
//允許不加版本標(biāo)記直接調(diào)用接口
options.AssumeDefaultVersionWhenUnspecified = true;
//接口默認(rèn)版本
//options.DefaultApiVersion = new ApiVersion(1, 0);
//如果未加版本標(biāo)記默認(rèn)以當(dāng)前最高版本進(jìn)行處理
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 版本標(biāo)記的傳入方式是從 url query 傳遞還是從 http header 傳遞。
移除項(xiàng)目默認(rèn)的 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 注冊(cè) 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
其中用到了兩個(gè)自定義的類(lèi) SwaggerConfigureOptions 和 SwaggerOperationFilter ,
SwaggerConfigureOptions 是一個(gè)自定義的 Swagger 配置方法,主要用于根據(jù) api 控制器上的描述用來(lái)循環(huán)添加不同版本的 SwaggerDoc;
SwaggerOperationFilter 是一個(gè)自定義過(guò)濾器主要實(shí)現(xiàn)SwaggerUI 的版本參數(shù) api-version 必填驗(yàn)證和標(biāo)記過(guò)期的 api 的功能,具體內(nèi)容如下
SwaggerConfigureOptions .cs
/// <summary>
/// 配置swagger生成選項(xiàng)。
/// </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 + " 版本已棄用,請(qǐng)盡快升級(jí)新版";
}
return info;
}
}
SwaggerOperationFilter.cs
/// <summary>
/// swagger 集成多版本api自定義設(shè)置
/// </summary>
public class SwaggerOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var apiDescription = context.ApiDescription;
//判斷接口遺棄狀態(tài),對(duì)接口進(jìn)行標(biāo)記調(diào)整
operation.Deprecated |= apiDescription.IsDeprecated();
if (operation.Parameters == null)
{
return;
}
//為 api-version 參數(shù)添加必填驗(yàn)證
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;
}
}
}
這些都配置完成之后,開(kāi)始對(duì) 控制模塊進(jìn)行調(diào)整
為了方便代碼的版本區(qū)分,所以我這里在 Controllers 下按照版本建立的獨(dú)立的文件夾 v1 和 v2
然后在 v1 和 v2 的文件夾下防止了對(duì)于的 Controllers,如下圖的結(jié)構(gòu)
然后只要在對(duì)應(yīng)文件夾下的控制器頭部加入版本標(biāo)記
[ApiVersion("1")] [ApiVersion("2")] [ApiVersion("......")]
如下圖的兩個(gè)控制器
這樣就配置好了兩個(gè)版本的 UserController 具體控制器內(nèi)部的代碼可以不同,然后運(yùn)行 項(xiàng)目觀察 Swagger UI 就會(huì)發(fā)現(xiàn)如下圖:
? 可以通過(guò) SwaggerUI 右上角去切換各個(gè)版本的 SwaggerDoc
?點(diǎn)擊單個(gè)接口的 Try it out 時(shí)接口這邊也同樣會(huì)出現(xiàn)一個(gè) api-version 的字段,因?yàn)槲覀冞@邊是配置的從 Header 傳入該參數(shù)所以從界面中可以看出該字段是從 Header 傳遞的,如果想要從 url 傳遞,主要調(diào)整上面 注冊(cè) api 版本控制服務(wù) 那邊的設(shè)置為從 Query 傳入即可。
至此基礎(chǔ)的 api 版本控制邏輯就算完成了。
下面衍生講解一下如果 項(xiàng)目中有部分 api 控制器并不需要版本控制,是全局通用的如何處理,有時(shí)候我們一個(gè)項(xiàng)目中總會(huì)存在一些基礎(chǔ)的 api 是基本不會(huì)變的,如果每次 api 版本升級(jí)都把所有的 控制器都全部升級(jí)顯然太過(guò)繁瑣了,所以我們可以把一些全局通用的控制器單獨(dú)標(biāo)記出來(lái)。
只要在這些控制器頭部添加 [ApiVersionNeutral] 標(biāo)記即可,添加了 [ApiVersionNeutral] 標(biāo)記的控制器則表明該控制器退出了版本控制邏輯,無(wú)論 app 前端傳入的版本號(hào)的是多少,都可以正常進(jìn)入該控制的邏輯。如下
[ApiVersionNeutral]
[ApiController]
[Route("api/[controller]")]
public class FileController : ControllerBase
{
}
還有一種就是當(dāng)我們的 api 版本升級(jí)之后,我們希望標(biāo)記某個(gè) api 已經(jīng)是棄用的,則可以使用 Deprecated 來(lái)表示該版本的 api 已經(jīng)淘汰。
[ApiVersion("1", Deprecated = true)]
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
[HttpPost("CreateUser")]
public void CreateUser(DtoCreateUser createUser)
{
//內(nèi)部注冊(cè)邏輯此處省略
}
}
添加淘汰標(biāo)記之后運(yùn)行 SwaggerUI 就會(huì)出現(xiàn)下圖的樣式
? 通過(guò) SwaggerDoc 就可以很明確的看出 v1 版本的 api 已經(jīng)被淘汰了。