首页
/ ASP.NET Core Web API 自定义格式化器深度解析

ASP.NET Core Web API 自定义格式化器深度解析

2025-07-06 04:02:52作者:幸俭卉

什么是自定义格式化器

在 ASP.NET Core Web API 中,格式化器负责处理请求和响应数据的序列化与反序列化。系统内置了 JSON 和 XML 格式化器,但当我们需要支持其他数据格式时,就需要创建自定义格式化器。

何时需要使用自定义格式化器

以下场景适合使用自定义格式化器:

  • 需要支持系统未内置的数据格式(如 vCard、Protobuf 等)
  • 需要对现有格式进行特殊处理
  • 需要实现特定业务场景下的数据转换

创建自定义格式化器的步骤

1. 选择基类

根据处理的数据类型选择合适的基类:

  • 文本数据:继承 TextInputFormatterTextOutputFormatter
  • 二进制数据:继承 InputFormatterOutputFormatter

2. 配置支持的媒体类型和编码

在构造函数中指定支持的媒体类型和编码:

public VcardOutputFormatter()
{
    SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));
    SupportedEncodings.Add(Encoding.UTF8);
    SupportedEncodings.Add(Encoding.Unicode);
}

3. 实现核心方法

对于输出格式化器:

  • CanWriteType:确定格式化器能否处理特定类型
  • WriteResponseBodyAsync:实现实际的序列化逻辑

对于输入格式化器:

  • CanReadType:确定格式化器能否处理特定类型
  • ReadRequestBodyAsync:实现实际的反序列化逻辑

实战示例:vCard 格式化器

输出格式化器实现

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)
    {
        return 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
        {
            var contact = context.Object as Contact;
            FormatVcard(buffer, contact, 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("END:VCARD");
        logger.LogInformation($"Writing {contact.FirstName} {contact.LastName}");
    }
}

输入格式化器实现

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)
    {
        return 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 ReadUntilCondition(reader, (line) => line.StartsWith("FN:"));
            nameLine = await reader.ReadLineAsync();
            
            var contact = new Contact
            {
                FirstName = nameLine.Substring(3).Split(' ')[0],
                LastName = nameLine.Substring(3).Split(' ')[1]
            };
            
            logger.LogInformation("Read contact {FirstName} {LastName}", 
                contact.FirstName, contact.LastName);
                
            return await InputFormatterResult.SuccessAsync(contact);
        }
        catch
        {
            logger.LogError("Read failed");
            return await InputFormatterResult.FailureAsync();
        }
    }
    
    private async Task ReadUntilCondition(TextReader reader, Func<string, bool> condition)
    {
        string line;
        while ((line = await reader.ReadLineAsync()) != null)
        {
            if (condition(line)) break;
        }
    }
}

注册自定义格式化器

在服务配置中注册自定义格式化器:

var builder = WebApplication.CreateBuilder(args);

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

测试自定义格式化器

测试输出格式化器

发送 GET 请求到 /api/contacts,并设置 Accept 头为 text/vcard,将收到类似以下格式的响应:

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

测试输入格式化器

发送 POST 请求到 /api/contacts

  • 设置 Content-Type 头为 text/vcard
  • 请求体包含 vCard 格式的数据

高级主题

处理派生类型

当处理可能返回派生类型的操作时,可以重写 CanWriteResult 方法:

public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
    if (context.Object is Student)
    {
        return base.CanWriteResult(context);
    }
    return false;
}

性能考虑

  • 对于高吞吐量场景,考虑使用缓冲和池化技术
  • 避免在格式化器中进行复杂的计算
  • 考虑使用异步操作处理大型数据流

最佳实践

  1. 明确职责:格式化器应只关注数据的序列化和反序列化,不包含业务逻辑
  2. 错误处理:提供清晰的错误信息和日志记录
  3. 性能优化:对于文本格式化器,使用 StringBuilder 而不是字符串连接
  4. 编码支持:明确支持多种编码以增强兼容性
  5. 依赖注入:通过上下文获取服务,而不是构造函数注入

通过本文的详细讲解,您应该已经掌握了在 ASP.NET Core Web API 中创建和使用自定义格式化器的完整流程。自定义格式化器是扩展 Web API 功能的有力工具,合理使用可以大大增强 API 的灵活性和适用性。