咱们都知道,MVC在输入/输出中都需要模型绑定。因为HTTP请求发送的都是文本,为了使其能变成各种.NET 类型,于是在填充参数值之前需 ModelBinder 的参与,以将文本转换为 .NET 类型。

尽管 ASP.NET Core 已内置基础类型和复杂类型的各种 Binder,但有些数据还是不能处理的。比如老周下面要说的情况。

------------------------------------------------- 白金分割线 -------------------------------------------------------

情景假设:

1、我需要读取HTTP消息的整个 body 来填充 MVC 方法参数;

2、HTTP消息的 body 不是 form-data,而是完全的二进制内容。

最简单的方法就是不使用模型绑定,即在MVC方法中直接访问 HttpContext.Request.Body。

var request = HttpContext.Request;
using(StreamReader reader = new(request.Body))
{
……
}

这样很省事。不过这法子是不走模型绑定路线的,不时候我们是不希望这么弄,而是用这样的控制器。

// 魔鬼控制器
[HttpPost("/magic/post")]
public ActionResult PostSomething(Stream data)
{
// 计算个哈希
byte[] hash = SHA1.HashData(data);
// 长度
long len = data.Length;
// 响应
return Content($"你提交的数据长度:{len},SHA1:{Convert.ToHexString(hash)}");
}

这里我用单元测试来尝试调用它。

 [TestClass]
public class UnitTest1
{
[TestMethod]
public async Task TestMethod1()
{
Uri rootURL = new Uri("https://localhost:7194");
HttpClient client = new();
client.BaseAddress = rootURL;
// 随便弄点数据
byte[] data = new byte[512];
Random.Shared.NextBytes(data);
// 建立流
MemoryStream mmstream = new MemoryStream(data);
// 构建内容
StreamContent content = new StreamContent(mmstream);
// 设置标准头 application/octet-stream
content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Octet);
// 发输出一下哈希
string sha1 = Convert.ToHexString(SHA1.HashData(data));
Console.WriteLine("SHA1: {0}", sha1);
// 发送POST请求
var response = await client.PostAsync("/magic/post", content);
// 输出结果
Console.WriteLine($"响应代码:{response.StatusCode}");
Console.WriteLine("响应内容:{0}", await response.Content.ReadAsStringAsync()); Assert.IsTrue(response.StatusCode == System.Net.HttpStatusCode.OK);
}
}

先运行服务器,再运行单元测试。结果:Failed。

这个提示是说不能创建 Stream 类的实例。是的,因为这厮不是实现类,它很抽象,抽象到连 ComplexObjectModelBinder 都玩不下去了。这同时也说明,对于非基础类型,ASP.NET Core 默认是把参数当成复杂类型来绑定的。

于是咱们又冒出另一个思路:用 BodyModelBinder 试试。就是在参数上加个[FromBody]特性。

[HttpPost("/magic/post")]
public ActionResult PostSomething([FromBody]Stream data)
{
……
}

其实,Web API 说白了就是不用视图的 MVC 控制器。在控制器上应用 [ApiController] 特性后,在方法参数上可以省略 [FromBody] 特性。如果控制器上不应用 [ApiController] 特性,就要手动加 [FromBody] 特性。

再运行一下单元测试。结果还是 Failed。

这次返回的状态是 UnsupportedMediaType,即415。

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

接下来是无聊的理论知识,请准备好奶茶。

BodyModelBinder 在进行绑定时实际上是使用 IInputFormatter 来读取HTTP消息正文(body)的。允许使用多个 IInputFormatter,只要有一个能解析成功就行。默认情况下,仅支持 application/json、text/json 格式。这个咱们可以从源代码看出来。

 // Set up default input formatters.
options.InputFormatters.Add(new SystemTextJsonInputFormatter(_jsonOptions.Value, _loggerFactory.CreateLogger<SystemTextJsonInputFormatter>())); // Media type formatter mappings for JSON
options.FormatterMappings.SetMediaTypeMappingForFormat("json", MediaTypeHeaderValues.ApplicationJson);

于是,咱们把单元测试的代码改一下。

// 构建内容
//StreamContent content = new StreamContent(mmstream);
JsonContent content = JsonContent.Create<Stream>(data);
// 设置标准头 application/json
content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);

这样做也是不行的。

这次是 HashData 方法抛出的异常,问题还是出在 Stream 类型的参数不能实例化。若把操作方法的参数类型改为 byte[] 就没问题了。

 public ActionResult PostSomething([FromBody]byte[] data)

可是这样一改,就与我们当初的要求相差太大了,我就喜欢用 Stream 类型啊,咋办?

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

