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