Writing A Custom File Download Action Result For ASP.NET MVC

asp.net, asp.net mvc 0 comments suggest edit

NEW UPDATE: There is no longer need for this custom ActionResult because ASP.NET MVC now includes one in the box.

UPDATE: I’ve updated the sample to include a new lambda based action result. This also fixes an issue with the original download in which I included the wrong assembly.

The April CodePlex source drop of ASP.NET MVC introduces the concept of returning an ActionResult instance from action methods. ScottGu wrote about this change on his blog.

In this post, I’ll walk through building a custom action result for downloading files. As you’ll see, they are extremely easy to build. Let’s start at the end and see what the end-user behavior of this new result will be.

Here’s a page that contains a link to an action method named Download.This action method returns this new DownloadResult action result.

File Download
HomePage

Clicking on the link then pops up this dialog, prompting you to download and save the file.

File
Download

The code for this action is pretty simple.

public ActionResult Download() 
{
  return new DownloadResult 
    { VirtualPath="~/content/site.css", FileDownloadName = "TheSiteCss.css" };
}

Notice that you just need to give the result two pieces of information, the virtual path to the file to send to the browser and the default filename to save the file as on the browser.

The virtual path is set via VirtualPath property (surprise surprise!). Note that I could have chosen to make this parameter accept the full file path instead of a virtual path, but I didn’t want to force users of this class to fake out a Server.MapPath call in a unit test. In any case, the change is trivial for those who prefer that approach. I might add overloads that accept a Stream, etc…

The file download name is set via the FileDownloadName property. Notice that this is the filename that the user is prompted with.

If the FileDownloadName property is set, the ExecuteResult method makes sure to add the correct content-disposition header which causes the browser to prompt the user to save the file.

For those familiar with Design Patterns, action results follow the pattern commonly known as the Command Pattern. An action method returns an instance that embodies an command that the framework needs to perform next. This provides a means for delaying the execution of framework/pipeline code until after your action method is complete, rather than from within your action method, which makes unit testing much nicer.

Speaking of unit tests, here’s the unit test for that download action method I wrote. As you can see, it is quite simple.

[TestMethod]
public void DownloadActionSendsCorrectFile() {
  var controller = new HomeController();

  var result = controller.Download() as DownloadResult;

  Assert.AreEqual("TheSiteCss.css", result.FileDownloadName);
  Assert.AreEqual("~/content/site.css", result.VirtualPath);
}

Here’s the code for the DownloadResult class. This is the class that does all the work (not that there is much work to do). I do have unit tests of this class in the included source code which demonstrate how to unit test a custom action result.

public class DownloadResult : ActionResult {

  public DownloadResult() {
  }

  public DownloadResult(string virtualPath) {
    this.VirtualPath = virtualPath;
  }

  public string VirtualPath {
    get;
    set;
  }

  public string FileDownloadName {
    get;
    set;
  }

  public override void ExecuteResult(ControllerContext context) {
    if (!String.IsNullOrEmpty(FileDownloadName)) {
      context.HttpContext.Response.AddHeader("content-disposition", 
        "attachment; filename=" + this.FileDownloadName)
    }

    string filePath = context.HttpContext.Server.MapPath(this.VirtualPath);
    context.HttpContext.Response.TransmitFile(filePath);
    }
}

I removed the download since this code is no longer needed nor relevant.

Technorati Tags: aspnetmvc,actionresult

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

