本篇老周就和老伙伴们分享一下,对于客户端提交的不规范 Body 如何做模型绑定。不必多说,这种情况下,只能自定义 ModelBinder 了。而且最佳方案是不要注册为全局 Binder——毕竟这种特殊情况是针对极少数情形的,咱们没必要去干扰标准格式的正常运行(情况复杂,特殊 binder 注册为全局很危险,弄不好容易出“八阿哥”)。

你可能会说,用标准的 JSON 或 XML 不香吗,为什么要做不规范的数据?你可别说,实际开发中,就是有不少思维奇葩的客户,提出各种连外星人都感到神经病的需要。所以,倒了九辈子大霉遇到这样的需求,就不得不根据实际数据格式来自定义绑定了。

OK,老周深刻意识到,讲再多的理论各位都不感兴趣的,人们更习惯于听故事,不然的话为什么正史所记录的事情知者甚少,而很多虚构的故事会在民间大量传播。比如“狸猫换太子”什么的,都是故事,一只死猫能把皇子换掉,你以为皇宫是菜市场呢。

这里有这样的故事:某API,参数是 Room 对象。Room 类型有三个属性——Length、Width、Height,它们的类型都是浮点(float)。然后这个 API 调用时怎么 POST 的呢?三行文本,每一行表示一个属性,格式是 name: value,并且不区分大小写。也就是说,调用 API 时,正文部分这样传:

length:  3.5
width: 0.7
height: 12.5

也可能这样:

LENGTH: 5.5
WIDTH: 10.0
HEIGHT: 2.7

还可能是这样:

Length: 50.2
Width: 11.55
Height: 14.3

所以,待会儿咱们实现属性名称查找时,统一转为小写,这样好比较。

下面,开始干活。

1、定义模型

public class Room
{
public float Length { get; set; }
public float Width { get; set; }
public float Height { get; set; }
}

2、定义 API 控制器

[ApiController, Route("api/test")]
public class WhatController : ControllerBase
{
[HttpPost, Route("abc")]
public float TestDone(Room rom)
{
if (rom.Height <= 0f || rom.Width <= 0f || rom.Length <= 0f)
return -1f;
return rom.Height * rom.Width * rom.Length;
}
}

3、自定义 ValueProvider

由于在本例中,body 的内容既不是标准的 XML 和 JSON,也不是 Form-data 结构,所以 .NET Core 默认注册的几个 ValueProvider 是不起作用的。咱们得自己实现一个。

    public class CustValueProvider : IValueProvider
{
private readonly IDictionary<string, string> innerDic; public CustValueProvider(HttpContext ctx)
{
innerDic = new Dictionary<string, string>();
var req = ctx.Request;
HttpRequestStreamReader reader = new(req.Body, Encoding.UTF8);
string? line;
// 一行一行地读
for (line = reader.ReadLineAsync().GetAwaiter().GetResult(); line != null; line = reader.ReadLineAsync().GetAwaiter().GetResult())
{
string[] parts = line!.Split(":");
// 左边是name,右边是value
string name = parts[0].Trim().ToLower();
string value = parts[1].Trim();
innerDic[name] = value;
}
reader.Dispose();
} public bool ContainsPrefix(string prefix)
{
return true;
} public ValueProviderResult GetValue(string key)
{
if (!innerDic.ContainsKey(key))
return ValueProviderResult.None;
return new ValueProviderResult(innerDic[key]);
}
}

通过构造函数的参数获得 HttpContext 对象,这样就可以访问到 Body,它是个流对象。

然后使用一个叫 HttpRequestStreamReader 的类,它位于 Microsoft.AspNetCore.WebUtilities 命名空间。这个类很好用,可以读文本文件那样读 Body。这里咱们一行一行地读就行,注意要调用 ReadLineAsync 方法,不能调用同步方法(除非你配置 Kestrel 充许同步调用)。异步调用可以确保响应能力,所以还是推荐异步调用。不过,此处是从构造函数调用的,就同步获其结果了。

分析过程是这样的:读出一行文本,然后用英文的冒号分隔字符串,变成一个二元素数组,[0] 就是 name,[1] 就是 value 了。把所有分析出来的东东都添加到字典对象中,方便后面提取值。

ContainsPrefix 方法是分析是否包含前缀,比如前X篇文章中提到的像 stu.name、stu.age 等,stu 就是前缀。这此咱们不考虑这个,所以直接返回 true。

GetValue 方法是关键,这是供给外面调用的规范方法,通过 key 从字典对象中查找出值。查找的 key 用的就是 Room 类的属性名称。

4、自定义 ModelBinder

    public class RoomBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
