API 版本控制:使用 ABP vNext 实现版本化 API 系统

🚀API 版本控制:使用 ABP vNext 实现版本化 API 系统



一、背景切入 🧭

在需求快速迭代的时代,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,最后按 Header
                options.ApiVersionReader = ApiVersionReader.Combine(
                    new UrlSegmentApiVersionReader(),           // /api/v1.0/...
                    new QueryStringApiVersionReader("v"),       // /api/... ?v=1.0
                    new 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 过滤 API
                options.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")匹配的 API
                    return versions.Contains(docName);
                });

                // 移除版本参数(避免在 Swagger UI 中显示 {version} 占位符)
                options.OperationFilter<RemoveVersionFromParameter>();

                // 将路径中的 {version:apiVersion} 占位符替换为具体版本号
                options.DocumentFilter<ReplaceVersionWithExactValueInPath>();
            });
        }
    }
}

说明

  1. AbpAspNetCoreMvcVersioningModule 模块会自动引入 Microsoft.AspNetCore.Mvc.Versioning,无需额外手动添加。
  2. 若只想使用单一的版本读取方式(如仅用 QueryString),可在 ApiVersionReader.Combine(...) 中删除多余的 Reader。
  3. RemoveVersionFromParameterReplaceVersionWithExactValueInPath 的实现可参考下文示例或 官方文档

2.1.1 版本解析流程图 🗺️
有版本号
无版本号
有参数 ?v=
无参数
有 Header
无 Header
客户端请求
检查 URL Segment?
使用 URL Segment 版本
检查 QueryString?
使用 QueryString 版本
检查 Header?
使用 Header 版本
使用默认版本 (v1.0)
进入对应版本逻辑

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 中编写 DocInclusionPredicateOperationFilterDocumentFilter 等逻辑,详见 2.1。

2.3.3 Swagger 文档生成流程图 📊
不是
扫描 Controller 特性
是否 ApiVersionAttribute?
收集版本列表
是否 ApiVersionNeutral?
包含在所有文档
忽略该 API
根据版本分组生成 SwaggerDoc
应用 RemoveVersionFromParameter
应用 ReplaceVersionWithExactValueInPath
输出多版本 Swagger JSON

三、实战演示 🎬

下面分别示范 URL SegmentQueryStringHeader 三种模式下的版本化实现方式,并给出调用示例与注意事项。💡


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/products
  Header: x-api-version: 1.0   → 返回 "Product from v1.0" 📬

  GET /api/products
  Header: 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 参数可丢失、不美观
HeaderHeader: x-api-version: {version}版本号不暴露在 URL,更灵活;与 URL 解耦客户端需额外设置 Header,调试不直观

参考链接

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kookoos

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值