
Editorial illustration.
Originally published 23 June 2023; rewritten and reviewed on 15 October 2026.
A download button offers several documents as one ZIP file. The convenience is obvious; the less visible work is deciding who may download each document and how the archive behaves when files are large or missing.
Resolve files through application records
Start from authorised document identifiers, not arbitrary filesystem paths supplied by a browser. Recheck access to every included file. Give archive entries controlled relative names so internal server paths are not exposed.
For a small, bounded set of already authorised files, the core pattern is:
using var output = new MemoryStream();
using (var archive = new ZipArchive(
output, ZipArchiveMode.Create, leaveOpen: true))
{
foreach (var file in authorisedFiles)
{
var entry = archive.CreateEntry(file.DownloadName);
using var destination = entry.Open();
using var source = File.OpenRead(file.ServerPath);
source.CopyTo(destination);
}
}
byte[] zipBytes = output.ToArray();
The surrounding MVC action returns those bytes with application/zip and a suitable download name. The snippet assumes authorisedFiles has already been validated; its property names describe an application model, not a framework type.
Finish the archive before sending it
Disposing ZipArchive writes the final archive structures. Returning the buffer too early can produce an unreadable download. Keep stream ownership clear so closing the archive does not unexpectedly close a stream the response still needs.
A memory buffer also has a cost. Bound the combined input size, or choose a supported streaming or background-export design for large jobs.
Decide how partial failure looks
If a requested file disappears, either fail the export clearly or use a documented partial-export policy. Do not quietly label an incomplete archive as complete. Test empty selections, duplicate names and a missing file, then open the resulting ZIP with a separate reader. The useful result is a downloadable package whose contents match the request and the user’s permissions.