openapi
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOpenAPI
OpenAPI
Core Principles
核心原则
- Built-in, not Swashbuckle — .NET 10 ships as the official, framework-maintained OpenAPI solution. Swashbuckle was removed from templates in .NET 9 and is no longer recommended.
Microsoft.AspNetCore.OpenApi - TypedResults drive the schema — automatically generates correct OpenAPI response schemas.
TypedResults.Ok<T>()does not. Always useResults.Ok().TypedResults - Transformers over workarounds — Document, operation, and schema transformers compose cleanly. Use them for security schemes, global responses, and schema customization.
- Metadata on every endpoint — Use ,
.WithName(),.WithSummary()on every endpoint. This metadata feeds directly into the OpenAPI spec and client generators..WithTags()
- 内置支持,无需 Swashbuckle — .NET 10 提供 作为官方、框架维护的 OpenAPI 解决方案。Swashbuckle 已在 .NET 9 的模板中移除,不再推荐使用。
Microsoft.AspNetCore.OpenApi - TypedResults 驱动 schema — 会自动生成正确的 OpenAPI 响应 schema。
TypedResults.Ok<T>()则不会。请始终使用Results.Ok()。TypedResults - 优先使用转换器而非临时方案 — 文档、操作和 schema 转换器可实现清晰的组合。使用它们配置安全方案、全局响应和自定义 schema。
- 为每个端点添加元数据 — 为每个端点使用 、
.WithName()、.WithSummary()。这些元数据会直接传入 OpenAPI 规范和客户端生成器。.WithTags()
Patterns
实践模式
Basic Setup
基础配置
csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi(); // Serves at /openapi/v1.json
}csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi(); // 服务地址为 /openapi/v1.json
}Endpoint Metadata
端点元数据
csharp
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);With , response metadata is inferred automatically:
TypedResultscsharp
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);
}csharp
group.MapPost("/", CreateOrder)
.WithName("CreateOrder")
.WithSummary("创建新订单")
.WithDescription("为指定客户创建新订单。")
.Produces<OrderResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.ProducesProblem(StatusCodes.Status500InternalServerError);使用 时,响应元数据会自动推断:
TypedResultscsharp
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 Token Security Scheme
Bearer Token 安全方案
csharp
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)] = []
});
}
}
}csharp
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)] = []
});
}
}
}Document Info Transformer
文档信息转换器
csharp
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;
});
});csharp
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, context, ct) =>
{
document.Info = new()
{
Title = "Checkout API",
Version = "v1",
Description = "用于处理订单和支付的 API。"
};
return Task.CompletedTask;
});
});Multiple OpenAPI Documents
多 OpenAPI 文档
csharp
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");Endpoints without appear in all documents.
.WithGroupName()csharp
builder.Services.AddOpenApi("v1");
builder.Services.AddOpenApi("internal", options =>
{
options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});
// 端点通过 WithGroupName 选择所属文档
app.MapGet("/public", () => "Hello").WithGroupName("v1");
app.MapGet("/admin", () => "Secret").WithGroupName("internal");未使用 的端点会出现在所有文档中。
.WithGroupName()XML Documentation Comments (.NET 10)
XML 文档注释(.NET 10)
Enable in the project file — the source generator extracts , , tags automatically:
<summary><param><response>xml
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>csharp
/// <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();
}XML comments on lambdas are not captured by the compiler. Use named methods.
在项目文件中启用——源代码生成器会自动提取 、、 标签:
<summary><param><response>xml
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>csharp
/// <summary>根据 ID 获取项目看板。</summary>
/// <param name="id">项目看板 ID。</param>
/// <response code="200">返回项目看板。</response>
/// <response code="404">未找到看板。</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 注释,请使用命名方法。
Schema Transformer
Schema 转换器
csharp
options.AddSchemaTransformer((schema, context, ct) =>
{
if (context.JsonTypeInfo.Type == typeof(decimal))
{
schema.Format = "decimal";
}
return Task.CompletedTask;
});csharp
options.AddSchemaTransformer((schema, context, ct) =>
{
if (context.JsonTypeInfo.Type == typeof(decimal))
{
schema.Format = "decimal";
}
return Task.CompletedTask;
});Per-Endpoint Operation Transformer (.NET 10)
单端点操作转换器(.NET 10)
csharp
app.MapGet("/old", () => "deprecated")
.AddOpenApiOperationTransformer((operation, context, ct) =>
{
operation.Deprecated = true;
return Task.CompletedTask;
});csharp
app.MapGet("/old", () => "deprecated")
.AddOpenApiOperationTransformer((operation, context, ct) =>
{
operation.Deprecated = true;
return Task.CompletedTask;
});Build-Time Document Generation
构建时文档生成
xml
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="*" />
<PropertyGroup>
<OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory>
</PropertyGroup>The spec file is generated in the output directory during build.
xml
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="*" />
<PropertyGroup>
<OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory>
</PropertyGroup>规范文件会在构建过程中生成到输出目录。
YAML Endpoint (.NET 10)
YAML 端点(.NET 10)
csharp
app.MapOpenApi("/openapi/{documentName}.yaml");csharp
app.MapOpenApi("/openapi/{documentName}.yaml");Anti-patterns
反模式
Don't Use Swashbuckle for New Projects
新项目不要使用 Swashbuckle
csharp
// BAD — removed from .NET 9+ templates, maintenance concerns
builder.Services.AddSwaggerGen();
app.UseSwagger();
app.UseSwaggerUI();
// GOOD — built-in OpenAPI
builder.Services.AddOpenApi();
app.MapOpenApi();csharp
// 错误——已从 .NET 9+ 模板移除,存在维护问题
builder.Services.AddSwaggerGen();
app.UseSwagger();
app.UseSwaggerUI();
// 正确——使用内置 OpenAPI
builder.Services.AddOpenApi();
app.MapOpenApi();Don't Use WithOpenApi() in .NET 10
.NET 10 中不要使用 WithOpenApi()
csharp
// 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;
});csharp
// 错误——已弃用,会产生 ASPDEPR002 警告
app.MapGet("/", () => "hello").WithOpenApi(op => { op.Deprecated = true; return op; });
// 正确——使用单端点操作转换器
app.MapGet("/", () => "hello")
.AddOpenApiOperationTransformer((op, ctx, ct) =>
{
op.Deprecated = true;
return Task.CompletedTask;
});Don't Use Untyped Results
不要使用非类型化 Results
csharp
// 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();
}csharp
// 错误——Results.Ok 不会为 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();
}
// 正确——使用带有联合返回类型的 TypedResults
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();
}Don't Skip WithName on Endpoints
不要省略端点的 WithName
csharp
// 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");csharp
// 错误——没有 operationId,客户端生成器会生成糟糕的方法名
group.MapGet("/{id:guid}", GetOrder);
// 正确——operationId 会传入生成的客户端方法名
group.MapGet("/{id:guid}", GetOrder).WithName("GetOrder");Don't Use OpenApiAny in .NET 10
.NET 10 中不要使用 OpenApiAny
csharp
// 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");csharp
// 错误——Microsoft.OpenApi v2.x 中已移除 OpenApiAny 类型
schema.Example = new OpenApiString("2025-01-01");
// 正确——使用 System.Text.Json.Nodes 中的 JsonNode
schema.Example = JsonValue.Create("2025-01-01");Decision Guide
决策指南
| Scenario | Recommendation |
|---|---|
| New API project | |
| API documentation UI | Scalar ( |
| Security schemes in docs | Document transformer with |
| Response documentation | |
| XML doc integration | |
| Multiple API versions | Multiple |
| Client code generation | Kiota (Microsoft recommended) or NSwag |
| Build-time spec | |
| OpenAPI version | 3.1 (default in .NET 10), force 3.0 if consumers require it |
| Per-endpoint customization | |
| 场景 | 推荐方案 |
|---|---|
| 新 API 项目 | |
| API 文档 UI | Scalar ( |
| 文档中添加安全方案 | 使用 |
| 响应文档 | 带有联合返回类型的 |
| XML 文档集成 | |
| 多 API 版本 | 多次调用 |
| 客户端代码生成 | Kiota(微软推荐)或 NSwag |
| 构建时生成规范 | |
| OpenAPI 版本 | 3.1(.NET 10 默认),若消费者需要可强制使用 3.0 |
| 单端点自定义 | 在端点上使用 |