前言

我们在使用
ASP.NET Core WebApi
时它支持使用指定的输入和输出格式来交换数据。输入数据靠模型绑定的机制处理,输出数据则需要用格式化的方式进行处理。
ASP.NET Core
框架已经内置了处理
JSON

XML
的输入和输出方式,默认的情况我们提交
JSON
格式的内容,它可以自行进行模型绑定,也可以把对象类型的返回值输出成
JSON
格式,这都归功于内置的
JSON
格式化程序。本篇文章我们将通过自定义一个
YAML
格式的转换器开始,逐步了解它到底是如何工作的。以及通过自带的
JSON
格式化输入输出源码,加深对
Formatter
程序的了解。

自定义开始

要想先了解
Formatter
的工作原理,当然需要从自定义开始。因为一般自定义的时候我们一般会选用自己最简单最擅长的方式去扩展,然后逐步完善加深理解。格式化器分为两种,一种是用来处理输入数据格式的
InputFormatter
,另一种是用来处理返回数据格式的
OutputFormatter
。本篇文章示例,我们从自定义
YAML
格式的转换器开始。因为目前
YAML
格式确实比较流行,得益于它简单明了的格式,目前也有很多中间件都是用
YAML
格式。这里我们使用的是
YamlDotNet
这款组件,具体的引入信息如下所示

<PackageReference Include="YamlDotNet" Version="15.1.0" />

YamlInputFormatter

首先我们来看一下自定义请求数据的格式化也就是
InputFormatter
,它用来处理了请求数据的格式,也就是我们在
Http请求体
里的数据格式如何处理,手下我们需要定义个
YamlInputFormatter
类,继承自
TextInputFormatter
抽象类

public class YamlInputFormatter : TextInputFormatter
{
    private readonly IDeserializer _deserializer;

    public YamlInputFormatter(DeserializerBuilder deserializerBuilder)
    {
        _deserializer = deserializerBuilder.Build();

        //添加与之绑定的MediaType,这里其实绑定的提交的ContentType的值
        //如果请求ContentType:text/yaml或ContentType:text/yml才能命中该YamlInputFormatter
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/yaml"));
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/yml"));
        
        //添加编码类型比如application/json;charset=UTF-8后面的这种charset
        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(encoding);

        //获取请求Body
        var readStream = context.HttpContext.Request.Body;
        
        object? model;
        try
        {
            TextReader textReader = new StreamReader(readStream);
            //获取Action参数类型
            var type = context.ModelType;
            //把yaml字符串转换成具体的对象
            model = _deserializer.Deserialize(textReader, type);
        }
        catch (YamlException ex) 
        {
            context.ModelState.TryAddModelError(context.ModelName, ex.Message);
            throw new InputFormatterException("反序列化输入数据时出错\n\n", ex.InnerException!);
        }

        if (model == null && !context.TreatEmptyInputAsDefaultValue)
        {
            return InputFormatterResult.NoValue();
        }
        else
        {
            return InputFormatterResult.Success(model);
        }
    }
}

这里需要注意的是配置
SupportedMediaTypes
,也就是添加与
YamlInputFormatter
绑定的
MediaType
,也就是我们请求时设置的
Content-Type
的值,这个配置是必须要的,否则没办法判断当前
YamlInputFormatter
与哪种
Content-Type
进行绑定。接下来定义完了之后如何把它接入程序使用它呢?也很简单在
MvcOptions
中配置即可,如下所示

builder.Services.AddControllers(options => {
    options.InputFormatters.Add(new YamlInputFormatter(new DeserializerBuilder()));
});

接下来我们定义一个简单类型和Action来演示一下,类和代码不具备任何实际意义,只是为了演示

[HttpPost("AddAddress")]
public Address AddAddress(Address address)
{
    return address;
}

public class Address
{
    public string City { get; set; }
    public string Country { get; set; }
    public string Phone { get; set; }
    public string ZipCode { get; set; }
    public List<string> Tags { get; set; }
}

我们用
Postman
测试一下,提交一个
yaml
类型的格式,效果如下所示

这里需要注意的是我们需要在
Postman
中设置
Content-Type

text/yml

text/yaml

YamlOutputFormatter

上面我们演示了如何定义
InputFormatter
它的作用是将请求的数据格式化成具体类型。无独有偶,既然请求数据格式可以定义,那么输出的数据格式同样可以定义,这里就需要用到
OutputFormatter
。接下来我们定义一个
YamlOutputFormatter
继承自
TextOutputFormatter
抽象类,代码如下所示

public class YamlOutputFormatter : TextOutputFormatter
{
    private readonly ISerializer _serializer;

    public YamlOutputFormatter(SerializerBuilder serializerBuilder)
    {
        //添加与之绑定的MediaType,这里其实绑定的提交的Accept的值
        //如果请求Accept:text/yaml或Accept:text/yml才能命中该YamlOutputFormatter
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/yaml"));
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/yml"));

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

        _serializer = serializerBuilder.Build();
    }

