Streaming a Zip File on ASP.NET Core 2.2

ngovu.dl@gmail.com | 385 day | 1014


NoteAt the moment MS have been supported too many responses about files such as "FileStreamResult" "FileContentResult" ..., so with the MS's support is only for a big file exists already, so with a big file it does not exist and we need to go many places to retrieve it, MS does not support. Please take a look at a real scenario like below, I think have many people get the same issue with me.

c-sharp.vn

As the image above you see before response a zip from server to client we need to wait for download all files from many providers then zip all of them become a zip file and response to the client, with this step take a lot of time maybe the client will be a time-out. So How can push a stream zip file, it means each time completed download a file from another provider I want to push stream immediate to client look like live-stream. And after 2 days ago I completed it, no I will how to do it.

First of all, I will explain to you how does ms stream a file.

c-sharp.vn

There’s a couple of interesting notes here: we pass the MediaTypeHeaderValue directly to the constructor of the FileStreamResult, which takes care of setting the Content-Type header on the response. Also, we set the FileDownloadName property, which sets the Content-Disposition header on the response. The “ASP.NET Core way” is more about expressing intent rather than modifying the response directly.

The FileResult base type has several derived types, each with a different purpose:

  • PhysicalFileResult sends an on-disk file identified by a physical path.
  • VirtualFileResult sends a file identified by a virtual path.
  • FileContentResult sends the file content as an in-memory byte array.
  • FileStreamResult sends the file content as a stream.

You can get the ms code from GitHub, with net core everything is open-source you can access a take a look about it.

 So here we need to create the response of a new type and it will inherit from FileResult like this.

public class PushStreamResult: FileResult
    {
        private readonly Func<Stream, Task> _onStreamAvailabe;

        private readonly string _fileDownloadName;

        public PushStreamResult(Func<Stream, Task> onStreamAvailabe,
            string contentType, string fileDownloadName) : base(contentType)
        {
            _onStreamAvailabe = onStreamAvailabe;

            _fileDownloadName = string.IsNullOrEmpty(fileDownloadName) ? Guid.NewGuid().ToString("n") : fileDownloadName;
        }

        public override async Task ExecuteResultAsync(ActionContext context)
        {
            var response = context.HttpContext.Response;

            response.Headers["Content-Disposition"] = $"attachment; filename= {_fileDownloadName}";

            response.ContentType = ContentType.ToString();

            await _onStreamAvailabe(context.HttpContext.Response.Body);

        }
    }

As you see we inherit from a FileResult and override ExecuteResultAsync handle follows our way instead of ms.

So here we will create a callback function, and it is easy to customize, and I will show for you 2 block code like that.

MS code

  [HttpGet("download-zip-file")]
        public IActionResult DownloadZipFile1()
        {
            return new FileStreamResult(new FileStream("aaa", FileMode.Open), "");
        }

 My Code.

 [HttpGet("download-zip-file")]
        public IActionResult DownloadZipFile1()
        {
            return new PushStreamResult(async (outputStream) =>
            {
                //TODO Some logic
            },
           "application/octet-stream",
           $"{Guid.NewGuid().ToString("n")}.zip");
        }

And you see with 2 block code above is ms code has fixed and you can add more condition or logic on it.

with my code, you can add any logic you want.

Next, I will add full a stream code from an API.

[HttpGet("download-zip-file")]
        public IActionResult 
            return new PushStreamResult(async (outputStream) =>
            {
                using (var zipArchive = new ZipArchive(new WriteOnlyStreamWrapper(outputStream), ZipArchiveMode.Create))
                {
                    foreach (var file in files)
                    {
                        ZipArchiveEntry zipEntry = zipArchive.CreateEntry(file.FileName);
                        using (var zipStream = zipEntry.Open())
                        using (var stream = new MemoryStream(file .Buffer))
                            await stream.CopyToAsync(zipStream);
                    }
                }
            },
            "application/octet-stream",
            $"{Guid.NewGuid().ToString("n")}.zip");
        }

 I will explain here with "ZipArchive" it's a lib from ms you can access to the link and get more information about it, at here we need to focus a line.

using (var zipArchive = new ZipArchive(new WriteOnlyStreamWrapper(outputStream), ZipArchiveMode.Create))

You see with a outputStream it's response.body, and here we need to write many streams to body and response to the client such fluent. 

And how to write buffer to the body, at here we need to inherit from class Stream of ms and custom it like that.

public class WriteOnlyStreamWrapper : Stream
    {
        private readonly Stream _stream;

        private long _position;

        public WriteOnlyStreamWrapper(Stream stream)
        {
            _stream = stream;
        }

        public override bool CanRead => false;
        public override bool CanSeek => false;
        public override bool CanWrite => true;

        public override long Position
        {
            get { return _position; }
            set
            {
                throw new NotSupportedException();
            }
        }
        public override void Write(byte[] buffer, int offset, int count)
        {
            _position += count;
            _stream.Write(buffer, offset, count);
        }

        public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
        {
            _position += count;
            return _stream.BeginWrite(buffer, offset, count, callback, state);
        }

        public override void EndWrite(IAsyncResult asyncResult) => _stream.EndWrite(asyncResult);

        public override void WriteByte(byte value)
        {
            _position += 1;
            _stream.WriteByte(value);
        }

        public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
        {
            _position += count;
            return _stream.WriteAsync(buffer, offset, count, cancellationToken);
        }

        public override bool CanTimeout => _stream.CanTimeout;

        public override int ReadTimeout
        {
            get { return _stream.ReadTimeout; }
            set { _stream.ReadTimeout = value; }
        }

        public override int WriteTimeout
        {
            get { return _stream.WriteTimeout; }
            set { _stream.WriteTimeout = value; }
        }

        public override void Flush() => _stream.Flush();

        public override Task FlushAsync(CancellationToken cancellationToken) => _stream.FlushAsync(cancellationToken);

        public override void Close()
        {
            _stream.Close();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
                _stream.Dispose();
        }

        public override long Length
        {
            get
            {
                throw new NotSupportedException();
            }
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotSupportedException();
        }

        public override void SetLength(long value)
        {
            throw new NotSupportedException();
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            throw new NotSupportedException();
        }
    }

 So I hope the article can you help solve the problem the same me, and this is full source code GitHub.

Remind: With current source code it's only working with net core 2.2, and not work with net core 3.0, I will update code as soon as possible. 

if you don't understand or you need to ask something do not hesitate to comment like below.


Top Articles

Bất Đầu Với WebApi Và Dot Net Core (.Net Core)

1276 day
Butter Ngo
Views 8931
Comments 0

Dot Net Core Bearer Token With (JWT) (.Net Core)

1208 day
Butter Ngo
Views 5557
Comments 0

Repository Và Unit Of Work (Entity Framework)

1195 day
ndtung449@gmail.com
Views 5040
Comments 0

Bắt Đầu Với Dot NET Core (.Net Core)

1288 day
Butter Ngo
Views 4425
Comments 0

Top Question

Bi lỗi Invalid Column Name khi sử dụng LinQ (.Net)

1135 day
Bảo Dương
Views 1338
Answers 2

.NET CORE API JWT (.Net Core)

428 day
huynhminhnhut97@gmail.com
Views 940
Answers 2

Làm thế nào để lấy information từ token (.Net Core)

525 day
ngovu.dl@gmail.com
Views 851
Answers 1