生成 OpenAPI 文档

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

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

包安装

安装 Microsoft.AspNetCore.OpenApi 包:

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

Install-Package Microsoft.AspNetCore.OpenApi -IncludePrerelease

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

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

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

配置 OpenAPI 文档生成

下面的代码:

  • 添加 OpenAPI 服务。
  • 启用终结点以查看 JSON 格式的 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]
参数说明 [EndpointDescription]
requestBody [FromBody] Accepts
responses [Produces] Produces, ProducesProblem TypedResults
排除终结点 [ExcludeFromDescription], [ApiExplorerSettings] 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 支持将每个终结点上的标记指定为分类形式。

在最小 API 中,可以使用 [Tags] 属性或 WithTags 扩展方法设置标记。

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

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

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

operationId

OpenAPI 支持每个终结点上的 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 参数进行注释。

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

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

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

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

描述请求正文

requestBody OpenAPI 中的字段描述了 API 客户端可以发送到服务器的请求正文,包括支持的内容类型和正文内容的架构。

当终结点处理程序方法接受从请求正文绑定的参数时,ASP.NET Core 会生成与 OpenAPI 文档中的操作对应的 requestBody 参数。 还可以使用属性或扩展方法指定请求正文的元数据。 可以使用文档转换器或操作转换器设置其他元数据。

如果终结点未定义绑定到请求正文的任何参数,而是直接 HttpContext 使用请求正文,ASP.NET Core 提供用于指定请求正文元数据的机制。 这是处理请求正文作为流的终结点的常见方案。

可以从路由处理程序方法的或FromForm参数确定FromBody某些请求正文元数据。

可以使用具有或 FromForm. 的参数FromBody的属性设置[Description]请求正文的说明。

FromBody如果参数不可为 null 且EmptyBodyBehavior未在属性中FromBody设置为Allow,则请求正文是必需的requiredrequestBody,且字段设置为true在生成的 OpenAPI 文档中。 表单正文始终是必需的,并且已 required 设置为 true

使用文档转换器操作转换器设置exampleexamplesencoding字段,或者为生成的 OpenAPI 文档中的请求正文添加规范扩展。

设置请求正文元数据的其他机制取决于正在开发的应用类型,以下部分进行了介绍。

生成的 OpenAPI 文档中请求正文的内容类型取决于绑定到请求正文或使用 Accepts 扩展方法指定的参数的类型。 默认情况下,参数的内容类型为application/json或参数application/x-www-form-urlencoded的内容类型FromFormmultipart/form-dataFromBody

对这些默认内容类型的支持内置于最小 API 中,而其他内容类型可以使用自定义绑定进行处理。 有关详细信息,请参阅最小 API 文档的自定义绑定主题。

有多种方法可为请求正文指定不同的内容类型。 如果参数的类型 FromBody 实现 IEndpointParameterMetadataProvider,ASP.NET Core 使用此接口来确定请求正文中的内容类型。 框架使用此 PopulateMetadata 接口的方法设置请求正文的内容类型和正文内容的类型。 例如,接受任一Todoapplication/xml类型或text/xml内容类型的类可用于IEndpointParameterMetadataProvider向框架提供此信息。

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

扩展 Accepts 方法还可用于指定请求正文的内容类型。 在以下示例中,终结点接受请求正文中的 Todo 对象,其预期内容类型为 application/xml

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

由于 application/xml 不是内置内容类型,因此该 Todo 类必须实现 IBindableFromHttpContext<TSelf> 接口才能为请求正文提供自定义绑定。 例如:

public class Todo : IBindableFromHttpContext<Todo>
{
    public static async ValueTask<Todo?> BindAsync(HttpContext context, ParameterInfo parameter)
    {
        var xmlDoc = await XDocument.LoadAsync(context.Request.Body, LoadOptions.None, context.RequestAborted);
        var serializer = new XmlSerializer(typeof(Todo));
        return (Todo?)serializer.Deserialize(xmlDoc.CreateReader());
    }

如果终结点未定义绑定到请求正文的任何参数,请使用 Accepts 扩展方法指定终结点接受的内容类型。

如果多次指定 <AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Accepts%2A> ,则仅使用最后一个项的元数据 -- 它们不会组合在一起。

描述响应类型

OpenAPI 支持提供从 API 返回的响应的说明。 ASP.NET Core 提供了多个策略来设置终结点的响应元数据。 可以设置的响应元数据包括状态代码、响应正文的类型和响应的内容类型。 OpenAPI 中的响应可能具有其他元数据,例如说明、标头、链接和示例。 可以使用文档转换器或操作转换器设置此附加元数据。

设置响应元数据的特定机制取决于正在开发的应用类型。

在最小 API 应用中,ASP.NET Core 可以提取终结点上的扩展方法添加的响应元数据、路由处理程序的属性以及路由处理程序的返回类型。

请注意,ProducesProducesProblem/或同时RouteGroupBuilder支持RouteHandlerBuilder扩展方法。 例如,这允许为组中的所有操作定义一组常见的错误响应。

如果上述策略之一未指定,则:

  • 响应的状态代码默认为 200。
  • 可以从终结点方法的隐式或显式返回类型推断响应正文的架构,例如,从 TTask<TResult>推断;否则,它被视为未指定。
  • 指定或推断的响应正文的内容类型为“application/json”。

在最小 API 中 Produces ,扩展方法和 [ProducesResponseType] 属性仅设置终结点的响应元数据。 它们不会修改或约束终结点的行为,该行为可能会返回与元数据指定的状态代码或响应正文类型不同的状态代码或响应正文类型,并且内容类型由路由处理程序方法的返回类型确定,而不管属性或扩展方法中指定的任何内容类型。

扩展 Produces 方法可以指定终结点的响应类型,默认状态代码为 200,默认内容类型为 application/json。 以下示例对此进行了说明:

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

[ProducesResponseType]可用于向终结点添加响应元数据。 请注意,该属性应用于路由处理程序方法,而不是用于创建路由的方法调用,如以下示例所示:

app.MapGet("/todos",
    [ProducesResponseType<List<Todo>>(200)]
    async (TodoDb db) => await db.Todos.ToListAsync());

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

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

仅返回在 OpenAPI 文档中实现 IEndpointMetadataProvider 创建 responses 条目的类型。 下面是生成responses条目的TypedResults一些帮助程序方法的部分列表:

TypedResults 帮助程序方法 状态代码
Ok() 200
Created() 201
CreatedAtRoute() 201
Accepted() 202
AcceptedAtRoute() 202
NoContent() 204
BadRequest() 400
ValidationProblem() 400
NotFound() 404
冲突() 409
UnprocessableEntity() 422

除指定响应正文类型的泛型重载外 NoContent ,所有这些方法除外。

可以实现类来设置终结点元数据,并从路由处理程序返回它。

ProblemDetails 设置响应

为可能返回 ProblemDetails 响应的终结点设置响应类型时,可以使用以下命令为终结点添加相应的响应元数据:

有关如何将最小 API 应用配置为返回 ProblemDetails 响应的详细信息,请参阅 在最小 API 中处理错误。

多个响应类型

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

  • 多次调用 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,TResult3,TResult4,TResult5,TResult6>,在处理程序的正文中使用 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 应用程序中的默认设置)时,架构中的属性名称是类或记录属性名称的驼峰式大小写形式。 [JsonPropertyName] 可用于单个属性,以指定架构中属性的名称。

类型和格式

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

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 命名空间中为生成的架构提供元数据的属性:

属性 说明
[Description] 设置架构中属性的 description
[Required] 在架构中将属性标记为 required
[DefaultValue] 设置架构中属性的 default 值。
[Range] 设置整数或数字的 minimummaximum 值。
[MinLength] 设置字符串的 minLength
[MaxLength] 设置字符串的 maxLength
[RegularExpression] 设置字符串的 pattern

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

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

必答

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

enum

C# 中的枚举类型是基于整数的,但可以在 JSON 中使用 [JsonConverter]JsonStringEnumConverter 表示为字符串。 当枚举类型在 JSON 中表示为字符串时,所生成的架构将具有 enum 属性,其中包含枚举的字符串值。 没有 [JsonConverter] 的枚举类型将在生成的架构中定义为 type: integer

备注:[AllowedValues] 不会设置属性的 enum 值。

nullable

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

additionalProperties

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

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

多态类型的元数据

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

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

具有 [JsonPolymorphic] 属性的抽象类在架构中具有 discriminator 字段,但具有 [JsonPolymorphic] 属性的具体类没有 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 方法将转换器注册到文档。 以下代码片段演示了将转换器注册到文档的不同方法:

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();

使用文档转换器

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

文档转换器还可以转换已生成的 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 路径和方法的唯一组合。 当修改符合以下情况时,操作转换器非常有用:

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

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

例如,以下操作转换器将 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 文档的请求和响应正文中使用的数据模型。 架构转换器在以下情况中非常有用:

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

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

  • 操作所属文档的名称。
  • 与目标架构关联的 JSON 类型信息。
  • 文档生成中使用的 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 或 JSON 中。
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 使用的输入进行注释。 这些输入分为两个类别:

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

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

若要定义作为请求正文传输的输入类型,请使用 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");