Kolby Kolby - 2 months ago 24
C# Question

Initiate file download in .Net ApiController

I'm trying to generate a PDF file with HTML content that's being sent via AJAX from JavaScript.

I have the HTML being sent properly, and the PDF being generated correctly using the HTML, but I'm stuck trying to figure out how to initiate the file download on the user's browser.

This is what I'm currently trying. This works in a

IHttpHandler
file when hitting the URL for the file directly, but doesn't work in the
ApiController
when posting via AJAX.

I've also tried posting to the IHttpHandler file with the same results. Everything works fine until I try to initiate the download by using
BinaryWrite
.

public class DownloadScheduleController : ApiController
{
public void Post(HtmlModel data)
{
var htmlContent = data.html;

var pdfBytes = (new NReco.PdfGenerator.HtmlToPdfConverter()).GeneratePdf(htmlContent);

using (var mstream = new System.IO.MemoryStream())
{
HttpContext.Current.Response.ContentType = "application/pdf";
HttpContext.Current.Response.AppendHeader("content-disposition", "attachment; filename=UserSchedule.pdf");
HttpContext.Current.Response.BinaryWrite(pdfBytes);
HttpContext.Current.Response.End();
}
}
}


Here is the Ajax request. It's using Angular's $http.

$http({
method: 'POST',
url: 'api/downloadSchedule',
dataType: "json",
contentType: "application/json; charset=utf-8",
data: data
});


Alternatively I could return the PDF binary to JavaScript, but then how do I use JavaScript to save the pdf?

Any help will be greatly appreciated.

Answer

For security reasons, the browser cannot initiate a file download from an AJAX request. It can only be done by navigating to a page that sends a file.

Usually if you need to initiate a download from Javascript, you would either set window.location.href = urlToFile, or create a new iframe pointing to that url.

In your case this would not serve, because the above methods only perform a GET request, and you need a POST, so I only see two possible solutions to this:

  • you could modify your JS to - instead of submitting the request with $http - create an HTML form with fields that correspond to what you originally posted with your AJAX request, then append the form to the page, populate the fields and submit the form
  • or you could change your server-side code as well as your Javascript.

If you opt for the second solution, you could follow the approach of using two methods on the server side:

  • one POST that generates the file and saves it in the cache (there are probably better solutions that using the cache, especially if you're on a server farm, but let's keep it simple)

  • one GET that that retrieves the cached content and returns it to the user. In this way you will be able to post the data via an AJAX call, and then getting the file by navigating to the right url.

Your C# code would look something like this:

public class DownloadScheduleController : ApiController
{
    public object Post(HtmlModel data)
    {
        var htmlContent = data.html;

        var pdfBytes = (new NReco.PdfGenerator.HtmlToPdfConverter()).GeneratePdf(htmlContent);
        var policy = new CacheItemPolicy { AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(30), Priority = CacheItemPriority.NotRemovable };
        var cacheId = Guid.NewGuid().ToString();
        MemoryCache.Default.Add("pdfBytes_" + cacheId, pdfBytes, policy);
        return new { id = cacheId };
    }

    public void Get(string id)
    {
        var pdfBytes = MemoryCache.Default.Get("pdfBytes_" + id);
        MemoryCache.Default.Remove("pdfBytes_" + id);

        using (var mstream = new System.IO.MemoryStream())
        {
            HttpContext.Current.Response.ContentType = "application/pdf";
            HttpContext.Current.Response.AppendHeader("content-disposition", "attachment; filename=UserSchedule.pdf");
            HttpContext.Current.Response.BinaryWrite(pdfBytes);
            HttpContext.Current.Response.End();
        }
    }
}

Your frontend could then be something like:

$http({
    method: 'POST',
    url: 'api/downloadSchedule',
    dataType: "json",
    contentType: "application/json; charset=utf-8",
    data: data
}).success(function(response) {
    // note: the url path might be different depending on your route configuration
    window.location.href = 'api/downloadSchedule/' + response.id;
});

Keep in mind that my code is just meant to show you the approach, it's not meant to be used as it is in production. For example, you should definitely clear the cache immediately after use. You should also make sanity checks in your Get method for what is retrieved from the cache, before using it. Also, you might want to take additional security precautions on the data that you store and retrieve, depending on your requirements.