使用 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 元数据

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 [Produces][ProducesProblem] ProducesProducesProblem 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 响应的终结点设置响应类型时,ProducesProblem 扩展方法或 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 文档生成的选项

以下部分演示如何自定义 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 对象的 UseTransformer 调用针对文档进行注册。 以下代码片段演示了将转换器注册到文档的不同方法:

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

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.UseTransformer((document, context, cancellationToken) 
                             => Task.CompletedTask);
    options.UseTransformer(new MyDocumentTransformer());
    options.UseTransformer<MyDocumentTransformer>();
    options.UseOperationTransformer((operation, context, cancellationToken)
                            => Task.CompletedTask);
});

var app = builder.Build();

app.MapOpenApi();

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

app.Run();

转换器的执行顺序

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

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.UseOperationTransformer((operation, context, cancellationToken)
                                     => Task.CompletedTask);
    options.UseTransformer((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.UseTransformer((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.UseTransformer<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.UseTransformer<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.UseOperationTransformer((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 文档

OpenAPI 文档可插入现有工具的广泛生态系统,用于测试、文档编制和本地开发。

使用 Swagger UI 进行本地临时测试

默认情况下,Microsoft.AspNetCore.OpenApi 包不附带用于直观显示 OpenAPI 文档或与之交互的内置支持。 用于可视化或与 OpenAPI 文档交互的常用工具包括 Swagger UIReDoc。 Swagger UI 和 ReDoc 可以通过多种方式集成到应用中。 Visual Studio 和 VS Code 等编辑器提供了针对 OpenAPI 文档进行测试的扩展和内置体验。

Swashbuckle.AspNetCore.SwaggerUi 包提供了一组 Swagger UI 的 Web 资产,供在应用中使用。 此包可用于呈现生成的文档的 UI。 若要对此进行配置,请安装 Swashbuckle.AspNetCore.SwaggerUi 包。

使用对先前注册的 OpenAPI 路由的引用来启用 swagger-ui 中间件。 若要限制信息泄漏和安全漏洞问题,请仅在开发环境中启用 Swagger UI。

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

var app = builder.Build();

app.MapOpenApi();
if (app.Environment.IsDevelopment())
{
    app.UseSwaggerUI(options =>
    {
        options.SwaggerEndpoint("/openapi/v1.json", "v1");
    });

}

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

app.Run();

使用 Scalar 交互式 API 文档

Scalar 是 OpenAPI 的开源交互式文档 UI。 Scalar 可与 ASP.NET Core 提供的 OpenAPI 终结点集成。 若要配置 Scalar,请安装 Scalar.AspNetCore 包。

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

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

if (app.Environment.IsDevelopment())
{
    app.MapScalarApiReference();
}

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

app.Run();

使用 Spectral 对生成的 OpenAPI 文档进行 Lint 分析

Spectral 是一个开源的 OpenAPI 文档 Linter。 Spectral 可整合到应用生成中,以验证生成的 OpenAPI 文档的质量。 根据包安装说明安装 Spectral。

若要利用 Spectral,请安装 Microsoft.Extensions.ApiDescription.Server 包以启用生成时 OpenAPI 文档生成。

通过在应用的 .csproj 文件中设置以下属性,启用生成时文档生成:

<PropertyGroup>
    <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
    <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
</PropertyGroup>

运行 dotnet build 以生成文档。

dotnet build

创建具有以下内容的 .spectral.yml 文件。

extends: ["spectral:oas"]

对生成的文件运行 spectral lint

spectral lint WebMinOpenApi.json
...

The output shows any issues with the OpenAPI document.

```output
1:1  warning  oas3-api-servers       OpenAPI "servers" must be present and non-empty array.
3:10  warning  info-contact           Info object must have "contact" object.                        info
3:10  warning  info-description       Info "description" must be present and non-empty string.       info
9:13  warning  operation-description  Operation "description" must be present and non-empty string.  paths./.get
9:13  warning  operation-operationId  Operation must have "operationId".                             paths./.get

✖ 5 problems (0 errors, 5 warnings, 0 infos, 0 hints)

最小 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 扩展方法或 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 使用的输入进行注释。 这些输入分为两个类别:

  • 出现在路径、查询字符串、标头或 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");