    public override bool CanWriteResult(OutputFormatterCanWriteContext context)
    {
        //什么条件可以使用yaml结果输出,至于为什么要重写CanWriteResult方法,我们在后面分析源码的时候会解释
        string accept = context.HttpContext.Request.Headers.Accept.ToString() ?? "";
        if (string.IsNullOrWhiteSpace(accept))
        {
            return false;
        }

        var parsedContentType = new MediaType(accept);
        for (var i = 0; i < SupportedMediaTypes.Count; i++)
        {
            var supportedMediaType = new MediaType(SupportedMediaTypes[i]);
            if (parsedContentType.IsSubsetOf(supportedMediaType))
            {
                return true;
            }
        }
        return false;
    }

    public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(selectedEncoding);

        try
        {
            var httpContext = context.HttpContext;
            //获取输出的对象,转成成yaml字符串并输出
            string respContent = _serializer.Serialize(context.Object);
            await httpContext.Response.WriteAsync(respContent);
        }
        catch (YamlException ex)
        {
            throw new InputFormatterException("序列化输入数据时出错\n\n", ex.InnerException!);
        }
    }
}

同样的这里我们也添加了
SupportedMediaTypes
的值,它的作用是我们请求时设置的
Accept
的值,这个配置也是必须要的,也就是请求的头中为
Accept:text/yaml

Accept:text/yml
才能命中该
YamlOutputFormatter
。配置的时候同样也在
MvcOptions
中配置即可

builder.Services.AddControllers(options => {
    options.OutputFormatters.Add(new YamlOutputFormatter(new SerializerBuilder()));
});

接下来我们同样还是使用上面的代码进行演示,只是我们这里更换一下重新设置一下相关Header即可,这次我们直接提交
json
类型的数据,它会输出
yaml
格式,代码什么的完全不用变,结果如下所示

这里需要注意的请求头的设置发生了变化

小结

上面我们讲解了控制请求数据格式的
TextInputFormatter
和控制输出格式的
TextOutputFormatter
。其中
InputFormatter
负责给
ModelBinding
输送类型对象,
OutputFormatter
负责给
ObjectResult
输出值,这我们可以看到它们只能控制
WebAPI

Controller/Action
的且返回
ObjectResult
的这种情况才生效,其它的比如
MinimalApi

GRPC
是起不到效果的。通过上面的示例,有同学心里可能会存在疑问,上面在
AddControllers
方法中注册
TextInputFormatter

TextOutputFormatter
的时候,没办法完成注入的服务,比如如果
YamlInputFormatter

YamlOutputFormatter
构造实例的时候无法获取
DI容器
中的实例。确实,如果使用上面的方式我们确实没办法完成这个需求,不过我们可以通过其它方法实现,那就是去扩展
MvcOptions
选项,实现如下所示

public class YamlMvcOptionsSetup : IConfigureOptions<MvcOptions>
{
    private readonly ILoggerFactory _loggerFactory;

    public YamlMvcOptionsSetup(ILoggerFactory loggerFactory)
    {
        _loggerFactory = loggerFactory;
    }

    public void Configure(MvcOptions options)
    {
        var yamlInputLogger = _loggerFactory.CreateLogger<YamlInputFormatter>();
        options.InputFormatters.Add(new YamlInputFormatter(new DeserializerBuilder()));

        var yamlOutputLogger = _loggerFactory.CreateLogger<YamlOutputFormatter>();
        options.OutputFormatters.Add(new YamlOutputFormatter(new SerializerBuilder()));
    }
}

我们定义了
YamlMvcOptionsSetup
去扩展
MvcOptions
选项,然后我们将
YamlMvcOptionsSetup
注册到容器即可

builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, YamlMvcOptionsSetup>());

探究工作方式

上面我们演示了如何自定义
InputFormatter

OutputFormatter
,也讲解了
InputFormatter
负责给
ModelBinding
输送类型对象,
OutputFormatter
负责给
ObjectResult
输出值。接下来我们就通过阅读其中的源码来看一下
InputFormatter

OutputFormatter
是如何工作来影响
模型绑定

ObjectResult
的结果。

需要注意的是!我们展示的源码是删减过的,只关注我们需要关注的地方,因为源码中涉及的内容太多,不方便观看,所以只保留我们关注的地方,还望谅解。

TextInputFormatter如何工作


上面我们看到了
YamlInputFormatter
是继承了
TextInputFormatter
抽象类,并重写了
ReadRequestBodyAsync
方法。接下来我们就从
TextInputFormatter

ReadRequestBodyAsync
方法来入手,我们来看一下源码定义[

点击查看TextInputFormatter源码

标签: none

添加新评论