细聊ASP.NET Core WebAPI格式化程序
前言
我们在使用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源码]
public abstract class TextInputFormatter : InputFormatter
{
public IList<Encoding> SupportedEncodings { get; } = new List<Encoding>();
public override 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方法
public abstract 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];
}
}
}
return null;
}
return SupportedEncodings[0];
}
}
整体来说TextInputFormatter
抽象类思路相对清晰,我们实现了ReadRequestBodyAsync
抽象方法,这个抽象方法被当前类的重载方法ReadRequestBodyAsync(InputFormatterContext)
方法中调用。果然熟悉设计模式之后会发现设计模式无处不在,这里就是设计模式里的模板方法模式
。好了,我们继续看源码TextInputFormatter
类又继承了InputFormatter
抽象类,我们继续来看它的实现[点击查看InputFormatter源码]
public abstract class InputFormatter : IInputFormatter, IApiRequestFormatMetadataProvider
{
//这里定义了SupportedMediaTypes
public MediaTypeCollection SupportedMediaTypes { get; } = new MediaTypeCollection();
//根据ContentType判断当前的值是否可以满足调用当前InputFormatter
public virtual bool CanRead(InputFormatterContext context)
{
if (SupportedMediaTypes.Count == 0)
{
throw new InvalidOperationException();
}
if (!CanReadType(context.ModelType))
{
return false;
}
//获取ContentType的值
var contentType = context.HttpContext.Request.ContentType;
if (string.IsNullOrEmpty(contentType))
{
return false;
}
//判断SupportedMediaTypes是否包含ContentType包含的值
return IsSubsetOfAnySupportedContentType(contentType);
}
private bool IsSubsetOfAnySupportedContentType(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))
{
return true;
}
}
return false;
}
protected virtual bool CanReadType(Type type)
{
return true;
}
//核心方法
public virtual Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
return ReadRequestBodyAsync(context);
}
//抽象方法ReadRequestBodyAsync
public abstract Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context);
//获取当前InputFormatter支持的dContentType
public virtual IReadOnlyList<string>? GetSupportedContentTypes(string contentType, Type objectType)
{
if (SupportedMediaTypes.Count == 0)
{
throw new InvalidOperationException();
}
if (!CanReadType(objectType))
{
return null;
}
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
接口,看一下它的定义
public interface IInputFormatter
{
bool CanRead(InputFormatterContext context);
Task<InputFormatterResult> ReadAsync(InputFormatterContext context);
}
通过IInputFormatter
接口的定义我们流看到了,它只包含两个方法CanRead
和ReadAsync
。其中CanRead
方法用来校验当前请求是否满足命中IInputFormatter
实现类,ReadAsync
方法来执行具体的策略完成请求数据到具体类型的转换。接下来我们看一下重头戏,在模型绑定中是如何调用IInputFormatter
接口集合的
public class BodyModelBinderProvider : IModelBinderProvider
{
private readonly IList<IInputFormatter> _formatters;
private readonly 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)
{
throw new InvalidOperationException();
}
var treatEmptyInputAsDefaultValue = CalculateAllowEmptyBody(context.BindingInfo.EmptyBodyBehavior, _options);
return new BodyModelBinder(_formatters, _readerFactory, _loggerFactory, _options)
{
AllowEmptyBody = treatEmptyInputAsDefaultValue,
};
}
return null;
}
}
通过BodyModelBinderProvider
类我们可以看到,我们设置的IInputFormatter
接口的实现类只能满足绑定Body
的场景,包含我们上面示例中演示的示例和[FromBody]
这种形式。接下来我们来看BodyModelBinder
类中的实现[点击查看BodyModelBinder源码]
public partial class BodyModelBinder : IModelBinder
{
private readonly IList<IInputFormatter> _formatters;
private readonly Func<Stream, Encoding, TextReader> _readerFactory;
private readonly MvcOptions? _options;
public BodyModelBinder(
IList<IInputFormatter> formatters,
IHttpRequestStreamReaderFactory readerFactory,
MvcOptions? options)
{
_formatters = formatters;
_readerFactory = readerFactory.CreateReader;
_options = options;
}
internal bool AllowEmptyBody { get; set; }
public async 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源码]
public abstract class TextOutputFormatter : OutputFormatter
{
public override 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
{
throw new 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);
}
public abstract Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding);
}
需要注意的是我们省略了很多源码,只关注我们关注的地方。这里没啥可说的和上面TextInputFormatter
思路整体类似,也是基于模板方法模式
入口方法其实是WriteAsync
方法。不过这里需要注意的是在WriteAsync
方法中ContentType
,这里的ContentType
并非我们在请求时设置的值,而是我们给响应头中设置值。TextOutputFormatter
继承自OutputFormatter
接下来我们继续看一下它的实现[点击查看OutputFormatter源码]
public abstract class OutputFormatter : IOutputFormatter, IApiResponseTypeMetadataProvider
{
protected virtual bool CanWriteType(Type? type)
{
return true;
}
//判断当前请求是否满足调用当前OutputFormatter实例
public virtual bool CanWriteResult(OutputFormatterCanWriteContext context)
{
if (SupportedMediaTypes.Count == 0)
{
throw new InvalidOperationException();
}
//当前Action参数类型是否满足设定
if (!CanWriteType(context.ObjectType))
{
return false;
}
//这里的ContentType依然是设置的响应头而非请求头
if (!context.ContentType.HasValue)
{
context.ContentType = new StringSegment(SupportedMediaTypes[0]);
return true;
}
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))
{
return true;
}
}
else
{
if (supportedMediaType.IsSubsetOf(parsedContentType))
{
context.ContentType = new StringSegment(SupportedMediaTypes[i]);
return true;
}
}
}
}
return false;
}
//WriteAsync虚方法也就是TextOutputFormatter中从写的方法
public virtual Task WriteAsync(OutputFormatterWriteContext context)
{
WriteResponseHeaders(context);
return WriteResponseBodyAsync(context);
}
public virtual void WriteResponseHeaders(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
接口,它的定义如下所示
public interface IOutputFormatter
{
bool CanWriteResult(OutputFormatterCanWriteContext context);
Task WriteAsync(OutputFormatterWriteContext context);
}
一目了然,IOutputFormatter
接口暴露了CanWriteResult
和WriteAsync
两个能力。咱们上面已经解释了这两个方法的用途,在这里就不再赘述了。我们知道使用IOutputFormatter
的地方,在ObjectResultExecutor
类中,ObjectResultExecutor
类则是在ObjectResult
类中被调用,我们看一下ObjectResult
调用ObjectResultExecutor
的地方
public class ObjectResult : ActionResult, IStatusCodeActionResult
{
public override Task ExecuteResultAsync(ActionContext context)
{
var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<ObjectResult>>();
return executor.ExecuteAsync(context, this);
}
}
上面代码中获取的IActionResultExecutor<ObjectResult>
实例正是ObjectResultExecutor
实例,这个可以在MvcCoreServiceCollectionExtensions
类中可以看到[点击查看MvcCoreServiceCollectionExtensions源码]
services.TryAddSingleton<IActionResultExecutor<ObjectResult>, ObjectResultExecutor>();
好了,回到整体,我们看一下ObjectResultExecutor
的定义[点击查看ObjectResultExecutor源码]
public partial class ObjectResultExecutor : IActionResultExecutor<ObjectResult>
{
public ObjectResultExecutor(OutputFormatterSelector formatterSelector)
{
FormatterSelector = formatterSelector;
}
//ObjectResult方法中调用的是该方法
public virtual Task ExecuteAsync(ActionContext context, ObjectResult result)
{
var objectType = result.DeclaredType;
//获取返回对象类型
if (objectType == null || objectType == typeof(object))
{
objectType = result.Value?.GetType();
}
var value = 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源码]
public partial class DefaultOutputFormatterSelector : OutputFormatterSelector
{
public override 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
方法
private static 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;
}
}
}
return null;
}
这个方法是通过请求头的Accept
值来选择满足条件的IOutputFormatter
实例。还记得上面的OutputFormatter
类中的CanWriteResult
方法吗就是根据ContentType
判断是否符合条件,使用的就是这里的Request的Accept值。第二个出现的选择方法则是SelectFormatterUsingSortedAcceptHeadersAndContentTypes
方法
private static 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;
}
}
}
}
}
return null;
}
这个方法是通过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;
}
}
return null;
}
这个方法是选择第一个满足条件的IOutputFormatter
实例。还记得上面定义YamlOutputFormatter
类的时候重写了CanWriteResult
方法吗?就是为了杜绝被默认选中的情况,重写了CanWriteResult
方法,里面添加了验证逻辑就不会被默认选中。
private static 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;
}
}
}
return null;
}
这个方法说的比较简单,就是通过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
- 内置的SystemTextJson格式化程序相关
- 内置的Xml格式化程序相关
- ActionResultOfT.Convert()方法
- ActionResultTypeMapper
- SyncObjectResultExecutor
- FormatFilter
总结
本篇文章我们通过演示示例和讲解源码的方式了解了ASP.NET Core WebAPI
中的格式化程序,知道结构的话了解它其实并不难,只是其中的细节比较多,需要慢慢梳理。涉及到的源码比较多,可以把本文当做一个简单的教程来看。写文章既是把自己对事物的看法分享出来,也是把自己的感悟记录下来方便翻阅。我个人很喜欢阅读源码,通过开始阅读源码感觉自己的水平有了很大的提升,阅读源码与其说是一个行为不如说是一种意识。明显的好处,一个是为了让自己对这些理解更透彻,阅读过程中可以引发很多的思考。二是通过源码可以解决很多实际的问题,毕竟大家都这么说源码之下无秘密。
细聊ASP.NET Core WebAPI格式化程序的更多相关文章
- 【免费视频】使用VS Code开发ASP.NET Core WebAPI应用程序
1.使用VS Code开发ASP.NET Core WebAPI应用程序 1.使用Visual Studio Code开发Asp.Net Core基础入门实战 毕竟从.net过度过来的我们已经习惯了使 ...
- asp.net core webapi 使用ef 对mysql进行增删改查,并生成Docker镜像构建容器运行
1.构建运行mysql容器,添加数据库user 参考Docker创建运行多个mysql容器,地址 http://www.cnblogs.com/heyangyi/p/9288402.html 添加us ...
- ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了
引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必是件很痛苦的事情吧,但文档又必须写,而且文档的格式如果没有具体要求的话,最终完成的文档则完全取决于开发者 ...
- ASP.NET Core WebApi使用Swagger生成api
引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必是件很痛苦的事情吧,但文档又必须写,而且文档的格式如果没有具体要求的话,最终完成的文档则完全取决于开发者 ...
- ASP.NET Core WebApi使用Swagger生成api说明文档
1. Swagger是什么? Swagger 是一个规范和完整的框架,用于生成.描述.调用和可视化 RESTful 风格的 Web 服务.总体目标是使客户端和文件系统作为服务器以同样的速度来更新.文件 ...
- 【转】ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了
原文链接:https://www.cnblogs.com/yilezhu/p/9241261.html 引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必 ...
- 零基础ASP.NET Core WebAPI团队协作开发
零基础ASP.NET Core WebAPI团队协作开发 相信大家对“前后端分离”和“微服务”这两个词应该是耳熟能详了.网上也有很多介绍这方面的文章,写的都很好.我这里提这个是因为接下来我要分享的内容 ...
- ASP.NET Core WebApi构建API接口服务实战演练
一.ASP.NET Core WebApi课程介绍 人生苦短,我用.NET Core!提到Api接口,一般会想到以前用到的WebService和WCF服务,这三个技术都是用来创建服务接口,只不过Web ...
- 创建ASP.NET Core MVC应用程序(6)-添加验证
创建ASP.NET Core MVC应用程序(6)-添加验证 DRY原则 DRY("Don't Repeat Yourself")是MVC的设计原则之一.ASP.NET MVC鼓励 ...
- asp.net core webapi之跨域(Cors)访问
这里说的跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同域的框架中(iframe)的数据.只要协议.域名.端口有任何一个不同,都被当作 ...
随机推荐
- 关于信创CPU测试的一些想法和思路
关于信创CPU测试的一些想法和思路 背景 最近荷兰政府颁布了关于半导体设备出口管制的最新条例. 好像45nm以下的工艺的设备都可能收到限制. 对中国的相关厂商比如长鑫还有华虹的影响应该都比较大. 认为 ...
- [转帖]ESXi下查看CPU 频率
https://www.jianshu.com/p/8943a4223ed7 查看CPU的固定频率 [root@localhost:/bin] esxcli hardware cpu list|gr ...
- 【转帖】SRE 高延迟问题的罪魁祸首 System.gc()
https://www.infoq.cn/article/lXTRgYb9ecVBu*72fT7O jstact -gccause pid 3000 30 01 案例一: 某日,支付平台的开发人员找到 ...
- [转帖]AnolisOS8安装ntp同步时间
https://www.wlnmp.com/post-673.html 在AnolisOS8中默认不再支持ntp软件包,时间同步将由chrony来实现,如果你习惯了使用ntp来同步时间,一时难以去适应 ...
- 【转贴】西数全新推出企业级金盘SSD:2.5寸U.2接口、最大7.68TB、96层TLC
西数全新推出企业级金盘SSD:2.5寸U.2接口.最大7.68TB.96层TLC https://www.cnbeta.com/articles/tech/951353.htm 硬件发展日新月异 &q ...
- echarts更改x和y轴的颜色
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...
- vs2019系统内置方法无提示
有个同事问我为什么他的vs编写C#代码,对于引用System.dll中的方法,鼠标移上去没有方法的使用说明或接口注释,具体可以看下面的截图,而我绝大多数情况下是使用Rider开发,并没有遇到这个问题, ...
- Go中sync.map使用小结
sync.map 前言 深入了解下 查看下具体的实现 Load Store Delete LoadOrStore 总结 流程图片 参考 sync.map 前言 Go中的map不是并发安全的,在Go1. ...
- 9.9 Windows驱动开发:内核远程线程实现DLL注入
在笔者上一篇文章<内核RIP劫持实现DLL注入>介绍了通过劫持RIP指针控制程序执行流实现插入DLL的目的,本章将继续探索全新的注入方式,通过NtCreateThreadEx这个内核函数实 ...
- C/C++ 关于继承与多态笔记
继承的基本语法: 继承的目的就是用于提高代码的可用性,减少代码的重复内容,高效开发. #include <iostream> using namespace std; class Base ...