前言
我们在使用
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 { get; set; }
publicstring Country { get; set; }
publicstring Phone { get; set; }
publicstring ZipCode { get; set; }
public List<string> Tags { get; set; }
}
我们用
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 : IInputFormatter, IApiRequestFormatMetadataProvider
{
//这里定义了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 { get; set; }
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 : IOutputFormatter, IApiResponseTypeMetadataProvider
{
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 : ActionResult, IStatusCodeActionResult
{
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, object? value)
{
//组装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