Streaming files with httpclient and multiple controllers

In a recent project I was faced with a requirement which stated that all access to databases, file system and what not, had to go via trusted endpoints. It’s not uncommon but you do hit some roadblocks. Since the application was file system intensive I had to look for a way to stream files across machine and web application boundaries. Simplified, the application looked like this:

If you want to stream files in this situation you don’t want to load the entire file in memory in the web api part, then sent it to the public facing web app which in turn loads the complete file in memory and then gives it to the client browser. To increase performance of the system the file should be able to get streamed from where its stored to the browser of the user.

The API controller in the private API part is pretty straightforward. Lookup the file and pass it along using a custom HttpActionResult: FileResult.

[RoutePrefix("api/files")]
[Authorize]
public class FilesController : ApiController
{       
    [Route("{fileId:long}")]
    [HttpGet]
    public IHttpActionResult Get(long fileId)
    {
        FileMetaData metadata = LoadFileMetaData(fileId);
        if (File.Exists(metadata.Location))
        {
            return new FileResult(File.Open(metadata.Location, FileMode.Open, FileAccess.Read), metadata.ContentType, metadata.FileName);
        }
        else
        {
            return NotFound();
        }   
    }
}

Creating a custom HttpActionResult is pretty straightforward, if you look around online you’ll find plenty of examples. This is the one I ended up with myself. What’s important in this case, apart from loading the file, is populating the mimetype and setting the contentdisposition header.

class FileResult : IHttpActionResult
{
    private readonly string _filePath;
    private readonly string _contentType;
    private readonly string _filename;
    private readonly Stream _stream;
 
    public FileResult(string filePath, string contentType = null, string filename = null)
    {
        if (filePath == null) throw new ArgumentNullException("filePath");
 
        _filePath = filePath;
        _contentType = contentType;
        _filename = filename;
    }
 
    public FileResult(Stream stream, string contentType = null, string filename = null)
    {
        if (stream == null) throw new ArgumentNullException("stream");
 
        _stream = stream;
        _contentType = contentType;
        _filename = filename;
    }
 
    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StreamContent(_stream ?? File.OpenRead(_filePath))
        };
 
        var contentType = _contentType ?? MimeMapping.GetMimeMapping(Path.GetExtension(_filePath));
        response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
        if (!string.IsNullOrWhiteSpace(_filename))
        {
            response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
            {
                FileName = _filename
            };
        }
 
        return Task.FromResult(response);
    }
}

Then comes the tricky part: streaming the file directly to the user from the public facing web application. In this case the public application was an MVC site. I’m using HttpClient to call the private API and indicate I want my code to process the headers immediately after they are received. This allows me to see if the file was found or not. If we don’t have a 401 error code I check if I might have another error, if not I asynchronously load the response stream and pass it along the standard FileActionResult. An essential part is the Response.BufferOutput part. By default it’s set to true and will force the web app to load the file completely in memory before giving it to the client.

[Route("file/{fileId:long}")]
[HttpGet]
public virtual async Task<ActionResult> File(long fileId)
{
    var httpClient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
 
    var response = await httpClient.GetAsync(ConfigurationManager.AppSettings["App.PrivateApi"] + $"/{fileId}", HttpCompletionOption.ResponseHeadersRead);
    if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
    {
        return HttpNotFound();
    }
    else
    {
        response.EnsureSuccessStatusCode();
        var fileStream = await response.Content.ReadAsStreamAsync();
        Response.BufferOutput = false;
        return File(fileStream, response.Content.Headers?.ContentType?.MediaType, response.Content.Headers?.ContentDisposition?.FileName);
    }
}

Update MVC4 project to MVC5 within Visual Studio

If you are using VS2012 and start a new ASP.NET MVC4 project, you will be greeted by an enormous list of packages which can be updated when clicking through to the NuGet package managers.

Capture01

 

With the new release of Visual Studio 2013, MVC5, WebAPI2,… a lot of new binaries are ready to be used in your application. So updating the packages in Visual Studio should get you going. After clicking “yes” and “I agree” several times though, you will receive this error message:

broken

If you now close the NuGet package manager and then open it again, only one package needs to be updated at the moment (ANTLRv3). So click update once more.

If you now start the application, instead of receiving a nice MVC start screen you will run into a yellow screen of death:

yellowscreenodeath

 

We are almost there. Navigate to the Web.config inside of the Views directory and change all references from MVC 4.0.0.0 to 5.0.0.0 and the Razor version from 2.0.0.0 to 3.0.0.0. I’ve included the changes in this gist.

You are now ready to go!

UPDATE: Ran into this MSDN article which shows you the steps I mentioned and more!