Custom formatters (bộ định dạng tùy chỉnh) trong 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.
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:
- Để tuần tự hóa dữ liệu gửi đến client, tạo lớp output formatter.
- Để giải tuần tự hóa dữ liệu nhận từ client, tạo lớp input formatter.
- Thêm instance (thực thể) của lớp formatter vào collection
InputFormattersvàOutputFormatterstrongMvcOptions.
Tạo custom formatter
Để tạo formatter:
- Kế thừa lớp từ base class (lớp cơ sở) phù hợp. Ứng dụng mẫu kế thừa từ
TextOutputFormattervàTextInputFormatter. - Chỉ định các media type và encoding được hỗ trợ trong constructor (hàm khởi tạo).
- Override (ghi đè) phương thức
CanReadTypevàCanWriteType. - Override phương thức
ReadRequestBodyAsyncvàWriteResponseBodyAsync.
Đoạn code sau là lớp VcardOutputFormatter từ ứng dụng mẫu:
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:
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 SupportedMediaTypes và SupportedEncodings:
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:
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:
- Action method trả về kiểu model.
- Có các lớp dẫn xuất (derived class) có thể được trả về lúc runtime (thời gian chạy).
- Lớp dẫn xuất mà action trả về phải được biết tại runtime.
Ví dụ, giả sử action method:
- Signature khai báo trả về kiểu
Person. - Có thể trả về kiểu
StudenthoặcInstructorkế thừa từPerson.
Để 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:
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:
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:
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:
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ớ:
- Gửi request
Posttới/api/contactsbằng công cụ như http-repl. - Đặt
Content-Typeheader làtext/vcard. - Đặt văn bản vCard trong body theo định dạng trên.