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

作者: Kirk LarkinTom DykstraBy Kirk Larkin and Tom Dykstra.

ASP.NET Core MVC 使用输入和输出格式化程序支持 Web API 中的数据交换。ASP.NET Core MVC supports data exchange in Web APIs using input and output formatters. 模型绑定使用输入格式化程序。Input formatters are used by Model Binding. 输出格式化程序用于设置响应的格式Output formatters are used to format responses.

该框架为 JSON 和 XML 提供内置的输入和输出格式化程序。The framework provides built-in input and output formatters for JSON and XML. 它为纯文本提供内置的输出格式化程序,但不为纯文本提供输入格式化程序。It provides a built-in output formatter for plain text, but doesn't provide an input formatter for plain text.

本文展示如何通过创建自定义格式化程序,添加对其他格式的支持。This article shows how to add support for additional formats by creating custom formatters. 有关自定义纯文本输入格式化程序的示例,请参阅 GitHub 上的TextPlainInputFormatterFor an example of a custom plain text input formatter, see TextPlainInputFormatter on GitHub.

查看或下载示例代码如何下载View or download sample code (how to download)

何时使用自定义格式化程序When to use custom formatters

使用自定义格式化程序添加对不是由内置格式化程序处理的内容类型的支持。Use a custom formatter to add support for a content type that isn't handled by the built-in formatters.

有关如何使用自定义格式化程序的概述Overview of how to use a custom formatter

创建自定义格式化程序:To create a custom formatter:

  • 若要对发送到客户端的数据进行序列化,请创建输出格式化程序类。For serializing data sent to the client, create an output formatter class.
  • 若要对从客户端收到的数据进行反序列化,请创建一个输入格式化程序类。For deserializing data received from the client, create an input formatter class.
  • 将格式化程序类的实例添加 InputFormattersOutputFormatters 中的和集合 MvcOptionsAdd instances of formatter classes to the InputFormatters and OutputFormatters collections in MvcOptions.

如何创建自定义格式化程序类How to create a custom formatter class

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

下面的代码演示了 VcardOutputFormatter 示例中的类:The following code shows the VcardOutputFormatter class from the sample:

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

从相应的基类中派生Derive from the appropriate base class

对于文本媒体类型(例如 vCard),派生自 TextInputFormatterTextOutputFormatter 基类。For text media types (for example, vCard), derive from the TextInputFormatter or TextOutputFormatter base class.

public class VcardOutputFormatter : TextOutputFormatter

对于二进制类型,派生自 InputFormatterOutputFormatter 基类。For binary types, derive from the InputFormatter or OutputFormatter base class.

指定有效的媒体类型和编码Specify valid media types and encodings

在构造函数中,通过添加到 SupportedMediaTypesSupportedEncodings 集合来指定有效的媒体类型和编码。In the constructor, specify valid media types and encodings by adding to the SupportedMediaTypes and SupportedEncodings collections.

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

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

格式化程序类能将构造函数注入用于其依赖项。A formatter class can not use constructor injection for its dependencies. 例如, ILogger<VcardOutputFormatter> 不能作为参数添加到构造函数。For example, ILogger<VcardOutputFormatter> cannot be added as a parameter to the constructor. 若要访问服务,请使用传递给方法的上下文对象。To access services, use the context object that gets passed in to the methods. 本文中的代码示例和示例演示了如何执行此操作。A code example in this article and the sample show how to do this.

重写 CanReadType 和 CanWriteTypeOverride CanReadType and CanWriteType

通过重写或方法指定要反序列化或序列化的类型 CanReadType CanWriteTypeSpecify the type to deserialize into or serialize from by overriding the CanReadType or CanWriteType methods. 例如,从某一类型创建 vCard 文本, Contact 反之亦然。For example, creating vCard text from a Contact type and vice versa.

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

CanWriteResult 方法The CanWriteResult method

在某些情况下, CanWriteResult 必须重写而不是 CanWriteTypeIn some scenarios, CanWriteResult must be overridden rather than CanWriteType. 如果满足以下条件,则使用 CanWriteResultUse CanWriteResult if the following conditions are true:

  • 操作方法返回模型类。The action method returns a model class.
  • 具有可能在运行时返回的派生类。There are derived classes which might be returned at runtime.
  • 操作返回的派生类必须在运行时已知。The derived class returned by the action must be known at runtime.

例如,假定操作方法:For example, suppose the action method:

  • 签名返回一个 Person 类型。Signature returns a Person type.
  • 可以返回 Student Instructor 派生自的或类型 PersonCan return a Student or Instructor type that derives from Person.

要使格式化程序仅处理 Student 对象,请 Object 在提供给方法的上下文对象中检查的类型 CanWriteResultFor the formatter to handle only Student objects, check the type of Object in the context object provided to the CanWriteResult method. 当操作方法返回时 IActionResultWhen the action method returns IActionResult:

  • 不需要使用 CanWriteResultIt's not necessary to use CanWriteResult.
  • CanWriteType方法接收运行时类型。The CanWriteType method receives the runtime type.

重写 ReadRequestBodyAsync 和 WriteResponseBodyAsyncOverride ReadRequestBodyAsync and WriteResponseBodyAsync

反序列化或序列化在或中执行 ReadRequestBodyAsync WriteResponseBodyAsyncDeserialization or serialization is performed in ReadRequestBodyAsync or WriteResponseBodyAsync. 下面的示例演示如何从依赖关系注入容器中获取服务。The following example shows how to get services from the dependency injection container. 无法从构造函数参数获取服务。Services can't be obtained from constructor parameters.

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 配置为使用自定义格式化程序How to configure MVC to use a custom formatter

若要使用自定义格式化程序,请将格式化程序类的实例添加到 InputFormattersOutputFormatters 集合。To use a custom formatter, add an instance of the formatter class to the InputFormatters or OutputFormatters collection.

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

按格式化程序的插入顺序对其进行计算。Formatters are evaluated in the order you insert them. 第一个优先。The first one takes precedence.

完整 VcardInputFormatterThe complete VcardInputFormatter class

下面的代码演示了 VcardInputFormatter 示例中的类:The following code shows the VcardInputFormatter class from the sample:

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

测试应用Test the app

运行本文的示例应用程序,它实现基本 vCard 输入和输出格式化程序。Run the sample app for this article, which implements basic vCard input and output formatters. 此应用程序读取和写入电子名片,如下所示:The app reads and writes vCards similar to the following:

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

若要查看 vCard 输出,请运行应用,并将 Get 请求发送到 Accept 标头 text/vcard https://localhost:5001/api/contactsTo see vCard output, run the app and send a Get request with Accept header text/vcard to https://localhost:5001/api/contacts.

将 vCard 添加到内存中的联系人集合:To add a vCard to the in-memory collection of contacts:

  • Post /api/contacts 使用 Postman 之类的工具将请求发送到。Send a Post request to /api/contacts with a tool like Postman.
  • Content-Type 标头设置为 text/vcardSet the Content-Type header to text/vcard.
  • 设置 vCard 正文中的文本,格式与前面的示例类似。Set vCard text in the body, formatted like the preceding example.

其他资源Additional resources