Downloading multiple files in ASP.NET

This short article examines the baffling issue that a lot of people seem to encounter when attempting to send multiple files to the client from an ASP.NET application, and provides the solution.

Every so often a question pops up on the forum asking why their multiple file download code only sends the first file. Typically, the code consists of a loop that iterates a collection of files and attempts to use Response.TransmitFile or a FileResult in MVC to dispatch each file to the client. The reason why this doesn't appear to work is because it is basically not possible. This isn't supported for security reasons. The potential exploit that enabling this scenario would open is known as a Drive-by Download whereby a malicious webmaster could send a whole load of malware to the client in addition to the requested file.

The workaround is to compress the files into one archive file, and download that instead. There are quite a few examples that show how to do this using a variety of third party zip libraries. That's mainly because there was nothing specifically designed for this in the .NET framework until some new stuff was introduced in System.IO.Compression in .NET 4.5, specifically the ZipFile class that enables you to create and work with .zip files.

The simple examples that follow illustrates the use of the ZipFile.CreateFromDirectory method in ASP.NET MVC and Web Forms. In both cases, the user is presented with a list of checkboxes representing a selection of files to choose from. Submitting the form will result in just those files being packaged up into one zip file and downloaded.

ASP.NET MVC

The list of files is passed to the view via ViewBag:

public ActionResult Index()
{
    ViewBag.Files = Directory.EnumerateFiles(Server.MapPath("~/pdfs"));
    return View();
}

The files are listed within a form with a set of checkboxes:

<h2>Select downloads</h2>
@using(Html.BeginForm("Download", "Home"))
{
    foreach(string file in ViewBag.Files)
    {
        <input type="checkbox" name="files" value="@file" /> @:&nbsp;  
             @Path.GetFileNameWithoutExtension(file) <br />
    }
    <div>
        <button class="btn btn-default">Submit</button>
    </div>
}

The form posts to an action called Download which consists of the following code:

[HttpPost]
public FileResult Download(List<string> files)
{
    var archive = Server.MapPath("~/archive.zip");
    var temp = Server.MapPath("~/temp");

    // clear any existing archive
    if (System.IO.File.Exists(archive))
    {
        System.IO.File.Delete(archive);
    }
    // empty the temp folder
    Directory.EnumerateFiles(temp).ToList().ForEach(f => System.IO.File.Delete(f));

    // copy the selected files to the temp folder
    files.ForEach(f => System.IO.File.Copy(f, Path.Combine(temp, Path.GetFileName(f))));

    // create a new archive
    ZipFile.CreateFromDirectory(temp, archive);

    return File(archive, "application/zip", "archive.zip");
}

The code above relies on two additional using directives in the controller class:

using System.IO;
using System.IO.Compression;

The user selection is captured in the files parameter. The code checks to see if a file called archive.zip exists from previous operations and it it does, it is deleted. Then a folder called temp is cleared of any existing files. Next,the selected files are copied from their source directory to the temp folder. The ZipFile.CreateFromDirectory method generates a zip file from the temp directory contents and saves it as archive.zip. Finally, it is written to the Response.

Web Forms

This solution features one page called Download.aspx. The aspx file contains markup for a CheckBoxList and a Button control:

<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
    <asp:CheckBoxList ID="CheckBoxList1" runat="server" OnDataBound="CheckBoxList1_DataBound" />
    <asp:Button ID="Button1" runat="server" Text="Submit" />
</asp:Content>

The Page_Load method in the code behind file is very similar to the Action method in the MVC example. If the page hasn't been posted back, the list of files is obtained and bound to the CheckBoxList.

public partial class Download : Page
{
        
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            CheckBoxList1.DataSource = Directory.GetFiles(Server.MapPath("~/PDFs"));
            CheckBoxList1.DataBind();
        }
        else
        {
            var files = CheckBoxList1.Items.Cast<ListItem>()
                                                    .Where(li => li.Selected).Select(li => li.Value)
                                                    .ToList();
            var archive = Server.MapPath("~/archive.zip");
            var temp = Server.MapPath("~/temp");

            // clear any existing archive
            if (System.IO.File.Exists(archive))
            {
                System.IO.File.Delete(archive);
            }
            // empty the temp folder
            Directory.EnumerateFiles(temp).ToList().ForEach(f => System.IO.File.Delete(f));

            // copy the selected files to the temp folder
            files.ForEach(f => System.IO.File.Copy(f, Path.Combine(temp, Path.GetFileName(f))));

            // create a new archive
            ZipFile.CreateFromDirectory(temp, archive);
            Response.ContentType = "application/zip";
            Response.AddHeader("Content-Disposition", "attachment; filename=archive.zip");
            Response.TransmitFile(archive);
        }
    }

    protected void CheckBoxList1_DataBound(object sender, EventArgs e)
    {
        foreach (ListItem item in CheckBoxList1.Items)
        {
            item.Text = Path.GetFileName(item.Text);
        }
    }
}

I have used the DataBound event of the CheckBoxList to format the Text property of each item to remove the full path to the file. If the form has been posted back, the selected items are stored in a List<string> and then the very same code is used to clear previous files, copy the selected ones to the temp folder and then generate the zip archive. Finally, the content type and disposition of the Response are set appropriately and the archive is sent to the client.

Summary

This short article discussed the reason why multiple file downloads are not enabled, and presented a workaround using only .NET framework code to provide a solution in both ASP.NET MVC and Web Forms scenarios.