ASP.NET Core – Custom Input formatters For Partial Update and Handle Under-posting
前言
之前的文章有谈过关于 ASP.NET Core 处理 under-posting 的方式.
它会使用 class default value. 许多时候这可能不是我们期望的. 比如当我们想要 patch update resource 的时候.
一种解决方法是把 DTO 改成 nullable 来表示 under-posting, 但这也不总是正确的, 毕竟也有可能它是想把 value set to null.
另一个方法是使用 JSON Patch, 但这个方法会让前端比较麻烦. 那为了实现需求只能做一些 Custom 的方案了
Model Binding
参考: Model Binding in ASP.NET Core
在说解决思路之前, 先了解一些基础知识. Model Binding 是 ASP.NET Core 把 Request 的信息投影到 Action parameter 的一个执行过程.

这就是为什么在 Action 阶段获取到的不是 JSON string 而是已经 deserialize 好的 instance. 就是这个 Model Binding 干的事儿.
Input Formatters
而在整个 Model Binding 中, Input Formatters 则是最终负责 deserialize JSON 的.
解决思路
想解决 under-posting 的问题, 就必须在 ASP.NET Core 处理 JSON 之前, 一旦它 deserialize JSON 后, 我们就失去了 under-posting 的信息了.
所以效仿 JsonPatch 的做法就是 Custom Input Formatters.

我们可以让 deserialize 的 class implement 特定的 interface, 比如叫 IUnderPosting, 里面则拥有一个 UnderPostingPropertyNames.
拦截 format 以后, 先做一轮原生的 JSON deserialize, 然后在做一个 JsonNode parse, 然后把 under-posting 的 property 记入到对象中.
这个概念和 JSON overflow 类似 Handle overflow JSON.
于是我们就拥有了 under-posting 的信息. 往后, 无论是 validation, mapping 都可以利用这个信息去做事情, 比如 under-posting 的 property 就 ignore validation 同时也不需要 update entity.
预想的结果
按思路走大概是这样的
public interface IUnderPosting
{
List<string> UnderPostingPropertyNames { get; set; }
} public class UpdateProductDTO : IUnderPosting
{
public string Name { get; set; } = "";
public decimal Amount { get; set; }
public List<string> UnderPostingPropertyNames { get; set; } = new();
}
在 Controller 可以依据 UnderPostingPropertyNames 去做逻辑处理.
[ApiController]
[Route("api/products")]
public class ProductController : ControllerBase
{
[HttpPatch]
public IActionResult UpdateProduct(UpdateProductDTO dto)
{
return Ok();
}
}
探路
看源码
既然我们是想替代 ASP.NET Core build-in 的 JSON formatter, 那就必须先看看它长什么样. 要如何 customize.
SystemTextJsonInputFormatter.cs

TextInputFormatter 好理解, Custom formatters in ASP.NET Core Web API 里也用这个, IInputFormatterExceptionPolicy 就不太清楚了.
最基本需要实现的接口
public class MyInputFormatter2 : TextInputFormatter, IInputFormatterExceptionPolicy
{
public InputFormatterExceptionPolicy ExceptionPolicy => throw new NotImplementedException();
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
throw new NotImplementedException();
}
}
所以整个核心代码就在 ReadRequestBodyAsync 里

可以大致看出它的实现方式, 从 HttpContext 读取 body stream, 然后 deserialze, ModelType 就是要 binding 到的 class, 如果有 exception 比如 type mismatch, 就写入到 ModelError.
显然它没有允许扩展, 没有办法简单的继承 override 去 customize 它. 另外还有一个难题.

它有 Dependency Injection 的, 但是 formatter 是不允许有 Dependency Injection 的

想要依赖 service 需要靠 context 去拿才正确.

