Tạo response (phản hồi) trong ứng dụng Minimal API
Bài viết này giải thích cách tạo response cho các endpoint trong Minimal API của ASP.NET Core. Minimal API cung cấp nhiều cách để trả về dữ liệu và HTTP status code (mã trạng thái HTTP).
Các endpoint Minimal hỗ trợ các kiểu giá trị trả về sau:
string- Bao gồmTask<string>vàValueTask<string>.T(Bất kỳ kiểu nào khác) - Bao gồmTask<T>vàValueTask<T>.- Dựa trên
IResult- Bao gồmTask<IResult>vàValueTask<IResult>.
Giá trị trả về kiểu string
| Hành vi | Content-Type |
|---|---|
| Framework ghi chuỗi trực tiếp vào response. | text/plain |
Route handler sau trả về văn bản Hello World:
app.MapGet("/hello", () => "Hello World");Status code 200 được trả về với Content-Type header text/plain và nội dung sau:
Hello World
Giá trị trả về kiểu T (Bất kỳ kiểu nào khác)
| Hành vi | Content-Type |
|---|---|
| Framework JSON-serialize response. | application/json |
Route handler sau trả về anonymous type (kiểu ẩn danh) chứa thuộc tính string Message:
app.MapGet("/hello", () => new { Message = "Hello World" });Status code 200 được trả về với Content-Type header application/json và nội dung sau:
{"message":"Hello World"}Giá trị trả về kiểu IResult
| Hành vi | Content-Type |
|---|---|
Framework gọi IResult.ExecuteAsync. | Được quyết định bởi implementation của IResult. |
Interface IResult định nghĩa contract (hợp đồng) đại diện cho kết quả của HTTP endpoint. Lớp static Results và lớp static TypedResults được dùng để tạo ra các đối tượng IResult đại diện cho các loại response khác nhau.
TypedResults và Results
Các lớp static Results và TypedResults cung cấp các helpers kết quả tương tự nhau. Lớp TypedResults là phiên bản có kiểu dữ liệu (typed) của lớp Results. Tuy nhiên, kiểu trả về của Results helpers là IResult, trong khi mỗi TypedResults helper trả về một trong các kiểu implementation của IResult. Sự khác biệt này có nghĩa là với Results helpers cần có conversion khi cần kiểu cụ thể, ví dụ trong unit testing. Các kiểu implementation được định nghĩa trong namespace Microsoft.AspNetCore.Http.HttpResults.
Ưu điểm khi trả về TypedResults thay vì Results:
TypedResultshelpers trả về các đối tượng strongly typed (có kiểu dữ liệu mạnh), giúp cải thiện khả năng đọc code, unit testing và giảm lỗi runtime.- Implementation type tự động cung cấp response type metadata (siêu dữ liệu kiểu response) cho OpenAPI để mô tả endpoint.
Xem endpoint sau, nơi status code 200 OK với JSON response được tạo ra:
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();Để tài liệu hóa endpoint này đúng cách, phương thức extension Produces được gọi. Tuy nhiên, không cần gọi Produces nếu dùng TypedResults thay vì Results. TypedResults tự động cung cấp metadata cho endpoint:
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));Vì tất cả các phương thức trên Results trả về IResult trong signature, compiler tự động suy ra kiểu đó là kiểu trả về khi trả về các kết quả khác nhau từ một endpoint. TypedResults yêu cầu dùng Results<T1, TN> từ các delegate như vậy.
Phương thức sau biên dịch được vì cả Results.Ok và Results.NotFound đều được khai báo là trả về IResult:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());Phương thức sau không biên dịch được vì TypedResults.Ok và TypedResults.NotFound được khai báo trả về các kiểu khác nhau:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());Để dùng TypedResults, kiểu trả về phải được khai báo đầy đủ; khi bất đồng bộ, khai báo cần bao trong Task<>. Dùng TypedResults verbose hơn, nhưng đó là sự đánh đổi để thông tin kiểu có thể tự mô tả cho OpenAPI:
app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());Results<TResult1, TResultN>
Dùng Results<TResult1, TResultN> làm kiểu trả về của endpoint handler thay vì IResult khi:
- Nhiều kiểu implementation của
IResultđược trả về từ endpoint handler. - Lớp static
TypedResultđược dùng để tạo các đối tượngIResult.
Ví dụ về endpoint trả về status code 400 BadRequest khi orderId lớn hơn 999, ngược lại trả về 200 OK:
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));Kết quả tích hợp sẵn (Built-in results)
Các result helper phổ biến tồn tại trong lớp static Results và TypedResults. Nên dùng TypedResults hơn là Results.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));WriteAsJsonAsync là cách thay thế để trả về JSON:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));Custom Status Code (Mã trạng thái tùy chỉnh)
app.MapGet("/405", () => Results.StatusCode(405));Problem và ValidationProblem
app.MapGet("/problem", () =>
{
var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
return TypedResults.Problem("This is an error with extensions",
extensions: extensions);
});Text (Văn bản)
app.MapGet("/text", () => Results.Text("This is some text"));Stream (Luồng dữ liệu)
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy response dưới dạng JSON
return Results.Stream(stream, "application/json");
});
app.Run();Redirect (Chuyển hướng)
app.MapGet("/old-path", () => Results.Redirect("/new-path"));File (Tệp)
app.MapGet("/download", () => Results.File("myfile.text"));Giao diện HttpResult
Các interface sau trong namespace Microsoft.AspNetCore.Http cung cấp cách phát hiện kiểu IResult tại runtime, đây là pattern phổ biến trong các filter implementation:
IContentTypeHttpResultIFileHttpResultINestedHttpResultIStatusCodeHttpResultIValueHttpResultIValueHttpResult<TValue>
Ví dụ về filter dùng một trong các interface này:
app.MapGet("/weatherforecast", (int days) =>
{
if (days <= 0)
{
return Results.BadRequest();
}
var forecast = Enumerable.Range(1, days).Select(index =>
new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
.ToArray();
return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
var result = await next(context);
return result switch
{
IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
_ => result
};
});Sửa đổi Headers (Modifying Headers)
Dùng đối tượng HttpResponse để sửa đổi response headers:
app.MapGet("/", (HttpContext context) => {
// Đặt custom header
context.Response.Headers["X-Custom-Header"] = "CustomValue";
// Đặt header đã biết
context.Response.Headers.CacheControl = $"public,max-age=3600";
return "Hello World";
});Tùy chỉnh response (Customizing responses)
Ứng dụng có thể kiểm soát response bằng cách triển khai kiểu IResult tùy chỉnh. Code sau là ví dụ về kiểu HTML result:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}Nên thêm extension method vào Microsoft.AspNetCore.Http.IResultExtensions để các custom result dễ khám phá hơn:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();Cấu hình JSON serialization options (Tùy chọn tuần tự hóa JSON)
Theo mặc định, ứng dụng Minimal API dùng tùy chọn Web defaults trong quá trình JSON serialization (tuần tự hóa JSON) và deserialization (giải tuần tự hóa).
Cấu hình JSON serialization options toàn cục
Các tùy chọn có thể được cấu hình toàn cục bằng cách gọi ConfigureHttpJsonOptions:
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}Cấu hình JSON serialization options cho endpoint
Để cấu hình serialization options cho endpoint, gọi Results.Json và truyền đối tượng JsonSerializerOptions:
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{ WriteIndented = true };
app.MapGet("/", () =>
Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}