在 ASP.NET Core 应用中包括 OpenAPI 元数据

本文介绍如何在 ASP.NET Core 应用中添加 OpenAPI 元数据。

包括终结点的 OpenAPI 元数据

ASP.NET 从 Web 应用的终结点收集元数据,并使用它生成 OpenAPI 文档。

在基于控制器的应用中,当控制器具有[EndpointDescription]属性时,元数据会从属性(例如[HttpPost][Produces][ApiController])中收集。

在最小 API 中,可以从属性中收集元数据,但也可以使用扩展方法和其他策略(例如从路由处理程序返回 TypedResults )来设置元数据。

下表概述了收集的元数据以及设置元数据的策略。

Metadata Attribute 扩展方法 其他策略
摘要 [EndpointSummary] WithSummary
description [EndpointDescription] WithDescription
tags [Tags] WithTags
operationId [EndpointName] WithName
parameters [FromQuery]、、[FromRoute][FromHeader]、、[FromForm]
参数说明 [Description]
requestBody [FromBody] Accepts
responses [Produces] ProducesProducesProblem TypedResults
排除终结点 [ExcludeFromDescription][ApiExplorerSettings] ExcludeFromDescription

ASP.NET Core 还可以从 XML 文档注释中收集元数据。 有关详细信息,请参阅 ASP.NET Core ASP.NET Core OpenAPI XML 文档注释支持

以下部分演示如何在应用中包含元数据,以自定义生成的 OpenAPI 文档。

摘要和说明

可以使用 [EndpointSummary][EndpointDescription] 属性来设置终结点摘要和说明,或者在最小 API 中使用 WithSummaryWithDescription 扩展方法进行设置。

以下示例演示了设置摘要和说明的不同策略。

请注意,属性是放置在委托方法上,而不是放置在 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!");

tags

OpenAPI 支持将每个终结点上的标记指定为分类形式。

在Minimal 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 参数进行注释。

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

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

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

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

描述请求正文

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

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

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

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

可以通过在参数上使用 [Description] 属性,结合 FromBodyFromForm,来设置请求正文的说明。

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

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

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

生成的 OpenAPI 文档中请求正文的内容类型取决于绑定到请求正文或使用 Accepts 扩展方法指定的参数的类型。 在默认情况下,FromBody参数的内容类型将会是application/json,而FromForm参数的内容类型则将是multipart/form-dataapplication/x-www-form-urlencoded

对这些默认内容类型的支持内置于最小 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 扩展方法指定终结点接受的内容类型。

如果多次指定 Accepts ,则只有最后一个元数据被使用 -- 它们不会组合在一起。

描述响应类型

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

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

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

请注意,ProducesProducesProblem扩展方法在RouteHandlerBuilderRouteGroupBuilder上都受到支持。 例如,这允许为组中的所有操作定义一组常见的错误响应。

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

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

在 Minimal APIs 中,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());

[ProducesResponseType][Produces][ProducesDefaultResponseType]还支持一个名为Description的可选字符串属性,可用于描述响应。 这可用于解释客户端为何或何时需要特定响应:

app.MapGet("/todos/{id}",
    [ProducesResponseType<Todo>(200, 
        Description = "Returns the requested Todo item.")]
    [ProducesResponseType(404, Description = "Requested item not found.")]
    [ProducesDefault(Description = "Undocumented status code.")]
    async (int id, TodoDb db) => /* Code here */);

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

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

只有实现了IEndpointMetadataProvider的返回类型才会在 OpenAPI 文档中创建responses条目。 以下是一些产生 TypedResults 条目的 responses 帮助程序方法的部分列表:

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

除了 NoContent 外,所有这些方法都有一个通用的重载,指定了响应正文的类型。

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

ProblemDetails 设置响应

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

有关如何将最小 API 应用配置为返回 ProblemDetails 响应的详细信息,请参阅 在 ASP.NET 核心 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 Schema 库根据应用中使用的 typeformat 属性,将标准 C# 数字类型映射到 OpenAPI NumberHandlingJsonSerializerOptions。 在 ASP.NET 核心 Web API 应用中,此属性的默认值为 JsonNumberHandling.AllowReadingFromString

NumberHandling当属性设置为JsonNumberHandling.AllowReadingFromString时,数值类型将按如下所示进行映射:

C# 类型 OpenAPI type OpenAPI format 其他断言
int [integer,string] int32 模式 <digits>
long [integer,string] int64 模式 <digits>
short [integer,string] int16 模式 <digits>
字节 [integer,string] uint8 模式 <digits>
float [number,string] float 模式 <digits with decimal >
double [number,string] double 模式 <digits with decimal >
十进制 [number,string] double 模式 <digits with decimal >

如果应用被配置为生成 OpenAPI 3.0 或 OpenAPI v2 文档,而此时 type 字段不能有数组值,则 type 字段会被删除。

NumberHandling当属性设置为JsonNumberHandling.Strict时,数值类型将按如下所示进行映射:

C# 类型 OpenAPI type OpenAPI format
int 整数 int32
long 整数 int64
short 整数 int16
字节 整数 uint8
float number float
double number double
十进制 number double

字符串类型

下表显示了 C# 类型如何映射到 string 生成的 OpenAPI 文档中的类型属性:

C# 类型 OpenAPI type OpenAPI format 其他断言
字符串 字符串
字符型 字符串 字符型 最小长度: 1, 最大长度: 1
byte[] 字符串 字节
DateTimeOffset 字符串 date-time
DateOnly 字符串 date
TimeOnly 字符串 time
Uri 字符串 uri
Guid 字符串 uuid

其他类型的

其他 C# 类型在生成的 OpenAPI 文档中表示,如下表所示:

C# 类型 OpenAPI type OpenAPI format
bool boolean
对象 omitted
dynamic omitted

使用属性添加元数据

ASP.NET 使用类或记录属性的属性中包含的元数据来设置生成架构的相应属性的元数据。

下表汇总了 System.ComponentModel 命名空间中为生成的架构提供元数据的属性:

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

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

属性也可以放置在记录定义的参数列表中,但必须包含 property 修饰符。 例如:

public record Todo(
    [property: Required]
    [property: Description("The unique identifier for the todo")]
    int Id,
    [property: Description("The title of the todo")]
    [property: MaxLength(120)]
    string Title,
    [property: Description("Whether the todo has been completed")]
    bool Completed
) {}

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

required

在类、结构或记录中,具有 [Required] 属性或 所需 修饰符的属性始终 required 位于相应的架构中。

还可以根据类、结构或记录的构造函数(隐式和显式)要求其他属性。

  • 对于具有单个公共构造函数的类或记录类,在相应的架构中需要具有相同类型和名称(不区分大小写匹配)的任何属性作为构造函数的参数。
  • 对于具有多个公共构造函数的类或记录类,不需要其他属性。
  • 对于结构或记录结构,不需要其他属性,因为 C# 始终为结构定义隐式无参数构造函数。

枚举

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

以下示例演示如何使用JsonStringEnumConverter在JSON中将枚举表示为字符串:

[JsonConverter(typeof(JsonStringEnumConverter<DayOfTheWeekAsString>))]
public enum DayOfTheWeekAsString
{
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
}

一种特殊情况是枚举类型具有 [Flags] 属性时,该属性指示枚举可以视为位字段;即一组标志。 具有 enum[JsonConverterAttribute] 的标志在生成的架构中被定义为 type: string,没有 enum 属性。 不会 enum 生成任何属性,因为该值可以是枚举值的任意组合。 例如,以下enum值可能具有以下值,例如"Pepperoni, Sausage""Sausage, Mushrooms, Anchovies"

[Flags, JsonConverter(typeof(JsonStringEnumConverter<PizzaToppings>))]
public enum PizzaToppings { 
    Pepperoni = 1,
    Sausage = 2,
    Mushrooms = 4,
    Anchovies = 8
}

没有 [JsonConverter] 的枚举类型将在生成的架构中定义为 type: integer

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

全局设置 JSON 选项 显示如何全局设置 JsonStringEnumConverter

可为空

定义为可为 null 的值或引用类型的属性以 type 关键字出现在生成的模式中,该关键字的值是一个包含 null 作为类型之一的数组。 这与 System.Text.Json 反序列化程序的默认行为一致,接受将 null 作为可为空属性的有效值。

例如,定义为string?的C#属性在生成的架构中表示为:

  "nullableString": {
    "description": "A property defined as string?",
    "type": [
      "null",
      "string"
    ]
  },