// 参数不能为 null
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext)); // 特殊处理,把全局的 ValueProvider 替换
bindingContext.ValueProvider = new CustValueProvider(bindingContext.HttpContext); var objMetadata = bindingContext.ModelMetadata;
// 目标类型有三个属性
if (objMetadata.Properties.Count == 0)
return Task.CompletedTask; Type modeltype = objMetadata.ModelType;
// 创建实例
object? modelObj = Activator.CreateInstance(modeltype);
if (modelObj is null)
return Task.CompletedTask; // 给属性赋值
foreach (var prop in objMetadata.Properties)
{
// 找到 key
string key = prop.Name!.ToLower();
var wrapValue = bindingContext.ValueProvider.GetValue(key);
if (wrapValue == ValueProviderResult.None)
continue; //无值,有请下一位
// 取内容
string realVal = wrapValue.FirstValue!;
// null 或者长度为 0,不可用
if (realVal is null or { Length: 0 })
continue; //没有用的值,有请下一位
// 看看能不能转换
var proptype = prop.ModelType; //这是属性值的类型
object? propval;
try
{
// 把取出的字符串转换为属性值的类型
propval = Convert.ChangeType(realVal, proptype);
}
catch { continue; }
// 设置属性值
// PropertyGetter:获取属性的值
// PropertySetter:设置属性的值
if (propval != null && prop.PropertySetter != null)
{
prop.PropertySetter(modelObj!, propval);
}
}
// 重要:一定要设置绑定的结果
bindingContext.Result = ModelBindingResult.Success(modelObj); return Task.CompletedTask;
}
}

由于是针对特殊情形的绑定,所以这里老周就不分析 Content-Type,假设它任意内容均可,文本提交一般用 text/* 或者明确一点 text/plain。

这里咱们也不使用全局的 ValueProvider,所以把 bindingContext 的也替换掉。

   bindingContext.ValueProvider = new CustValueProvider(bindingContext.HttpContext);

因为针对性强,这里其实你可以直接 new 一个 Room 实例,然后从 ValueProvider 中找出三个属性的值,直接转换为 float 值,赋给对象的三个属性即可。不过,老周为了装逼,写得复杂了一点。

ModelMetadata 包含目标类型相关的信息,比如它的 Type,它有几个属性(Properties),每个属性也能获取到 ModelMetaData,表示属性值的类型信息。

所以,首先咱们从 ModelType 得到 Room 类的 Type,接着,用 Activator 来创建它的实例。

从 Properties 中取出各个属性的 ModelMetaData,并根据属性名(有定义属性的类型通常不会为null)来查找出各属性的值,类型是字符串。然后获取属性值的 Type,再用 Convert.ChangeType 做类型转换,转为属性值的类型(这里其实是 float)。

再通过 PropertySetter 来设置属性的值,它是委托类型,和属性的 set 访问器绑定。

最后 ModelBindingResult.Success 设置填充 Room 对象。

------------------------------------------------------------------------------------------------

测试

POST /api/test/abc HTTP/1.1
Content-Type: text/plain
User-Agent: PostmanRuntime/7.29.0
Accept: */*
Host: localhost:5018
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 36 width: 3.6
height: 5.5
length: 8.0
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 26 Mar 2022 05:00:16 GMT
Server: Kestrel
Transfer-Encoding: chunked 158.4

好了,这个特殊绑定就顺利完成了。

其实,自定义 InputFormatter 也可以实现同样功能的,这个老周下一篇再扯。

