生成 OpenAPI 文档

Microsoft.AspNetCore.OpenApi 包为 ASP.NET Core 中的 OpenAPI 文档生成提供内置支持。 该包提供以下功能:

  • 支持在运行时生成 OpenAPI 文档,并通过应用程序上的终结点访问它们。
  • 支持“转换器”API,允许修改生成的文档。
  • 支持从单个应用生成多个 OpenAPI 文档。
  • 利用 提供的 ON 架构支持。
  • 与本机 AoT 兼容。

包安装

安装 Microsoft.AspNetCore.OpenApi 包:

程序包管理器控制台运行以下命令:

Install-Package Microsoft.AspNetCore.OpenApi -IncludePrerelease

若要在生成时添加对生成 OpenAPI 文档的支持,请安装 Microsoft.Extensions.ApiDescription.Server 包:

程序包管理器控制台运行以下命令:

Install-Package Microsoft.Extensions.ApiDescription.Server -IncludePrerelease

配置 OpenAPI 文档生成

下面的代码:

  • 添加 OpenAPI 服务。
  • 启用终结点以查看 ON 格式的 OpenAPI 文档。
var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

启动应用并导航到 https://localhost:<port>/openapi/v1.json 以查看生成的 OpenAPI 文档。

在 ASP.NET Web 应用中包括 OpenAPI 元数据

包括终结点的 OpenAPI 元数据

ASP.NET 从 Web 应用的终结点收集元数据,并使用它生成 OpenAPI 文档。 在基于控制器的应用中,元数据是从 [EndpointDescription][HttpPost][Produces] 等属性中收集的。 在最小 API 中,可以从属性中收集元数据,但也可以通过使用扩展方法和其他策略(例如从路由处理程序返回 TypedResults)来设置元数据。 下表概述了收集的元数据以及设置元数据的策略。

元数据 属性 扩展方法 其他策略
summary [EndpointSummary] WithSummary
description [EndpointDescription] WithDescription
标记 [Tags] WithTags
operationId [EndpointName] WithName
parameters [FromQuery]、、[FromRoute][FromHeader]、、[FromForm]
参数说明 [Description]
requestBody [FromBody] Accepts
responses %> %> TypedResults
排除终结点 [ExcludeFromDescription] ExcludeFromDescription

ASP.NET Core 不会从 XML 文档注释中收集元数据。

以下部分演示如何在应用中包含元数据,以自定义生成的 OpenAPI 文档。

摘要和说明

可以使用 [EndpointSummary][EndpointDescription] 属性设置终结点摘要和说明,也可以使用 WithSummaryWithDescription 扩展方法在最小 API 中设置。

  • %>
  • %>
  • %>
  • %>

以下示例演示了设置摘要和说明的不同策略。

请注意,属性是放置在委托方法上,而不是放置在 app.MapGet 方法上。

app.MapGet("/extension-methods", () => "Hello world!")
  .WithSummary("This is a summary.")
  .WithDescription("This is a description.");

app.MapGet("/attributes",
  [EndpointSummary("This is a summary.")]
  [EndpointDescription("This is a description.")]
  () => "Hello world!");

标记

OpenAPI 支持将每个终结点上的标记指定为分类形式。 在基于控制器的应用中,控制器名称会自动作为标记添加到其每个终结点上,但可以使用 [Tags] 属性重写此名称。 在最小 API 中,可以使用 [Tags] 属性或 WithTags 扩展方法设置标记。

  • %>
  • %>

以下示例演示了设置标记的不同策略。

app.MapGet("/extension-methods", () => "Hello world!")
  .WithTags("todos", "projects");

app.MapGet("/attributes",
  [Tags("todos", "projects")]
  () => "Hello world!");

operationId

OpenAPI 支持每个终结点上的 operationId 作为操作的唯一标识符或名称。 在基于控制器的应用中,可以使用 [EndpointName] 属性设置 operationId。 在最小 API 中,可以使用 [EndpointName] 属性或 WithName 扩展方法设置 operationId。

  • %>
  • %>

以下示例演示了设置 operationId 的不同策略。

app.MapGet("/extension-methods", () => "Hello world!")
  .WithName("FromExtensionMethods");

app.MapGet("/attributes",
  [EndpointName("FromAttributes")]
  () => "Hello world!");

parameters

