This code should work, but it will actually fail (as of .NET 4.6.2). It will throw a NotSupportedException from within the PushStreamContent callback, so it’s annoying to find and debug.

Here’s the problem:

Streams can be “seekable.” Seekable streams allow reading the Position property and updating it via the Seek method.

The ASP.NET response output stream is not seekable. This is a common aspect of all network streams.

ZipArchive should work with write-only (non-seekable) streams. However (and this is the bug), it will actually readPosition even for non-seekable streams in order to build up its list of zip entry offsets in the zip file.

The only tricky part about this wrapper is that we want to be sure to override the asynchronous methods as well as the synchronous ones. This is because the Stream base class will provide a default implementation of these that just runs the synchronous APIs on a thread pool thread. In other words, it’s using fake asynchrony by default! So we need to override them to provide true asynchrony.

With that wrapper in place, our action method can be updated:

privatestaticHttpClientClient{get;}=newHttpClient();publicHttpResponseMessageGet(){varfilenamesAndUrls=newDictionary<string,string>{{"README.md","https://raw.githubusercontent.com/StephenClearyExamples/AsyncDynamicZip/master/README.md"},{".gitignore","https://raw.githubusercontent.com/StephenClearyExamples/AsyncDynamicZip/master/.gitignore"},};varresult=newHttpResponseMessage(HttpStatusCode.OK){Content=newPushStreamContent(async(outputStream,httpContext,transportContext)=>{// The only change is in this line:using(varzipArchive=newZipArchive(newWriteOnlyStreamWrapper(outputStream),ZipArchiveMode.Create)){foreach(varkvpinfilenamesAndUrls){varzipEntry=zipArchive.CreateEntry(kvp.Key);using(varzipStream=zipEntry.Open())using(varstream=awaitClient.GetStreamAsync(kvp.Value))awaitstream.CopyToAsync(zipStream);}}}),};result.Content.Headers.ContentType=newMediaTypeHeaderValue("application/octet-stream");result.Content.Headers.ContentDisposition=newContentDispositionHeaderValue("attachment"){FileName="MyZipfile.zip"};returnresult;}

And ZipArchive is quite happy with the stream wrapper!

Code

A fully-working solution for ASP.NET 4.6 (using the built-in ZipArchive) is available on GitHub.