那只好自己写 Binder 了,反正也不难。

    public class StreamModelBinder : IModelBinder
{
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
if(bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
} // 数据源要来自body
Console.WriteLine($"Binding Source: {bindingContext.BindingSource?.Id}");
if(bindingContext.BindingSource == null || bindingContext.BindingSource != BindingSource.Body)
{
return;
}
var request = bindingContext.HttpContext.Request;
// 咱们不关心Content-Type是啥
long? len = request.ContentLength;
// 只关心有没有正文
if(len == null && len == 0L)
{
return;
}
// 由于这个流类型有些成员不支持(比如Length属性),所以复制到内存流中
MemoryStream mstream = new MemoryStream();
await request.Body.CopyToAsync(mstream);
// 回位
mstream.Position = 0L;
bindingContext.Result = ModelBindingResult.Success(mstream);
}
}

然后改一下控制器方法,并将上面的 Binder 通过 [ModelBinder] 特性应用到 Stream 类型的参数上。

[HttpPost("/magic/post")]
public async Task<ActionResult> PostSomething([FromBody, ModelBinder(typeof(StreamModelBinder))]Stream data)
{
// 计算个哈希
byte[] hash = await SHA1.HashDataAsync(data);
// 长度
long len = data.Length;
// 响应
return Content($"你提交的数据长度:{len}\nSHA1:{Convert.ToHexString(hash)}");
}

[ModelBinder] 特性可以局部使用自定义的 ModelBinder。此处老周建议不需要全局注册,仅在有 Stream 类型的输入参数时才用,毕竟这货也不是通用型的。

如果要全局应用,你得实现 IModelBinderProvider 接口,让 GetBinder 方法返回 StreamModelBinder 实例。然后把这个实现 IModelBinderProvider 的类型添加到 MvcOptions 选项类的 ModelBinderProviders  列表中。

经过这么一弄,嘿,有门!

只有两个哈希值相同才表明数据被正确传输。

有大伙伴肯定又有疑问了:在 StreamModelBinder 中把 Body 复制到内存流,再用内存流来为模型赋值。这……这……这不闲得肛门疼吗?在注释里老周写明了,因为 Body 那个是 HttpRequest 网络流,像 Length 属性等成员是不支持的,在控制器方法中访问会抛异常。

你也可以节能一下,直接用 Body 来设置模型值,但在控制器代码中不能用 Length 属性来读取长度了。

public class StreamModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if(bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
} // 数据源要来自body
//Console.WriteLine($"Binding Source: {bindingContext.BindingSource?.Id}");
if(bindingContext.BindingSource == null || bindingContext.BindingSource != BindingSource.Body)
{
return Task.CompletedTask;
}
var request = bindingContext.HttpContext.Request;
// 咱们不关心Content-Type是啥
long? len = request.ContentLength;
// 只关心有没有正文
if(len == null && len == 0L)
{
return Task.CompletedTask;
}
// 直接赋值
bindingContext.Result = ModelBindingResult.Success(request.Body);
return Task.CompletedTask;
}
}

控制器中的代码可以改为绑定 HTTP 消息头来获取长度。

[HttpPost("/magic/post")]
public async Task<ActionResult> PostSomething([FromBody, ModelBinder(typeof(StreamModelBinder))]Stream data, [FromHeader(Name = "Content-Length")]long len)
{
// 计算个哈希
byte[] hash = await SHA1.HashDataAsync(data);
// 响应
return Content($"你提交的数据长度:{len}\nSHA1:{Convert.ToHexString(hash)}");
}

len 参数的值来自 Content-Length 消息头。

运行服务器,再执行一下单元测试,结果是有效的。