OpenAPI 支持对 API 使用的路径、查询字符串、标头和 cookie 参数进行注释。

框架根据路由处理程序的签名自动推断请求参数的类型。

[Description] 属性可用于提供参数的说明。

以下示例演示如何设置参数的说明。

app.MapGet("/attributes",
  ([Description("This is a description.")] string name) => "Hello world!");

requestBody

若要定义作为请求正文传输的输入类型,请使用 Accepts 扩展方法配置属性,以定义请求处理程序预期的对象类型和内容类型。 在以下示例中,终结点接受请求正文中的 Todo 对象,其预期内容类型为 application/xml

app.MapPost("/todos/{id}", (int id, Todo todo) => ...)
  .Accepts<Todo>("application/xml");

除了 Accepts 扩展方法外,参数类型还可以通过实现 IEndpointParameterMetadataProvider 接口来描述自己的注释。 例如,以下 Todo 类型添加一个注释,该注释需要具有 application/xml 内容类型的请求正文。

public class Todo : IEndpointParameterMetadataProvider
{
    public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
    {
        builder.Metadata.Add(new AcceptsMetadata(["application/xml", "text/xml"], typeof(XmlBody)));
    }
}

如果未提供显式注释,则框架将尝试确定默认请求类型(如果终结点处理程序中有请求正文参数)。 推理使用以下启发法生成注释:

  • 通过 [FromForm] 属性从窗体读取的请求正文参数使用 multipart/form-data 内容类型进行描述。
  • 所有其他请求正文参数均使用 application/json 内容类型进行描述。
  • 如果请求正文可为空,或者在 FromBody 特性上设置 AllowEmpty 属性,则请求正文被视为可选。

描述响应类型

OpenAPI 支持提供从 API 返回的响应的说明。 最小 API 支持使用三种策略来设置终结点的响应类型:

Produces 扩展方法可用于将 Produces 元数据添加到终结点。 如果未提供任何参数,则扩展方法将在 200 状态代码和 application/json 内容类型下为目标类型填充元数据。

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
  .Produces<IList<Todo>>();

在终结点路由处理程序中使用其实现中的 TypedResults,就可以自动包含终结点的响应类型元数据。 例如,以下代码通过在 200 状态代码和 application/json 内容类型下的响应,自动对终结点进行注释。

app.MapGet("/todos", async (TodoDb db) =>
{
    var todos = await db.Todos.ToListAsync();
    return TypedResults.Ok(todos);
});
ProblemDetails 设置响应

为可能返回 ProblemDetails 响应的终结点设置响应类型时,可以使用 ProducesProblemProducesValidationProblem 扩展方法或 TypedResults.Problem 向终结点的元数据添加相应的注释。

如果这些策略之一未提供显式注释,则框架会尝试通过检查响应的签名来确定默认响应类型。 此默认响应是在 OpenAPI 定义中的 200 状态代码下填充的。

多个响应类型

如果终结点可以在不同的方案中返回不同的响应类型,则可以通过以下方式提供元数据:

  • 多次调用 Produces 扩展方法,如以下示例所示:

    app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
             await db.Todos.FindAsync(id) 
             is Todo todo
             ? Results.Ok(todo) 
             : Results.NotFound())
       .Produces<Todo>(StatusCodes.Status200OK)
       .Produces(StatusCodes.Status404NotFound);
    
  • 在签名中使用 Results<TResult1,TResult2,TResultN>,在处理程序的正文中使用 TypedResults,如以下示例所示:

    app.MapGet("/book{id}", Results<Ok<Book>, NotFound> (int id, List<Book> bookList) =>
    {
        return bookList.FirstOrDefault((i) => i.Id == id) is Book book
         ? TypedResults.Ok(book)
         : TypedResults.NotFound();
    });
    

    Results<TResult1,TResult2,TResultN> 联合类型声明路由处理程序返回多个 IResult 实现具体类型,并且实现 IEndpointMetadataProvider 的其中任何一个类型都将参与终结点的元数据。

    联合类型实现隐式强制转换运算符。 通过这些运算符,编译器可以自动将泛型参数中指定的类型转换为联合类型的实例。 此功能增加了一个好处,即提供编译时检查,路由处理程序只返回声明它的结果。 尝试返回未声明为 Results<TResult1,TResult2,TResultN> 泛型参数之一的类型会导致编译错误。

