I created a tiny project which provides a Web Api endpoint to download files from Azure Blob Storage as zip archive. In this article, I will explain some fo the important pieces.

Add Zip Archive Entry From Cloud Blob Directory

private static async Task AddZipArchiveEntryFromCloudBlobDirectory(CloudBlobDirectory cloudBlobDirectory, ZipArchive zipArchive)
{
    BlobContinuationToken blobContinuationToken = null;
    do
    {
        var blobResultSegment = await cloudBlobDirectory.ListBlobsSegmentedAsync(blobContinuationToken);
        blobContinuationToken = blobResultSegment.ContinuationToken;
        foreach (var listBlobItem in blobResultSegment.Results)
        {
            switch (listBlobItem)
            {
                case CloudBlobDirectory subCloudBlobDirectory:
                    await AddZipArchiveEntryFromCloudBlobDirectory(subCloudBlobDirectory, zipArchive);
                    break;
                case CloudBlockBlob cloudBlockBlob:
                {
                    var entry = zipArchive.CreateEntry(cloudBlockBlob.Name);
                    if (cloudBlockBlob.Properties.LastModified.HasValue)
                    {
                        entry.LastWriteTime = cloudBlockBlob.Properties.LastModified.Value;
                    }
                    using (var entryStream = entry.Open())
                    {
                        await cloudBlockBlob.DownloadToStreamAsync(entryStream);
                    }

                    break;
                }
            }
        }
    } while (blobContinuationToken != null);
}

This is a private function in BlobStorageZipArchiveService. It loops through each item in the directory and creates an entry in zip archive. If the item itself is a directory, the function will call itself. So this is a recursive function.

Generate Zip Archive Stream

public async Task GenerateZipArchiveStream(Stream stream, string containerName, string relativeAddress)
{
    var cloudBlobContainer = _cloudBlobClient.GetContainerReference(containerName);
    // make sure container exists
    var exists = await cloudBlobContainer.ExistsAsync();
    if (!exists)
    {
        throw new BlobStorageZipArchiveException("cannot find container");
    }

    var cloudBlobDirectory = cloudBlobContainer.GetDirectoryReference(relativeAddress);
    // make sure there are blobs under the relative address
    var results = await cloudBlobDirectory.ListBlobsSegmentedAsync(null);
    if (!results.Results.Any())
    {
        throw new BlobStorageZipArchiveException("cannot find any file under the relative address");
    }

    // generate zip archive stream
    using (var zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true))
    {
        await AddZipArchiveEntryFromCloudBlobDirectory(cloudBlobDirectory, zipArchive);
    }

    // reset stream position
    stream.Seek(0, SeekOrigin.Begin);
}

This function resides in BlobStorageZipArchiveService as well. We have to check if the container exists and also if the relative address contains any files before we create the zip archive. Otherwise, it will generate an empty archive file which is invalid. leaveOpen will keep the stream open, so we can read it later. The last statement reset stream position. If we forget to do that, controller will try to download from the end of the stream which has nothing and the API endpoint will freeze.

Blob Storage Zip Archive Controller

[HttpGet]
public async Task<ActionResult<IEnumerable<string>>> Get()
{
    var memoryStream = new MemoryStream();
    const string containerName = "containername";
    const string relativeAddress = "relativeAddress";
    await _blobStorageZipArchiveService.GenerateZipArchiveStream(memoryStream, containerName, relativeAddress);
    return File(memoryStream, "application/octet-stream", "fileDownloadName.zip");
}

This is the final endpoint. We cannot use using when instantiate MemoryStream instance, otherwise it will close the stream before File method tries to read it. We also leave memoryStream to File method to close.