Implement File Download Capabilities in ASP.NET

Most of the time, the APIs we create are returning semi-structured data of some sort to its callers – a JSON payload, or maybe some XML if you still have some clients that use that format. At times we may even return a string response back. But there are times when we may want to ship out a binary file, just as it is. Perhaps it is a PDF file or an Excel file. Our bank websites usually let us download our statements as PDFs. Our credit cards usually allow the export of our transactions as an Excel file. Similarly, let’s look at how to send a binary file response back from our ASP.NET APIs.

If the file that you want to ship out to the caller is a static file that’s just sitting on the server in some folder, you can try one of the following options.

PhysicalFileResult

This option allows you to reference a file in the file system using its absolute, physical path.

[Route("static-file")]
[HttpGet]
public IActionResult GetStaticFile()
{
    var physicalLocation = "/Users/tvaidyan/Projects/file-download-examples/file-download-examples/wwwroot/demo.pdf";
    return new PhysicalFileResult(physicalLocation, "application/pdf");
}

Call the endpoint in your favorite browser and you can see the PDF file downloaded and opened within the browser window.

browser window showing the execution of the static-file endpoint that displays a rendered PDF

Congratulations! You have successfully shipped a binary file to the browser from an ASP.NET endpoint. But, what if you didn’t want the browser to automatically open and render that PDF? What if you wanted the browser to treat it as a “download” and save the file to your downloads folder instead? You can do so by adding a response header to let the browser know that the response is an attachment that should be downloaded.

[Route("static-file")]
[HttpGet]
public IActionResult GetStaticFile()
{
    Response.Headers.Add("Content-Disposition", "attachment;filename=demo.pdf");
    var physicalLocation = "/Users/tvaidyan/Projects/file-download-examples/file-download-examples/wwwroot/demo.pdf";
    return new PhysicalFileResult(physicalLocation, "application/pdf");
}

With this header added, the browser now treats the response as an attachment that must be dowloaded, separately.

Browser window showing the execution of the static-file endpoint with the response as a file download.

VirtualFileResult

This response type allows you to refer to the file that you want to send using a relative path. In many cases, the file that you are trying to send down is sitting somewhere within the context of your web application folder and the VirtualFileResult option is well suited for such a use-case.

[Route("virtual-file")]
[HttpGet]
public IActionResult GetVirtualile()
{
    Response.Headers.Add("Content-Disposition", "attachment;filename=demo.pdf");
    var path = "demo.pdf";
    return new VirtualFileResult(path, "application/pdf");
}

FileContentResult

What if the file that you’re trying to send is not on the file system? Perhaps you are generating one, on the fly, based on some data that you’re fetching from the database. In such a case, it doesn’t make sense to save the file to disk just for the purposes of sending it across the wire. Doing so may add the extra overhead of then deleting that file from disk once it is shipped out. Instead, you can just generate the file in memory (assuming that it is a relatively small file that is not going to otherwise burden your server) and then send out the resulting bytes (literally) across the wire. Here’s an example of that.

[Route("on-the-fly")]
[HttpGet]
public IActionResult GetFileOnTheFly()
{
    Response.Headers.Add("Content-Disposition", "attachment;filename=demo.txt");
    byte[] fileContentBytes = default!;
    using (var ms = new MemoryStream())
    {
        using TextWriter textWriter = new StreamWriter(ms);
        textWriter.WriteLine("Hello world!");
        textWriter.Flush();

        ms.Position = 0;
        fileContentBytes = ms.ToArray();
    }

    return new FileContentResult(fileContentBytes, "text/plain");
}

In my example above, I use the TextWriter class to write a plain text file. I’m passing a new MemoryStream object to it so that the content can be written to that stream. Finally, I yield the bytes from that stream to be send out to the client.

FileStreamResult

Take a second look at my example above of the FileContentResult. You may have noticed that I had a MemoryStream object containing my file contents and I then converted it into a byte array to send back to the client. ASP.NET provides an alternate approach to this in FileStreamResult allowing you to output that stream directly back to the client.

[Route("stream")]
[HttpGet]
public IActionResult GetStream()
{
    Response.Headers.Add("Content-Disposition", "attachment;filename=demo.txt");
    var ms = new MemoryStream();
    TextWriter textWriter = new StreamWriter(ms);
    textWriter.WriteLine("Hello world!");
    textWriter.Flush();
    ms.Position = 0;
    return new FileStreamResult(ms, "text/plain");
}

So, when should you use a FileStreamResult or when should you use a FileContentResult? It depends on what you have available. If the content is already in the form of a byte array, use the FileContentResult. If you’re loading data into a stream prior to shipping it off, use the FileStreamResult instead.

Closing Thoughts

Let’s end where we started. Remember we started off with rendering a PDF directly into a browser window and then in all subsequent examples, added a response header to signal to the browser that the incoming response is an attachment for download? Well, ASP.NET provides a neat helper that we can use in our controllers – the ControllerBase.File method. Using its various overloads (24 of them!), you can try all sorts of combinations – pass it a virtual path or a byte array, with a download filename or one without and it will infer the expected outcome and send the response accordingly. For instance, if you give it a download filename, it will assume that you want your content to be treated as a separate attachment download and set the correct response content-disposition header accordingly.

Check out my companion repo on GitHub, here:

tvaidyan/aspnet-file-downloads: Companion repo to my “Implement File Download Capabilities in ASP.NET” blog post on tvaidyan.com (github.com)

Leave a Comment

Your email address will not be published. Required fields are marked *