从生成的文档中排除终结点

默认情况下,在应用中定义的所有终结点都记录在生成的 OpenAPI 文件中。 最小 API 支持两种从 OpenAPI 文档中排除给定终结点的策略,使用:

以下示例演示了从生成的 OpenAPI 文档中排除给定终结点的不同策略。

app.MapGet("/extension-method", () => "Hello world!")
  .ExcludeFromDescription();

app.MapGet("/attributes",
  [ExcludeFromDescription]
  () => "Hello world!");

包括数据类型的 OpenAPI 元数据

请求或响应正文中使用的 C# 类或记录在生成的 OpenAPI 文档中表示为架构。 默认情况下,只有公共属性在架构中表示,但也有 JsonSerializerOptions 来为字段创建架构属性。

PropertyNamingPolicy 设置为驼峰式大小写(这是 ASP.NET Web 应用程序中的默认设置)时,架构中的属性名称是类或记录属性名称的驼峰式大小写形式。 JsonPropertyNameAttribute 可用于单个属性,以指定架构中属性的名称。

类型和格式

type ON 架构库将标准 C# 类型映射到 OpenAPI 和 ,如下所示:

C# 类型 OpenAPI type OpenAPI format
int integer int32
long integer int64
short integer int16
byte integer uint8
float 数字 FLOAT
Double 数字 double
十进制 数字 double
布尔 boolean
string string
char string char
byte[] string byte
DateTimeOffset string 日期时间
DateOnly string date
TimeOnly string 时间
Uri string uri
Guid string uuid
object 已省略
动态 已省略

请注意,对象和动态类型在 OpenAPI 中没有定义类型,因为这些类型可以包含任何类型的数据,包括 int 或字符串等基元类型。

typeformat 还可以使用架构转换器进行设置。 例如,你可能希望十进制类型的 formatdecimal 而不是 double

使用属性添加元数据

ASP.NET 使用来自类或记录属性上的属性的元数据来设置所生成架构的相应属性上的元数据。

下表汇总了 System.ComponentModel 命名空间中为生成的架构提供元数据的属性:

属性 说明
DescriptionAttribute 设置架构中属性的 description
RequiredAttribute 在架构中将属性标记为 required
DefaultValueAttribute 设置架构中属性的 default 值。
RangeAttribute 设置整数或数字的 minimummaximum 值。
MinLengthAttribute 设置字符串的 minLength
MaxLengthAttribute 设置字符串的 maxLength
RegularExpressionAttribute 设置字符串的 pattern

请注意,在基于控制器的应用中,这些属性会向操作添加过滤器,以验证任何传入数据是否满足约束条件。 在最小 API 中,这些属性在生成的架构中设置元数据,但必须通过终结点筛选器、路由处理程序逻辑或第三方包显式执行验证。

所生成架构的其他元数据源

必答

还可以使用必需的修饰符将属性标记为 required

enum

C# 中的枚举类型是基于整数的,但可以用 JsonConverterAttribute ON 中带有 和 的字符串表示。 当枚举类型在 ON 中表示为字符串时,生成的架构将具有 属性,其中包含枚举的字符串值。 没有 JsonConverterAttribute 的枚举类型将在生成的架构中定义为 type: integer

注意:AllowedValuesAttribute 不设置属性的 enum 值。

nullable

定义为可为 null 的值或引用类型的属性在生成的架构中具有 nullable: true。 这与 System.Text.Json 反序列化程序的默认行为一致,该反序列化程序接受 null 作为可为空属性的有效值。

additionalProperties

默认情况下,生成架构时没有 additionalProperties 断言,这意味着默认值为 true。 这与 反序列化程序的默认行为一致,该行为以静默方式忽略 ON 对象中的其他属性。

如果架构的其他属性应仅具有特定类型的值,请将属性或类定义为 Dictionary<string, type>。 字典的键类型必须为 string。 这会生成一个架构,其中 additionalProperties 将“type”的架构指定为所需的值类型。

多态类型的元数据

使用父类上的 JsonPolymorphicAttributeJsonDerivedTypeAttribute 属性来指定多态类型的鉴别器字段和子类型。

