Nguon: Microsoft Learn · .NET 8.0

Thêm metadata OpenAPI trong ứng dụng ASP.NET Core

Nguồn: Include OpenAPI metadata

Thêm metadata OpenAPI cho các endpoint

ASP.NET Core thu thập metadata (siêu dữ liệu) từ các endpoint của ứng dụng web để tạo tài liệu OpenAPI (đặc tả API mở). Cách thu thập khác nhau giữa phương pháp controller-based (dựa trên controller) và Minimal API.

Tổng quan về thu thập Metadata

MetadataAttribute (thuộc tính)Extension method (phương thức mở rộng)Chiến lược khác
summary (tóm tắt)[EndpointSummary]WithSummary()
description (mô tả)[EndpointDescription]WithDescription()
tags (thẻ)[Tags]WithTags()
operationId (định danh thao tác)[EndpointName]WithName()
parameters (tham số)[FromQuery], [FromRoute], [FromHeader], [FromForm]
parameter description (mô tả tham số)[Description]
requestBody (nội dung yêu cầu)[FromBody]Accepts()
responses (phản hồi)[Produces]Produces(), ProducesProblem()TypedResults
Loại trừ endpoints[ExcludeFromDescription], [ApiExplorerSettings]ExcludeFromDescription()

Summary (Tóm tắt) và Description (Mô tả)

Minimal APIs:

csharp
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!");

Controllers:

csharp
[EndpointSummary("This is a summary.")]
[EndpointDescription("This is a description.")]
[HttpGet("attributes")]
public IResult Attributes()
{
    return Results.Ok("Hello world!");
}

Tags (Thẻ)

OpenAPI sử dụng tags để phân loại các endpoint.

Minimal APIs:

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

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

Controllers:

csharp
[Tags(["todos", "projects"])]
[HttpGet("attributes")]
public IResult Attributes()
{
    return Results.Ok("Hello world!");
}

OperationId (Định danh thao tác)

Cung cấp định danh duy nhất cho mỗi thao tác.

Minimal APIs:

csharp
app.MapGet("/extension-methods", () => "Hello world!")
    .WithName("FromExtensionMethods");

app.MapGet("/attributes",
    [EndpointName("FromAttributes")]
    () => "Hello world!");

Controllers:

csharp
[EndpointName("FromAttributes")]
[HttpGet("attributes")]
public IResult Attributes()
{
    return Results.Ok("Hello world!");
}

Parameters (Tham số)

Minimal APIs:

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

Controllers:

csharp
[HttpGet("attributes")]
public IResult Attributes(
    [Description("This is a description.")] string name)
{
    return Results.Ok("Hello world!");
}

Request Body (Nội dung yêu cầu)

Minimal APIs:

Các content type (kiểu nội dung) được xác định từ kiểu tham số hoặc phương thức mở rộng Accepts(). Content type mặc định là application/json cho FromBodymultipart/form-data/application/x-www-form-urlencoded cho FromForm.

csharp
app.MapPut("/todos/{id}", (int id, Todo todo) => ...)
    .Accepts<Todo>("application/xml");

Để dùng custom content types (kiểu nội dung tùy chỉnh), hãy implement (triển khai) IEndpointParameterMetadataProvider:

csharp
public class Todo : IEndpointParameterMetadataProvider
{
    public static void PopulateMetadata(
        ParameterInfo parameter,
        EndpointBuilder builder)
    {
        builder.Metadata.Add(
            new AcceptsMetadata(
                ["application/xml", "text/xml"], 
                typeof(Todo)
            )
        );
    }
}

Cũng có thể implement IBindableFromHttpContext<Todo> để custom binding (liên kết tùy chỉnh):

csharp
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());
    }
}

Controllers:

Content type được xác định từ input formatter (bộ định dạng đầu vào) hoặc attribute [Consumes]. Các formatter tích hợp sẵn hỗ trợ JSON và XML.

Response Types (Kiểu phản hồi)

Minimal APIs:

csharp
// Phản hồi đơn giản
app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .Produces<IList<Todo>>();

// Dùng attributes
app.MapGet("/todos",
    [ProducesResponseType<List<Todo>>(200)]
    async (TodoDb db) => await db.Todos.ToListAsync());

// Với mô tả
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 */);

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

Bảng ánh xạ mã trạng thái của TypedResults:

MethodStatus Code
Ok()200
Created()201
CreatedAtRoute()201
Accepted()202
AcceptedAtRoute()202
NoContent()204
BadRequest()400
ValidationProblem()400
NotFound()404
Conflict()409
UnprocessableEntity()422

