openapi

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

OpenAPI

OpenAPI

Core Principles

核心原则

  1. Built-in, not Swashbuckle — .NET 10 ships
    Microsoft.AspNetCore.OpenApi
    as the official, framework-maintained OpenAPI solution. Swashbuckle was removed from templates in .NET 9 and is no longer recommended.
  2. TypedResults drive the schema
    TypedResults.Ok<T>()
    automatically generates correct OpenAPI response schemas.
    Results.Ok()
    does not. Always use
    TypedResults
    .
  3. Transformers over workarounds — Document, operation, and schema transformers compose cleanly. Use them for security schemes, global responses, and schema customization.
  4. Metadata on every endpoint — Use
    .WithName()
    ,
    .WithSummary()
    ,
    .WithTags()
    on every endpoint. This metadata feeds directly into the OpenAPI spec and client generators.
  1. 内置支持,无需 Swashbuckle — .NET 10 提供
    Microsoft.AspNetCore.OpenApi
    作为官方、框架维护的 OpenAPI 解决方案。Swashbuckle 已在 .NET 9 的模板中移除,不再推荐使用。
  2. TypedResults 驱动 schema
    TypedResults.Ok<T>()
    会自动生成正确的 OpenAPI 响应 schema。
    Results.Ok()
    则不会。请始终使用
    TypedResults
  3. 优先使用转换器而非临时方案 — 文档、操作和 schema 转换器可实现清晰的组合。使用它们配置安全方案、全局响应和自定义 schema。
  4. 为每个端点添加元数据 — 为每个端点使用
    .WithName()
    .WithSummary()
    .WithTags()
    。这些元数据会直接传入 OpenAPI 规范和客户端生成器。

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
TypedResults
, response metadata is inferred automatically:
csharp
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);
使用
TypedResults
时,响应元数据会自动推断:
csharp
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
.WithGroupName()
appear in all documents.
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
<summary>
,
<param>
,
<response>
tags automatically:
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

决策指南

ScenarioRecommendation
New API project
AddOpenApi()
+
MapOpenApi()
(built-in)
API documentation UIScalar (
MapScalarApiReference()
)
Security schemes in docsDocument transformer with
IOpenApiDocumentTransformer
Response documentation
TypedResults
with union return types
XML doc integration
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Multiple API versionsMultiple
AddOpenApi("v1")
calls +
WithGroupName()
Client code generationKiota (Microsoft recommended) or NSwag
Build-time spec
Microsoft.Extensions.ApiDescription.Server
package
OpenAPI version3.1 (default in .NET 10), force 3.0 if consumers require it
Per-endpoint customization
.AddOpenApiOperationTransformer()
on the endpoint
场景推荐方案
新 API 项目
AddOpenApi()
+
MapOpenApi()
(内置方案)
API 文档 UIScalar (
MapScalarApiReference()
)
文档中添加安全方案使用
IOpenApiDocumentTransformer
的文档转换器
响应文档带有联合返回类型的
TypedResults
XML 文档集成
<GenerateDocumentationFile>true</GenerateDocumentationFile>
多 API 版本多次调用
AddOpenApi("v1")
+
WithGroupName()
客户端代码生成Kiota(微软推荐)或 NSwag
构建时生成规范
Microsoft.Extensions.ApiDescription.Server
OpenAPI 版本3.1(.NET 10 默认),若消费者需要可强制使用 3.0
单端点自定义在端点上使用
.AddOpenApiOperationTransformer()