【ASP.NET Core】MVC操作方法如何绑定Stream类型的参数

asp,net,core,mvc,操作方法,如何,绑定,stream,类型,参数 · 浏览次数 : 1475

小编点评

## Summary of the Code The code addresses the issue of deserializing a Stream-typed parameter in a controller action. Here's a breakdown: **Exception Cause:** * Initializing the `StreamModelBinder` throws an exception if the `bindingContext` is null. **Solution:** * The `BindModelAsync` method receives the `bindingContext` and handles the deserialization process. * It checks for `bindingContext.BindingSource` and `bindingContext.BindingSource` to ensure they are not null. * It reads the `contentLength` from the `request.ContentLength` property. * If the `contentLength` is null or zero, the method returns early to avoid unnecessary processing. * It uses a `MemoryStream` to directly copy the data from the `request.Body` to the `mstream` without copying it to memory first. * The `mstream` is then assigned to the `bindingContext.Result` to transfer the data to the model. **Controller Method Binding:** * The `PostSomething` action uses the `ModelBinder` attribute to specify the `StreamModelBinder` class for deserialization. * This allows the binder to handle the `Stream` parameter directly. **Key Points:** * Stream-typed parameter binding requires the `ModelBinder` attribute. * `ModelBinder` uses `ModelBinderProvider` for global application. * The provided code demonstrates how to bind a Stream parameter using `ModelBinder`. * The `ModelBinder` handles deserialization and model binding for the `Stream` parameter. **Additional Notes:** * The code also checks for valid `Content-Length` header and returns appropriate responses for invalid values. * It provides a concise and efficient approach to handling Stream parameter deserialization. **Overall, the code demonstrates how to effectively utilize Stream-typed parameters by employing the `ModelBinder` pattern and handling deserialization within the `BindModelAsync` method.**

正文

咱们都知道,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 消息头。

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

 

最后,补充一下,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 的。

 

与【ASP.NET Core】MVC操作方法如何绑定Stream类型的参数相似的内容:

【ASP.NET Core】MVC操作方法如何绑定Stream类型的参数

咱们都知道,MVC在输入/输出中都需要模型绑定。因为HTTP请求发送的都是文本,为了使其能变成各种.NET 类型,于是在填充参数值之前需 ModelBinder 的参与,以将文本转换为 .NET 类型。 尽管 ASP.NET Core 已内置基础类型和复杂类型的各种 Binder,但有些数据还是不能

做一个windos服务和api搭配,获取电脑的mac地址

创建webapi项目,只是搭配服务用,什么三层mvc都不弄了,默认的模板直接用就好。 简单分析下,采用signalr通信来传递mac地址,所以先安装个signalr的包(如果简单操作的话可以不装最新的,微软自带一个,不过好像是弃用的) using Microsoft.AspNetCore.Signa

【ASP.NET Core】MVC控制器的各种自定义:修改参数的名称

在上一篇中,老周演示了通过实现约定接口的方式自定义控制器的名称。 至于说自定义操作方法的名称,就很简单了,因为有内置的特性类可以用。看看下面的例子。 [Route("[controller]/[action]")] public class StockController : Controller

【ASP.NET Core】MVC控制器的各种自定义:应用程序约定的接口与模型

从本篇起,老周会连发N篇水文,总结一下在 MVC 项目中控制器的各种自定义配置。 本文内容相对轻松,重点讨论一下 MVC 项目中的各种约定接口。毕竟你要对控制器做各种自定义时,多数情况会涉及到约定接口。约定接口的结构都差不多,均包含一个 Apply 方法,实现类需要通过这个方法修改关联的模型设置。

【ASP.NET Core】MVC控制器的各种自定义:特性化的路由规则

MVC的路由规则配置方式比较多,咱们用得最多的是两种: A、全局规则。就是我们熟悉的”{controller}/{action}“。 app.MapControllerRoute( name: "bug", pattern: "{controller}/{action}" ); app.MapCon

【ASP.NET Core】MVC控制器的各种自定义:IActionHttpMethodProvider 接口

IActionHttpMethodProvider 接口的结构很简单,实现该接口只要实现一个属性即可——HttpMethods。该属性是一个字符串序列。 这啥意思呢?这个字符串序列代表的就是受支持的 HTTP 请求方式。比如,如果此属性返回 GET POST,那么被修饰的对象既支持 HTTP-GET

如何将 Autofac 整合进 Net6.0 Core MVC 项目中

一、前言 1、简而言之 Asp.Net Core Mvc,我也用了很长一段时间了,它现在的编程模型和方式还是特别棒的,都是组件开发,什么都可以替换,当然了,您别抬杠,有些还是不能替换的。自从我们进入了跨平台开发的时代,IOC容器也成了一个不可或缺的东西了。微软为我们提供了一个默认实现,那就是 ISe

【ASP.NET Core】动态映射MVC路由

ASP.NET Core 中的几大功能模块(Razor Pages、MVC、SignalR/Blazor、Mini-API 等等)都以终结点(End Point)的方式公开。在HTTP管道上调用时,其扩展方法基本是以 Map 开头,如 MapControllers、MapBlazorHub。 对于

Asp.net MVC 跨域设置

.Net Core 跨域

异常过滤器—MVC中异常过滤器使用

## 一、什么是异常过滤器? 异常过滤器(**Exception Filters**)是 ASP.NET Core 中用于处理全局异常的机制。它们允许你在发生异常时捕获、处理和记录异常,并提供自定义的异常处理逻辑。异常过滤器在整个应用程序范围内生效,可以用于处理各种异常情况。用于实现常见的错误处理策