PAYATHON 2026

OpenAPI:如何为同一实现的不同 API 版本设置不同的 operation IDs?

技术老齐

可以为同一实现生成带版本号的 operationId。如果同一个端点出现在多个版本的文档中,建议保留固定的端点名称,在生成 OpenAPI 文档时追加版本标识。WithName 不会像路由模板那样替换 {apiVersion}。

为什么不能直接在 WithName 中使用版本占位符?

下面的 {apiVersion:apiVersion} 是路由模板的一部分:

app.MapGroup("api/v{apiVersion:apiVersion}")
    .WithApiVersionSet(apiVersionSet);

WithName 接收端点名称字符串,不解析路由参数。例如:

.WithName("GetItems_{apiVersion}")

这会将端点名称设为字面量 GetItems_{apiVersion}。

端点名称在注册时确定。同一个端点出现在多个版本的 OpenAPI 文档中时,使用的仍是同一份名称元数据。如果 OpenAPI 生成器直接用这个名称作为 operationId,各版本文档中的值就会相同。

使用 Swashbuckle 时,在生成 operation ID 时追加版本

项目使用 AddSwaggerGen 时,可以通过 CustomOperationIds 定义命名规则。

以下示例要求:

  • API Versioning 的 API Explorer 已按版本生成 API 描述,ApiDescription.GroupName 为 v1、v2 等版本分组名。
  • 参与文档生成的端点都已通过 WithName 指定名称。

将下面的配置合并到现有的 AddSwaggerGen 配置中,并保留原有的文档注册和筛选逻辑:

using System;
using System.Linq;
using Microsoft.AspNetCore.Routing;

builder.Services.AddSwaggerGen(options =>
{
    options.CustomOperationIds(apiDescription =>
    {
        var endpointName = apiDescription.ActionDescriptor.EndpointMetadata
            .OfType<IEndpointNameMetadata>()
            .LastOrDefault()
            ?.EndpointName;

        var versionGroup = apiDescription.GroupName;

        if (string.IsNullOrWhiteSpace(endpointName))
        {
            throw new InvalidOperationException(
                "An endpoint name is required to generate the operation ID.");
        }

        if (string.IsNullOrWhiteSpace(versionGroup))
        {
            throw new InvalidOperationException(
                "A version group is required to generate the operation ID.");
        }

        return $"{endpointName}_{versionGroup}";
    });
});

共享端点仍使用固定名称:

versionedGroup.MapGet("/items", () => Results.Ok(new[] { "item" }))
    .WithName("GetItems");

当这个端点的 API 描述分别属于 v1 和 v2 分组时,会生成:

GetItems_v1
GetItems_v2

这套规则依赖 API Explorer 的分组信息,因此要先确认项目中的 GroupName 代表版本。如果它为空、使用的是业务分组,或者同一份 API 描述被手动加入多个文档,就需要调整规则。

使用 ASP.NET Core 内置 OpenAPI 时,通过 operation transformer 追加版本

如果项目使用的 Microsoft.AspNetCore.OpenApi 支持 AddOperationTransformer,可以用当前文档名称作为后缀。

在已有的各版本 AddOpenApi 注册中加入 transformer。假设文档名为 v1、v2:

using System;
using System.Threading.Tasks;

foreach (var documentName in new[] { "v1", "v2" })
{
    builder.Services.AddOpenApi(documentName, options =>
    {
        // 在这里保留项目已有的版本文档筛选配置。

        options.AddOperationTransformer(
            (operation, context, cancellationToken) =>
            {
                if (string.IsNullOrWhiteSpace(operation.OperationId))
                {
                    throw new InvalidOperationException(
                        "An operation ID is required before adding the version suffix.");
                }

                operation.OperationId =
                    $"{operation.OperationId}_{context.DocumentName}";

                return Task.CompletedTask;
            });
    });
}

这个示例假设 WithName 已为操作提供基础 operationId。Transformer 只处理命名,各版本文档包含哪些端点,仍由现有的 API Versioning 和文档筛选配置决定。

CustomOperationIds 和 AddOperationTransformer 分属不同的生成器,按项目实际使用的组件选择即可。

哪些情况下可以在 WithName 中直接拼接版本?

如果应用为每个版本分别注册端点,且注册代码中已有明确的版本变量,就可以使用普通的 C# 字符串插值:

.WithName($"GetItems_v{version}")

每次注册的端点名称必须不同,端点与 API 版本的映射也必须正确。多个端点可以复用同一个处理函数,无须复制业务实现。

只注册一次、同时支持多个版本的共享端点,单次 WithName 调用只能设置一个名称,更适合在文档生成阶段追加版本标识。

注意唯一性的范围

OpenAPI 要求 operationId 在同一份文档内唯一。两份独立的版本文档都包含 GetItems,并不违反这个要求。如果下游工具会合并文档,或将多个版本的客户端生成到同一个命名空间,才可能需要跨文档唯一的命名规则。

ASP.NET Core 则要求端点名称在应用内唯一。修改 OpenAPI 文档中的 operationId 不会修改运行时端点名称,也不会改变依赖端点名称的链接生成行为。

追加版本后,需要检查最终文档是否有重复值,以及客户端生成器是否会将不同标识规范化为同一个方法名。对于已经发布 SDK 的项目,还要留意修改 operationId 可能导致生成的客户端方法名发生变化。

备注:内容仅供参考。