前言

之前的文章有谈过关于 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?

Stream 基础和常用

ASP.NET Core – Custom Input formatters For Partial Update and Handle Under-posting的更多相关文章

  1. ASP.NET Core MVC 之局部视图(Partial Views)

    1.什么是局部视图 局部视图是在其他视图中呈现的视图.通过执行局部视图生成的HTML输出呈现在调用视图中.与视图一样,局部视图使用 .cshtml 文件扩展名.当希望在不同视图之间共享网页的可重用部分 ...

  2. ASP.NET Core 中文文档 第四章 MVC(3.7 )局部视图(partial)

    原文:Partial Views 作者:Steve Smith 翻译:张海龙(jiechen).刘怡(AlexLEWIS) 校对:许登洋(Seay).何镇汐.魏美娟(初见) ASP.NET Core ...

  3. [转]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 ...

  4. [转]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 ...

  5. 在ASP.NET Core使用Middleware模拟Custom Error Page功能

    一.使用场景 在传统的ASP.NET MVC中,我们可以使用HandleErrorAttribute特性来具体指定如何处理Action抛出的异常.只要某个Action设置了HandleErrorAtt ...

  6. 在ASP.NET Core中实现自定义验证特性(Custom Validation Attribute)

    这是我们在实际ASP.NET Core项目中用到的,验证用户名中是否包含空格. 开始是这么实现的(继承ValidationAttribute,重写IsValid方法): public class No ...

  7. [转]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 ...

  8. [转]Create Custom Exception Filter in ASP.NET Core

    本文转自:http://www.binaryintellect.net/articles/5df6e275-1148-45a1-a8b3-0ba2c7c9cea1.aspx In my previou ...

  9. [转]在ASP.NET Core使用Middleware模拟Custom Error Page功能

    本文转自:http://www.cnblogs.com/maxzhang1985/p/5974429.html 阅读目录 一.使用场景 二..NET Core实现 三.源代码 回到目录 一.使用场景 ...

  10. [译]Writing Custom Middleware in ASP.NET Core 1.0

    原文: https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/ Middleware是ASP. ...

随机推荐

  1. ComfyUI进阶:Comfyroll插件 (一)

    ComfyUI进阶:Comfyroll插件 (一) 前言: 学习ComfyUI是一场持久战,而Comfyroll Studio 是一款功能强大的自定义节点集合,专为 ComfyUI 用户打造,旨在提供 ...

  2. MViT:性能杠杠的多尺度ViT | ICCV 2021

    论文提出了多尺度视觉Transformer模型MViT,将多尺度层级特征的基本概念与Transformer模型联系起来,在逐层扩展特征复杂度同时降低特征的分辨率.在视频识别和图像分类的任务中,MViT ...

  3. Win10下安装LabelImg以及使用--LabelImg

    labelImg是图片标注软件,用于数据集的制作.标注等等.下面介绍labelImg的安装过程. 我用的是anaconda,所以以anaconda prompt作为终端: 在Anaconda Prom ...

  4. 计算机网络中的检验和(checksum)(包括计算文件的检验和附有c++代码)

    介绍: 检验和(checksum),在数据处理和数据通信领域中,用于校验目的地一组数据项的和.它通常是以十六进制为数制表示的形式.如果校验和的数值超过十六进制的FF,也就是255. 就要求其补码作为校 ...

  5. mysql 忘记root密码怎么办?

    忘记root可以跳过grant table来登录 1.打开命令行输入以下命令 mysqld -nt --grant-skip-tables 2.在打开一个新命令行,输入以下命令可以登录, mysql ...

  6. jmeter目录结构含义

    backups :脚本备份目录.格式JMXbin目录∶存放Jmeter的启动脚本,配置文件.模块文件.jmeter.bat启动Jmeterjmeter.properties核心配置文件.docs:离线 ...

  7. 百度翻译network里没有sug(文章发布时间2022年10月)

    百度翻译已经更新,现在的百度翻译分为两个阶段翻译,第一个阶段识别你的翻译字符是什么类型语言 第二阶段生成随机sign加携带token以post表单方式上传数据,返回json数据 尚硅谷在B站发布的的爬 ...

  8. web3 产品介绍:metamask 钱包 使用dapp更方便的钱包

    Metamask钱包是一种基于区块链技术的数字货币钱包,它允许用户在安全.便捷的环境下管理自己的加密资产.Metamask钱包是以太坊生态系统中最流行的钱包之一,它具有易于使用.安全性高和功能强大等优 ...

  9. 【ECharts】03 样式

    ECharts4 开始,除了默认主题外,内置了两套主题,分别为 light 和 dark. 设置方式: var chart = echarts.init(dom, 'light'); var char ...

  10. 基于Unity开发的强化学习环境(游戏环境):ml-agents —— Unity ML-Agents

    介绍: https://medium.com/nerd-for-tech/an-introduction-to-machine-learning-with-unity-ml-agents-af7193 ...