JsonDerivedTypeAttribute 将鉴别器字段添加到每个子类的架构中,并使用枚举指定子类的特定鉴别器值。 此属性还修改每个派生类的构造函数以设置鉴别器值。

具有 JsonPolymorphicAttribute 属性的抽象类在架构中具有 discriminator 字段,但具有 JsonPolymorphicAttribute 属性的具体类没有 discriminator 字段。 OpenAPI 要求鉴别器属性是架构中的必需属性,但由于鉴别器属性未在具体基类中定义,因此该架构不能包含 discriminator 字段。

使用架构转换器添加元数据

架构转换器可用于替代任何默认元数据或向生成的架构添加其他元数据(如 example 值)。 更多信息请参阅《使用架构转换器》。

用于自定义 OpenAPI 文档生成的选项

以下部分演示如何自定义 OpenAPI 文档生成。

自定义 OpenAPI 文档名称

应用中的每个 OpenAPI 文档都具有唯一的名称。 注册的默认文档名称为 v1

builder.Services.AddOpenApi(); // Document name is v1

可通过将名称作为参数传递到 AddOpenApi 调用来修改文档名称。

builder.Services.AddOpenApi("internal"); // Document name is internal

文档名称在 OpenAPI 实现中的多个位置显示。

提取生成的 OpenAPI 文档时,文档名称作为请求中的 documentName 参数提供。 以下请求将解析 v1internal 文档。

GET http://localhost:5000/openapi/v1.json
GET http://localhost:5000/openapi/internal.json

自定义所生成文档的 OpenAPI 版本

默认情况下,OpenAPI 文档生成将创建一个符合 3.0 版 OpenAPI 规范的文档。 以下代码演示了如何修改 OpenAPI 文档的默认版本:

builder.Services.AddOpenApi(options =>
{
    options.OpenApiVersion = OpenApiSpecVersion.OpenApi2_0;
});

自定义 OpenAPI 终结点路由

默认情况下,通过对 MapOpenApi 的调用注册的 OpenAPI 终结点将在 /openapi/{documentName}.json 终结点公开文档。 以下代码演示了如何自定义 OpenAPI 文档注册到的路由:

app.MapOpenApi("/openapi/{documentName}/openapi.json");

可以从终结点路由中删除 documentName 路由参数,但不建议这样做。 从终结点路由中移除 documentName 路由参数时,框架会尝试从查询参数解析文档名称。 在路由或查询中不提供 documentName 可能会导致意外行为。

自定义 OpenAPI 终结点

由于 OpenAPI 文档是通过路由处理程序终结点提供的,因此适用于标准最小终结点的任何自定义项也同样适用于 OpenAPI 终结点。

只有已获授权的用户才能访问 OpenAPI 文档

默认情况下,OpenAPI 终结点不会启用任何授权检查。 但是,授权检查可应用于 OpenAPI 文档。 在以下代码中,只有具有 tester 角色的用户可以访问 OpenAPI 文档:

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization(o =>
{
    o.AddPolicy("ApiTesterPolicy", b => b.RequireRole("tester"));
});
builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi()
    .RequireAuthorization("ApiTesterPolicy");

app.MapGet("/", () => "Hello world!");

app.Run();

缓存生成的 OpenAPI 文档

每次向 OpenAPI 终结点发送请求时,都会重新生成 OpenAPI 文档。 重新生成使转换器能够将动态应用程序状态合并到其操作中。 例如,使用 HTTP 上下文的详细信息重新生成请求。 在适用的情况下,可以缓存 OpenAPI 文档,以避免针对每个 HTTP 请求执行文档生成管道。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(policy => policy.Expire(TimeSpan.FromMinutes(10)));
});
builder.Services.AddOpenApi();

var app = builder.Build();

app.UseOutputCache();

app.MapOpenApi()
    .CacheOutput();

app.MapGet("/", () => "Hello world!");

app.Run();

OpenAPI 文档转换器

本部分演示如何使用转换器自定义 OpenAPI 文档。

使用转换器自定义 OpenAPI 文档

转换器提供了一个 API,用于使用用户定义的自定义项修改 OpenAPI 文档。 转换器适用于以下方案:

  • 向文档中的所有操作添加参数。
  • 修改参数或操作的说明。
  • 将顶级信息添加到 OpenAPI 文档。