如果应用配置为生成 OpenAPI v3.0 或 OpenAPI v2 文档,则生成的架构中具有可为 null 的值或引用类型 nullable: true ,因为这些 OpenAPI 版本不允许 type 字段成为数组。

additionalProperties

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

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

多态类型

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

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

具有 [JsonPolymorphic] 属性的抽象类在架构中具有 discriminator 字段,但具有 [JsonPolymorphic] 属性的具体类没有 discriminator 字段。 OpenAPI 要求鉴别器属性是架构中的必需属性,但由于鉴别器属性未在具体基类中定义,因此该架构不能包含 discriminator 字段。

使用架构转换器添加元数据

架构转换器可用于替代任何默认元数据或向生成的架构添加其他元数据(如 example 值)。 有关详细信息 ,请参阅“使用架构转换器 ”。

全局设置 JSON 序列化选项

以下代码全局为最小 API 和基于控制器的 API 配置一些 JSON 选项:

using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http.Json;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();

builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.Converters.Add(
        new JsonStringEnumConverter<DayOfTheWeekAsString>());
    options.SerializerOptions.DefaultIgnoreCondition =
        JsonIgnoreCondition.WhenWritingNull;
    options.SerializerOptions.PropertyNamingPolicy =
        JsonNamingPolicy.CamelCase;

});

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(
            new JsonStringEnumConverter<DayOfTheWeekAsString>());
        options.JsonSerializerOptions.DefaultIgnoreCondition =
            JsonIgnoreCondition.WhenWritingNull;
        options.JsonSerializerOptions.PropertyNamingPolicy =
            JsonNamingPolicy.CamelCase;
    });

var app = builder.Build();

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

app.UseHttpsRedirection();

app.MapGet("/", () =>
{
    var day = DayOfTheWeekAsString.Friday;
    return Results.Json(day);
});

app.MapPost("/", (DayOfTheWeekAsString day) =>
{
    return Results.Json($"Received: {day}");
});

app.UseRouting();
app.MapControllers();

app.Run();

MVC JSON 选项和全局 JSON 选项

下表显示了 MVC JSON 选项和全局最小 API JSON 选项之间的主要区别:

Aspect MVC JSON 选项 全局 JSON 选项
Scope 仅限于 MVC 控制器和终结点。 最小 API 和 OpenAPI 文档。
Configuration AddControllers().AddJsonOptions() Configure<JsonOptions>()
Purpose 处理 API 中 JSON 请求和响应的序列化和反序列化。 为最小 API 和 OpenAPI 架构定义全局 JSON 处理。
对 OpenAPI 的影响 None 直接影响 OpenAPI 架构生成。

其他资源

ASP.NET 从 Web 应用的终结点收集元数据,并使用它生成 OpenAPI 文档。

在基于控制器的应用中,当控制器具有[EndpointDescription]属性时,元数据会从属性(例如[HttpPost][Produces][ApiController])中收集。

在最小 API 中,可以从属性中收集元数据,但也可以使用扩展方法和其他策略(例如从路由处理程序返回 TypedResults )来设置元数据。

下表概述了收集的元数据以及设置元数据的策略。

Metadata Attribute 扩展方法 其他策略
摘要 [EndpointSummary] WithSummary
description [EndpointDescription] WithDescription
tags [Tags] WithTags
operationId [EndpointName] WithName
parameters [FromQuery]、、[FromRoute][FromHeader]、、[FromForm]
参数说明 [Description]
requestBody [FromBody] Accepts
responses [Produces] ProducesProducesProblem TypedResults
排除终结点 [ExcludeFromDescription][ApiExplorerSettings] ExcludeFromDescription

ASP.NET Core 不会从 XML 文档注释中收集元数据。

以下部分演示如何在应用中包含元数据,以自定义生成的 OpenAPI 文档。

摘要和说明

可以使用[EndpointSummary][EndpointDescription]属性设置终结点摘要和说明,或者在最小 API 中使用WithSummaryWithDescription扩展方法进行设置。

以下示例演示了设置摘要和说明的不同策略。

请注意,属性是放置在委托方法上,而不是放置在 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!");

tags

OpenAPI 支持将每个终结点上的标记指定为分类形式。

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

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

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

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

operationId

OpenAPI 支持每个终结点上的 operationId 作为操作的唯一标识符或名称。

