Nguon: Microsoft Learn · .NET 8.0

Custom formatters (bộ định dạng tùy chỉnh) trong ASP.NET Core Web API

Nguồn: Custom formatters in ASP.NET Core Web API

ASP.NET Core MVC hỗ trợ trao đổi dữ liệu trong Web API bằng input formatter (bộ định dạng đầu vào) và output formatter (bộ định dạng đầu ra). Input formatter được dùng cho Model Binding (ràng buộc mô hình). Output formatter được dùng để định dạng response (phản hồi).

Framework cung cấp formatter tích hợp cho JSON và XML cho cả input và output. Nó cung cấp output formatter tích hợp cho plain text (văn bản thuần túy), nhưng không cung cấp input formatter cho plain text.

Bài viết này trình bày cách thêm hỗ trợ cho các định dạng bổ sung bằng cách tạo custom formatter.

Xem hoặc tải mã mẫu

Khi nào dùng custom formatter

Dùng custom formatter để thêm hỗ trợ cho content type (loại nội dung) không được formatter tích hợp xử lý.

Tổng quan cách tạo custom formatter

Để tạo custom formatter:

Tạo custom formatter

Để tạo formatter:

Đoạn code sau là lớp VcardOutputFormatter từ ứng dụng mẫu:

csharp
public class VcardOutputFormatter : TextOutputFormatter
{
    public VcardOutputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanWriteType(Type? type)
        => typeof(Contact).IsAssignableFrom(type)
            || typeof(IEnumerable<Contact>).IsAssignableFrom(type);

    public override async Task WriteResponseBodyAsync(
        OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;

        var logger = serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
        var buffer = new StringBuilder();

        if (context.Object is IEnumerable<Contact> contacts)
        {
            foreach (var contact in contacts)
            {
                FormatVcard(buffer, contact, logger);
            }
        }
        else
        {
            FormatVcard(buffer, (Contact)context.Object!, logger);
        }

        await httpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
    }

    private static void FormatVcard(
        StringBuilder buffer, Contact contact, ILogger logger)
    {
        buffer.AppendLine("BEGIN:VCARD");
        buffer.AppendLine("VERSION:2.1");
        buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
        buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
        buffer.AppendLine($"UID:{contact.Id}");
        buffer.AppendLine("END:VCARD");

        logger.LogInformation("Writing {FirstName} {LastName}",
            contact.FirstName, contact.LastName);
    }
}

Kế thừa từ base class phù hợp

Đối với media type văn bản (ví dụ vCard), kế thừa từ base class TextInputFormatter hoặc TextOutputFormatter:

csharp
public class VcardOutputFormatter : TextOutputFormatter

Đối với kiểu nhị phân (binary), kế thừa từ base class InputFormatter hoặc OutputFormatter.

Chỉ định media type và encoding được hỗ trợ

Trong constructor, chỉ định media type và encoding được hỗ trợ bằng cách thêm vào collection SupportedMediaTypesSupportedEncodings:

csharp
public VcardOutputFormatter()
{
    SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

    SupportedEncodings.Add(Encoding.UTF8);
    SupportedEncodings.Add(Encoding.Unicode);
}

Lớp formatter không thể dùng constructor injection (tiêm phụ thuộc qua constructor) cho các dependency (phụ thuộc). Ví dụ, không thể thêm ILogger<VcardOutputFormatter> làm tham số constructor. Để truy cập service (dịch vụ), hãy dùng đối tượng context được truyền vào các phương thức.

Override CanReadType và CanWriteType

Chỉ định kiểu cần giải tuần tự hóa hoặc tuần tự hóa bằng cách override phương thức CanReadType hoặc CanWriteType. Ví dụ để tạo văn bản vCard từ kiểu Contact và ngược lại:

csharp
protected override bool CanWriteType(Type? type)
    => typeof(Contact).IsAssignableFrom(type)
        || typeof(IEnumerable<Contact>).IsAssignableFrom(type);

Phương thức CanWriteResult

Trong một số trường hợp, phải override CanWriteResult thay vì CanWriteType. Dùng CanWriteResult khi:

Ví dụ, giả sử action method:

Để formatter chỉ xử lý đối tượng Student, hãy kiểm tra kiểu của Object trong đối tượng context cung cấp cho phương thức CanWriteResult. Khi action method trả về IActionResult, không cần dùng CanWriteResult — phương thức CanWriteType nhận kiểu lúc runtime.

