生成 OpenAPI 文档
Microsoft.AspNetCore.OpenApi
包为 ASP.NET Core 中的 OpenAPI 文档生成提供内置支持。 该包提供以下功能:
- 支持在运行时生成 OpenAPI 文档,并通过应用程序上的终结点访问它们。
- 支持“转换器”API,允许修改生成的文档。
- 支持从单个应用生成多个 OpenAPI 文档。
- 利用
System.Text.Json
提供的 JSON 架构支持。 - 与本机 AoT 兼容。
包安装
安装 Microsoft.AspNetCore.OpenApi
包:
从程序包管理器控制台运行以下命令:
Install-Package Microsoft.AspNetCore.OpenApi
配置 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 文档生成的选项
以下部分演示如何自定义 OpenAPI 文档生成。
自定义 OpenAPI 文档名称
应用中的每个 OpenAPI 文档都具有唯一的名称。 注册的默认文档名称为 v1
。
builder.Services.AddOpenApi(); // Document name is v1
可通过将名称作为参数传递到 AddOpenApi
调用来修改文档名称。
builder.Services.AddOpenApi("internal"); // Document name is internal
文档名称在 OpenAPI 实现中的多个位置显示。
提取生成的 OpenAPI 文档时,文档名称作为请求中的 documentName
参数提供。 以下请求将解析 v1
和 internal
文档。
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 文档
在典型的 Web 应用程序中,OpenAPI 文档在运行时生成,并通过对应用程序服务器的 HTTP 请求提供服务。
在某些情况下,在应用程序的生成步骤中生成 OpenAPI 文档会很有帮助。 这些方案包括:
- 生成提交到源代码管理的 OpenAPI 文档。
- 生成用于基于规范的集成测试的 OpenAPI 文档。
- 生成从 Web 服务器静态提供的 OpenAPI 文档。
若要在生成时添加对生成 OpenAPI 文档的支持,请安装 Microsoft.Extensions.ApiDescription.Server
包:
从程序包管理器控制台运行以下命令:
Install-Package Microsoft.Extensions.ApiDescription.Server
安装后,此包将在生成过程中自动生成与应用程序关联的 Open API 文档,并将其填充到应用程序的输出目录中。
$ dotnet build
$ cat bin/Debug/net9.0/{ProjectName}.json
自定义生成生成时文档
修改生成的 Open API 文件的输出目录
默认情况下,生成的 OpenAPI 文档将发送到应用程序的输出目录。 若要修改所发出文件的位置,请设置属性中的 OpenApiDocumentsDirectory
目标路径。
<PropertyGroup>
<OpenApiDocumentsDirectory>./</OpenApiDocumentsDirectory>
</PropertyGroup>
相对于项目文件解析的值 OpenApiDocumentsDirectory
。 ./
使用上述值将在项目文件所在的同一目录中发出 OpenAPI 文档。
修改输出文件名
默认情况下,生成的 OpenAPI 文档的名称与应用程序的项目文件相同。 若要修改发出的文件的名称,请设置 --file-name
属性中的 OpenApiGenerateDocumentsOptions
参数。
<PropertyGroup>
<OpenApiGenerateDocumentsOptions>--file-name my-open-api</OpenApiGenerateDocumentsOptions>
</PropertyGroup>
选择要生成的 OpenAPI 文档
某些应用程序可能配置为针对 API 的各种版本发出多个 OpenAPI 文档,或区分公共 API 和内部 API。 默认情况下,生成时文档生成器将为应用程序中配置的所有文档发出文件。 若要仅针对单个文档名称发出,请OpenApiGenerateDocumentsOptions
设置--document-name
属性中的参数。
<PropertyGroup>
<OpenApiGenerateDocumentsOptions>--document-name v2</OpenApiGenerateDocumentsOptions>
</PropertyGroup>
在生成生成时文档期间自定义运行时行为
在内部,通过启动应用程序的入口点和插入服务器实现,生成时 OpenAPI 文档生成函数。 这是生成准确的 OpenAPI 文档的要求,因为无法静态分析 OpenAPI 文档中的所有信息。 由于调用应用程序的入口点,因此将调用应用程序启动中的任何逻辑。 这包括将服务注入 DI 容器或从配置中读取的代码。 在某些情况下,必须限制从生成时调用应用程序入口点时将运行的代码路径。 这些方案包括:
- 不从某些配置字符串读取。
- 未注册与数据库相关的服务。
为了限制生成时间生成管道调用这些代码路径,可以在条目程序集的检查后面进行条件,如下所示:
using System.Reflection;
var builder = WebApplication.CreateBuilder();
if (Assembly.GetEntryAssembly()?.GetName().Name != "GetDocument.Insider")
{
builder.Services.AddDefaults();
}
最小 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.AspNetCore
与 Microsoft.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
扩展方法 - 通过路由处理程序上的
ProducesResponseType
属性 - 通过从路由处理程序返回
TypedResults
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 扩展方法 ProducesValidationProblem 或 TypedResults.Problem
向终结点的元数据添加相应的注释。 请注意,在 .NET 8 及更早版本中,ProducesProblem
和 ProducesValidationProblem
扩展方法不能与路由组一起使用。
如果上述策略之一未提供显式注释,则框架会尝试通过检查响应的签名来确定默认响应类型。 此默认响应是在 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");