关于 openapi
面向 .NET 10 开发者的 OpenAPI 实践指南,介绍使用框架内置的 Microsoft.AspNetCore.OpenApi 生成文档。内容涵盖端点元数据、类型化响应、转换器、多文档和构建时生成。
主要功能
- 01基于 TypedResults 生成响应架构
- 02端点元数据与转换器
- 03多个 OpenAPI 文档
- 04集成 XML 文档注释
- 05构建时生成规范文件
适用场景
- 01为新的 .NET API 编写文档
- 02自定义安全方案和架构
- 03生成供客户端代码工具使用的规范
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill openapi安装后在 Claude Code 中输入/openapi确认已加载。
详细介绍
OpenAPI
核心原则
- 内置方案,而非 Swashbuckle — .NET 10 将
Microsoft.AspNetCore.OpenApi作为官方且由框架维护的 OpenAPI 解决方案。Swashbuckle 已从 .NET 9 模板中移除,不再推荐使用。 - 由 TypedResults 驱动架构 —
TypedResults.Ok<T>()会自动生成正确的 OpenAPI 响应架构,而Results.Ok()不会。始终使用TypedResults。 - 使用转换器,而非变通方案 — 文档、操作和架构转换器可以干净地组合使用。使用它们来配置安全方案、全局响应和架构自定义。
- 为每个端点添加元数据 — 为每个端点使用
.WithName()、.WithSummary()和.WithTags()。这些元数据会直接写入 OpenAPI 规范,并供客户端生成器使用。
模式
基本设置
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi(); // Serves at /openapi/v1.json
}
端点元数据
group.MapPost("/", CreateOrder)
.WithName("CreateOrder")
.WithSummary("Create a new order")
.WithDescription("Creates a new order for the specified customer.")
.Produces<OrderResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.ProducesProblem(StatusCodes.Status500InternalServerError);
使用 TypedResults 时,响应元数据会自动推断:
static async Task<Results<Created<OrderResponse>, ValidationProblem>> CreateOrder(
CreateOrderRequest request, ISender sender, CancellationToken ct)
{
var result = await sender.Send(new CreateOrder.Command(request), ct);
return result.IsSuccess
? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
: TypedResults.ValidationProblem(result.Errors);
}
Bearer 令牌安全方案
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});
internal sealed class BearerSecuritySchemeTransformer(
IAuthenticationSchemeProvider authSchemeProvider) : IOpenApiDocumentTransformer
{
public async Task TransformAsync(OpenApiDocument document,
OpenApiDocumentTransformerContext context, CancellationToken ct)
{
var schemes = await authSchemeProvider.GetAllSchemesAsync();
if (!schemes.Any(s => s.Name == "Bearer"))
return;
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes = new Dictionary<string, IOpenApiSecurityScheme>
{
["Bearer"] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header
}
};
foreach (var operation in document.Paths.Values.SelectMany(p => p.Operations))
{
operation.Value.Security ??= [];
operation.Value.Security.Add(new OpenApiSecurityRequirement
{
[new OpenApiSecuritySchemeReference("Bearer", document)] = []
});
}
}
}
文档信息转换器
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, context, ct) =>
{
document.Info = new()
{
Title = "Checkout API",
Version = "v1",
Description = "API for processing orders and payments."
};
return Task.CompletedTask;
});
});
多个 OpenAPI 文档
builder.Services.AddOpenApi("v1");
builder.Services.AddOpenApi("internal", options =>
{
options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});
// Endpoints choose their document via WithGroupName
app.MapGet("/public", () => "Hello").WithGroupName("v1");
app.MapGet("/admin", () => "Secret").WithGroupName("internal");
未设置 .WithGroupName() 的端点会出现在所有文档中。
XML 文档注释(.NET 10)
在项目文件中启用此功能 — 源生成器会自动提取 <summary>、<param> 和 <response> 标签:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
/// <summary>Retrieves a project board by ID.</summary>
/// <param name="id">The project board ID.</param>
/// <response code="200">Returns the project board.</response>
/// <response code="404">Board not found.</response>
static async Task<Results<Ok<Board>, NotFound>> GetBoard(int id, AppDbContext db)
{
var board = await db.Boards.FindAsync(id);
return board is not null ? TypedResults.Ok(board) : TypedResults.NotFound();
}
编译器不会捕获 lambda 上的 XML 注释。请使用具名方法。
架构转换器
options.AddSchemaTransformer((schema, context, ct) =>
{
if (context.JsonTypeInfo.Type == typeof(decimal))
{
schema.Format = "decimal";
}
return Task.CompletedTask;
});
每个端点的操作转换器(.NET 10)
app.MapGet("/old", () => "deprecated")
.AddOpenApiOperationTransformer((operation, context, ct) =>
{
operation.Deprecated = true;
return Task.CompletedTask;
});
构建时生成文档
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="*" />
<PropertyGroup>
<OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory>
</PropertyGroup>
构建期间,规范文件会生成到输出目录中。
YAML 端点(.NET 10)
app.MapOpenApi("/openapi/{documentName}.yaml");
反模式
新项目不要使用 Swashbuckle
// BAD — removed from .NET 9+ templates, maintenance concerns
builder.Services.AddSwaggerGen();
app.UseSwagger();
app.UseSwaggerUI();
// GOOD — built-in OpenAPI
builder.Services.AddOpenApi();
app.MapOpenApi();
在 .NET 10 中不要使用 WithOpenApi()
// BAD — deprecated, produces ASPDEPR002 warning
app.MapGet("/", () => "hello").WithOpenApi(op => { op.Deprecated = true; return op; });
// GOOD — use per-endpoint operation transformer
app.MapGet("/", () => "hello")
.AddOpenApiOperationTransformer((op, ctx, ct) =>
{
op.Deprecated = true;
return Task.CompletedTask;
});
不要使用非类型化结果
// BAD — Results.Ok doesn't contribute to OpenAPI schema
static async Task<IResult> GetOrder(Guid id, AppDbContext db)
{
var order = await db.Orders.FindAsync(id);
return order is not null ? Results.Ok(order) : Results.NotFound();
}
// GOOD — TypedResults with union return type
static async Task<Results<Ok<Order>, NotFound>> GetOrder(Guid id, AppDbContext db)
{
var order = await db.Orders.FindAsync(id);
return order is not null ? TypedResults.Ok(order) : TypedResults.NotFound();
}
不要省略端点上的 WithName
// BAD — client generators produce poor method names without operationId
group.MapGet("/{id:guid}", GetOrder);
// GOOD — operationId feeds into generated client method names
group.MapGet("/{id:guid}", GetOrder).WithName("GetOrder");
在 .NET 10 中不要使用 OpenApiAny
// BAD — OpenApiAny types removed in Microsoft.OpenApi v2.x
schema.Example = new OpenApiString("2025-01-01");
// GOOD — use JsonNode from System.Text.Json.Nodes
schema.Example = JsonValue.Create("2025-01-01");
决策指南
| 场景 | 建议 |
|---|---|
| 新建 API 项目 | AddOpenApi() + MapOpenApi()(内置) |
| API 文档 UI | Scalar(MapScalarApiReference()) |
| 文档中的安全方案 | 使用 IOpenApiDocumentTransformer 的文档转换器 |
| 响应文档 | 使用带联合返回类型的 TypedResults |
| 集成 XML 文档 | <GenerateDocumentationFile>true</GenerateDocumentationFile> |
| 多个 API 版本 | 多次调用 AddOpenApi("v1") + WithGroupName() |
| 客户端代码生成 | Kiota(Microsoft 推荐)或 NSwag |
| 构建时生成规范 | Microsoft.Extensions.ApiDescription.Server 包 |
| OpenAPI 版本 | 3.1(.NET 10 中的默认版本);如果使用方需要,可强制使用 3.0 |
| 按端点自定义 | 在端点上使用 .AddOpenApiOperationTransformer() |
评论
支持 Markdown