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.
Hi, I’m Sam.
I’m a programmer and a DIYer. When I’m not finding things to build I enjoy cooking, hiking, camping and traveling the world with my best friend. Say Hello!