【ASP.NET Core】MVC模型绑定:非规范正文内容的处理的更多相关文章

  1. ASP.NET Core MVC 模型绑定用法及原理

    前言 查询了一下关于 MVC 中的模型绑定,大部分都是关于如何使用的,以及模型绑定过程中的一些用法和概念,很少有关于模型绑定的内部机制实现的文章,本文就来讲解一下在 ASP.NET Core MVC ...

  2. ASP.NET Core MVC 模型绑定 (转载)

    ASP.NET Core MVC的Model Binding会将HTTP Request数据,以映射的方式对应到参数中.基本上跟ASP.NET MVC差不多,但能Binding的来源更多了一些.本篇将 ...

  3. .net core mvc 模型绑定 之 json and urlencoded

    .net core mvc 模型绑定, FromQuery,对应 url 中的 urlencoded string ("?key1=value1&key2=value2") ...

  4. ASP.NET Core 中文文档 第四章 MVC(01)ASP.NET Core MVC 概览

    原文:Overview of ASP.NET Core MVC 作者:Steve Smith 翻译:张海龙(jiechen) 校对:高嵩 ASP.NET Core MVC 是使用模型-视图-控制器(M ...

  5. ASP.NET Core MVC 概述

    https://docs.microsoft.com/zh-cn/aspnet/core/mvc/overview?view=aspnetcore-2.2 ASP.NET Core MVC 概述 20 ...

  6. 你所不知道的ASP.NET Core MVC/WebApi基础系列(二)

    前言 好久没冒泡了,算起来估计有快半年没更新博客了,估计是我第一次停更如此之久,人总有懒惰的时候,时间越长越懒惰,但是呢,不学又不行,持续的惰性是不行dei,要不然会被时光所抛弃,技术所淘汰,好吧,进 ...

  7. 你所不知道的ASP.NET Core MVC/WebApi基础系列 (二)

    转自博客:https://www.cnblogs.com/CreateMyself/p/10604293.html 前言 本节内容,我们来讲讲.NET Core当中的模型绑定系统.模型绑定原理.自定义 ...

  8. ASP.NET Core 入门教程 3、ASP.NET Core MVC路由入门

    一.前言 1.本文主要内容 ASP.NET Core MVC路由工作原理概述 ASP.NET Core MVC带路径参数的路由示例 ASP.NET Core MVC固定前/后缀的路由示例 ASP.NE ...

  9. ASP.NET Core 入门笔记4,ASP.NET Core MVC路由入门

    敲了一部分,懒得全部敲完,直接复制大佬的博客了,如有侵权,请通知我尽快删除修改 摘抄自https://www.cnblogs.com/ken-io/p/aspnet-core-tutorial-mvc ...

随机推荐

  1. Solution -「CF 1375G」Tree Modification

    \(\mathcal{Description}\)   Link.   给定一棵 \(n\) 个结点的树,每次操作选择三个结点 \(a,b,c\),满足 \((a,b),(b,c)\in E\),并令 ...

  2. MyBatis封装JDBC具体实现

    关于xml等解析的工具基本均包含在org.apache.ibatis.builder包中,有包名可知基本使用建造者模式.这个包的主要功能两个: 1.解析XML配置文件和映射文件,xml子包中: 2.解 ...

  3. ServiceStack.Redis的源码分析(连接与连接池)

    前几天在生产环境上redis创建连接方面的故障,分析过程中对ServiceStack.Redis的连接创建和连接池机制有了进一步了解.问题分析结束后,通过此文系统的将学习到的知识点整理出来. 从连接池 ...

  4. SpringBoot2.6.x默认禁用循环依赖后的应对策略

    一.序言 SpringBoot 2.6.x不推荐使用循环依赖,这是一个好消息,SpringBoot从底层逐渐引导开发者书写规范的代码,同时也是个忧伤的消息,循环依赖的应用场景实在是太广泛了. 如果从低 ...

  5. 【Kotlin】初识Kotlin(二)

    [Kotlin]初识Kotlin(二) 1.Kotlin的流程控制 流程控制是一门语言中最重要的部分之一,从最经典的if...else...,到之后的switch,再到循环控制的for循环和while ...

  6. CentOS单机安装k8s并部署.NET 6程序 压测 记录

    前面部分依照CentOS单机安装k8s并部署.NET 6程序来进行,内存.cpu.centos版本一致,之前222元买的三年8M 2c4g腾讯云轻量服务器,这个教程算是写的很详细的了,基本可以一致执行 ...

  7. 华为模拟器ensp AR启动失败 代码40 解决方案

    前几天更换了一台电脑,安装好Windows10 ,安装ensp 模拟器,安装好ensp后,发现AR都启动不了.卸载重新安装还是不行,此时度娘了一下,发现都说是协助不彻底,没有清理干净安装插件.我做了最 ...

  8. Renix绑定流详解——网络测试仪实操

    一.测试环境 使用测试仪模拟两台主机直接发流,中间有路由器.如下图,测试仪port1端口模拟的主机IP为10.1.1.2,port2端口模拟的主机IP为10.2.1.2 二.预约测试资源 打开Reni ...

  9. 实力再获认可!Smartbi入选爱分析·产业数字化厂商全景报告

    近日,中国独立的第三方产业数字化研究与咨询机构--爱分析发布<2021爱分析·产业数字化厂商全景报告>.思迈特软件凭借优秀的解决方案能力及丰富的业务实践经验,入选报告中银行数字化.保险数字 ...

  10. Vue框架简介和环境搭建

    前言: 此篇随笔为个人学习前端框架Vue,js的技术笔记,主要记录一些自己在学习Vue框架的心得体会和技术总结,作为回顾和笔记使用. 这种写博客的方式,对刚开始学习Vue框架的我,也是一种激励,我相信 ...