🚀API 版本控制:使用 ABP vNext 實現版本化 API 系統
📚 目錄
- 🚀API 版本控制:使用 ABP vNext 實現版本化 API 系統
- 一、背景切入 🧭
- 二、核心配置規則 📋
- 2.1 前置準備:NuGet 包與 `using` 📦
- 2.2 啟用版本化服務 🔧
- 2.1.1 版本解析流程圖 🗺?
- 2.1.2 `RemoveVersionFromParameter` 實現 🛠?
- 2.1.3 `ReplaceVersionWithExactValueInPath` 實現 🔄
- 2.2 聲明支持的版本 📝
- 2.3 客戶端代理與 Swagger 分組生成 ??
- 2.3.1 手動執行命令
- 2.3.2 自動生成配置
- 2.3.3 Swagger 文檔生成流程圖 📊
- 三、實戰演示 🎬
- 3.1 URL Segment 模式(示例一:同類分支)🔀
- 3.2 URL Segment 模式(示例二:拆分控制器)??
- 3.3 QueryString 模式 🔍
- 3.4 Header 模式 📬
- 3.5 三種模式對比表 📋
一、背景切入 🧭
在需求快速迭代的時代,API 不再是一次性設計后一勞永逸的產物。為了保證舊版本客戶端繼續運行,同時平滑引入新功能,API 版本化(API Versioning) 就成為了一項必不可少的技術手段。🔧
ABP vNext 依托 ASP.NET Core 的版本化機制,提供了高強度可配置、完善體系化的版本控制解決方案。其底層實質是對 Microsoft.AspNetCore.Mvc.Versioning
的一層封裝,在 ABP 框架下可以一行代碼啟用版本化,同時兼容 ABP 模塊化、依賴注入等特性。本文將結合實際場景,詳細講解如何配置 ABP vNext 的 API 版本化支持,并通過實戰代碼展示各種版本讀取方式與 Swagger 分組生成。?
二、核心配置規則 📋
2.1 前置準備:NuGet 包與 using
📦
在開始配置之前,請先確保項目已經添加了以下 NuGet 包(示例版本號可根據實際情況調整)——直接在 .csproj
文件中加入下面的 <PackageReference>
,或通過 dotnet add package
命令安裝:
<PackageReference Include="Volo.Abp.AspNetCore.Mvc.Versioning" Version="4.6.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
在代碼文件頂部,需要引用以下命名空間,確保示例能夠正常編譯:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Versioning;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Volo.Abp;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.Versioning;
using Volo.Abp.Modularity;
using System.Linq;
2.2 啟用版本化服務 🔧
在 YourProject.HttpApi.Host
項目的 YourProjectHttpApiHostModule
中,覆蓋 ConfigureServices
方法,調用 AddAbpApiVersioning
配置版本化選項。同時配置 Swagger 分組以生成多版本文檔。示例如下:
namespace YourProject.HttpApi.Host
{[DependsOn(typeof(AbpAspNetCoreMvcModule),typeof(AbpAspNetCoreMvcVersioningModule) // 自動引入 Microsoft.AspNetCore.Mvc.Versioning)]public class YourProjectHttpApiHostModule : AbpModule{public override void ConfigureServices(ServiceConfigurationContext context){// 注冊 API 版本化服務context.Services.AddAbpApiVersioning(options =>{// 默認使用 1.0 版本options.DefaultApiVersion = new ApiVersion(1, 0);// 如果未指定版本,采用默認版本options.AssumeDefaultVersionWhenUnspecified = true;// 返回當前支持的版本信息到響應頭 (api-supported-versions)options.ReportApiVersions = true;// 支持 URL Segment、QueryString、Header 三種方式讀取版本// 順序決定優先級:先按 URL Segment,再按 QueryString,最后按 Headeroptions.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(), // /api/v1.0/...new QueryStringApiVersionReader("v"), // /api/... ?v=1.0new HeaderApiVersionReader("x-api-version") // Header: x-api-version: 1.0);});// 注冊 Swagger 分組支持context.Services.AddSwaggerGen(options =>{// 為每個版本定義一個 Swagger 文檔options.SwaggerDoc("v1.0", new OpenApiInfo{Title = "Your API V1.0",Version = "v1.0"});options.SwaggerDoc("v2.0", new OpenApiInfo{Title = "Your API V2.0",Version = "v2.0"});// 按 GroupName 過濾 APIoptions.DocInclusionPredicate((docName, apiDesc) =>{// 如果沒有 ApiVersionAttribute,也不是中立版本,則不包含var hasVersionAttribute = apiDesc.CustomAttributes().OfType<ApiVersionAttribute>().Any();var isNeutral = apiDesc.CustomAttributes().OfType<ApiVersionNeutralAttribute>().Any();if (isNeutral){// 將中立版本也展示在所有文檔中return true;}if (!hasVersionAttribute){return false;}// 取得所有標注的版本號,例如 "1.0", "2.0"var versions = apiDesc.CustomAttributes().OfType<ApiVersionAttribute>().SelectMany(attr => attr.Versions).Select(v => $"v{v.ToString()}");// 只包含與當前 docName(如 "v1.0")匹配的 APIreturn versions.Contains(docName);});// 移除版本參數(避免在 Swagger UI 中顯示 {version} 占位符)options.OperationFilter<RemoveVersionFromParameter>();// 將路徑中的 {version:apiVersion} 占位符替換為具體版本號options.DocumentFilter<ReplaceVersionWithExactValueInPath>();});}}
}
說明:
AbpAspNetCoreMvcVersioningModule
模塊會自動引入Microsoft.AspNetCore.Mvc.Versioning
,無需額外手動添加。- 若只想使用單一的版本讀取方式(如僅用 QueryString),可在
ApiVersionReader.Combine(...)
中刪除多余的 Reader。RemoveVersionFromParameter
與ReplaceVersionWithExactValueInPath
的實現可參考下文示例或 官方文檔。
2.1.1 版本解析流程圖 🗺?
2.1.2 RemoveVersionFromParameter
實現 🛠?
// 移除 Swagger 中的 version 參數
public class RemoveVersionFromParameter : IOperationFilter
{public void Apply(OpenApiOperation operation, OperationFilterContext context){if (operation.Parameters == null){return;}var versionParameter = operation.Parameters.FirstOrDefault(p => p.Name.Equals("version", StringComparison.InvariantCultureIgnoreCase));if (versionParameter != null){operation.Parameters.Remove(versionParameter);}}
}
2.1.3 ReplaceVersionWithExactValueInPath
實現 🔄
// 將路徑中的 {version:apiVersion} 占位符替換為具體版本號
public class ReplaceVersionWithExactValueInPath : IDocumentFilter
{public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context){var paths = new OpenApiPaths();foreach (var (key, value) in swaggerDoc.Paths){// 將路徑中的占位符 {version} 替換為 swaggerDoc.Info.Version(例如 "v1.0")var updatedKey = key.Replace("{version}", swaggerDoc.Info.Version);paths.Add(updatedKey, value);}swaggerDoc.Paths = paths;}
}
2.2 聲明支持的版本 📝
當某個 Controller 需要同時響應多個版本時,可在類上使用 [ApiVersion]
標注,并在方法上使用 MapToApiVersion
指定具體版本。示例如下:
using Microsoft.AspNetCore.Mvc;namespace YourProject.HttpApi.Controllers
{// 同時支持 v1.0 和 v2.0[ApiVersion("1.0")][ApiVersion("2.0")][Route("api/v{version:apiVersion}/products")]public class ProductController : ControllerBase{// 僅在 v1.0 時暴露該方法[HttpGet, MapToApiVersion("1.0")]public IActionResult GetV1(){return Ok("Product from v1.0");}// 僅在 v2.0 時暴露該方法[HttpGet, MapToApiVersion("2.0")]public IActionResult GetV2(){return Ok("Product from v2.0");}}
}
- 說明:
- 路由路徑為
/api/v{version}/products
,版本號通過 URL Segment 方式傳遞(如/api/v1.0/products
或/api/v2.0/products
)。 - 如果在同一個 Controller 內邏輯分支較多,可使用
MapToApiVersion
在同一個類中實現多版本映射,避免類數量過多。 - 若想做“版本中立”(即對所有版本通用),在類上使用
[ApiVersionNeutral]
即可:
- 路由路徑為
[ApiVersionNeutral][Route("api/health")]public class HealthController : ControllerBase{[HttpGet]public IActionResult Get() => Ok("Health OK");}
2.3 客戶端代理與 Swagger 分組生成 ??
當啟用版本化后,ABP 會自動在 Swagger UI 上按版本分組暴露文檔。開發者可以通過以下幾種方式生成客戶端代理:
2.3.1 手動執行命令
abp suite generate-proxy
該命令會根據當前已發布的 Swagger 文檔(包括多個版本)在 *.HttpApi.Client
項目中生成對應的 TypeScript/C# 代理文件,并自動分文件夾存放。📁
2.3.2 自動生成配置
如果希望每次項目啟動時自動生成,可在 YourProjectHttpApiHostModule
中添加以下配置,并確保已啟用 Swagger 生成配置(見 2.1 中的 AddSwaggerGen
):
using Volo.Abp.AspNetCore.Mvc.ApiExplorer;public override void ConfigureServices(ServiceConfigurationContext context){// ... 上述版本化和 Swagger 配置 ...Configure<AbpApiDescriptionModelOptions>(options =>{options.IsControllerModelEnabled = true;});}
注意:
- 僅在你想要每次運行項目時自動生成 TypeScript/C# 代理時配置
IsControllerModelEnabled = true
。若只想手動觸發生成,可忽略此配置。- 為了讓 Swagger UI 正常顯示分組,需要在
AddSwaggerGen
中編寫DocInclusionPredicate
、OperationFilter
、DocumentFilter
等邏輯,詳見 2.1。
2.3.3 Swagger 文檔生成流程圖 📊
三、實戰演示 🎬
下面分別示范 URL Segment、QueryString、Header 三種模式下的版本化實現方式,并給出調用示例與注意事項。💡
3.1 URL Segment 模式(示例一:同類分支)🔀
using Microsoft.AspNetCore.Mvc;namespace YourProject.HttpApi.Controllers
{// 同時支持 v1.0 和 v2.0[ApiVersion("1.0")][ApiVersion("2.0")][Route("api/v{version:apiVersion}/products")]public class ProductController : ControllerBase{// 僅在 v1.0 時暴露該方法[HttpGet, MapToApiVersion("1.0")]public IActionResult GetV1(){return Ok("Product from v1.0");}// 僅在 v2.0 時暴露該方法[HttpGet, MapToApiVersion("2.0")]public IActionResult GetV2(){return Ok("Product from v2.0");}}
}
- 啟動項目后測試:
GET /api/v1.0/products → 返回 "Product from v1.0" 🥇GET /api/v2.0/products → 返回 "Product from v2.0" 🥈
注意:如果同一個 Controller 內既使用 URL Segment 又使用 QueryString,可在請求中同時帶兩種版本號,例如
/api/v1.0/products?v=2.0
,框架會優先按照UrlSegmentApiVersionReader
(v1.0)解析;若想優先使用 QueryString,需將QueryStringApiVersionReader("v")
放在Combine
方法第一個參數位置。
3.2 URL Segment 模式(示例二:拆分控制器)??
當兩個版本邏輯差異較大,或者想將不同版本拆分到獨立類時,可編寫如下示例:
using Microsoft.AspNetCore.Mvc;namespace YourProject.HttpApi.Controllers
{// v1.0 Controller[ApiVersion("1.0")][Route("api/v{version:apiVersion}/products")]public class ProductV1Controller : ControllerBase{[HttpGet]public IActionResult Get() => Ok("Product from v1.0");}// v2.0 Controller[ApiVersion("2.0")][Route("api/v{version:apiVersion}/products")]public class ProductV2Controller : ControllerBase{[HttpGet]public IActionResult Get() => Ok("Product from v2.0");}
}
- 測試示例:
GET /api/v1.0/products → 返回 "Product from v1.0" 🥇GET /api/v2.0/products → 返回 "Product from v2.0" 🥈
對比說明:
- 同類分支(見 3.1):同一個 Controller 內通過
MapToApiVersion
在方法層面區分版本,代碼復用率高,但類文件大小可能增加。- 拆分控制器:將各版本邏輯完全隔離到不同類,類名更能明確版本含義,便于后續維護;但若大部分邏輯相同,會導致重復代碼。🔍
3.3 QueryString 模式 🔍
如果想將版本號放在 QueryString 中,而不在路由路徑里,則需在 2.1
中對 ApiVersionReader.Combine(...)
只保留 QueryStringApiVersionReader("v")
或者把它放到第一個位置。下面示例演示只使用 QueryString:
using Microsoft.AspNetCore.Mvc;namespace YourProject.HttpApi.Controllers
{[ApiVersion("1.0")][ApiVersion("2.0")][Route("api/products")]public class ProductQueryController : ControllerBase{// 默認 v1.0[HttpGet, MapToApiVersion("1.0")]public IActionResult GetV1() => Ok("Product from v1.0");// v2.0[HttpGet, MapToApiVersion("2.0")]public IActionResult GetV2() => Ok("Product from v2.0");}
}
- 請求示例:
GET /api/products?v=1.0 → 返回 "Product from v1.0" 🎯GET /api/products?v=2.0 → 返回 "Product from v2.0" 🎯
- 如果客戶端既不帶
v
參數(因配置了AssumeDefaultVersionWhenUnspecified = true
),也不放在 URL 中,則會默認調用 v1.0。
注意:確保在
AddAbpApiVersioning
中的ApiVersionReader.Combine(...)
順序中,將new QueryStringApiVersionReader("v")
放在首位,否則若同時存在 URL、QueryString,會優先按照 URL Segment 解析。??
3.4 Header 模式 📬
Header 模式適用于不想在 URL 中顯式暴露版本號的場景。示例如下:
using Microsoft.AspNetCore.Mvc;namespace YourProject.HttpApi.Controllers
{[ApiVersion("1.0")][ApiVersion("2.0")][Route("api/products")]public class ProductHeaderController : ControllerBase{[HttpGet, MapToApiVersion("1.0")]public IActionResult GetV1() => Ok("Product from v1.0");[HttpGet, MapToApiVersion("2.0")]public IActionResult GetV2() => Ok("Product from v2.0");}
}
- 請求示例:
GET /api/productsHeader: x-api-version: 1.0 → 返回 "Product from v1.0" 📬GET /api/productsHeader: x-api-version: 2.0 → 返回 "Product from v2.0" 📬
- 如果既不在 URL,也不在 Header 中指定版本,則會使用默認版本(1.0)。
3.5 三種模式對比表 📋
版本傳遞方式 | 傳遞形式 | 優點 | 缺點 |
---|---|---|---|
URL Segment | /api/v{version}/resource | 路由清晰、便于緩存、SEO 友好 | 路徑變化需兼容舊客戶端 |
QueryString | /api/resource?v={version} | 簡潔易用、易于測試 | URL 參數可丟失、不美觀 |
Header | Header: x-api-version: {version} | 版本號不暴露在 URL,更靈活;與 URL 解耦 | 客戶端需額外設置 Header,調試不直觀 |
參考鏈接:
- ABP vNext 官方文檔
- Swagger 分組示例代碼