转换器分为三类:

  • 文档转换器有权访问整个 OpenAPI 文档。 这些可用于对文档进行全局修改。
  • 操作转换器适用于每个单独的操作。 每个单独的操作都是路径和 HTTP 方法的组合。 这些可用于修改终结点上的参数或响应。
  • 架构转换器适用于文档中的每个架构。 这些转换器可用于修改请求或响应正文的架构或任何嵌套架构。

可以通过对 OpenApiOptions 对象调用 AddDocumentTransformer 方法将转换器注册到文档。 以下代码片段演示了将转换器注册到文档的不同方法:

  • 使用委托注册文档转换器。
  • 使用 IOpenApiDocumentTransformer 的实例注册文档转换器。
  • 使用已激活 DI 的 IOpenApiDocumentTransformer 注册文档转换器。
  • 使用委托注册操作转换器。
  • 使用 IOpenApiOperationTransformer 的实例注册操作转换器。
  • 使用已激活 DI 的 IOpenApiOperationTransformer 注册操作转换器。
  • 使用委托注册架构转换器。
  • 使用 IOpenApiSchemaTransformer 的实例注册架构转换器。
  • 使用已激活 DI 的 IOpenApiSchemaTransformer 注册架构转换器。
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken)
                             => Task.CompletedTask);
    options.AddDocumentTransformer(new MyDocumentTransformer());
    options.AddDocumentTransformer<MyDocumentTransformer>();
    options.AddOperationTransformer((operation, context, cancellationToken)
                            => Task.CompletedTask);
    options.AddOperationTransformer(new MyOperationTransformer());
    options.AddOperationTransformer<MyOperationTransformer>();
    options.AddSchemaTransformer((schema, context, cancellationToken)
                            => Task.CompletedTask);
    options.AddSchemaTransformer(new MySchemaTransformer());
    options.AddSchemaTransformer<MySchemaTransformer>();
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

转换器的执行顺序

转换器根据注册按先进先出的顺序执行。 在下面的代码片段中,文档转换器有权访问操作转换器所做的修改:

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.AddOperationTransformer((operation, context, cancellationToken)
                                     => Task.CompletedTask);
    options.AddDocumentTransformer((document, context, cancellationToken)
                                     => Task.CompletedTask);
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

使用文档转换器

文档转换器有权访问上下文对象,其中包括:

  • 正在修改的文档的名称。
  • 与该文档关联的 ApiDescriptionGroups 列表。
  • 文档生成中使用的 IServiceProvider

文档转换器还可以转换已生成的 OpenAPI 文档。 以下示例演示一个文档转换器,该转换器向 OpenAPI 文档添加有关 API 的一些信息。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken) =>
    {
        document.Info = new()
        {
            Title = "Checkout API",
            Version = "v1",
            Description = "API for processing checkouts from cart."
        };
        return Task.CompletedTask;
    });
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

服务激活的文档转换器可以利用 DI 中的实例来修改应用。 以下示例演示了使用身份验证层中的 IAuthenticationSchemeProvider 服务的文档转换器。 它会检查应用中是否注册了任何与 JWT 持有者相关的方案,并将其添加到 OpenAPI 文档的顶层:

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
        if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
        {
            var requirements = new Dictionary<string, OpenApiSecurityScheme>
            {
                ["Bearer"] = new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer", // "bearer" refers to the header name here
                    In = ParameterLocation.Header,
                    BearerFormat = "Json Web Token"
                }
            };
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes = requirements;
        }
    }
}

文档转换器对于与之关联的文档实例是唯一的。 在下面的示例中,转换器:

  • internal 文档注册与身份验证相关的要求。
  • 不修改 public 文档。
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi("internal", options =>
{
    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});
builder.Services.AddOpenApi("public");

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/world", () => "Hello world!")
    .WithGroupName("internal");
app.MapGet("/", () => "Hello universe!")
    .WithGroupName("public");

app.Run();

internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
        if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
        {
            // Add the security scheme at the document level
            var requirements = new Dictionary<string, OpenApiSecurityScheme>
            {
                ["Bearer"] = new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer", // "bearer" refers to the header name here
                    In = ParameterLocation.Header,
                    BearerFormat = "Json Web Token"
                }
            };
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes = requirements;

            // Apply it as a requirement for all operations
            foreach (var operation in document.Paths.Values.SelectMany(path => path.Operations))
            {
                operation.Value.Security.Add(new OpenApiSecurityRequirement
                {
                    [new OpenApiSecurityScheme { Reference = new OpenApiReference { Id = "Bearer", Type = ReferenceType.SecurityScheme } }] = Array.Empty<string>()
                });
            }
        }
    }
}

