如何使用ASP.NET Core创建一个多部分HTTP响应

23
我想在我的ASP.NET Core控制器中创建一个操作方法,该方法返回一个包含多个文件的Multipart HTTP响应。我知道对于网站来说,使用.zip文件是推荐的方法,但我正在考虑为API使用这样的请求。

我能够找到的ASP.NET Core示例中,能够找到的例子都与上传文件时的多部分HTTP请求有关。在我的情况下,我想下载文件。

更新

我已提出以下GitHub问题:#4933

2个回答

20

我编写了一个更加通用的MultipartResult类,它只是继承自ActionResult

使用示例

[Route("[controller]")]
public class MultipartController : Controller
{
    private readonly IHostingEnvironment hostingEnvironment;

    public MultipartController(IHostingEnvironment hostingEnvironment)
    {
        this.hostingEnvironment = hostingEnvironment;
    }

    [HttpGet("")]
    public IActionResult Get()
    {
        return new MultipartResult()
        {
            new MultipartContent()
            {
                ContentType = "text/plain",
                FileName = "File.txt",
                Stream = this.OpenFile("File.txt")
            },
            new MultipartContent()
            {
                ContentType = "application/json",
                FileName = "File.json",
                Stream = this.OpenFile("File.json")
            }
        };
    }

    private Stream OpenFile(string relativePath)
    {
        return System.IO.File.Open(
            Path.Combine(this.hostingEnvironment.WebRootPath, relativePath),
            FileMode.Open,
            FileAccess.Read);
    }
}

实现

public class MultipartContent
{
    public string ContentType { get; set; }

    public string FileName { get; set; }

    public Stream Stream { get; set; }
}

public class MultipartResult : Collection<MultipartContent>, IActionResult
{
    private readonly System.Net.Http.MultipartContent content;

    public MultipartResult(string subtype = "byteranges", string boundary = null)
    {
        if (boundary == null)
        {
            this.content = new System.Net.Http.MultipartContent(subtype);
        }
        else
        {
            this.content = new System.Net.Http.MultipartContent(subtype, boundary);
        }
    }

    public async Task ExecuteResultAsync(ActionContext context)
    {
        foreach (var item in this)
        {
            if (item.Stream != null)
            {
                var content = new StreamContent(item.Stream);

                if (item.ContentType != null)
                {
                    content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(item.ContentType);
                }

                if (item.FileName != null)
                {
                    var contentDisposition = new ContentDispositionHeaderValue("attachment");
                    contentDisposition.SetHttpFileName(item.FileName);
                    content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
                    content.Headers.ContentDisposition.FileName = contentDisposition.FileName;
                    content.Headers.ContentDisposition.FileNameStar = contentDisposition.FileNameStar;
                }

                this.content.Add(content);
            }
        }

        context.HttpContext.Response.ContentLength = content.Headers.ContentLength;
        context.HttpContext.Response.ContentType = content.Headers.ContentType.ToString();

        await content.CopyToAsync(context.HttpContext.Response.Body);
    }
}

在您的问题中提到:“这不是从浏览器下载文件的好方法,因为缺乏支持,但我正在编写一个API并拥有一个客户端,我也可以控制。”而Shaun Luttin的答案中特别提到了Firefox。为了澄清,您的答案适用于哪些情况? - chris
这个方法适用于需要返回多个文件的API。在我的情况下,由于高延迟,我需要减少HTTP请求的数量。但是,对于需要下载多个文件的网站来说,这种方法不适用,因为只有Firefox浏览器支持(我试过了)。希望这可以帮到你。 - Muhammad Rehan Saeed
我该如何实现相反的操作:我有一个控制器,其请求中包含multipart/mixed。我该如何读取各个部分/流? - Shimmy Weitzhandler
由于没有接收到流数据,所以我不得不添加以下代码: item.Stream.Position = 0; // <== 将流位置重置为零 StreamContent content = new StreamContent(item.Stream); - Moe Howard
我该如何调用这个端点并获取响应内容?我尝试了ReadAsMultipartAsync但没有成功。 - Q-bertsuit

9

来自MSDN

MSDN有一份文档列出了许多multipart子类型。 multipart/byteranges 似乎最适合用于通过HTTP响应向客户端应用程序发送多个文件以供下载。粗体部分特别相关。