在Minimal 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 OpenAPI 中的字段描述了 API 客户端可以发送到服务器的请求正文,包括支持的内容类型和正文内容的架构。

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

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

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

可以通过在参数上使用 [Description] 属性,结合 FromBodyFromForm,来设置请求正文的说明。

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

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

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

生成的 OpenAPI 文档中请求正文的内容类型取决于绑定到请求正文或使用 Accepts 扩展方法指定的参数的类型。 在默认情况下,FromBody参数的内容类型将会是application/json,而FromForm参数的内容类型则将是multipart/form-dataapplication/x-www-form-urlencoded

对这些默认内容类型的支持内置于最小 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扩展方法在RouteHandlerBuilderRouteGroupBuilder上都受到支持。 例如,这允许为组中的所有操作定义一组常见的错误响应。

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

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

在 Minimal APIs 中,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);
});

只有实现了IEndpointMetadataProvider的返回类型才会在 OpenAPI 文档中创建responses条目。 以下是一些产生 TypedResults 条目的 responses 帮助程序方法的部分列表:

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

除了 NoContent 外,所有这些方法都有一个通用的重载,指定了响应正文的类型。

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

ProblemDetails 设置响应

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

有关如何将最小 API 应用配置为返回 ProblemDetails 响应的详细信息,请参阅 在 ASP.NET 核心 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 整数 int32
long 整数 int64
short 整数 int16
字节 整数 uint8
float number float
double number double
十进制 number double
bool boolean
字符串 字符串
字符型 字符串 字符型
byte[] 字符串 字节
DateTimeOffset 字符串 date-time
DateOnly 字符串 date
TimeOnly 字符串 time
Uri 字符串 uri
Guid 字符串 uuid
对象 omitted
dynamic omitted

请注意,对象和动态类型在 OpenAPI 中 没有 定义类型,因为这些类型可以包含任何类型的数据,包括 int 或字符串等基元类型。

type format还可以使用架构转换器进行设置。 例如,你可能希望十进制类型的 formatdecimal 而不是 double

使用属性添加元数据

ASP.NET 使用类或记录属性的属性中包含的元数据来设置生成架构的相应属性的元数据。

下表汇总了 System.ComponentModel 命名空间中为生成的架构提供元数据的属性:

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

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

属性也可以放置在记录定义的参数列表中,但必须包含 property 修饰符。 例如:

public record Todo(
    [property: Required]
    [property: Description("The unique identifier for the todo")]
    int Id,
    [property: Description("The title of the todo")]
    [property: MaxLength(120)]
    string Title,
    [property: Description("Whether the todo has been completed")]
    bool Completed
) {}

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

required

在类、结构或记录中,具有 [Required] 属性或 所需 修饰符的属性始终 required 位于相应的架构中。

还可以根据类、结构或记录的构造函数(隐式和显式)要求其他属性。

  • 对于具有单个公共构造函数的类或记录类,在相应的架构中需要具有相同类型和名称(不区分大小写匹配)的任何属性作为构造函数的参数。
  • 对于具有多个公共构造函数的类或记录类,不需要其他属性。
  • 对于结构或记录结构,不需要其他属性,因为 C# 始终为结构定义隐式无参数构造函数。

枚举

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

以下示例演示如何使用JsonStringEnumConverter在JSON中将枚举表示为字符串:

[JsonConverter(typeof(JsonStringEnumConverter<DayOfTheWeekAsString>))]
public enum DayOfTheWeekAsString
{
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
}

一种特殊情况是枚举类型具有 [Flags] 属性时,该属性指示枚举可以视为位字段;即一组标志。 带有 a [JsonConverterAttribute] 的标志枚举在生成的架构中定义为 type: stringenum 属性,因为该值可以是枚举值的任意组合。 例如,以下枚举:

[Flags, JsonConverter(typeof(JsonStringEnumConverter<PizzaToppings>))]
public enum PizzaToppings { Pepperoni = 1, Sausage = 2, Mushrooms = 4, Anchovies = 8 }

可以具有值,例如 "Pepperoni, Sausage""Sausage, Mushrooms, Anchovies"

没有 [JsonConverter] 的枚举类型将在生成的架构中定义为 type: integer

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

可为空

定义为可为空值或引用类型的属性在生成的架构中具有 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 值)。 有关详细信息 ,请参阅“使用架构转换器 ”。

其他资源