当前位置: 欣欣网 > 码农

细聊ASP.NET Core WebAPI格式化程序

2024-02-27码农

前言

我们在使用 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 classYamlInputFormatter : TextInputFormatter
{
privatereadonly IDeserializer _deserializer;
publicYamlInputFormatter(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);
}
publicoverrideasync 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);
thrownew 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 classAddress
{
publicstring City { getset; }
publicstring Country { getset; }
publicstring Phone { getset; }
publicstring ZipCode { getset; }
public List<string> Tags { getset; }
}

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

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

YamlOutputFormatter

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

public classYamlOutputFormatter : TextOutputFormatter
{
privatereadonly ISerializer _serializer;
publicYamlOutputFormatter(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();
}
publicoverrideboolCanWriteResult(OutputFormatterCanWriteContext context)
{
//什么条件可以使用yaml结果输出,至于为什么要重写CanWriteResult方法,我们在后面分析源码的时候会解释
string accept = context.HttpContext.Request.Headers.Accept.ToString() ?? "";
if (string.IsNullOrWhiteSpace(accept))
{
returnfalse;
}
var parsedContentType = new MediaType(accept);
for (var i = 0; i < SupportedMediaTypes.Count; i++)
{
var supportedMediaType = new MediaType(SupportedMediaTypes[i]);
if (parsedContentType.IsSubsetOf(supportedMediaType))
{
returntrue;
}
}
returnfalse;
}
publicoverrideasync 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)
{
thrownew 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 classYamlMvcOptionsSetup : IConfigureOptions<MvcOptions>
{
privatereadonly ILoggerFactory _loggerFactory;
publicYamlMvcOptionsSetup(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
}
publicvoidConfigure(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源码👈 [1] ]

publicabstract classTextInputFormatter : InputFormatter
{
public IList<Encoding> SupportedEncodings { get; } = new List<Encoding>();
publicoverride Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
//判断Encoding是否符合我们设置的SupportedEncodings中的值
var selectedEncoding = SelectCharacterEncoding(context);
if (selectedEncoding == null)
{
var exception = new UnsupportedContentTypeException(message);
context.ModelState.AddModelError(context.ModelName, exception, context.Metadata);
return InputFormatterResult.FailureAsync();
}
//这里调用了ReadRequestBodyAsync方法
return ReadRequestBodyAsync(context, selectedEncoding);
}
//这就是我们在YamlInputFormatter中实现的ReadRequestBodyAsync方法
publicabstract Task<InputFormatterResult> ReadRequestBodyAsync(
InputFormatterContext context,
Encoding encoding);
protected Encoding? SelectCharacterEncoding(InputFormatterContext context)
{
var requestContentType = context.HttpContext.Request.ContentType;
//解析ContentType
var requestMediaType = string.IsNullOrEmpty(requestContentType) ? default : new MediaType(requestContentType);
if (requestMediaType.Charset.HasValue)
{
var requestEncoding = requestMediaType.Encoding;
if (requestEncoding != null)
{
//在我们设置SupportedEncodings的查找符合ContentType中包含值的
for (int i = 0; i < SupportedEncodings.Count; i++)
{
if (string.Equals(requestEncoding.WebName, SupportedEncodings[i].WebName, StringComparison.OrdinalIgnoreCase))
{
return SupportedEncodings[i];
}
}
}
returnnull;
}
return SupportedEncodings[0];
}
}



整体来说 TextInputFormatter 抽象类思路相对清晰,我们实现了 ReadRequestBodyAsync 抽象方法,这个抽象方法被当前类的重载方法 ReadRequestBodyAsync(InputFormatterContext) 方法中调用。果然熟悉设计模式之后会发现设计模式无处不在,这里就是设计模式里的 模板方法模式 。好了,我们继续看源码 TextInputFormatter 类又继承了 InputFormatter 抽象类,我们继续来看它的实现[ 点击查看InputFormatter源码👈 [2] ]

publicabstract classInputFormatter : IInputFormatterIApiRequestFormatMetadataProvider
{
//这里定义了SupportedMediaTypes
public MediaTypeCollection SupportedMediaTypes { get; } = new MediaTypeCollection();
//根据ContentType判断当前的值是否可以满足调用当前InputFormatter
publicvirtualboolCanRead(InputFormatterContext context)
{
if (SupportedMediaTypes.Count == 0)
{
thrownew InvalidOperationException();
}
if (!CanReadType(context.ModelType))
{
returnfalse;
}
//获取ContentType的值
var contentType = context.HttpContext.Request.ContentType;
if (string.IsNullOrEmpty(contentType))
{
returnfalse;
}
//判断SupportedMediaTypes是否包含ContentType包含的值
return IsSubsetOfAnySupportedContentType(contentType);
}
privateboolIsSubsetOfAnySupportedContentType(string contentType)
{
var parsedContentType = new MediaType(contentType);
//判断设置的SupportedMediaTypes是否匹配ContentType的值
for (var i = 0; i < SupportedMediaTypes.Count; i++)
{
var supportedMediaType = new MediaType(SupportedMediaTypes[i]);
if (parsedContentType.IsSubsetOf(supportedMediaType))
{
returntrue;
}
}
returnfalse;
}
protectedvirtualboolCanReadType(Type type)
{
returntrue;
}
//核心方法
publicvirtual Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
return ReadRequestBodyAsync(context);
}
//抽象方法ReadRequestBodyAsync
publicabstract Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context);
//获取当前InputFormatter支持的dContentType
publicvirtual IReadOnlyList<string>? GetSupportedContentTypes(string contentType, Type objectType)
{
if (SupportedMediaTypes.Count == 0)
{
thrownew InvalidOperationException();
}
if (!CanReadType(objectType))
{
returnnull;
}
if (contentType == null)
{
return SupportedMediaTypes;
}
else
{
var parsedContentType = new MediaType(contentType);
List<string>? mediaTypes = null;
foreach (var mediaType in SupportedMediaTypes)
{
var parsedMediaType = new MediaType(mediaType);
if (parsedMediaType.IsSubsetOf(parsedContentType))
{
if (mediaTypes == null)
{
mediaTypes = new List<string>(SupportedMediaTypes.Count);
}
mediaTypes.Add(mediaType);
}
}
return mediaTypes;
}
}
}













