ASP.NET Core Web API 中的自定义格式化程序

ASP.NET Core MVC 使用输入和输出格式化程序支持 Web API 中的数据交换。 模型绑定使用输入格式化程序。 格式响应使用输出格式化程序。

该框架为 JSON 和 XML 提供内置的输入和输出格式化程序。 它为纯文本提供内置的输出格式化程序,但不为纯文本提供输入格式化程序。

本文展示如何通过创建自定义格式化程序,添加对其他格式的支持。 有关自定义纯文本输入格式化程序的示例,请参阅 GitHub 上的 TextPlainInputFormatter

查看或下载示例代码如何下载

何时使用自定义格式化程序

使用自定义格式化程序为内置格式化程序无法处理的内容类型添加支持。

有关如何使用自定义格式化程序的概述

若要创建自定义格式化程序:

  • 以序列化发送到客户端的数据,请创建输出格式化程序类。
  • 以反序列化从客户端收到的数据,请创建输入格式化程序类。
  • 将格式化程序类的实例添加到 MvcOptions 中的 InputFormattersOutputFormatters 集合。

创建自定义格式化程序

若要创建格式化程序,请执行以下操作:

以下代码展示了示例中的 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)
        => 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);
    }
}

从相应的基类中派生

对于文本媒体类型(例如 vCard),从 TextInputFormatterTextOutputFormatter 基类派生。

public class VcardOutputFormatter : TextOutputFormatter

对于二进制类型,从 InputFormatterOutputFormatter 基类派生。

指定支持的媒体类型和编码

在构造函数中,通过添加到 SupportedMediaTypesSupportedEncodings 集合来指定支持的媒体类型和编码。

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

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

格式化程序类不能对其依赖项使用构造函数注入。 例如,ILogger<VcardOutputFormatter> 不能作为参数添加到构造函数。 若要访问服务,请使用传入方法的上下文对象。 本文中的代码示例和示例展示了如何执行此操作。

替代 CanReadType 和 CanWriteType

通过替代 CanReadTypeCanWriteType 方法,指定要反序列化为或从其序列化的类型。 例如,从 Contact 类型创建 vCard 文本,反之亦然。

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

CanWriteResult 方法

在某些情况下,必须替代 CanWriteResult 而不是 CanWriteType。 如果满足以下条件,则使用 CanWriteResult

  • 操作方法返回模型类。
  • 存在可能在运行时返回的派生类。
  • 操作返回的派生类必须在运行时已知。

例如,假定操作方法:

  • 签名返回 Person 类型。
  • 可以返回派生自 PersonStudentInstructor 类型。

若要让格式化程序仅处理 Student 对象,请检查提供给 CanWriteResult 方法的上下文对象中的 Object 类型。 当操作方法返回 IActionResult 时:

  • 无需使用 CanWriteResult
  • CanWriteType 方法接收运行时类型。

替代 ReadRequestBodyAsync 和 WriteResponseBodyAsync

反序列化或序列化在 ReadRequestBodyAsyncWriteResponseBodyAsync 中执行。 以下示例展示如何从依赖项注入容器中获取服务。 无法从构造函数参数中获取服务。

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

如何将 MVC 配置为使用自定义格式化程序

若要使用自定义格式化程序,请将格式化程序类的实例添加到 MvcOptions.InputFormattersMvcOptions.OutputFormatters 集合:

var builder = WebApplication.CreateBuilder(args);

builder.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)
        => 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;
    }
}

测试应用程序

运行本文的示例应用,它将实现基本的 vCard 输入和输出格式化程序。 该应用读取和写入类似于以下格式的 vCard:

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

若要查看 vCard 输出,请运行应用并向 https://localhost:<port>/api/contacts 发送带有 Accept 标头 text/vcard 的 Get 请求。

若要将 vCard 添加到内存中的联系人集合,请执行以下操作:

  • 使用 http-repl 等工具向 /api/contacts 发送 Post 请求。
  • Content-Type 标头设置为 text/vcard
  • 设置主体中的 vCard 文本,格式与前面的示例类似。