Phản hồi file nhị phân:

csharp
app.MapPost("/filecontentresult", () =>
{
    var content = "This endpoint returns a FileContentResult!"u8.ToArray();
    return TypedResults.File(content);
})
.Produces<FileContentResult>(contentType: MediaTypeNames.Application.Octet);

Nhiều kiểu phản hồi:

csharp
// Nhiều lần gọi 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);

// Dùng Results union types
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();
    });

Controllers:

csharp
[HttpGet("/todos/{id}")]
[ProducesResponseType<Todo>(StatusCodes.Status200OK,
    "application/json", Description = "Returns the requested Todo item.")]
[ProducesResponseType(StatusCodes.Status404NotFound,
    Description = "Requested Todo item not found.")]
[ProducesDefault(Description = "Undocumented status code.")]
public async Task<ActionResult<Todo>> GetTodoItem(string id, Todo todo)

Loại trừ Endpoints

Minimal APIs:

csharp
app.MapGet("/extension-method", () => "Hello world!")
    .ExcludeFromDescription();

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

Controllers:

csharp
[HttpGet("/private")]
[ApiExplorerSettings(IgnoreApi = true)]
public IActionResult PrivateEndpoint() {
    return Ok("This is a private endpoint");
}

Thêm metadata OpenAPI cho kiểu dữ liệu

Các lớp và record C# được biểu diễn dưới dạng schema (lược đồ) trong tài liệu OpenAPI. Theo mặc định, chỉ các thuộc tính public mới được đưa vào.

Ánh xạ kiểu và định dạng

Kiểu số (với JsonNumberHandling.AllowReadingFromString):

Kiểu C#Kiểu OpenAPIĐịnh dạng OpenAPIAssertions khác
int[integer,string]int32pattern <digits>
long[integer,string]int64pattern <digits>
short[integer,string]int16pattern <digits>
byte[integer,string]uint8pattern <digits>
float[number,string]floatpattern <digits with decimal>
double[number,string]doublepattern <digits with decimal>
decimal[number,string]doublepattern <digits with decimal>

Kiểu số (với JsonNumberHandling.Strict):

Kiểu C#Kiểu OpenAPIĐịnh dạng OpenAPI
intintegerint32
longintegerint64
shortintegerint16
byteintegeruint8
floatnumberfloat
doublenumberdouble
decimalnumberdouble

Kiểu chuỗi:

Kiểu C#Kiểu OpenAPIĐịnh dạng OpenAPI
stringstring
charstringchar
byte[]stringbyte
DateTimeOffsetstringdate-time
DateOnlystringdate
TimeOnlystringtime
Uristringuri
Guidstringuuid

Kiểu khác:

Kiểu C#Kiểu OpenAPIĐịnh dạng OpenAPI
boolboolean
objectbỏ qua
dynamicbỏ qua

Sử dụng Attributes cho Metadata

csharp
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
) {}

Các Attribute metadata:

AttributeMô tả
[Description]Đặt mô tả của thuộc tính
[Required]Đánh dấu thuộc tính là bắt buộc
[DefaultValue]Đặt giá trị mặc định
[Range]Đặt giá trị tối thiểu và tối đa
[MinLength]Đặt minLength cho chuỗi hoặc minItems cho mảng
[MaxLength]Đặt maxLength cho chuỗi hoặc maxItems cho mảng
[RegularExpression]Đặt pattern (mẫu) cho chuỗi

Xử lý Enum

Enum dạng chuỗi:

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

Flags Enum:

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

Thuộc tính bắt buộc

Thuộc tính được coi là bắt buộc nếu:

Thuộc tính nullable

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

Kiểu đa hình (Polymorphic Types)

csharp
[JsonPolymorphic]
[JsonDerivedType(typeof(Cat), "cat")]
[JsonDerivedType(typeof(Dog), "dog")]
public abstract class Animal
{
    public string Name { get; set; } = "";
}

Đặt tùy chọn JSON serialization toàn cục

csharp
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 Options vs Global JSON Options

Khía cạnhMVC JSON OptionsGlobal JSON Options
Phạm viChỉ MVC controllers và endpointsMinimal APIs và tài liệu OpenAPI
Cấu hìnhAddControllers().AddJsonOptions()Configure<JsonOptions>()
Mục đíchXử lý serialization/deserializationĐịnh nghĩa xử lý JSON toàn cục
Ảnh hưởng đến OpenAPIKhông cóẢnh hưởng trực tiếp đến việc tạo schema OpenAPI