钱行慕

导航

【译】ASP.NET Core Web API中的自定义格式化器

原文链接:传送门

ASP.NET MVC使用输入输出格式化器在Web API中支持数据交换。输入格式化器用在模型绑定中,而输出格式化器用来格式化响应数据。

框架为JSON和XML提供了内置的输入及输出格式化器。其也为纯文本提供了内置的输出格式化器,但其并没有为纯文本提供内置的输入格式化器。

这篇文章展示了如何通过创建自定义的格式化器来添加对其他额外格式的支持。至于自定义的纯文本输入格式化器的示例,请参考TextPlainInputFormatter

什么时候使用自定义格式化器

使用自定义的格式化器来添加对内置的格式化器不能处理的Content-type的支持。

如何使用自定义格式化器:概述

为了 创建自定义的格式化器:

  • 为了序列化发送给客户端的数据,创建一个输出格式化器类
  • 为了反序列化从客户端接收到的数据,创建一个输入格式化器类
  • MvcOptions中给InputFormatters 和OutputFormatters集合添加创建的格式化器的实例

如何创建一个自定义格式化器类

为了创建一个格式化器:

如下实例展示了示例代码中的VcardOutputFormatter类:

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
        {
            FormatVcard(buffer, (Contact)context.Object, logger);
        }

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

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

从合适的基类派生

对于文本媒体类型(比如vCard),从 TextInputFormatter 或者 TextOutputFormatter 基类进行派生。

public class VcardOutputFormatter : TextOutputFormatter

对于二进制类型,从 InputFormatter 或者 OutputFormatter 基类进行派生。

指定有效的媒体类型和编码

在构造函数中,通过添加给  SupportedMediaTypes 和SupportedEncodings集合来指定有效的媒体类型和编码。

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

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

一个格式化器不能为其依赖进行构造函数注入。举个例子,ILogger<VcardOutputFormatter> 不能作为作为参数添加给构造函数。为了访问服务,请使用方法中的上下文对象。举个例子,从Contact 类型创建一个vCard文本,反之亦然。

protected override bool CanWriteType(Type type)
{
    return typeof(Contact).IsAssignableFrom(type) ||
        typeof(IEnumerable<Contact>).IsAssignableFrom(type);
}

CanWriteResult 方法

在某些场景下,CanWriteResult方法必须被重写,而不是CanWriteType方法。如果满足以下条件,请使用CanWriteResult:

  • Action方法返回了一个模型类
  • 运行时可能会返回派生类
  • Action返回的派生类必须是运行时可知的

举个例子,假设Action:

  • 签名返回一个Person类
  • 可以返回一个派生自Person类的Studen类或者Instructor类型

对于仅能 处理Person对象的格式化器,检查提供给CanWriteResult方CanWriteType 法的上下文对象中的 Object 类型,当Action方法返回一个IActionResult时:

  • 不必使用CanWriteResult
  • CanWriteType 方法接收运行时类型

重写ReadRequestBodyAsync 和WriteResponseBodyAsync

序列化及反序列化在ReadRequestBodyAsync 或WriteResponseBodyAsync方法中执行。如下的示例展示了如何从依赖注入容器中获得服务。服务不能从构造函数参数中获得。

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());
}

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

如何配置MVC来使用自定义格式化器

为了使用自定义格式化器,你需要向InputFormatters 或OutputFormatters集合添加格式化器的实例。

.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
    {
        options.InputFormatters.Insert(0, new VcardInputFormatter());
        options.OutputFormatters.Insert(0, new VcardOutputFormatter());
    });
}

完整的VcardInputFormatter类

如下示例展示了来自示例代码中的VcardInputFormatter类:

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 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
            {
                LastName = split[0].Substring(2),
                FirstName = split[1]
            };

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

测试app

运行本章节的示例代码,其实现了基本的输入输出格式化器。app会读写类似于下的vCards数据:

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

为了查看vCard的输出,运行app并发送一个Content-type为;text/vcard的请求给https://localhost:5001/api/contacts。

为了给内存中的Contacts集合添加一个vCard:

  • 使用比如Postman这样的工具向/api/contacts发送一个Post请求
  • 将 Content-Type 头设置为text/vcard
  • 在请求体中设置vCard文本,格式像上述示例那样

额外资源

posted on 2020-11-16 13:53  钱行慕  阅读(455)  评论(0编辑  收藏  举报