【ASP.NET Core】MVC操作方法如何绑定Stream类型的参数的更多相关文章

  1. ASP.NET Core MVC/WebAPi 模型绑定探索

    前言 相信一直关注我的园友都知道,我写的博文都没有特别枯燥理论性的东西,主要是当每开启一门新的技术之旅时,刚开始就直接去看底层实现原理,第一会感觉索然无味,第二也不明白到底为何要这样做,所以只有当你用 ...

  2. ASP.NET Core MVC/WebAPi 模型绑定探索 转载https://www.cnblogs.com/CreateMyself/p/6246977.html

    前言 相信一直关注我的园友都知道,我写的博文都没有特别枯燥理论性的东西,主要是当每开启一门新的技术之旅时,刚开始就直接去看底层实现原理,第一会感觉索然无味,第二也不明白到底为何要这样做,所以只有当你用 ...

  3. 【转】ASP.NET Core MVC/WebAPi 模型绑定探索

    前言 相信一直关注我的园友都知道,我写的博文都没有特别枯燥理论性的东西,主要是当每开启一门新的技术之旅时,刚开始就直接去看底层实现原理,第一会感觉索然无味,第二也不明白到底为何要这样做,所以只有当你用 ...

  4. ASP.NET Core MVC/WebAPi 模型绑定

    public class Person { public string Name { get; set; } public string Address { get; set; } public in ...

  5. 你想要的都在这里,ASP.NET Core MVC四种枚举绑定方式

    前言 本节我们来讲讲在ASP.NET Core MVC又为我们提供了哪些方便,之前我们探讨过在ASP.NET MVC中下拉框绑定方式,这节我们来再来重点看看枚举绑定的方式,充分实现你所能想到的场景,满 ...

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

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

  7. ASP.NET Core MVC四种枚举绑定方式

    前言 本节我们来讲讲在ASP.NET Core MVC又为我们提供了哪些方便,之前我们探讨过在ASP.NET MVC中下拉框绑定方式,这节我们来再来重点看看枚举绑定的方式,充分实现你所能想到的场景,满 ...

  8. ASP.NET Core MVC如何上传文件及处理大文件上传

    用文件模型绑定接口:IFormFile (小文件上传) 当你使用IFormFile接口来上传文件的时候,一定要注意,IFormFile会将一个Http请求中的所有文件都读取到服务器内存后,才会触发AS ...

  9. ASP.NET Core 四种方式绑定枚举值

    前言 本节我们来讲讲在ASP.NET Core MVC又为我们提供了哪些方便,之前我们探讨过在ASP.NET MVC中下拉框绑定方式,这节我们来再来重点看看枚举绑定的方式,充分实现你所能想到的场景,满 ...

  10. ASP.NET Core MVC中URL和数据模型的匹配

    Http GET方法 首先我们来看看GET方法的Http请求,URL参数和ASP.NET Core MVC中Controller的Action方法参数匹配情况. 我定义一个UserController ...

随机推荐

  1. 51单片机下实现软件模拟IIC通信

    1.IIC协议简易概述 IIC全称Inter-Integrated Circuit (集成电路总线),是由PHILIPS公司在80年代开发的两线式串行总线,用于连接微控制器及其外围设备.IIC属于半双 ...

  2. 实现fastdfs防盗链功能

    目录 1.背景 2.实现原理 2.1 开启防盗链 2.2 重启 nginx 2.3 Java代码生成token 1.token生成规则 2.java生成token 3.测试 3.1 带正确token访 ...

  3. 鸟哥linux私房菜习题总结

    第零章 1.请找出世界上跑得最快的超级计算机的相关信息? 系统名称:天河二号. 所在位置:中国广州中山大学超算中心. 使用的cpu型号与规格:采用的是Intel的Ivy Bridge处理器与Xeon ...

  4. .NET 7 RC 2 发布,倒计时一个月发布正式版

    微软2022-10-22 发布了 .NET 7 RC 2,下一站是.NET 7正式发布,就在下个月Net Conf 2022(11月8日)期间正式发布. 经过长达一年时间的开发,.NET 7 规划的所 ...

  5. MongoDB、Redis、elasticSearch、hbase的对比

    MongoDB.Redis.elasticSearch.hbase的对比 MongoDB 优点: (1) 最大的特点是表结构灵活可变,字段类型可以随时修改. (2) 插入数据时,不必考虑表结构的限制. ...

  6. 如何检查“lateinit”变量是否已初始化?

    kotlin中经常会使用延迟初始化,如果要校验lateinit var 变量是否初始化.可以使用属性引用上的.isInitialized. 原文中是这样描述的:To check whether a l ...

  7. html和php里引用文件

    html里引入css文件: <link href="base.css" rel="stylesheet" type="text/css" ...

  8. Ruoyi表单构建

    Ruoyi表单构建通过拖动组件就能自动生成前端代码,很方便,所以本文简单通过上层函数源码来梳理一下大致流程,如有需要再自行仔细一行行分析底层代码. 组件拖动 实现组件拖动功能主要依赖第三方库:VueD ...

  9. UBOOT编译--- make xxx_deconfig过程详解(一)

    make xxx_deconfig过程详解 1. 前言 2. 概述 3. build变量的定义 4. 目标%config的定义 4.1 依赖 scripts_basic 4.1.1 语句$(if $ ...

  10. 图数据 3D 可视化在 Explorer 中的应用

    本文首发于 NebulaGraph 公众号 前言图数据可视化是现代 Web 可视化技术中比较常见的一种展示方式,NebulaGraph Explorer 作为基于 NebulaGraph 的可视化产品 ...