使用操作转换器

操作是 OpenAPI 文档中 HTTP 路径和方法的唯一组合。 当修改符合以下情况时,操作转换器非常有用:

  • 应该对应用中的每个终结点进行,或
  • 有条件地应用于某些路由。

操作转换器有权访问上下文对象,其中包括:

  • 操作所属的文档的名称。
  • 与该操作关联的 ApiDescription
  • 文档生成中使用的 IServiceProvider

例如,以下操作转换器将 500 添加为文档中所有操作支持的响应状态代码。

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi(options =>
{
    options.AddOperationTransformer((operation, context, cancellationToken) =>
    {
        operation.Responses.Add("500", new OpenApiResponse { Description = "Internal server error" });
        return Task.CompletedTask;
    });
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

使用架构转换器

架构是在 OpenAPI 文档的请求和响应正文中使用的数据模型。 架构转换器在以下情况下非常有用:

  • 应对文档中的每个架构进行修改时,或
  • 当修改有条件地应用于某些架构时。

架构转换器有权访问上下文对象,其中包括:

  • 架构所属的文档的名称。
  • 与目标架构关联的 ON 类型信息。
  • 文档生成中使用的 IServiceProvider

例如,以下架构转换器将十进制类型的 format 设置为 decimal 而不是 double

using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options => {
    // Schema transformer to set the format of decimal to 'decimal'
    options.AddSchemaTransformer((schema, context, cancellationToken) =>
    {
        if (context.JsonTypeInfo.Type == typeof(decimal))
        {
            schema.Format = "decimal";
        }
        return Task.CompletedTask;
    });
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => new Body { Amount = 1.1m });

app.Run();

public class Body {
    public decimal Amount { get; set; }
}

其他资源

最小 API 提供内置支持,用于通过 Microsoft.AspNetCore.OpenApi 包生成有关应用中终结点的信息。 通过视觉 UI 公开生成的 OpenAPI 定义需要第三方包。 有关在基于控制器的 API 中对 OpenAPI 的支持的信息,请参阅本文的 .NET 9 版本

以下代码由 ASP.NET Core 最小 Web API 模板生成,并使用 OpenAPI:

using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateTime.Now.AddDays(index),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.Run();

internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

在上述突出显示的代码中:

  • Microsoft.AspNetCore.OpenApi 将在下一部分中进行介绍。
  • AddEndpointsApiExplorer:将应用配置为使用 API Explorer 发现和描述具有默认注释的终结点。 WithOpenApi 将 API Explorer 生成的匹配默认注释替代为 Microsoft.AspNetCore.OpenApi 包中生成的注释。
  • UseSwagger 添加 Swagger 中间件
  • `UseSwaggerUI` 启用 Swagger UI 工具的嵌入版本。
  • WithName:终结点上的 IEndpointNameMetadata 用于链接生成,并被视为给定终结点的 OpenAPI 规范中的操作 ID。
  • 本文后面将介绍 WithOpenApi

Microsoft.AspNetCore.OpenApi NuGet 包

ASP.NET Core 提供 Microsoft.AspNetCore.OpenApi 包以与终结点的 OpenAPI 规范进行交互。 该包充当 Microsoft.AspNetCore.OpenApi 包中定义的 OpenAPI 模型和 Minimal API 中定义的终结点之间的链接。 该包提供一个 API,用于检查终结点的参数、响应和元数据,以构造用于描述终结点的 OpenAPI 注释类型。

Microsoft.AspNetCore.OpenApi 作为 PackageReference 添加到项目文件:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>    
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.*-*" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
  </ItemGroup>

</Project>

Swashbuckle.AspNetCoreMicrosoft.AspNetCore.OpenApi 一起使用时,必须使用 Swashbuckle.AspNetCore 6.4.0 或更高版本。 Microsoft.OpenApi 1.4.3 或更高版本必须用于在 WithOpenApi 调用中利用复制构造函数。

通过 WithOpenApi 向终结点添加 OpenAPI 注释

对终结点调用 WithOpenApi 会添加到终结点元数据。 此元数据可以:

  • Swashbuckle.AspNetCore 等第三方包中使用。
  • 显示在 Swagger 用户界面或为定义 API 而生成的 YAML 或 ON 中。
app.MapPost("/todoitems/{id}", async (int id, Todo todo, TodoDb db) =>
{
    todo.Id = id;
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi();

修改 WithOpenApi 中的 OpenAPI 注释

WithOpenApi 方法接受可用于修改 OpenAPI 注释的函数。 例如,在以下代码中,将说明添加到终结点的第一个参数:

app.MapPost("/todo2/{id}", async (int id, Todo todo, TodoDb db) =>
{
    todo.Id = id;
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi(generatedOperation =>
{
    var parameter = generatedOperation.Parameters[0];
    parameter.Description = "The ID associated with the created Todo";
    return generatedOperation;
});

将操作 ID 添加到 OpenAPI

操作 ID 用于唯一标识 OpenAPI 中的给定终结点。 WithName 扩展方法可用于设置供方法使用的操作 ID。

app.MapGet("/todoitems2", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithName("GetToDoItems");

也可以直接在 OpenAPI 注释上设置 OperationId 属性。

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        OperationId = "GetTodos"
    });

将标记添加到 OpenAPI 说明

OpenAPI 支持使用标记对象对操作进行分类。 这些标记通常用于对 Swagger UI 中的操作进行分组。 可以通过调用具有所需标记的终结点上的 WithTags 扩展方法,将这些标记添加到操作中。

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithTags("TodoGroup");

或者,可以通过 WithOpenApi 扩展方法在 OpenAPI 注释上设置 OpenApiTags 列表。

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        Tags = new List<OpenApiTag> { new() { Name = "Todos" } }
    });

添加终结点摘要或说明

可以通过调用 WithOpenApi 扩展方法添加终结点摘要和说明。 在以下代码中,直接在 OpenAPI 注释上设置摘要。

app.MapGet("/todoitems2", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        Summary = "This is a summary",
        Description = "This is a description"
    });

排除 OpenAPI 说明

在下面的示例中,/skipme 终结点从生成 OpenAPI 说明中排除:

using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapGet("/swag", () => "Hello Swagger!")
    .WithOpenApi();
app.MapGet("/skipme", () => "Skipping Swagger.")
                    .ExcludeFromDescription();

app.Run();

将 API 标记为已过时

若要将终结点标记为已过时,请在 OpenAPI 注释上设置 Deprecated 属性。

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        Deprecated = true
    });