39 responses

  1. Avatar for Jonathan Carter
    Jonathan Carter May 10th, 2008

    Nice example dude. The ActionResult possibilities are endless!

  2. Avatar for Steve
    Steve May 10th, 2008

    I haven't downloaded these bits yet, I'm using preview 2.
    So I wonder - how is this 'return ActionResult' going to effect things where in several instances I use a response.write from an ajax call, or where I create an excel spreadsheet and push it back to the client with response.end, etc...
    Will I have to write a unique actionresult for each of these?

  3. Avatar for Steve
    Steve May 10th, 2008

    I get this error running the code:
    Could not load file or assembly 'System.Web.Mvc' or one of its dependencies. Strong name signature could not be verified. The assembly may have been tampered with, or it was delay signed but not fully signed with the correct private key. (Exception from HRESULT: 0x80131045)

  4. Avatar for Zack Owens
    Zack Owens May 10th, 2008

    ActionResult is pretty addicting. It fills the void where an ActionFilterAttribute could not go. Whoever thought of it deserves a raise ;)
    This sample saves some time for sure! Thanks Phil!!!

  5. Avatar for caoge
    caoge May 10th, 2008

    我来自中国(I'm from China).Thanks for your sample```

  6. Avatar for Lamin Barrow
    Lamin Barrow May 10th, 2008

    Hi Phil,
    I know it is typical to write a different public controller method for each view you want to render. I have noticed you have hard coded the the DownloadResult properties in your ActionResult method and i have been wondering ... do i have to write a separate render method to handle lets say two file download links on my page using your approach?

  7. Avatar for Haacked
    Haacked May 10th, 2008

    @Steve sorry about that, I included the wrong assembly. I've updated the sample to include a new DelegatingActionResult which allows you to pass a delegate in. That way, you don't have to write a custom action result for everything.
    @Lamin I could've made the Download action method parameterized. That way there would've been no need to hardcode anything.

  8. Avatar for Steve
    Steve May 11th, 2008

    I must be missing something - still get that same error ?
    I'm saving this code though, will try it with Preview 3 - great stuff!

  9. Avatar for Shiju Varghese
    Shiju Varghese May 11th, 2008

    Hi Phil,
    Nice example it shows the opportunities of ActionResult.
    Thanks,
    Shiju

  10. Avatar for json
    json May 11th, 2008

    And all this is so much better than using an ashx-handler, because...???? I'm sorry but all this just adds code, and code added means more work later - I strive to reduce the "more work later" part of programming.

  11. Avatar for Anastasiosyal
    Anastasiosyal May 11th, 2008

    ActionResult seems simple yet powerful.
    Just a note on the "content-disposition" header: I'd wrap the FileDownloadName in quotes. The reason being is that Firefox will truncate the filename to the first space if the file name includes spaces and is not quoted (as per spec).
    So you would have:
    context.HttpContext.Response.AddHeader("content-disposition",
    "attachment; filename=\"" + this.FileDownloadName + "\"")

  12. Avatar for Maarten Balliauw
    Maarten Balliauw May 12th, 2008

    Nice example! I actually did something similar as an alternative to writing ASP.NET HttpHandlers for rendering images. (http://blog.maartenballiauw...

  13. Avatar for ganesh
    ganesh May 22nd, 2008

    pls tell the code

  14. Avatar for Jason
    Jason December 16th, 2008

    Excellent, just the answer I was looking for. I was expecting this to be a lot more complicated - I can finally get to bed!

  15. Avatar for Jason
    Jason December 16th, 2008

    Just when I though I was going to get an early night...seems I needed to add 'Content-type' to the headers as well otherwise Google Chrome and Firefox kept adding ".htm" on the the end of my file names! Seems to be part of a long-standing browser argument over whether the browser should pay much attention to 'Content-disposition'. Anyhow, this addition solved the issue for me:
    context.HttpContext.Response.AddHeader("Content-type", "application/force-download");
    Don't know if anyone else has come across this? Now I'm going to be all tired and grumpy tomorrow :(

  16. Avatar for Avi
    Avi February 27th, 2009

    How to download files from web server as well as from remote file server using asp.net and C#. Visit following post:
    www.etechplanet.com/.../...using-aspnet-and-C.aspx

  17. Avatar for Edmund Herbert
    Edmund Herbert February 5th, 2010

    Hi I am trying to down load sample above, get error not valid archive can you email me source, thanks edmund

  18. Avatar for KarlZ
    KarlZ February 15th, 2010

    I too am getting the "not a valid archive" error when I try to download the .zip file. Is there an issue with that file? Or the way that IE 8 downloads it?
    Thanks,
    Karl

  19. Avatar for WGAO
    WGAO February 24th, 2010

    Does anyone know how to use httpcontext.response.transmitfile or any other httpcontext function to download a file from a network driver instead of local driver? When I tried to use context.Response.TransmitFile(DownloadPath) to download files, the DownloadPath has to be local driver. If it's a network driver, the download failed. Thanks in advance...

  20. Avatar for Hrishikesh Ratnakar Kulkarni
    Hrishikesh Ratnakar Kulkarni March 4th, 2010

    Hi, I just want to ask that how can I write a code to download a file in asp.net at Client side(The Client will be downloading the Txt file).Just like "Question Bank" Uploaded by professors can be downloaded by a student .Actually we are developing a "Students networking site"for our final year project.....
    pls let me know how can i achieve it before our last date of our submission comes it is the last module remaining

  21. Avatar for Efwoiar
    Efwoiar July 14th, 2010

    Have you gotten the code?
    Would you like to give me one?
    Thanks!

  22. Avatar for LW
    LW September 16th, 2010

    Hi, can you send me the code?

  23. Avatar for csac
    csac October 24th, 2010

    vrev

  24. Avatar for roche plando
    roche plando November 11th, 2010

    pls send me the code. i tried clicking the link for the source code but it fails.

  25. Avatar for Patrick barry
    Patrick barry November 14th, 2010

    public class DownLoadZipFile : ActionResult
    {
    public string FileDownloadName { get; set; }
    public List<string> ZipFileList { get; set; }
    public DownLoadZipFile()
    {
    }
    public DownLoadZipFile(string fileDownloadName, List<string> zipFileList)
    {
    FileDownloadName = fileDownloadName;
    ZipFileList = zipFileList;
    }

    public override void ExecuteResult(ControllerContext context)
    {
    if (!String.IsNullOrEmpty(FileDownloadName))
    {
    var response = context.HttpContext.Response;
    response.Clear();
    response.AddHeader("content-disposition", "attachment; filename=" + this.FileDownloadName);
    response.ContentType = "application/zip";
    //response.ContentType = "application/octet-stream";
    response.CacheControl = "Private";
    response.Cache.SetExpires(DateTime.Now.AddMinutes(3));
    }
    DownloadZipToBrowser(context);
    }
    public void DownloadZipToBrowser(ControllerContext context)
    {
    ZipOutputStream zipOutputStream = null;
    var response = context.HttpContext.Response;
    try
    {
    byte[] buffer = new byte[4096];
    zipOutputStream = new ZipOutputStream(response.OutputStream);
    zipOutputStream.SetLevel(3); //0-9, 9 being the highest level of compression
    foreach (string fileName in ZipFileList)
    {
    Stream fs = File.OpenRead(fileName);
    ZipEntry entry = new ZipEntry(Path.GetFileName(fileName));
    entry.Size = fs.Length;
    zipOutputStream.PutNextEntry(entry);
    int count = fs.Read(buffer, 0, buffer.Length);
    while (count > 0)
    {
    zipOutputStream.Write(buffer, 0, count);
    count = fs.Read(buffer, 0, buffer.Length);
    if (!response.IsClientConnected)
    {
    break;
    }
    response.Flush();
    }
    fs.Close();
    }
    }
    catch (Exception ex)
    {
    Logging.WriteError(ex);
    }
    finally
    {
    if (zipOutputStream != null)
    zipOutputStream.Close();
    response.Flush();
    response.End();
    }
    }
    }

  26. Avatar for femi
    femi January 3rd, 2011

    the sample code download is not working...

  27. Avatar for mim
    mim May 17th, 2011

    Hi, The link haacked.com/.../FileNotFound.aspx is not working. I am not able to download the demo

  28. Avatar for pujie
    pujie May 24th, 2011

    great Tutorial, thanks a lot. It works !!!

  29. Avatar for David
    David August 29th, 2011

    Now it's integrated in asp.net MVC.
    Just use :
    return File(FileStreamOrPath, ContentType, fileDownloadName);

  30. Avatar for Jeff Circeo
    Jeff Circeo October 1st, 2011

    You could expand and improve your code by adding client side caching, below is an article in ASP.NET and Delphi that does part of this (I haven't tried it)... porting this code looks simple but who knows :)
    http://edn.embarcadero.com/article/38123

  31. Avatar for geo
    geo December 8th, 2011

    File(FileStreamOrPath, ContentType, fileDownloadName) always specify "Content-Disposition: attachment; filename=fileDownloadName" in the header.
    What if I need to specify: "Content-Disposition: inline; filename=fileDownloadName" in the header?

  32. Avatar for haacked
    haacked December 9th, 2011

    @geo the current helpers don't support that. You could return a custom FileResult and override ExecuteResult to do what you want.

  33. Avatar for FullMoonMadness
    FullMoonMadness July 17th, 2012

    Thank you for your post.
    But, it's not working properly on Opera!
    The download dialog appears, but the filename is saved as .htm (!!)by default and not my filename's right extension!
    Any advice?
    Thanks.

  34. Avatar for Jason
    Jason August 15th, 2012

    Make sure your file names have an extension. I was removing the extensions from the names and all my files kept saving as "file" types.

  35. Avatar for Master.man
    Master.man August 20th, 2012

    What if the file size is large say >2GB?

  36. Avatar for SasiReddy
    SasiReddy September 12th, 2013

    Thank u for posting this.I done this one

  37. Avatar for monika garg
    monika garg September 24th, 2013

    same here. :) really its looking so tough by name..

  38. Avatar for monika garg
    monika garg September 25th, 2013

    @cdf546b601bf29a7eb4ca777544d11cd:disqus Mr. haack can u pls suggest how can i make a browser output of my mvc4 application downloadable as pdf ?

  39. Avatar for Marty Ramirez
    Marty Ramirez October 23rd, 2017

    Great example! Thanks a bunch!!!