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
| Metadata | Attribute (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:
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:
[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:
app.MapGet("/extension-methods", () => "Hello world!")
.WithTags("todos", "projects");
app.MapGet("/attributes",
[Tags("todos", "projects")]
() => "Hello world!");Controllers:
[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:
app.MapGet("/extension-methods", () => "Hello world!")
.WithName("FromExtensionMethods");
app.MapGet("/attributes",
[EndpointName("FromAttributes")]
() => "Hello world!");Controllers:
[EndpointName("FromAttributes")]
[HttpGet("attributes")]
public IResult Attributes()
{
return Results.Ok("Hello world!");
}Parameters (Tham số)
Minimal APIs:
app.MapGet("/attributes",
([Description("This is a description.")] string name) => "Hello world!");Controllers:
[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 FromBody và multipart/form-data/application/x-www-form-urlencoded cho FromForm.
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:
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):
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:
// 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:
| Method | Status 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:
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:
// 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:
[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:
app.MapGet("/extension-method", () => "Hello world!")
.ExcludeFromDescription();
app.MapGet("/attributes",
[ExcludeFromDescription]
() => "Hello world!");Controllers:
[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 OpenAPI | Assertions khác |
|---|---|---|---|
| int | [integer,string] | int32 | pattern <digits> |
| long | [integer,string] | int64 | pattern <digits> |
| short | [integer,string] | int16 | pattern <digits> |
| byte | [integer,string] | uint8 | pattern <digits> |
| float | [number,string] | float | pattern <digits with decimal> |
| double | [number,string] | double | pattern <digits with decimal> |
| decimal | [number,string] | double | pattern <digits with decimal> |
Kiểu số (với JsonNumberHandling.Strict):
| Kiểu C# | Kiểu OpenAPI | Định dạng OpenAPI |
|---|---|---|
| int | integer | int32 |
| long | integer | int64 |
| short | integer | int16 |
| byte | integer | uint8 |
| float | number | float |
| double | number | double |
| decimal | number | double |
Kiểu chuỗi:
| Kiểu C# | Kiểu OpenAPI | Định dạng OpenAPI |
|---|---|---|
| string | string | |
| char | string | char |
| byte[] | string | byte |
| DateTimeOffset | string | date-time |
| DateOnly | string | date |
| TimeOnly | string | time |
| Uri | string | uri |
| Guid | string | uuid |
Kiểu khác:
| Kiểu C# | Kiểu OpenAPI | Định dạng OpenAPI |
|---|---|---|
| bool | boolean | |
| object | bỏ qua | |
| dynamic | bỏ qua |
Sử dụng Attributes cho Metadata
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:
| Attribute | Mô 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:
[JsonConverter(typeof(JsonStringEnumConverter<DayOfTheWeekAsString>))]
public enum DayOfTheWeekAsString
{
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}Flags Enum:
[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:
- Có attribute
[Required]hoặc modifierrequired - Khớp với tham số constructor (với class/record có một constructor public duy nhất)
Thuộc tính nullable
{
"nullableString": {
"description": "A property defined as string?",
"type": [
"null",
"string"
]
}
}Kiểu đa hình (Polymorphic Types)
[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
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ạnh | MVC JSON Options | Global JSON Options |
|---|---|---|
| Phạm vi | Chỉ MVC controllers và endpoints | Minimal APIs và tài liệu OpenAPI |
| Cấu hình | AddControllers().AddJsonOptions() | Configure<JsonOptions>() |
| Mục đích | Xử lý serialization/deserialization | Định nghĩa xử lý JSON toàn cục |
| Ảnh hưởng đến OpenAPI | Không có | Ảnh hưởng trực tiếp đến việc tạo schema OpenAPI |