描述响应类型

OpenAPI 支持提供从 API 返回的响应的说明。 最小 API 支持使用三种策略来设置终结点的响应类型:

Produces 扩展方法可用于将 Produces 元数据添加到终结点。 如果未提供任何参数,则扩展方法将在 200 状态代码和 application/json 内容类型下为目标类型填充元数据。

app
    .MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .Produces<IList<Todo>>();

在终结点路由处理程序中使用其实现中的 TypedResults,就可以自动包含终结点的响应类型元数据。 例如,以下代码通过在 200 状态代码和 application/json 内容类型下的响应,自动对终结点进行注释。

app.MapGet("/todos", async (TodoDb db) =>
{
    var todos = await db.Todos.ToListAsync());
    return TypedResults.Ok(todos);
});

ProblemDetails 设置响应

为可能返回 ProblemDetails 响应的终结点设置响应类型时,可以使用 ProducesProblem 扩展方法 ProducesValidationProblemTypedResults.Problem 向终结点的元数据添加相应的注释。 请注意,在 .NET 8 及更早版本中,ProducesProblemProducesValidationProblem 扩展方法不能与路由组一起使用。

如果上述策略之一未提供显式注释,则框架会尝试通过检查响应的签名来确定默认响应类型。 此默认响应是在 OpenAPI 定义中的 200 状态代码下填充的。

多个响应类型

