细聊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这款组件,具体的引入信息如下所示
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来演示一下,类和代码不具备任何实际意义,只是为了演示
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);
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方法来入手,我们来看一下源码定义[<a href="https://github.com/dotnet/aspnetcore/blob/v8.0.2/src/Mvc/Mvc.Core/src/Formatters/TextInputFormatter.cs#L60" target="_blank" rel="noopener">点击查看TextInputFormatter源码
来源:https://www.cnblogs.com/wucy/p/18025196/aspnetcore_webapi_formatter
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页:
[1]