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 可能导致生成的客户端方法名发生变化。
备注:内容仅供参考。