其他资源

ASP.NET Core MVC 使用输入和输出格式化程序支持 Web API 中的数据交换。 模型绑定使用输入格式化程序。 格式响应使用输出格式化程序。

该框架为 JSON 和 XML 提供内置的输入和输出格式化程序。 它为纯文本提供内置的输出格式化程序,但不为纯文本提供输入格式化程序。

本文展示如何通过创建自定义格式化程序,添加对其他格式的支持。 有关自定义纯文本输入格式化程序的示例,请参阅 GitHub 上的 TextPlainInputFormatter

查看或下载示例代码如何下载

何时使用自定义格式化程序

使用自定义格式化程序为内置格式化程序无法处理的内容类型添加支持。

有关如何使用自定义格式化程序的概述

若要创建自定义格式化程序:

  • 以序列化发送到客户端的数据,请创建输出格式化程序类。
  • 以反序列化从客户端收到的数据,请创建输入格式化程序类。
  • 将格式化程序类的实例添加到 MvcOptions 中的 InputFormattersOutputFormatters 集合。

创建自定义格式化程序

若要创建格式化程序,请执行以下操作:

以下代码展示了示例中的 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(), 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);
    }
}

从相应的基类中派生

对于文本媒体类型(例如 vCard),从 TextInputFormatterTextOutputFormatter 基类派生。

public class VcardOutputFormatter : TextOutputFormatter

对于二进制类型,从 InputFormatterOutputFormatter 基类派生。

指定支持的媒体类型和编码

在构造函数中,通过添加到 SupportedMediaTypesSupportedEncodings 集合来指定支持的媒体类型和编码。

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

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

格式化程序类不能对其依赖项使用构造函数注入。 例如,ILogger<VcardOutputFormatter> 不能作为参数添加到构造函数。 若要访问服务,请使用传入方法的上下文对象。 本文中的代码示例和示例展示了如何执行此操作。

替代 CanReadType 和 CanWriteType

通过替代 CanReadTypeCanWriteType 方法,指定要反序列化为或从其序列化的类型。 例如,从 Contact 类型创建 vCard 文本,反之亦然。

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

CanWriteResult 方法

在某些情况下,必须替代 CanWriteResult 而不是 CanWriteType。 如果满足以下条件,则使用 CanWriteResult

  • 操作方法返回模型类。
  • 存在可能在运行时返回的派生类。
  • 操作返回的派生类必须在运行时已知。

例如,假定操作方法:

  • 签名返回 Person 类型。
  • 可以返回派生自 PersonStudentInstructor 类型。

若要让格式化程序仅处理 Student 对象,请检查提供给 CanWriteResult 方法的上下文对象中的 Object 类型。 当操作方法返回 IActionResult 时:

  • 无需使用 CanWriteResult
  • CanWriteType 方法接收运行时类型。

替代 ReadRequestBodyAsync 和 WriteResponseBodyAsync

反序列化或序列化在 ReadRequestBodyAsyncWriteResponseBodyAsync 中执行。 以下示例展示如何从依赖项注入容器中获取服务。 无法从构造函数参数中获取服务。

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

如何将 MVC 配置为使用自定义格式化程序

若要使用自定义格式化程序,请将格式化程序类的实例添加到 MvcOptions.InputFormattersMvcOptions.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;
    }
}

测试应用程序

运行本文的示例应用,它将实现基本的 vCard 输入和输出格式化程序。 该应用读取和写入类似于以下格式的 vCard:

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

若要查看 vCard 输出,请运行应用并向 https://localhost:5001/api/contacts 发送带有 Accept 标头 text/vcard 的 Get 请求。

若要将 vCard 添加到内存中的联系人集合,请执行以下操作:

  • 使用 curl 等工具向 /api/contacts 发送 Post 请求。
  • Content-Type 标头设置为 text/vcard
  • 设置主体中的 vCard 文本,格式与前面的示例类似。

其他资源