Nguon: Microsoft Learn · .NET 8.0

Tạo response (phản hồi) trong ứng dụng Minimal API

Nguồn: Create responses in Minimal API applications

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:

  1. string - Bao gồm Task<string>ValueTask<string>.
  2. T (Bất kỳ kiểu nào khác) - Bao gồm Task<T>ValueTask<T>.
  3. Dựa trên IResult - Bao gồm Task<IResult>ValueTask<IResult>.

Giá trị trả về kiểu string

Hành viContent-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:

csharp
app.MapGet("/hello", () => "Hello World");

Status code 200 được trả về với Content-Type header text/plain và nội dung sau:

text
Hello World

Giá trị trả về kiểu T (Bất kỳ kiểu nào khác)

Hành viContent-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:

csharp
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:

json
{"message":"Hello World"}

Giá trị trả về kiểu IResult

Hành viContent-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 ResultsTypedResults 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:

Xem endpoint sau, nơi status code 200 OK với JSON response được tạo ra:

csharp
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:

csharp
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.OkResults.NotFound đều được khai báo là trả về IResult:

csharp
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.OkTypedResults.NotFound được khai báo trả về các kiểu khác nhau:

csharp
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:

csharp
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&lt;TResult1, TResultN&gt;

Dùng Results<TResult1, TResultN> làm kiểu trả về của endpoint handler thay vì IResult khi:

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:

csharp
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 ResultsTypedResults. Nên dùng TypedResults hơn là Results.

JSON

csharp
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

WriteAsJsonAsync là cách thay thế để trả về JSON:

csharp
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));

Custom Status Code (Mã trạng thái tùy chỉnh)

csharp
app.MapGet("/405", () => Results.StatusCode(405));

Problem và ValidationProblem

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

csharp
app.MapGet("/text", () => Results.Text("This is some text"));

Stream (Luồng dữ liệu)

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

csharp
app.MapGet("/old-path", () => Results.Redirect("/new-path"));

File (Tệp)

csharp
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:

Ví dụ về filter dùng một trong các interface này:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

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