Muhammad Rehan Saeed Muhammad Rehan Saeed - 2 months ago 68x
ASP.NET (C#) Question

How to Create a Multipart HTTP Response With ASP.NET Core

I would like to create an action method in my ASP.NET Core controller which returns a Multipart HTTP Response containing several files. I know that using a .zip file is the recommended approach for websites but I am considering using such a request for an API.

The examples I have been able to find in the ASP.NET Core samples are to do with multipart HTTP requests when uploading files. In my case, I want to download files.


I've raised the following GitHub issue: #4933



MSDN has a document that lists a lot of the multipart subtypes. The multipart/byteranges seems most appropriate for sending multiple files in an HTTP Response for download by the client application. The bold part is particularly relevant.

The multipart/byteranges content type is defined as a part of the HTTP message protocol. It includes two or more parts, each with its own Content-Type and Content-Range fields. The parts are separated using a MIME boundary parameter. It allows for binary as well as 7-bit and 8-bit files to be sent as multiple parts with the lengths of the parts being specified in the header of each part. Note that while HTTP makes provisions for using MIME for HTTP documents, HTTP is not strictly MIME-compliant. (Emphasis added.)

From RFC2068

RFC2068, section 19.2 provides a description of multipart/byteranges. Again, the bold part is relevant. Each byterange can have its own Content-type and it turns out can also have its own Content-disposition.

The multipart/byteranges media type includes two or more parts, each with its own Content-Type and Content-Range fields. The parts are separated using a MIME boundary parameter. (Emphasis added.)

The RFC also provides this technical definition:

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

The best part of the RFC is its example, which the ASP.NET Core sample below illustrates.

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

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

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

...the second range

Note that they are sending two PDFs! That is just what you're needing.

One ASP.NET Core Approach

Here is a code sample that works on Firefox. That is, Firefox downloads three image files, which we can open with Paint. The source is on GitHub.

Firefox downloads the byte ranges.

The sample uses app.Run(). To adapt the sample to a controller action, inject IHttpContextAccessor into your controller and write to _httpContextAccessor.HttpContext.Response in your action method.

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)

    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);


    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(
            offset: 0,
            count: file.FileContents.Length);
        await response.WriteAsync(CrLf);

        await response.WriteAsync(Boundary + CrLf);

Note: this sample code requires refactoring before reaching production.