multipart/byteranges内容类型被定义为HTTP消息协议的一部分。它包括两个或多个部分,每个部分都有自己的Content-Type和Content-Range字段。这些部分使用MIME边界参数分隔。它允许二进制以及7位和8位文件作为多个部分发送,每个部分的长度在每个部分的头中指定。请注意,虽然HTTP为使用MIME传输HTTP文档提供了规定,但HTTP并不严格遵守MIME。(强调添加)

来自RFC2068

RFC2068,第19.2节提供了对 multipart/byteranges 的描述。同样,粗体部分是相关的。每个字节范围可以有自己的 Content-type,并且可以有自己的 Content-disposition

multipart/byteranges媒体类型包括两个或多个部分,每个部分都有自己的Content-Type和Content-Range字段。这些部分使用MIME边界参数分隔。(强调添加)

RFC还提供了以下技术定义:

Media Type name:           multipart
Media subtype name:        byteranges
Required parameters:       boundary
Optional parameters:       none
Encoding considerations:   only "7bit", "8bit", or "binary" are permitted
Security considerations:   none

RFC最好的部分是它的示例,下面的ASP.NET Core示例展示了这一点。
HTTP/1.1 206 Partial content
Date: Wed, 15 Nov 1995 06:25:24 GMT
Last-modified: Wed, 15 Nov 1995 04:58:08 GMT
Content-type: multipart/byteranges; boundary=THIS_STRING_SEPARATES

--THIS_STRING_SEPARATES
Content-type: application/pdf
Content-range: bytes 500-999/8000

...the first range...
--THIS_STRING_SEPARATES
Content-type: application/pdf
Content-range: bytes 7000-7999/8000

...the second range
--THIS_STRING_SEPARATES--

请注意,他们发送了两个PDF文件!这正是你需要的。

ASP.NET Core的统一处理方式

以下是适用于Firefox的代码示例。也就是说,Firefox下载了三个图像文件,我们可以用画图打开。 源代码在GitHub上。

Firefox下载字节范围。

该示例使用app.Run()。要将示例适应控制器操作,请将IHttpContextAccessor注入到您的控制器中,并在您的操作方法中写入_httpContextAccessor.HttpContext.Response

using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

public class Startup
{
    private const string CrLf = "\r\n";
    private const string Boundary = "--THIS_STRING_SEPARATES";
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            var response = context.Response;
            response.ContentType = $"multipart/byteranges; boundary={Boundary}";

            // TODO Softcode the 'Content-length' header.            
            response.ContentLength = 13646;
            var contentLength = response.ContentLength.Value;

            await response.WriteAsync(Boundary + CrLf);

            var blue = new FileInfo("./blue.jpg");
            var red = new FileInfo("./red.jpg");
            var green = new FileInfo("./green.jpg");

            long start = 0;
            long end = blue.Length;
            await AddImage(response, blue, start, end, contentLength);

            start = end + 1;
            end = start + red.Length;
            await AddImage(response, red, start, end, contentLength);

            start = end + 1;
            end = start + green.Length;
            await AddImage(response, green, start, end, contentLength);

            response.Body.Flush();
        });
    }

    private async Task AddImage(HttpResponse response, FileInfo fileInfo,
        long start, long end, long total)
    {
        var bytes = File.ReadAllBytes(fileInfo.FullName);
        var file = new FileContentResult(bytes, "image/jpg");

        await response
            .WriteAsync($"Content-type: {file.ContentType.ToString()}" + CrLf);

        await response
            .WriteAsync($"Content-disposition: attachment; filename={fileInfo.Name}" + CrLf);

        await response
            .WriteAsync($"Content-range: bytes {start}-{end}/{total}" + CrLf);

        await response.WriteAsync(CrLf);
        await response.Body.WriteAsync(
            file.FileContents,
            offset: 0,
            count: file.FileContents.Length);
        await response.WriteAsync(CrLf);

        await response.WriteAsync(Boundary + CrLf);
    }
}

注意:此示例代码在达到生产前需要进行重构。


3
原文:It turns out I did not need the Conte-range HTTP header, it is used if you are retrieving a particular part of a file and not the whole file as I am.翻译:事实证明我并不需要使用Content-Range HTTP头部,它仅在你需要检索文件的某一部分而不是整个文件时使用。而我需要检索整个文件。 - Muhammad Rehan Saeed

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