使用HttpClient将大文件流传递给MultipartFormDataContent

3

当我尝试使用MultipartFormDataContent和带有数据流的HttpClient时,遇到了问题。

上下文

我正在尝试将大文件上传到ASP.NET Core Web API。客户端应该通过POST请求表单数据将文件发送到前端API,然后前端API应将文件转发到后端API。

由于文件可能很大,我遵循了Microsoft示例,即我不想使用IFormFile类型,而是使用MultipartReader读取Request.Body。这是为了避免在服务器上将整个文件加载到内存中或将其保存在服务器硬盘上的临时文件中。

问题

后端API控制器动作如下(这几乎是直接从ASP.NET Core 5.0 示例应用程序中复制的,只进行了轻微简化):

        [HttpPost]
        [DisableRequestSizeLimit]
        public async Task<IActionResult> ReceiveLargeFile()
        {
            var request = HttpContext.Request;

            if (!request.HasFormContentType
                || !MediaTypeHeaderValue.TryParse(request.ContentType, out var mediaTypeHeader)
                || string.IsNullOrEmpty(mediaTypeHeader.Boundary.Value))
            {
                return new UnsupportedMediaTypeResult();
            }

            var reader = new MultipartReader(mediaTypeHeader.Boundary.Value, request.Body);
            /* This throws an IOException: Unexpected end of Stream, the content may have already been read by another component.  */
            var section = await reader.ReadNextSectionAsync();
            
            while (section != null)
            {
                var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition,
                    out var contentDisposition);

                if (hasContentDispositionHeader
                    && contentDisposition!.DispositionType.Equals("form-data")
                    && !string.IsNullOrEmpty(contentDisposition.FileName.Value))
                {
                    /* Fake copy to nothing since it doesn't even get here */
                    await section.Body.CopyToAsync(Stream.Null);
                    return Ok();
                }

                section = await reader.ReadNextSectionAsync();
            }

            return BadRequest("No files data in the request.");
        }

我使用 Microsoft.AspNetCore.Mvc.Testing NuGet 包制作了一个集成测试,稍微减轻了问题。以下测试替换了前端 API ,因此不是在 Web API 中读取 Request.Body 流,而是尝试将 StreamContent 添加到 MultipartFormDataContent 中,并通过 HttpClient 将其发布到后端 API:
        [Fact]
        public async Task Client_posting_to_Api_returns_Ok()
        {
            /* Arrange */
            await using var stream = new MemoryStream();
            await using var writer = new StreamWriter(stream);
            await writer.WriteLineAsync("FILE CONTENTS");
            await writer.FlushAsync();
            stream.Position = 0;

            using var client = _factory.CreateDefaultClient();

            /* Act */
            using var response =
                await client.PostAsync(
                    "Receive",
                    new MultipartFormDataContent
                    {
                        {
                            new StreamContent(stream),
                            "file",
                            "fileName"
                        }
                    });
            
            /* Assert */
            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        }

后端 API 控制器在 await reader.ReadNextSectionAsync() 处抛出 IOException,提示“流意外终止,内容可能已被其他组件读取”。

GitHub 代码库(完整示例)

我上传了一个包括后端 API 和测试的完整示例以解决这个问题,您可以在 GitHub 代码库 中查看。

问题

我一定做错了什么。如何将以 form-data 内容类型接收到的文件从一个服务(前端 API)转发到另一个服务(后端 API),而不会在前端 API 中加载整个文件到内存或硬盘,即只需将数据流转发到后端 API 中?

非常感谢任何帮助。


尝试应用Saga模式。将文件分成小部分并逐个(或并行)发送到第二个服务,直到完整的文件被读取。 - D A
谢谢@DA,你有一些资源可以让我阅读吗?我不知道这个模式,围绕着谷歌很多云特定的文章,或者使用MassTransit等库。我认为这不是我需要的? - Piotr
这个有帮助吗?链接 - Eldar
@Eldar 不是很准确,那个问题是关于ASP.NET Web API 2.0框架的,它与“新”的ASP.NET Core有很大的不同 :( - Piotr
你应该关注的部分不是Web API部分,而是HttpClient部分。特别是httpWebRequest.GetRequestStream()部分(与.net 5相同)。这使您可以通过复制来转发请求流。 - Eldar
好的,我会尝试看看是否能够以某种方式使其工作。谢谢 :)。 - Piotr
1个回答

0
我预料到和你一样的问题,结果发现 MediaTypeHeaderValue.TryParse 方法将边界值解析错误,因为它用 '"' 字符包装字符串,而 HttpClient 发送内容类型头如下:
multipart/form-data; boundary="blablabla"

对于我来说,解决方案是在边界上添加一个 Trim() 方法,然后将其传递给 MultipartReader。
var boundary = mediaTypeHeader.Boundary.Value.Trim('"');
var reader = new MultipartReader(boundary, request.Body);

谢谢,已经有一段时间了,说实话我只是咬紧牙关使用了 IFormFile。但是当我有时间研究时,我会尝试你的解决方法 :) - Piotr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接