🚀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 分组示例代码