怎么办... wrap 它!
能想到的方法就是 wrap 它来 customize
public class MyInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy
{
public MyInputFormatter()
{
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
SupportedEncodings.Add(UTF16EncodingLittleEndian);
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json").CopyAsReadOnly());
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/json").CopyAsReadOnly());
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/*+json").CopyAsReadOnly());
}
InputFormatterExceptionPolicy IInputFormatterExceptionPolicy.ExceptionPolicy => InputFormatterExceptionPolicy.MalformedInputExceptions; public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
var serviceProvider = context.HttpContext.RequestServices;
var logger = serviceProvider.GetRequiredService<ILogger<SystemTextJsonInputFormatter>>();
var jsonOptionsAccessor = serviceProvider.GetRequiredService<IOptions<JsonOptions>>();
var defaultFormatter = new SystemTextJsonInputFormatter(jsonOptionsAccessor.Value, logger);
InputFormatterResult result;
if (StgUtil.HasImplementInterface(context.ModelType, typeof(IUnderPosting)))
{
context.HttpContext.Request.EnableBuffering();
result = await defaultFormatter.ReadRequestBodyAsync(context, encoding);
context.HttpContext.Request.Body.Position = 0;
var model = (IUnderPosting)result.Model!;
using var document = await JsonDocument.ParseAsync(context.HttpContext.Request.Body);
context.HttpContext.Request.Body.Position = 0;
var rootElement = document.RootElement;
var properties = rootElement.EnumerateObject().Select(p => p.Name).ToList();
model.UnderPostingPropertyNames = properties;
}
else {
result = await defaultFormatter.ReadRequestBodyAsync(context, encoding);
}
return result;
}
}
这样我们就可以控制最终的 binding 结果了. 这里只是一个大概, 具体实现我懒得写了. 还有一个需要注意的是 body stream 默认只能 read 一次, 如果要多次需要打开设定.
参考:
Re-reading ASP.Net Core request bodies with EnableBuffering()
How to read request body in an asp.net core webapi controller?
ASP.NET Core – Custom Input formatters For Partial Update and Handle Under-posting的更多相关文章
- ASP.NET Core MVC 之局部视图(Partial Views)
1.什么是局部视图 局部视图是在其他视图中呈现的视图.通过执行局部视图生成的HTML输出呈现在调用视图中.与视图一样,局部视图使用 .cshtml 文件扩展名.当希望在不同视图之间共享网页的可重用部分 ...
- ASP.NET Core 中文文档 第四章 MVC(3.7 )局部视图(partial)
原文:Partial Views 作者:Steve Smith 翻译:张海龙(jiechen).刘怡(AlexLEWIS) 校对:许登洋(Seay).何镇汐.魏美娟(初见) ASP.NET Core ...
- [转]Writing Custom Middleware in ASP.NET Core 1.0
本文转自:https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/ One of the new ...
- [转]How do you create a custom AuthorizeAttribute in ASP.NET Core?
问: I'm trying to make a custom authorization attribute in ASP.NET Core. In previous versions it was ...
- 在ASP.NET Core使用Middleware模拟Custom Error Page功能
一.使用场景 在传统的ASP.NET MVC中,我们可以使用HandleErrorAttribute特性来具体指定如何处理Action抛出的异常.只要某个Action设置了HandleErrorAtt ...
- 在ASP.NET Core中实现自定义验证特性(Custom Validation Attribute)
这是我们在实际ASP.NET Core项目中用到的,验证用户名中是否包含空格. 开始是这么实现的(继承ValidationAttribute,重写IsValid方法): public class No ...
- [转]Using NLog for ASP.NET Core to write custom information to the database
本文转自:https://github.com/NLog/NLog/issues/1366 In the previous versions of NLog it was easily possibl ...
- [转]Create Custom Exception Filter in ASP.NET Core
本文转自:http://www.binaryintellect.net/articles/5df6e275-1148-45a1-a8b3-0ba2c7c9cea1.aspx In my previou ...
- [转]在ASP.NET Core使用Middleware模拟Custom Error Page功能
本文转自:http://www.cnblogs.com/maxzhang1985/p/5974429.html 阅读目录 一.使用场景 二..NET Core实现 三.源代码 回到目录 一.使用场景 ...
- [译]Writing Custom Middleware in ASP.NET Core 1.0
原文: https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/ Middleware是ASP. ...
随机推荐
- R语言基于表格文件的数据绘制具有多个系列的柱状图与直方图
本文介绍基于R语言中的readxl包与ggplot2包,读取Excel表格文件数据,并绘制具有多个系列的柱状图.条形图的方法. 首先,我们配置一下所需用到的R语言readxl包与ggplot2 ...
- hmall | 引入ES实现高效搜索与同步双写
在gitee.飞书.百度云.B站中,黑马都没有上传该部分资料,以下皆为个人观点,如有纰漏欢迎指正 1.先把item-service中的searchcontroller抽出来,抽到一个模块中并将其设为h ...
- 错误记录java: JDK isn't specified for module
跑苍穹外卖的时候遇到了 java: JDK isn't specified for module 'sky-pojo'这一问题 解决办法是通过修改JDK版本,这个项目用的springboot比较早,可 ...
- 【Java】 WebService 校验机制
测试环境域名 不可见 正式环境域名 不可见 1.2.安全校验凭证 accessId(授权ID) 测试/正式待定 securityKey(加密密钥) 测试/正式待定 1.3.安全校验机制 1.3.1.在 ...
- tcprewrite-man
官网手册:https://tcpreplay.appneta.com/wiki/tcprewrite-man.html https://linux.die.net/man/1/tcpprep 中文:h ...
- 阿里提供的免费DNS服务器
阿里提供的免费DNS服务器的介绍网页: https://developer.aliyun.com/mirror/DNS nameserver 223.5.5.5 nameserver 223.6.6. ...
- Python语言中当前工作目录(Current Working Directory, cwd)与模块搜索第一路径都是指什么???
相关: 查看并添加python中库的搜索路径 [python]自问自答:python -m参数? ( python3.7 版本 ) 本文主要解释Python语言中的两个基本概念: 当前工作目录(Cur ...
- Apache DolphinScheduler 3.2.1 版本发布:增强功能与安全性的全面升级
近期,Apache DolphinScheduler 社区激动地宣布 3.2.1 版本的发布.此次更新不仅着力解决了前一版本(3.2.0)中遗留的问题,而且引入了一系列的功能增强和优化措施. 原先的问 ...
- 3.2.0 版本预告!远程日志解决 Worker 故障获取不到日志的问题
Apache DolphinScheduler 3.2.0 版本已经呼之欲出,8 月 中下旬,这个大版本就要和用户见面了.为了让大家提前了解到此版本更新的主要内容,我们已经制作了几期视频和内容做了大致 ...
- Python-目标检测-将xml文件转换成.txt文件
代码说明:labels文件夹是工程下的一个文件夹,里面存放的是一些xml文件. 然后我们将这些xml文件中的内容取出来,放在路径path1的文件名下.这样也就完成了xml文件到txt文件的转化. 该代 ...