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. ...
随机推荐
- 部分解决 | ocrmypdf对中文pdf进行ocr识别后存在多余空格
1.问题 ocrmypdf安装采用的是在windows安装方法具体看 https://media.readthedocs.org/pdf/ocrmypdf/latest/ocrmypdf.pdf 由于 ...
- [oeasy]python0009 - 设置断点_break_point
调试程序 回忆上次内容 py 的程序是按照顺序执行的 是一行行挨排解释执行的 程序并不是数量越多越好 kpi也在不断演化 编辑 写的代码越多 出现的bug就越多 那什么是bug呢? 如何 ...
- Python shortuuid生成库学习小结
shortuuid生成库学习小结 by:授客 QQ:1033553122 实践环境 win10 Python 3.5.4 shortuuid-1.0.1-py3-none-any.whl 下载地址: ...
- 题解:P8144 [JRKSJ R4] BBWWBB
思路 分析题意可得,白方必定不会胜利,只能尽量让游戏无限进行下去.那么我们只考虑黑方能否胜利. 若想让戏能无限进行下去,必须满足以下条件. 白方先手. 若黑方先手必然可以吃掉一个白方,白方仅有一个棋子 ...
- 在MySQL中 Truncate Delect Drop 的区别
在MySQL中 Truncate Delect Drop 的区别 面试问题: -- -- 请详细描述MySQL中TRUNCATE TABLE.DELETE FROM和DROP TABLE三个命令的区别 ...
- 小技巧:初始化后查看容器内某一bean的信息
1.debug 2. 3.与容器名对应,可以看到容器的对应信息 4.输入表达式可以直接获取对应结果信息,这里查看的是默认SpringSecurity过滤链的bean
- XXL-JOB分片执行分布式任务
XXL-JOB相对于springtask来说优点之一就是分布式执行任务,可以在调度中心为执行器分发任务,实现分布式. 分片广播任务即当一个微服务形成集群的时候,任务会完整的下发给每一个执行器.而不像其 ...
- 【CI/CD】Centos7 下载安装 Jenkins
一.Docker安装Jenkins 参考: https://www.bilibili.com/video/BV11B4y1W7eH?p=5 安装Jenkins最新稳定版镜像: [root@Centos ...
- 【JavaScript】文件上传下载问题
问题原因 一般文件上传前端甚至可以不涉及JS来实现 input标签套在form标签,由form标签直接发送请求就可以实现上传功能 但是现在很多项目都使用前后端分离,AJAX一刀切所有. input标签 ...
- TensorBoard标量图中的平滑曲线是如何做的平滑?—— tensorflow TensorBoard标量图中“平滑”参数背后的数学原理是什么?—— 指数移动平均(EMA)
TensorFlow的tensorboard的平滑曲线的实现代码: 使用"指数移动平均"技术实现. 地址: https://github.com/tensorflow/tensor ...