前言

我们在使用ASP.NET Core WebApi时它支持使用指定的输入和输出格式来交换数据。输入数据靠模型绑定的机制处理,输出数据则需要用格式化的方式进行处理。ASP.NET Core框架已经内置了处理JSONXML的输入和输出方式,默认的情况我们提交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-Typetext/ymltext/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/yamlAccept:text/yml才能命中该YamlOutputFormatter。配置的时候同样也在MvcOptions中配置即可

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

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

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

小结

上面我们讲解了控制请求数据格式的TextInputFormatter和控制输出格式的TextOutputFormatter。其中InputFormatter负责给ModelBinding输送类型对象,OutputFormatter负责给ObjectResult输出值,这我们可以看到它们只能控制WebAPIController/Action的且返回ObjectResult的这种情况才生效,其它的比如MinimalApiGRPC是起不到效果的。通过上面的示例,有同学心里可能会存在疑问,上面在AddControllers方法中注册TextInputFormatterTextOutputFormatter的时候,没办法完成注入的服务,比如如果YamlInputFormatterYamlOutputFormatter构造实例的时候无法获取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>());

探究工作方式

上面我们演示了如何自定义InputFormatterOutputFormatter,也讲解了InputFormatter负责给ModelBinding输送类型对象,OutputFormatter负责给ObjectResult输出值。接下来我们就通过阅读其中的源码来看一下InputFormatterOutputFormatter是如何工作来影响模型绑定ObjectResult的结果。

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

TextInputFormatter如何工作

上面我们看到了YamlInputFormatter是继承了TextInputFormatter抽象类,并重写了ReadRequestBodyAsync方法。接下来我们就从TextInputFormatterReadRequestBodyAsync方法来入手,我们来看一下源码定义[点击查看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接口的定义我们流看到了,它只包含两个方法CanReadReadAsync。其中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;
}
}

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

public interface IOutputFormatter
{
bool CanWriteResult(OutputFormatterCanWriteContext context);
Task WriteAsync(OutputFormatterWriteContext context);
}

一目了然,IOutputFormatter接口暴露了CanWriteResultWriteAsync两个能力。咱们上面已经解释了这两个方法的用途,在这里就不再赘述了。我们知道使用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类中,通过OutputFormatterSelectorSelectFormatter方法来选择使用哪个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类型。

小结

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

  • 模型绑定类中BodyModelBinder调用了InputFormatter实例来进行对指定请求的内容进行格式转换,绑定到模型参数上的,
  • ObjectResult类的执行类ObjectResultExecutor类中通过调用满足条件OutputFormatter实例,来决定把模型输出成那种类型的数据格式,但是需要注意重写CanWriteResult方法防止被作为默认程序输出。

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

相关资料

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

总结

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

欢迎扫码关注我的公众号

细聊ASP.NET Core WebAPI格式化程序的更多相关文章

  1. 【免费视频】使用VS Code开发ASP.NET Core WebAPI应用程序

    1.使用VS Code开发ASP.NET Core WebAPI应用程序 1.使用Visual Studio Code开发Asp.Net Core基础入门实战 毕竟从.net过度过来的我们已经习惯了使 ...

  2. asp.net core webapi 使用ef 对mysql进行增删改查,并生成Docker镜像构建容器运行

    1.构建运行mysql容器,添加数据库user 参考Docker创建运行多个mysql容器,地址 http://www.cnblogs.com/heyangyi/p/9288402.html 添加us ...

  3. ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了

    引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必是件很痛苦的事情吧,但文档又必须写,而且文档的格式如果没有具体要求的话,最终完成的文档则完全取决于开发者 ...

  4. ASP.NET Core WebApi使用Swagger生成api

    引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必是件很痛苦的事情吧,但文档又必须写,而且文档的格式如果没有具体要求的话,最终完成的文档则完全取决于开发者 ...

  5. ASP.NET Core WebApi使用Swagger生成api说明文档

    1. Swagger是什么? Swagger 是一个规范和完整的框架,用于生成.描述.调用和可视化 RESTful 风格的 Web 服务.总体目标是使客户端和文件系统作为服务器以同样的速度来更新.文件 ...

  6. 【转】ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了

    原文链接:https://www.cnblogs.com/yilezhu/p/9241261.html 引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必 ...

  7. 零基础ASP.NET Core WebAPI团队协作开发

    零基础ASP.NET Core WebAPI团队协作开发 相信大家对“前后端分离”和“微服务”这两个词应该是耳熟能详了.网上也有很多介绍这方面的文章,写的都很好.我这里提这个是因为接下来我要分享的内容 ...

  8. ASP.NET Core WebApi构建API接口服务实战演练

    一.ASP.NET Core WebApi课程介绍 人生苦短,我用.NET Core!提到Api接口,一般会想到以前用到的WebService和WCF服务,这三个技术都是用来创建服务接口,只不过Web ...

  9. 创建ASP.NET Core MVC应用程序(6)-添加验证

    创建ASP.NET Core MVC应用程序(6)-添加验证 DRY原则 DRY("Don't Repeat Yourself")是MVC的设计原则之一.ASP.NET MVC鼓励 ...

  10. asp.net core webapi之跨域(Cors)访问

    这里说的跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同域的框架中(iframe)的数据.只要协议.域名.端口有任何一个不同,都被当作 ...

随机推荐

  1. 关于信创CPU测试的一些想法和思路

    关于信创CPU测试的一些想法和思路 背景 最近荷兰政府颁布了关于半导体设备出口管制的最新条例. 好像45nm以下的工艺的设备都可能收到限制. 对中国的相关厂商比如长鑫还有华虹的影响应该都比较大. 认为 ...

  2. [转帖]ESXi下查看CPU 频率

    https://www.jianshu.com/p/8943a4223ed7 查看CPU的固定频率 [root@localhost:/bin] esxcli hardware  cpu list|gr ...

  3. 【转帖】SRE 高延迟问题的罪魁祸首 System.gc()

    https://www.infoq.cn/article/lXTRgYb9ecVBu*72fT7O jstact -gccause pid 3000 30 01 案例一: 某日,支付平台的开发人员找到 ...

  4. [转帖]AnolisOS8安装ntp同步时间

    https://www.wlnmp.com/post-673.html 在AnolisOS8中默认不再支持ntp软件包,时间同步将由chrony来实现,如果你习惯了使用ntp来同步时间,一时难以去适应 ...

  5. 【转贴】西数全新推出企业级金盘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 ...

  6. echarts更改x和y轴的颜色

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  7. vs2019系统内置方法无提示

    有个同事问我为什么他的vs编写C#代码,对于引用System.dll中的方法,鼠标移上去没有方法的使用说明或接口注释,具体可以看下面的截图,而我绝大多数情况下是使用Rider开发,并没有遇到这个问题, ...

  8. Go中sync.map使用小结

    sync.map 前言 深入了解下 查看下具体的实现 Load Store Delete LoadOrStore 总结 流程图片 参考 sync.map 前言 Go中的map不是并发安全的,在Go1. ...

  9. 9.9 Windows驱动开发:内核远程线程实现DLL注入

    在笔者上一篇文章<内核RIP劫持实现DLL注入>介绍了通过劫持RIP指针控制程序执行流实现插入DLL的目的,本章将继续探索全新的注入方式,通过NtCreateThreadEx这个内核函数实 ...

  10. C/C++ 关于继承与多态笔记

    继承的基本语法: 继承的目的就是用于提高代码的可用性,减少代码的重复内容,高效开发. #include <iostream> using namespace std; class Base ...