ZipArchive’s interesting behavior

Today I had an ordinary assignment. Generate an in memory .zip archive with one text file inside and send it to a web api. With C# this is straight-forward. Here is my oversimplified first attempt:

using var zipStream = new MemoryStream();
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, true);

var textFileEntry = archive.CreateEntry("Test.txt", CompressionLevel.Optimal);
new MemoryStream(Encoding.UTF8.GetBytes("In memory file Content"))
    .CopyTo(textFileEntry.Open());

// Server sending logic

The code above first creates a MemoryStream which represents the complete in memory zip archive. After that, passes it to the ZipArchive’s constructor, which uses it as its underlying stream. Using the ZipArchive, creates an entry which represents a placeholder for a file that will be inside the zip. Since I want that file to be also in memory, next step is to create another MemoryStream with some content and copy it to the entry. Pretty straight-forward.

The server did receive the file, and everything was looking great… until I tried to download it. The zip was properly generated but the text file “Test.txt” was 0 kb in size. That’s odd.

After debugging it for an hour with no luck, I reached out to StackOverflow and as always came across to the exact same question.

Here I copied the code from one of the answers:

string fileName = "export_" + DateTime.Now.ToString("yyyyMMddhhmmss") + ".xlsx";
byte[] fileBytes = here is your file in bytes
byte[] compressedBytes;
string fileNameZip = "Export_" + DateTime.Now.ToString("yyyyMMddhhmmss") + ".zip";

using (var outStream = new MemoryStream())
{
    using (var archive = new ZipArchive(outStream, ZipArchiveMode.Create, true))
    {
        var fileInArchive = archive.CreateEntry(fileName, CompressionLevel.Optimal);
        using (var entryStream = fileInArchive.Open())
        using (var fileToCompressStream = new MemoryStream(fileBytes))
        {
            fileToCompressStream.CopyTo(entryStream);
        }
    }
    compressedBytes = outStream.ToArray();
}

It does the same thing only with .xlsx. Also, it looks very similar to mine. I tried to modify it to fit my use case:

using var outStream = new MemoryStream();
using (var archive = new ZipArchive(outStream, ZipArchiveMode.Create, true))
{
    var entry = archive.CreateEntry("Test.txt", CompressionLevel.Optimal);
    new MemoryStream(Encoding.UTF8.GetBytes("In memory file Content"))
        .CopyTo(entry.Open());
}

// Server sending logic

It came almost the same as the first one but…

Suprisingly it works! Why?

If you have a keen eye you will spot that this code’s ZipArchive is being disposed before sending it to the server. This sparked my curiosity even more. Thankfully C# is all open source. What a time to be alive. So I checked the source code of ZipArchive class particularly the Dispose method.

Here is how it looks like:

protected virtual void Dispose(bool disposing)
{
    if (disposing && !_isDisposed)
    {
        try
        {
            switch (_mode)
            {
                case ZipArchiveMode.Read:
                    break;
                case ZipArchiveMode.Create:
                case ZipArchiveMode.Update:
                default:
                    Debug.Assert(_mode == ZipArchiveMode.Update || 
                        _mode == ZipArchiveMode.Create);
                    WriteFile();
                    break;
            }
        }
        finally
        {
            CloseStreams();
            _isDisposed = true;
        }
    }
}

The _mode variable’s value is Create since this is what I’m passing to the constructor. Take a look at the default case and particularly on WriteFile() method. It turns out that ZipArchive materializes the zip entries only on Dispose. You do not get any errors either.

Did you encounter any weird behavior yourself lately? Share with me on the comments or send a tweet.