Override ReadRequestBodyAsync và WriteResponseBodyAsync

Việc giải tuần tự hóa hoặc tuần tự hóa được thực hiện trong ReadRequestBodyAsync hoặc WriteResponseBodyAsync. Ví dụ sau trình bày cách lấy service từ DI container (container tiêm phụ thuộc). Không thể lấy service từ tham số constructor:

csharp
public override async Task WriteResponseBodyAsync(
    OutputFormatterWriteContext context, Encoding selectedEncoding)
{
    var httpContext = context.HttpContext;
    var serviceProvider = httpContext.RequestServices;

    var logger = serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
    var buffer = new StringBuilder();

    if (context.Object is IEnumerable<Contact> contacts)
    {
        foreach (var contact in contacts)
        {
            FormatVcard(buffer, contact, logger);
        }
    }
    else
    {
        FormatVcard(buffer, (Contact)context.Object!, logger);
    }

    await httpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
}

private static void FormatVcard(
    StringBuilder buffer, Contact contact, ILogger logger)
{
    buffer.AppendLine("BEGIN:VCARD");
    buffer.AppendLine("VERSION:2.1");
    buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
    buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
    buffer.AppendLine($"UID:{contact.Id}");
    buffer.AppendLine("END:VCARD");

    logger.LogInformation("Writing {FirstName} {LastName}",
        contact.FirstName, contact.LastName);
}

Cấu hình MVC để dùng custom formatter

Để dùng custom formatter, thêm instance của lớp formatter vào collection MvcOptions.InputFormatters hoặc MvcOptions.OutputFormatters:

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.InputFormatters.Insert(0, new VcardInputFormatter());
    options.OutputFormatters.Insert(0, new VcardOutputFormatter());
});

Formatter được đánh giá theo thứ tự chèn, formatter đầu tiên được ưu tiên.

Lớp VcardInputFormatter đầy đủ

Đoạn code sau là lớp VcardInputFormatter từ ứng dụng mẫu:

csharp
public class VcardInputFormatter : TextInputFormatter
{
    public VcardInputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanReadType(Type type)
        => type == typeof(Contact);

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(
        InputFormatterContext context, Encoding effectiveEncoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;

        var logger = serviceProvider.GetRequiredService<ILogger<VcardInputFormatter>>();

        using var reader = new StreamReader(httpContext.Request.Body, effectiveEncoding);
        string? nameLine = null;

        try
        {
            await ReadLineAsync("BEGIN:VCARD", reader, context, logger);
            await ReadLineAsync("VERSION:", reader, context, logger);

            nameLine = await ReadLineAsync("N:", reader, context, logger);

            var split = nameLine.Split(";".ToCharArray());
            var contact = new Contact(FirstName: split[1], LastName: split[0].Substring(2));

            await ReadLineAsync("FN:", reader, context, logger);
            await ReadLineAsync("END:VCARD", reader, context, logger);

            logger.LogInformation("nameLine = {nameLine}", nameLine);

            return await InputFormatterResult.SuccessAsync(contact);
        }
        catch
        {
            logger.LogError("Read failed: nameLine = {nameLine}", nameLine);
            return await InputFormatterResult.FailureAsync();
        }
    }

    private static async Task<string> ReadLineAsync(
        string expectedText, StreamReader reader, InputFormatterContext context,
        ILogger logger)
    {
        var line = await reader.ReadLineAsync();

        if (line is null || !line.StartsWith(expectedText))
        {
            var errorMessage = $"Looked for '{expectedText}' and got '{line}'";

            context.ModelState.TryAddModelError(context.ModelName, errorMessage);
            logger.LogError(errorMessage);

            throw new Exception(errorMessage);
        }

        return line;
    }
}

Kiểm thử (Test) ứng dụng

Chạy ứng dụng mẫu, triển khai input và output formatter vCard cơ bản. Ứng dụng đọc và ghi vCard theo định dạng sau:

code
BEGIN:VCARD
VERSION:2.1
N:Davolio;Nancy
FN:Nancy Davolio
END:VCARD

Để xem kết quả vCard output, chạy ứng dụng và gửi request GET với Accept header text/vcard tới https://localhost:<port>/api/contacts.

Để thêm vCard vào collection contacts trong bộ nhớ:

Tài nguyên bổ sung