Sunday, June 8, 2014

How to stream a FileResult from one web server to another with ASP.NET MVC

MVC has a lot of great built in tooling, including the ability to stream very large file results straight from disk without having to load the whole file stream into memory.

What about the scenario where you want to stream a large file from one web server to another?

For example, I have an ASP.NET MVC application that needs to expose a download for a file hosted on another server, but I can not just redirect my users directly to the other URL. For that, we need to create a custom ActionResult type!

WebRequestFileResult

Here is a simple of example of what your controller might look like:

public class FileController : Controller
{
    public ActionResult LocalFile()
    {
        return new FilePathResult(@"c:\files\otherfile.zip", "application/zip");
    }
 
    public ActionResult RemoteFile()
    {
        return new WebRequestFileResult("http://otherserver/otherfile.zip");
    }
}

Here is the implementation for WebRequestFileResult:

public class WebRequestFileResult : FileResult
{
    private const int BufferSize = 32768; // 32 KB
    private const string DispositionHeader = "Content-Disposition";
    private const string LengthHeader = "Content-Length";
 
    private readonly string _url;
 
    public WebRequestFileResult(string url)
        : base("application/octet-stream")
    {
        if (String.IsNullOrWhiteSpace(url))
            throw new ArgumentNullException("url", "Url is required");
 
        _url = url;
    }
 
    protected override void WriteFile(HttpResponseBase localResponse)
    {
        var webRequest = WebRequest.Create(_url);
 
        using (var remoteResponse = webRequest.GetResponse())
        using (var remoteStream = remoteResponse.GetResponseStream())
        {
            if (remoteStream == null)
                throw new NullReferenceException(
                    "Request returned null stream: " + _url);
 
            var dispositionKey = remoteResponse.Headers.AllKeys.FirstOrDefault(
                k => k.Equals(
                    DispositionHeader, 
                    StringComparison.InvariantCultureIgnoreCase));
 
            if (!String.IsNullOrWhiteSpace(dispositionKey))
                localResponse.AddHeader(
                    DispositionHeader, 
                    remoteResponse.Headers[DispositionHeader]);
 
            var contentLenthString = remoteResponse.ContentLength
                .ToString(CultureInfo.InvariantCulture);
                
            localResponse.ContentType = remoteResponse.ContentType;
            localResponse.AddHeader(LengthHeader, contentLenthString);
 
            var buffer = new byte[BufferSize];
            var loopCount = 0;
 
            while (true)
            {
                if (!localResponse.IsClientConnected)
                    break;
 
                var read = remoteStream.Read(buffer, 0, BufferSize);
 
                if (read <= 0)
                    break;
 
                localResponse.OutputStream.Write(buffer, 0, read);
 
                // Flush after 1 MB; note that this
                // will prevent server side caching.
                loopCount++;
                if (loopCount % 32 == 0)
                    localResponse.Flush();
            }
        }
    }
}

Enjoy,
Tom

4 comments:

  1. You should use Stream.Copy method instead of while(true) loop.

    ReplyDelete
    Replies
    1. When using Stream.CopyTo the response stream would not always auto flush, and the server could built up a lot of memory pressure.

      Delete
    2. How can filestreamresult support resume download from url , Tom ?

      Delete

Real Time Web Analytics