我們在開發 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 已經被淘汰了。
至此 關於asp.net core webapi 中 api 版本控制的基本操作就講解完了,有任何不明白的,可以在文章下面評論或者私信我,歡迎大家積極的討論交流,有興趣的朋友可以關注我目前在維護的一個 .net 基礎框架專案 https://github.com/berkerdong/NetEngine.git