MVC操作方法如何绑定Stream类型的参数

news2024/12/30 2:35:29

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 消息头。

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

最后,补充一下,Mini-API 方式是支持使用 Stream 类型的参数的,不用自定义写代码。

app.MapPost("/dowork", async (Stream data) =>
{
    byte[] hash = await SHA1.HashDataAsync(data);
    string hashstr = Convert.ToHexString(hash);
    return Results.Content($"接收的数据的哈希:{hashstr}");
});
结果是 Success 的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/100197.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[附源码]计算机毕业设计Python的物品交换平台(程序+源码+LW文档)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

使用设备树给DM9000网卡_触摸屏指定中断

目录 1 在设备树中描述网卡中断 2 dm9dev9000c.c 3 在设备树中描述触摸屏中断 1 在设备树中描述网卡中断 srom-cs420000000 {compatible "simple-bus";#address-cells <1>;#size-cells <1>;reg <0x20000000 0x8000000>;ranges;ethernet20000…

ESP 常用的低功耗配置选项解析

此篇博客介绍 ESP 常用的低功耗配置选项。 1.常用功耗优化配置选项 1.1.动态调频 CPU 工作的频率越高&#xff0c;功耗消耗也越大。通过 DFS&#xff08;dynamic frequency scaling&#xff0c;动态调频&#xff09;可以让系统自动切换工作频率&#xff0c;达到功耗和性能间…

Transformers学习笔记1. 一些基本概念和编码器、字典

Transformers学习笔记1. 一些基本概念和编码器、字典一、基本概念1. Hugging Face简介2. Transformers&#xff08;1&#xff09;简介&#xff08;1&#xff09;预定义模型&#xff08;2&#xff09;使用方法3. Datasets查看有哪些公开数据集方法1&#xff1a; 使用datasets包的…

深入分析JVM执行引擎

程序和机器沟通的桥梁 一、闲聊 相信很多朋友在出国旅游&#xff0c;或者与外国友人沟通的过程中&#xff0c;都会遇到语言不通的烦恼。这时候我们就需要掌握对应的外语或者拥有一部翻译机。而笔者只会中文&#xff0c;所以需要借助一部翻译器才能与不懂中文的外国友人交流。咱…

Android入门第51天-使用Android的SharedPreference存取信息

简介 上一篇我们介绍了在android里如何读写本地文件。我们有一种场景&#xff0c;类似网页的cookie&#xff0c;要把用户的一些储如上一次登录、使用的痕迹等信息保存下来以便于每次不需要做重复“填表单”的操作&#xff0c;当在这种场景下我们如果也使用本地文件读写的话显然…

关于Unity使用Aspose.Words创建表格单元格垂直合并不生效情况说明

文章目录&#x1f449;一、前言&#x1f449;二、问题重现1、首先看一下我用下面两段代码创建的表格&#xff1a;2、被这个问题折磨的心路历程&#x1f449;三、分析原因&#x1f449;四、解决方法&#x1f449;一、前言 最近在使用Aspose.Words.dll实现创建表格功能时&#x…

Google Earth Engine APP(GEE)——用一个选择器选择不同城市的应用

我们很多时候在进行应用制作的时候,都会用到选择器用于添加不同的城市,从而进一步选择不同的区域进行分析,本文就将准备一个包含有城市的矢量数据,按照名字进行筛选,最终展示不同城市的所在范围,从而实现简单的select选择器的调用。本文最主要的就是这个回调函数。 具体…

C语言基础—指针(地址引用、指针数组、二次指针)

本章主要讲解指针的基本定义和指针的传递、偏移。后面继续讲解指针数组和多维指针、二级指针等 知识点&#xff1a; 指针的定义和指针分类各类指针的字节长度取决于系统位数指针的传递&#xff08;值传递和引用(地址传递)&#xff09;指针的偏移&#xff08;自增自减号&#x…

动态优化解决方案空间中的最小支持(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 相对于求解函数极值这类静态问题&#xff0c;许多存在于真实世界的优化问题都是在动态变化的&#xff0c;这一类问题被称为动态…

201732-35-6,H2N-AFP-pNA

AFP-pNA&#xff0c;来自牙周病原体牙龈卟啉单胞菌和黑普氏菌的脯氨酸三肽基氨基肽酶的底物。 编号: 189876中文名称: 三肽Xaa-Xaa-Pro tripeptidylpeptidase substrateCAS号: 201732-35-6单字母: H2N-AFP-pNA三字母: H2N-Ala-Phe-Pro-pNA氨基酸个数: 3分子式: C23H27N5O5平均分…

React 入门:脚手架代理配置

文章目录React AjaxAxios在 React 中使用 Axios脚手架代理配置React Ajax 理解 React 本身只关注于界面&#xff0c;并不包含发送 ajax 请求的代码。前端应用需要通过 ajax 请求与后台进行交互&#xff08;json 数据&#xff09;。React 应用中需要继承第三方 ajax 库&#xff…

C++ · 入门 | 准备知识

啊我摔倒了..有没有人扶我起来学习.... &#x1f471;个人主页&#xff1a;《CGod的个人主页》\color{Darkorange}{《CGod的个人主页》}《CGod的个人主页》交个朋友叭~ &#x1f492;个人社区&#xff1a;《编程成神技术交流社区》\color{Darkorange}{《编程成神技术交流社区》…

uniapp实现楼层导航 ,滚动定位,锚点导航

uniapp实现楼层导航的核心技术要点&#xff1a; 1、scroll-view作为视图容器&#xff0c; 2、用其属性scroll-into-view,用于完成点击联动 3、uni.createSelectorQuery().selectAll();获取右侧所有元素信息&#xff0c;获取top值存入数组&#xff0c;用于计算滑动时需要的联动…

Vue-cli工程中每个文件夹和文件的用处

dist 文件夹&#xff1a;默认 npm run build 命令打包生成的静态资源文件&#xff0c;用于生产部署 node_modules&#xff1a;存放npm命令下载的开发环境和生产环境的依赖包 public&#xff1a;有的叫assets&#xff1a;存放项目中需要用到的资源文件&#xff0c;css、js、im…

【Linux】软件包管理器yum

​&#x1f320; 作者&#xff1a;阿亮joy. &#x1f386;专栏&#xff1a;《学会Linux》 &#x1f387; 座右铭&#xff1a;每个优秀的人都有一段沉默的时光&#xff0c;那段时光是付出了很多努力却得不到结果的日子&#xff0c;我们把它叫做扎根 目录&#x1f449;Linux软件…

未来已来,光伏产业将走向何方?十大趋势待揭晓!

碳中和大背景下&#xff0c;光伏已经成为发展最迅猛的热门产业之一。在能源产业变革中&#xff0c;光伏将成为未来最大的绿电来源。 据预测&#xff0c;到2030年&#xff0c;全球可再生能源的占比将超过50%。届时&#xff0c;光伏发电和风电将成为全球可再生能源的主力军。根据…

Android入门第50天-读写本地文件

简介 为了这个系列&#xff0c;我的代码已经准备到了第150天了。接下来的内容会越来越精彩&#xff0c;我们也越来越开始进入Android的一些高级功能上的编程了。今天我们就要讲Android中对本地文件进行读写的全过程。 课程目标 输入文件名、输入文件内容后按【保存到SD卡】&a…

毕业设计 - 基于SSH的任务调度系统的设计与实现 【源码+论文】

文章目录前言一、项目设计1. 模块设计2. 实现效果二、部分源码项目源码前言 今天学长向大家分享一个 Java web 毕业设计项目: 基于SSH的任务调度系统的设计与实现 一、项目设计 1. 模块设计 根据需求调研结果确定本任务调度系统的功能结构&#xff0c;最终系统实现的系统将…

Django

文章目录基础知识创建项目启动项目创建超级用户创建项目构建个人博客网站简单构建开启本地虚拟环境初步创建blog应用常用的模板标签和过滤器注&#xff1a;常用的模板标签注&#xff1a;常用的过滤器模板嵌套全局模板文件夹模板文件设置建议使用css美化页面导航栏页面美化css框…