Microsoft.AspNetCore.OpenApi 入门

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

  • 与本机 AoT 兼容。
  • 利用 System.Text.Json 提供的 JSON 架构支持。
  • 提供用于修改生成的文档的转换器 API。
  • 支持在单个应用中管理多个 OpenAPI 文档。

包安装

安装 Microsoft.AspNetCore.OpenApi 包:

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

Install-Package Microsoft.AspNetCore.OpenApi -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 文档。

文档名称的重要性

应用中的每个 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 文档生成。

自定义所生成文档的 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 文档,请参阅如何在最小 API 应用中使用 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)