生成 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]
属性设置终结点摘要和说明,也可以使用 WithSummary 和 WithDescription 扩展方法在最小 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,则请求正文是必需的required
requestBody
,且字段设置为true
在生成的 OpenAPI 文档中。
表单正文始终是必需的,并且已 required
设置为 true
。
使用文档转换器或操作转换器设置example
、examples
或encoding
字段,或者为生成的 OpenAPI 文档中的请求正文添加规范扩展。
设置请求正文元数据的其他机制取决于正在开发的应用类型,以下部分进行了介绍。
生成的 OpenAPI 文档中请求正文的内容类型取决于绑定到请求正文或使用 Accepts 扩展方法指定的参数的类型。
默认情况下,参数的内容类型为application/json
或参数application/x-www-form-urlencoded
的内容类型FromForm
multipart/form-data
。FromBody
对这些默认内容类型的支持内置于最小 API 中,而其他内容类型可以使用自定义绑定进行处理。 有关详细信息,请参阅最小 API 文档的自定义绑定主题。
有多种方法可为请求正文指定不同的内容类型。
如果参数的类型 FromBody
实现 IEndpointParameterMetadataProvider,ASP.NET Core 使用此接口来确定请求正文中的内容类型。
框架使用此 PopulateMetadata 接口的方法设置请求正文的内容类型和正文内容的类型。 例如,接受任一Todo
application/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 可以提取终结点上的扩展方法添加的响应元数据、路由处理程序的属性以及路由处理程序的返回类型。
- 扩展 Produces 方法可用于终结点,以指定来自终结点的响应的状态代码、响应正文的类型和内容类型。
- 或
[ProducesResponseType]
ProducesResponseTypeAttribute<T>属性可用于指定响应正文的类型。 - 路由处理程序可用于返回实现 IEndpointMetadataProvider 以指定响应正文的类型和内容类型(s)。
- ProducesProblem终结点上的扩展方法可用于指定错误响应的状态代码和内容类型。
请注意,Produces和ProducesProblem/或同时RouteGroupBuilder支持RouteHandlerBuilder扩展方法。 例如,这允许为组中的所有操作定义一组常见的错误响应。
如果上述策略之一未指定,则:
- 响应的状态代码默认为 200。
- 可以从终结点方法的隐式或显式返回类型推断响应正文的架构,例如,从
T
中 Task<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 响应的终结点设置响应类型时,可以使用以下命令为终结点添加相应的响应元数据:
- ProducesProblem
- ProducesValidationProblem 扩展方法。
- TypedResults 在 (400-499) 范围内具有状态代码。
有关如何将最小 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 type
和 format
,如下所示:
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 或字符串等基元类型。
还可以使用架构转换器设置 type
和 format
。 例如,你可能希望十进制类型的 format
为 decimal
而不是 double
。
使用属性添加元数据
ASP.NET 使用来自类或记录属性上的属性的元数据来设置所生成架构的相应属性上的元数据。
下表汇总了 System.ComponentModel
命名空间中为生成的架构提供元数据的属性:
属性 | 说明 |
---|---|
[Description] |
设置架构中属性的 description 。 |
[Required] |
在架构中将属性标记为 required 。 |
[DefaultValue] |
设置架构中属性的 default 值。 |
[Range] |
设置整数或数字的 minimum 和 maximum 值。 |
[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
参数提供。 以下请求将解析 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 文档转换器
本部分演示如何使用转换器自定义 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 文档的请求和响应正文中使用的数据模型。 架构转换器在以下情况中非常有用:
- 应对文档中的每个架构进行修改时,或
- 当修改有条件地应用于某些架构时。
架构转换器有权访问上下文对象,其中包括:
- 操作所属文档的名称。
- 与目标架构关联的 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.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");