Download File with jQuery and Web API 2.0 IHttpActionResult

Download File with jQuery and Web API 2.0 IHttpActionResult

I came across a need to download a file through an AJAX request to a Web API 2.0 endpoint. A quick search revealed a fairly straightforward way to tackle this problem, which is to implement a custom IHttpActionResult (available in Web API 2.0) that sets up the HttpResponse in a way that can be digested by the client.

Create a Custom Web API 2.0 IHttpActionResult

The first step was to implement the IHttpActionResult as outlined below.

    public class ApiFileResult : IHttpActionResult {
        public ApiFileResult(string filePath, string contentType, string fileName = null) {
            if (string.IsNullOrWhiteSpace(filePath)) {
                throw new ArgumentNullException(nameof(filePath));
            }

            this.contentType = contentType ?? MimeMapping.GetMimeMapping(Path.GetExtension(filePath));
            var fileStream = File.OpenRead(filePath);
            var fileBytes = new byte[fileStream.Length];
            fileStream.Read(fileBytes, 0, fileBytes.Length);
            stream = new MemoryStream(fileBytes);

            this.fileName = fileName ?? Path.GetFileName(filePath);
        }

        public ApiFileResult(MemoryStream stream, string contentType, string fileName) {
            if (stream == null) {
                throw new ArgumentNullException(nameof(stream));
            }
            if (contentType == null) {
                throw new ArgumentNullException(nameof(contentType));
            }
            
            this.stream = stream;
            this.contentType = contentType;
            this.fileName = fileName;
        }

        private readonly string contentType;
        private readonly string fileName;
        private readonly MemoryStream stream;

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) {
            var bytes = stream.ToArray();
            var memoryStream = new MemoryStream(bytes);

            var response = new HttpResponseMessage(HttpStatusCode.OK) {
                Content = new StreamContent(memoryStream)
            };

            response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
            response.Content.Headers.ContentLength = memoryStream.Length;
            response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") {
                FileName = fileName
            };

            return Task.FromResult(response);
        }
    }

Once that’s in place we can use the new IHttpActionResult as our API’s return.

Create an ApiController Action That Returns Custom IHttpActionResult

Next up is creating an instance of this new custom Web API 2.0 IHttpActionResult class and using it as our API’s return type. Below is a fairly lightweight ApiController to demonstrate just that. The injected IBillingExportService class is used to generate a memory stream containing the contents of the file. We then new-up an instance of our custom ApiFileResult object, passing the file stream, file type and file name through.

    [RoutePrefix("api/v1/Billing")]
    public class BillingApiController : ApiController {
        public BillingApiController(IBillingExportService service) {
            this.service = service;
        }

        private readonly IBillingExportService service;

        [HttpPost, Route("Export", Name = "BillingExport")]
        public async Task<IHttpActionResult> GetBillingExport(int id) {
            var exportStream = await service.GetExportFile(id);
            return new ApiFileResult(exportStream, 
                                     "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 
                                     $"Billing Export {DateTime.Today:yyyy-MM-dd}{ExcelExport.WorkbookBuilder.ExcelFileExtension}");
        }
    }

Now that we have the API endpoint in place we can make a jQuery AJAX request to it, as follows:

    $.ajax({
            type: 'POST',
            cache: false,
            url: exportScheduledBillingUrl,
            data: serializeScheduledBillingGrid(),
            xhrFields: {
                    // make sure the response knows we're expecting a binary type in return.
                    // this is important, without it the excel file is marked corrupted.
                    responseType: 'arraybuffer'
                }
        })
    .done(function (data, status, xmlHeaderRequest) {
        var downloadLink = document.createElement('a');
        var blob = new Blob([data],
            {
                type: xmlHeaderRequest.getResponseHeader('Content-Type')
            });
        var url = window.URL || window.webkitURL;
        var downloadUrl = url.createObjectURL(blob);
        var fileName = '';

        // get the file name from the content disposition
        var disposition = xmlHttpRequest.getResponseHeader('Content-Disposition');
        if (disposition && disposition.indexOf('attachment') !== -1) {
            var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
            var matches = filenameRegex.exec(disposition);
            if (matches != null && matches[1]) {
                fileName = matches[1].replace(/['"]/g, '');
            }
        }

        // Blob download logic taken from: https://stackoverflow.com/questions/16086162/handle-file-download-from-ajax-post
        if (typeof window.navigator.msSaveBlob !== 'undefined') {
            // IE workaround for "HTML7007" and "Access Denied" error.
            window.navigator.msSaveBlob(blob, fileName);
        } else {
            if (fileName) {
                if (typeof downloadLink.download === 'undefined') {
                    window.location = downloadUrl;
                } else {
                    downloadLink.href = downloadUrl;
                    downloadLink.download = fileName;
                    document.body.appendChild(downloadLink);
                    downloadLink.click();
                }
            } else {
                window.location = downloadUrl;
            }

            setTimeout(function () {
                    url.revokeObjectURL(downloadUrl);
                },
                100);
        }
    });

When the above Ajax call is triggered (i.e. a user clicks a button on your page) it’ll return the new custom IHttpActionResult you created. We then use the information provided by that result to build a Blob object. This Blob object enables us to generate an object URL which can be used to download the contents of our file.

One Reply to “Download File with jQuery and Web API 2.0 IHttpActionResult”

I'd love to hear your thoughts