Search code examples
c#.net-4.5outputstreamhttpcontextasp.net-4.5

Writing to ZipArchive using the HttpContext OutputStream


I've been trying to get the "new" ZipArchive included in .NET 4.5 (System.IO.Compression.ZipArchive) to work in a ASP.NET site. But it seems like it doesn't like writing to the stream of HttpContext.Response.OutputStream.

My following code example will throw

System.NotSupportedException: Specified method is not supported

as soon as a write is attempted on the stream.

The CanWrite property on the stream returns true.

If I exchange the OutputStream with a filestream, pointing to a local directory, it works. What gives?

ZipArchive archive = new ZipArchive(HttpContext.Response.OutputStream, ZipArchiveMode.Create, false);

ZipArchiveEntry entry = archive.CreateEntry("filename");

using (StreamWriter writer = new StreamWriter(entry.Open()))
{
    writer.WriteLine("Information about this package.");
    writer.WriteLine("========================");
}

Stacktrace:

[NotSupportedException: Specified method is not supported.]
System.Web.HttpResponseStream.get_Position() +29
System.IO.Compression.ZipArchiveEntry.WriteLocalFileHeader(Boolean isEmptyFile) +389
System.IO.Compression.DirectToArchiveWriterStream.Write(Byte[] buffer, Int32 offset, Int32 count) +94
System.IO.Compression.WrappedStream.Write(Byte[] buffer, Int32 offset, Int32 count) +41

Solution

  • Note: This has been fixed in .Net Core 2.0. I'm not sure what is the status of the fix for .Net Framework.


    Calbertoferreira's answer has some useful information, but the conclusion is mostly wrong. To create an archive, you don't need seek, but you do need to be able to read the Position.

    According to the documentation, reading Position should be supported only for seekable streams, but ZipArchive seems to require this even from non-seekable streams, which is a bug.

    So, all you need to do to support writing ZIP files directly to OutputStream is to wrap it in a custom Stream that supports getting Position. Something like:

    class PositionWrapperStream : Stream
    {
        private readonly Stream wrapped;
    
        private long pos = 0;
    
        public PositionWrapperStream(Stream wrapped)
        {
            this.wrapped = wrapped;
        }
    
        public override bool CanSeek { get { return false; } }
    
        public override bool CanWrite { get { return true; } }
    
        public override long Position
        {
            get { return pos; }
            set { throw new NotSupportedException(); }
        }
    
        public override void Write(byte[] buffer, int offset, int count)
        {
            pos += count;
            wrapped.Write(buffer, offset, count);
        }
    
        public override void Flush()
        {
            wrapped.Flush();
        }
    
        protected override void Dispose(bool disposing)
        {
            wrapped.Dispose();
            base.Dispose(disposing);
        }
    
        // all the other required methods can throw NotSupportedException
    }
    

    Using this, the following code will write a ZIP archive into OutputStream:

    using (var outputStream = new PositionWrapperStream(Response.OutputStream))
    using (var archive = new ZipArchive(outputStream, ZipArchiveMode.Create, false))
    {
        var entry = archive.CreateEntry("filename");
    
        using (var writer = new StreamWriter(entry.Open()))
        {
            writer.WriteLine("Information about this package.");
            writer.WriteLine("========================");
        }
    }