这个类比较核心,我们来解析一下里面设计到的相关逻辑

  • • 先来看 ReadAsync 方法,这是被调用的根入口方法,这个方法调用了 ReadRequestBodyAsync 抽象方法,这也是 模板方法模式 ReadRequestBodyAsync 方法正是 TextInputFormatter 类中被实现的。

  • CanRead 方法的功能是根据请求头里的 Content-Type 是否可以命中当前 InputFormatter 子类,所以它是决定我们上面 YamlInputFormatter 方法的校验方法。

  • GetSupportedContentTypes 方法则是在 Content-Type 里解析出符合 SupportedMediaTypes 设置的 MediaType 。因为在Http的Header里,每一个键是可以设置多个值的,用 ; 分割即可。

  • 上面我们看到了 InputFormatter 类实现了 IInputFormatter 接口,看一下它的定义

    publicinterfaceIInputFormatter
    {
    boolCanRead(InputFormatterContext context);
    Task<InputFormatterResult> ReadAsync(InputFormatterContext context);
    }

    通过 IInputFormatter 接口的定义我们流看到了,它只包含两个方法 CanRead ReadAsync 。其中 CanRead 方法用来校验当前请求是否满足命中 IInputFormatter 实现类, ReadAsync 方法来执行具体的策略完成请求数据到具体类型的转换。接下来我们看一下重头戏,在模型绑定中是如何调用 IInputFormatter 接口集合的

    public classBodyModelBinderProvider : IModelBinderProvider
    {
    privatereadonly IList<IInputFormatter> _formatters;
    privatereadonly MvcOptions? _options;
    public IModelBinder? GetBinder(ModelBinderProviderContext context)
    {
    ArgumentNullException.ThrowIfNull(context);
    //判断当前Action参数是否可以满足当前模型绑定类
    if (context.BindingInfo.BindingSource != null &&
    context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Body))
    {
    if (_formatters.Count == 0)
    {
    thrownew InvalidOperationException();
    }
    var treatEmptyInputAsDefaultValue = CalculateAllowEmptyBody(context.BindingInfo.EmptyBodyBehavior, _options);
    returnnew BodyModelBinder(_formatters, _readerFactory, _loggerFactory, _options)
    {
    AllowEmptyBody = treatEmptyInputAsDefaultValue,
    };
    }
    returnnull;
    }
    }



    通过 BodyModelBinderProvider 类我们可以看到,我们设置的 IInputFormatter 接口的实现类只能满足绑定 Body 的场景,包含我们上面示例中演示的示例和 [FromBody] 这种形式。接下来我们来看 BodyModelBinder 类中的实现[ 点击查看BodyModelBinder源码👈 [3] ]

    publicpartial classBodyModelBinder : IModelBinder
    {
    privatereadonly IList<IInputFormatter> _formatters;
    privatereadonly Func<Stream, Encoding, TextReader> _readerFactory;
    privatereadonly MvcOptions? _options;
    publicBodyModelBinder(
    IList<IInputFormatter> formatters,
    IHttpRequestStreamReaderFactory readerFactory,
    MvcOptions? options)
    {
    _formatters = formatters;
    _readerFactory = readerFactory.CreateReader;
    _options = options;
    }
    internalbool AllowEmptyBody { getset; }
    publicasync Task BindModelAsync(ModelBindingContext bindingContext)
    {
    //获取Action绑定参数名称
    string modelBindingKey;
    if (bindingContext.IsTopLevelObject)
    {
    modelBindingKey = bindingContext.BinderModelName ?? string.Empty;
    }
    else
    {
    modelBindingKey = bindingContext.ModelName;
    }
    var httpContext = bindingContext.HttpContext;
    //组装InputFormatterContext
    var formatterContext = new InputFormatterContext(
    httpContext,
    modelBindingKey,
    bindingContext.ModelState,
    bindingContext.ModelMetadata,
    _readerFactory,
    AllowEmptyBody);
    var formatter = (IInputFormatter?)null;
    for (var i = 0; i < _formatters.Count; i++)
    {
    //通过IInputFormatter的CanRead方法来筛选IInputFormatter实例
    if (_formatters[i].CanRead(formatterContext))
    {
    formatter = _formatters[i];
    break;
    }
    }
    try
    {
    //调用IInputFormatter的ReadAsync方法,把请求的内容格式转换成实际的模型对象
    var result = await formatter.ReadAsync(formatterContext);
    if (result.IsModelSet)
    {
    //将结果绑定到Action的相关参数上
    var model = result.Model;
    bindingContext.Result = ModelBindingResult.Success(model);
    }
    }
    catch (Exception exception) when (exception is InputFormatterException || ShouldHandleException(formatter))
    {
    }
    }
    }





    通过阅读上面的源码,相信大家已经可以明白了我们定义的 YamlInputFormatter 是如何工作起来的。 YamlInputFormatter 本质是 IInputFormatter 实例。模型绑定类中 BodyModelBinder 调用了 ReadAsync 方法本质是调用到了 ReadRequestBodyAsync 方法。在这个方法里我们实现了请求 yml 格式到具体类型对象的转换,然后把转换后的对象绑定到了 Action 的参数上。

    TextOutputFormatter如何工作

    上面我们讲解了输入的格式化转换程序,知道了 ModelBinding 通过获取 IInputFormatter 实例来完成请求数据格式到对象的转换。接下来我们来看一下控制输出格式的 OutputFormatter 是如何工作的。通过上面自定义的 YamlOutputFormatter 我们可以看到它是继承自 TextOutputFormatter 抽象类。整体来说它的这个判断逻辑之类的和 TextInputFormatter 思路整体类似,所以咱们呢大致看一下关于工作过程的源码即可还是从 WriteResponseBodyAsync 方法入手[ 点击查看TextOutputFormatter源码👈 [4] ]

    publicabstract classTextOutputFormatter : OutputFormatter
    {
    publicoverride Task WriteAsync(OutputFormatterWriteContext context)
    {
    //获取ContentType,需要注意的是这里并非请求中设置的Content-Type的值,而是程序设置响应头中的Content-Type值
    var selectedMediaType = context.ContentType;
    if (!selectedMediaType.HasValue)
    {
    if (SupportedEncodings.Count > 0)
    {
    selectedMediaType = new StringSegment(SupportedMediaTypes[0]);
    }
    else
    {
    thrownew InvalidOperationException();
    }
    }
    //获取AcceptCharset的值
    var selectedEncoding = SelectCharacterEncoding(context);
    if (selectedEncoding != null)
    {
    var mediaTypeWithCharset = GetMediaTypeWithCharset(selectedMediaType.Value!, selectedEncoding);
    selectedMediaType = new StringSegment(mediaTypeWithCharset);
    }
    else
    {
    //省略部分代码
    return Task.CompletedTask;
    }
    context.ContentType = selectedMediaType;
    //写输出头
    WriteResponseHeaders(context);
    return WriteResponseBodyAsync(context, selectedEncoding);
    }
    publicabstract Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding);
    }


    需要注意的是我们省略了很多源码,只关注我们关注的地方。这里没啥可说的和上面 TextInputFormatter 思路整体类似,也是基于 模板方法模式 入口方法其实是 WriteAsync 方法。不过这里需要注意的是在 WriteAsync 方法中 ContentType ,这里的 ContentType 并非我们在请求时设置的值,而是我们给响应头中设置值。 TextOutputFormatter 继承自 OutputFormatter 接下来我们继续看一下它的实现[ 点击查看OutputFormatter源码👈 [5] ]

    publicabstract classOutputFormatter : IOutputFormatterIApiResponseTypeMetadataProvider
    {
    protectedvirtualboolCanWriteType(Type? type)
    {
    returntrue;
    }
    //判断当前请求是否满足调用当前OutputFormatter实例
    publicvirtualboolCanWriteResult(OutputFormatterCanWriteContext context)
    {
    if (SupportedMediaTypes.Count == 0)
    {
    thrownew InvalidOperationException();
    }
    //当前Action参数类型是否满足设定
    if (!CanWriteType(context.ObjectType))
    {
    returnfalse;
    }
    //这里的ContentType依然是设置的响应头而非请求头
    if (!context.ContentType.HasValue)
    {
    context.ContentType = new StringSegment(SupportedMediaTypes[0]);
    returntrue;
    }
    else
    {
    //根据设置的输出头的ContentType判断是否满足自定义时设置的SupportedMediaTypes类型
    //比如YamlOutputFormatter中设置的SupportedMediaTypes和设置的响应ContentType是否满足匹配关系
    var parsedContentType = new MediaType(context.ContentType);
    for (var i = 0; i < SupportedMediaTypes.Count; i++)
    {
    var supportedMediaType = new MediaType(SupportedMediaTypes[i]);
    if (supportedMediaType.HasWildcard)
    {
    if (context.ContentTypeIsServerDefined
    && parsedContentType.IsSubsetOf(supportedMediaType))
    {
    returntrue;
    }
    }
    else
    {
    if (supportedMediaType.IsSubsetOf(parsedContentType))
    {
    context.ContentType = new StringSegment(SupportedMediaTypes[i]);
    returntrue;
    }
    }
    }
    }
    returnfalse;
    }
    //WriteAsync虚方法也就是TextOutputFormatter中从写的方法
    publicvirtual Task WriteAsync(OutputFormatterWriteContext context)
    {
    WriteResponseHeaders(context);
    return WriteResponseBodyAsync(context);
    }
    publicvirtualvoidWriteResponseHeaders(OutputFormatterWriteContext context)
    {
    //这里可以看出写入的是输出的ContentType
    var response = context.HttpContext.Response;
    response.ContentType = context.ContentType.Value ?? string.Empty;
    }
    }




    这里我们只关注两个核心方法 CanWriteResult WriteAsync 方法,其中 CanWriteResult 方法判断当前输出是否满足定义的 OutputFormatter 中设定的媒体类型,比如 YamlOutputFormatter 中设置的 SupportedMediaTypes 和设置的响应 ContentType 是否满足匹配关系,如果显示的指明了输出头是否满足 text/yaml text/yml 才能执行 YamlOutputFormatter 中的 WriteResponseBodyAsync 方法。一旦满足 CanWriteResult 方法则会去调用 WriteAsync 方法。我们可以看到 OutputFormatter 类实现了 IOutputFormatter 接口,它的定义如下所示

    publicinterfaceIOutputFormatter
    {
    boolCanWriteResult(OutputFormatterCanWriteContext context);
    Task WriteAsync(OutputFormatterWriteContext context);
    }

    一目了然, IOutputFormatter 接口暴露了 CanWriteResult WriteAsync 两个能力。咱们上面已经解释了这两个方法的用途,在这里就不再赘述了。我们知道使用 IOutputFormatter 的地方,在 ObjectResultExecutor 类中, ObjectResultExecutor 类则是在 ObjectResult 类中被调用,我们看一下 ObjectResult 调用 ObjectResultExecutor 的地方

    public classObjectResult : ActionResultIStatusCodeActionResult
    {
    publicoverride Task ExecuteResultAsync(ActionContext context)
    {
    var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<ObjectResult>>();
    return executor.ExecuteAsync(context, this);
    }
    }

    上面代码中获取的 IActionResultExecutor<ObjectResult> 实例正是 ObjectResultExecutor 实例,这个可以在 MvcCoreServiceCollectionExtensions 类中可以看到[ 点击查看MvcCoreServiceCollectionExtensions源码👈 [6] ]

    services.TryAddSingleton<IActionResultExecutor<ObjectResult>, ObjectResultExecutor>();

    好了,回到整体,我们看一下 ObjectResultExecutor 的定义[ 点击查看ObjectResultExecutor源码👈 [7] ]

    publicpartial classObjectResultExecutor : IActionResultExecutor<ObjectResult>
    {
    publicObjectResultExecutor(OutputFormatterSelector formatterSelector)
    {
    FormatterSelector = formatterSelector;
    }
    //ObjectResult方法中调用的是该方法
    publicvirtual Task ExecuteAsync(ActionContext context, ObjectResult result)
    {
    var objectType = result.DeclaredType;
    //获取返回对象类型
    if (objectType == null || objectType == typeof(object))
    {
    objectType = result.Value?.GetType();
    }
    varvalue = result.Value;
    return ExecuteAsyncCore(context, result, objectType, value);
    }
    private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type? objectType, objectvalue)
    {
    //组装OutputFormatterWriteContext,objectType为当前返回对象类型,value为返回对象的值
    var formatterContext = new OutputFormatterWriteContext(
    context.HttpContext,
    WriterFactory,
    objectType,
    value);
    //获取符合当前请求输出处理程序IOutputFormatter,并传递了ObjectResult的ContentTypes值
    var selectedFormatter = FormatterSelector.SelectFormatter(
    formatterContext,
    (IList<IOutputFormatter>)result.Formatters ?? Array.Empty<IOutputFormatter>(),
    result.ContentTypes);
    //省略部分代码
    //调用IOutputFormatter的WriteAsync的方法
    return selectedFormatter.WriteAsync(formatterContext);
    }
    }





    上面的代码我们可以看到在 ObjectResultExecutor 类中,通过 OutputFormatterSelector SelectFormatter 方法来选择使用哪个 IOutputFormatter 实例,需要注意的是调用 SelectFormatter 方法的时候传递的 ContentTypes 值是来自 ObjectResult 对象的 ContentTypes 属性,也就是我们在设置 ObjectResult 对象的时候可以传递的输出的 Content-Type 值。选择完成之后在调用具体实例的 WriteAsync 方法。我们来看一下 OutputFormatterSelector 实现类的 SelectFormatter 方法如何实现的,在 OutputFormatterSelector 的默认实现类 DefaultOutputFormatterSelector 中[ 点击查看DefaultOutputFormatterSelector源码👈 [8] ]

    publicpartial classDefaultOutputFormatterSelector : OutputFormatterSelector
    {
    publicoverride IOutputFormatter? SelectFormatter(OutputFormatterCanWriteContext context, IList<IOutputFormatter> formatters, MediaTypeCollection contentTypes)
    {
    //省略部分代码
    var request = context.HttpContext.Request;
    //获取请求头Accept的值
    var acceptableMediaTypes = GetAcceptableMediaTypes(request);
    var selectFormatterWithoutRegardingAcceptHeader = false;
    IOutputFormatter? selectedFormatter = null;
    if (acceptableMediaTypes.Count == 0)
    {
    //如果请求头Accept没设置值
    selectFormatterWithoutRegardingAcceptHeader = true;
    }
    else
    {
    if (contentTypes.Count == 0)
    {
    //如果ObjectResult没设置ContentTypes则走这个逻辑
    selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
    context,
    formatters,
    acceptableMediaTypes);
    }
    else
    {
    //如果ObjectResult设置了ContentTypes则走这个逻辑
    selectedFormatter = SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
    context,
    formatters,
    acceptableMediaTypes,
    contentTypes);
    }
    //如果通过ObjectResult的ContentTypes没选择出来IOutputFormatter这设置该值
    if (selectedFormatter == null)
    {
    if (!_returnHttpNotAcceptable)
    {
    selectFormatterWithoutRegardingAcceptHeader = true;
    }
    }
    }
    if (selectFormatterWithoutRegardingAcceptHeader)
    {
    if (contentTypes.Count == 0)
    {
    selectedFormatter = SelectFormatterNotUsingContentType(context, formatters);
    }
    else
    {
    selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(context, formatters, contentTypes);
    }
    }
    return selectedFormatter;
    }
    private List<MediaTypeSegmentWithQuality> GetAcceptableMediaTypes(HttpRequest request)
    {
    var result = new List<MediaTypeSegmentWithQuality>();
    //获取请求头里的Accept的值,因为Accept的值可能有多个,也就是用;分割的情况
    AcceptHeaderParser.ParseAcceptHeader(request.Headers.Accept, result);
    for (var i = 0; i < result.Count; i++)
    {
    var mediaType = new MediaType(result[i].MediaType);
    if (!_respectBrowserAcceptHeader && mediaType.MatchesAllSubTypes && mediaType.MatchesAllTypes)
    {
    result.Clear();
    return result;
    }
    }
    result.Sort(_sortFunction);
    return result;
    }
    }






    上面的 SelectFormatter 方法就是通过各种条件判断来选择符合要求的 IOutputFormatter 实例,这里依次出现了几个方法,用来可以根据不同条件选择 IOutputFormatter ,接下来我们根据 出现的顺序 来解释一下这几个方法的逻辑,首先是 SelectFormatterUsingSortedAcceptHeaders 方法

    privatestatic IOutputFormatter? SelectFormatterUsingSortedAcceptHeaders(
    OutputFormatterCanWriteContext formatterContext,
    IList<IOutputFormatter> formatters,
    IList<MediaTypeSegmentWithQuality> sortedAcceptHeaders)
    {
    for (var i = 0; i < sortedAcceptHeaders.Count; i++)
    {
    var mediaType = sortedAcceptHeaders[i];
    //把Request的Accept值设置给Response的ContentT-ype
    formatterContext.ContentType = mediaType.MediaType;
    formatterContext.ContentTypeIsServerDefined = false;
    for (var j = 0; j < formatters.Count; j++)
    {
    var formatter = formatters[j];
    if (formatter.CanWriteResult(formatterContext))
    {
    return formatter;
    }
    }
    }
    returnnull;
    }

    这个方法是通过请求头的 Accept 值来选择满足条件的 IOutputFormatter 实例。还记得上面的 OutputFormatter 类中的 CanWriteResult 方法吗就是根据 ContentType 判断是否符合条件,使用的就是这里的Request的Accept值。第二个出现的选择方法则是 SelectFormatterUsingSortedAcceptHeadersAndContentTypes 方法

    privatestatic IOutputFormatter? SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
    OutputFormatterCanWriteContext formatterContext,
    IList<IOutputFormatter> formatters,
    IList<MediaTypeSegmentWithQuality> sortedAcceptableContentTypes,
    MediaTypeCollection possibleOutputContentTypes)
    {
    for (var i = 0; i < sortedAcceptableContentTypes.Count; i++)
    {
    var acceptableContentType = new MediaType(sortedAcceptableContentTypes[i].MediaType);
    for (var j = 0; j < possibleOutputContentTypes.Count; j++)
    {
    var candidateContentType = new MediaType(possibleOutputContentTypes[j]);
    if (candidateContentType.IsSubsetOf(acceptableContentType))
    {
    for (var k = 0; k < formatters.Count; k++)
    {
    var formatter = formatters[k];
    formatterContext.ContentType = new StringSegment(possibleOutputContentTypes[j]);
    formatterContext.ContentTypeIsServerDefined = true;
    if (formatter.CanWriteResult(formatterContext))
    {
    return formatter;
    }
    }
    }
    }
    }
    returnnull;
    }

    这个方法是通过 ObjectResult 设置了 ContentTypes 去匹配选择满足条件请求头的 Accept 值来选择 IOutputFormatter 实例。第三个出现的则是 SelectFormatterNotUsingContentType 方法

    private IOutputFormatter? SelectFormatterNotUsingContentType(
    OutputFormatterCanWriteContext formatterContext,
    IList<IOutputFormatter> formatters)
    {
    foreach (var formatter in formatters)
    {
    formatterContext.ContentType = new StringSegment();
    formatterContext.ContentTypeIsServerDefined = false;
    if (formatter.CanWriteResult(formatterContext))
    {
    return formatter;
    }
    }
    returnnull;
    }

    这个方法是选择第一个满足条件的 IOutputFormatter 实例。还记得上面定义 YamlOutputFormatter 类的时候重写了 CanWriteResult 方法吗?就是为了杜绝被默认选中的情况,重写了 CanWriteResult 方法,里面添加了验证逻辑就不会被默认选中。

    privatestatic IOutputFormatter? SelectFormatterUsingAnyAcceptableContentType(
    OutputFormatterCanWriteContext formatterContext,
    IList<IOutputFormatter> formatters,
    MediaTypeCollection acceptableContentTypes)
    {
    foreach (var formatter in formatters)
    {
    foreach (var contentType in acceptableContentTypes)
    {
    formatterContext.ContentType = new StringSegment(contentType);
    formatterContext.ContentTypeIsServerDefined = true;
    if (formatter.CanWriteResult(formatterContext))
    {
    return formatter;
    }
    }
    }
    returnnull;
    }

    这个方法说的比较简单,就是通过 ObjectResult 设置了 ContentTypes 去匹配选择满足条件的 IOutputFormatter 实例。

    到这里相信大家对关于 TextOutputFormatter 是如何工作的有了大致的了解,本质就是在 ObjectResultExecutor 类中选择合适的满足条件的 IOutputFormatter 实例。我们在 Action 中返回 POCO 对象、 ActionResult<Type> OkObjectResult 等本质都是返回的 ObjectResult 类型。

    小结

    相信通过这一小节对 TextInputFormatter TextOutputFormatter 源码的分析,和它们是如何工作的进行了大致的讲解。其实如果理解了源码,总结起来也很简单

  • • 模型绑定类中 BodyModelBinder 调用了 InputFormatter 实例来进行对指定请求的内容进行格式转换,绑定到模型参数上的,

  • ObjectResult 类的执行类 ObjectResultExecutor 类中通过调用满足条件 OutputFormatter 实例,来决定把模型输出成那种类型的数据格式,但是需要注意重写 CanWriteResult 方法防止被作为默认程序输出。

  • 控制了模型绑定和输出对象转换,我们也就可以直接控制请求数据和输出数据的格式控制了。当然想更好的了解更多的细节,解惑心中疑问,还是得阅读和调试具体的源码。

    相关资料

    由于文章中涉及到的源码都是关于格式化程序工作过程中涉及到的源码,其它的相关源码和地址我们并没有展示出来,这里我将罗列一下对该功能理解有帮助的相关地址,方便大家阅读

  • Custom formatters in ASP.NET Core Web API [9]

  • • 内置的SystemTextJson格式化程序相关

  • MvcCoreMvcOptionsSetup [10]

  • SystemTextJsonInputFormatter [11]

  • SystemTextJsonOutputFormatter [12]

  • • 内置的Xml格式化程序相关

  • XmlSerializerMvcOptionsSetup [13]

  • XmlSerializerInputFormatter [14]

  • XmlSerializerOutputFormatter [15]

  • ActionResultOfT.Convert()方法 [16]

  • ActionResultTypeMapper [17]

  • SyncObjectResultExecutor [18]

  • FormatFilter [19]

  • 总结

    本篇文章我们通过演示示例和讲解源码的方式了解了 ASP.NET Core WebAPI 中的格式化程序,知道结构的话了解它其实并不难,只是其中的细节比较多,需要慢慢梳理。涉及到的源码比较多,可以把本文当做一个简单的教程来看。写文章既是把自己对事物的看法分享出来,也是把自己的感悟记录下来方便翻阅。我个人很喜欢阅读源码,通过开始阅读源码感觉自己的水平有了很大的提升,阅读源码与其说是一个行为不如说是一种意识。明显的好处,一个是为了让自己对这些理解更透彻,阅读过程中可以引发很多的思考。二是通过源码可以解决很多实际的问题,毕竟大家都这么说源码之下无秘密。

    引用链接

    [1] 点击查看TextInputFormatter源码👈: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/Formatters/TextInputFormatter.cs#L60
    [2] 点击查看InputFormatter源码👈: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/Formatters/InputFormatter.cs
    [3] 点击查看BodyModelBinder源码👈: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs
    [4] 点击查看TextOutputFormatter源码👈: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/Formatters/TextOutputFormatter.cs#L168
    [5] 点击查看OutputFormatter源码👈: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/Formatters/OutputFormatter.cs#L157
    [6] 点击查看MvcCoreServiceCollectionExtensions源码👈: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs#L235
    [7] 点击查看ObjectResultExecutor源码👈: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs
    [8] 点击查看DefaultOutputFormatterSelector源码👈: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/Infrastructure/DefaultOutputFormatterSelector.cs#L50
    [9] Custom formatters in ASP.NET Core Web API: https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/custom-formatters?view=aspnetcore-8.0
    [10] MvcCoreMvcOptionsSetup: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs
    [11] SystemTextJsonInputFormatter: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs
    [12] SystemTextJsonOutputFormatter: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs
    [13] XmlSerializerMvcOptionsSetup: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Formatters.Xml/src/DependencyInjection/XmlSerializerMvcOptionsSetup.cs
    [14] XmlSerializerInputFormatter: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerInputFormatter.cs
    [15] XmlSerializerOutputFormatter: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerOutputFormatter.cs
    [16] ActionResultOfT.Convert()方法: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/ActionResultOfT.cs#L78
    [17] ActionResultTypeMapper: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/Infrastructure/ActionResultTypeMapper.cs
    [18] SyncObjectResultExecutor: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/Infrastructure/ActionMethodExecutor.cs#L156
    [19] FormatFilter: https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/Formatters/FormatFilter.cs