如果终结点可以在不同的方案中返回不同的响应类型,则可以通过以下方式提供元数据:

  • 多次调用 Produces 扩展方法,如以下示例所示:

    app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
             await db.Todos.FindAsync(id) 
             is Todo todo
             ? Results.Ok(todo) 
             : Results.NotFound())
       .Produces<Todo>(StatusCodes.Status200OK)
       .Produces(StatusCodes.Status404NotFound);
    
  • 在签名中使用 Results<TResult1,TResult2,TResultN>,在处理程序的正文中使用 TypedResults,如以下示例所示:

    app.MapGet("/book{id}", Results<Ok<Book>, NotFound> (int id, List<Book> bookList) =>
    {
        return bookList.FirstOrDefault((i) => i.Id == id) is Book book
         ? TypedResults.Ok(book)
         : TypedResults.NotFound();
    });
    

    Results<TResult1,TResult2,TResultN> 联合类型声明路由处理程序返回多个 IResult 实现具体类型,并且实现 IEndpointMetadataProvider 的其中任何一个类型都将参与终结点的元数据。

    联合类型实现隐式强制转换运算符。 通过这些运算符,编译器可以自动将泛型参数中指定的类型转换为联合类型的实例。 此功能增加了一个好处,即提供编译时检查,路由处理程序只返回声明它的结果。 尝试返回未声明为 Results<TResult1,TResult2,TResultN> 泛型参数之一的类型会导致编译错误。

描述请求正文和参数

除了描述终结点返回的类型外,OpenAPI 还支持对 API 使用的输入进行注释。 这些输入分为两个类别:

  • 出现在路径、查询字符串、标头或 中的参数
  • 作为请求正文的一部分传输的数据

框架根据路由处理程序的签名自动推断路径、查询和标头字符串中请求参数的类型。

若要定义作为请求正文传输的输入类型,请使用 Accepts 扩展方法配置属性,以定义请求处理程序预期的对象类型和内容类型。 在以下示例中,终结点接受请求正文中的 Todo 对象,其预期内容类型为 application/xml

app.MapPost("/todos/{id}", (int id, Todo todo) => ...)
  .Accepts<Todo>("application/xml");

除了 Accepts 扩展方法外,参数类型还可以通过实现 IEndpointParameterMetadataProvider 接口来描述自己的注释。 例如,以下 Todo 类型添加一个注释,该注释需要具有 application/xml 内容类型的请求正文。

public class Todo : IEndpointParameterMetadataProvider
{
    public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ConsumesAttribute(typeof(Todo), isOptional: false, "application/xml"));
    }
}

如果未提供显式注释,则框架将尝试确定默认请求类型(如果终结点处理程序中有请求正文参数)。 推理使用以下启发法生成注释:

  • 通过 [FromForm] 属性从窗体读取的请求正文参数使用 multipart/form-data 内容类型进行描述。
  • 所有其他请求正文参数均使用 application/json 内容类型进行描述。
  • 如果请求正文可为空,或者在 FromBody 特性上设置 AllowEmpty 属性,则请求正文被视为可选。

支持 API 版本控制

最小 API 支持通过 Asp.Versioning.Http 包进行 API 版本控制。 使用最小 API 配置版本控制的示例请见 API 版本控制存储库

GitHub 上的 ASP.NET Core OpenAPI 源代码

其他资源

最小 API 应用可以使用 Swashbuckle 描述路由处理程序的 OpenAPI 规范

有关在基于控制器的 API 中对 OpenAPI 的支持的信息,请参阅本文的 .NET 9 版本

以下代码是具有 OpenAPI 支持的典型 ASP.NET Core 应用:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new() { Title = builder.Environment.ApplicationName,
                               Version = "v1" });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger(); // UseSwaggerUI Protected by if (env.IsDevelopment())
    app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
                                    $"{builder.Environment.ApplicationName} v1"));
}

app.MapGet("/swag", () => "Hello Swagger!");

app.Run();

排除 OpenAPI 说明

在下面的示例中,/skipme 终结点从生成 OpenAPI 说明中排除:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(); // UseSwaggerUI Protected by if (env.IsDevelopment())
}

app.MapGet("/swag", () => "Hello Swagger!");
app.MapGet("/skipme", () => "Skipping Swagger.")
                    .ExcludeFromDescription();

app.Run();

描述响应类型

以下示例使用内置结果类型自定义响应:

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);

将操作 ID 添加到 OpenAPI

app.MapGet("/todoitems2", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithName("GetToDoItems");

将标记添加到 OpenAPI 说明

以下代码使用 OpenAPI 分组标记

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithTags("TodoGroup");