diff --git a/Mvc.sln b/Mvc.sln
index e13f657391..6907bee3b5 100644
--- a/Mvc.sln
+++ b/Mvc.sln
@@ -84,6 +84,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ApiExplorerWebSite", "test\
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ReflectedModelWebSite", "test\WebSites\ReflectedModelWebSite\ReflectedModelWebSite.kproj", "{C2EF54F8-8886-4260-A322-44F76245F95D}"
EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FilesWebSite", "test\WebSites\FilesWebSite\FilesWebSite.kproj", "{0EF9860B-10D7-452F-B0F4-A405B88BEBB3}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -414,6 +416,16 @@ Global
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|x86.ActiveCfg = Release|Any CPU
+ {0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {0EF9860B-10D7-452F-B0F4-A405B88BEBB3}.Release|x86.ActiveCfg = Release|Any CPU
{61061528-071E-424E-965A-07BCC2F02672}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{61061528-071E-424E-965A-07BCC2F02672}.Debug|Any CPU.Build.0 = Debug|Any CPU
{61061528-071E-424E-965A-07BCC2F02672}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@@ -472,6 +484,7 @@ Global
{1976AC4A-FEA4-4587-A158-D9F79736D2B6} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{96107AC0-18E2-474D-BAB4-2FFF2185FBCD} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{A192E504-2881-41DC-90D1-B7F1DD1134E8} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
+ {0EF9860B-10D7-452F-B0F4-A405B88BEBB3} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{61061528-071E-424E-965A-07BCC2F02672} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{C2EF54F8-8886-4260-A322-44F76245F95D} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
EndGlobalSection
diff --git a/samples/MvcSample.Web/HomeController.cs b/samples/MvcSample.Web/HomeController.cs
index 30341b570b..6bf0088cd8 100644
--- a/samples/MvcSample.Web/HomeController.cs
+++ b/samples/MvcSample.Web/HomeController.cs
@@ -34,6 +34,16 @@ namespace MvcSample.Web
return HttpNotFound();
}
+ public ActionResult SendFileFromDisk()
+ {
+ return File("sample.txt", "text/plain");
+ }
+
+ public ActionResult SendFileFromDiskWithName()
+ {
+ return File("sample.txt", "text/plain", "sample-file.txt");
+ }
+
public bool IsDefaultNameSpace()
{
var namespaceToken = ActionContext.RouteData.DataTokens["NameSpace"] as string;
diff --git a/samples/MvcSample.Web/sample.txt b/samples/MvcSample.Web/sample.txt
new file mode 100644
index 0000000000..6abaf945fa
--- /dev/null
+++ b/samples/MvcSample.Web/sample.txt
@@ -0,0 +1 @@
+This is a sample file returned from a controller
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/FileContentResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/FileContentResult.cs
new file mode 100644
index 0000000000..cb608f1ec8
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/FileContentResult.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Http;
+
+namespace Microsoft.AspNet.Mvc
+{
+ ///
+ /// Represents an that when executed will
+ /// write a binary file to the response.
+ ///
+ public class FileContentResult : FileResult
+ {
+ ///
+ /// Creates a new instance with
+ /// the provided and the
+ /// provided .
+ ///
+ /// The bytes that represent the file contents.
+ /// The Content-Type header of the response.
+ public FileContentResult([NotNull] byte[] fileContents, string contentType)
+ : base(contentType)
+ {
+ FileContents = fileContents;
+ }
+
+ ///
+ /// Gets the file contents.
+ ///
+ public byte[] FileContents { get; private set; }
+
+ ///
+ protected override Task WriteFileAsync(HttpResponse response, CancellationToken cancellation)
+ {
+ return response.Body.WriteAsync(FileContents, 0, FileContents.Length, cancellation);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/FilePathResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/FilePathResult.cs
new file mode 100644
index 0000000000..592c6e5fca
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/FilePathResult.cs
@@ -0,0 +1,84 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Http;
+using Microsoft.AspNet.HttpFeature;
+using Microsoft.AspNet.Mvc.Core;
+
+namespace Microsoft.AspNet.Mvc
+{
+ ///
+ /// Represents an that when executed will
+ /// write a file from disk to the response using mechanisms provided
+ /// by the host.
+ ///
+ public class FilePathResult : FileResult
+ {
+ private const int DefaultBufferSize = 0x1000;
+
+ ///
+ /// Creates a new instance with
+ /// the provided and the
+ /// provided .
+ ///
+ /// The path to the file. The path must be an absolute
+ /// path. Relative and virtual paths are not supported.
+ /// The Content-Type header of the response.
+ public FilePathResult([NotNull] string fileName, [NotNull] string contentType)
+ : base(contentType)
+ {
+ if (!Path.IsPathRooted(fileName))
+ {
+ var message = Resources.FormatFileResult_InvalidPathType_RelativeOrVirtualPath(fileName);
+ throw new ArgumentException(message, "fileName");
+ }
+
+ FileName = fileName;
+ }
+
+ ///
+ /// Gets the path to the file that will be sent back as the response.
+ ///
+ public string FileName { get; private set; }
+
+ ///
+ protected override Task WriteFileAsync(HttpResponse response, CancellationToken cancellation)
+ {
+ var sendFile = response.HttpContext.GetFeature();
+ if (sendFile != null)
+ {
+ return sendFile.SendFileAsync(
+ FileName,
+ offset: 0,
+ length: null,
+ cancellation: cancellation);
+ }
+ else
+ {
+ return CopyStreamToResponse(FileName, response, cancellation);
+ }
+ }
+
+ private static async Task CopyStreamToResponse(
+ string fileName,
+ HttpResponse response,
+ CancellationToken cancellation)
+ {
+ var fileStream = new FileStream(
+ fileName, FileMode.Open,
+ FileAccess.Read,
+ FileShare.ReadWrite,
+ DefaultBufferSize,
+ FileOptions.Asynchronous | FileOptions.SequentialScan);
+
+ using (fileStream)
+ {
+ await fileStream.CopyToAsync(response.Body, DefaultBufferSize, cancellation);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/FileResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/FileResult.cs
new file mode 100644
index 0000000000..1b7fba8f97
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/FileResult.cs
@@ -0,0 +1,266 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Http;
+
+namespace Microsoft.AspNet.Mvc
+{
+ ///
+ /// Represents an that when executed will
+ /// write a file as the response.
+ ///
+ public abstract class FileResult : ActionResult
+ {
+ private string _fileDownloadName;
+
+ ///
+ /// Creates a new instance with
+ /// the provided .
+ ///
+ /// The Content-Type header of the response.
+ protected FileResult([NotNull] string contentType)
+ {
+ ContentType = contentType;
+ }
+
+ ///
+ /// Gets the Content-Type header value that will be written to the response.
+ ///
+ public string ContentType { get; private set; }
+
+ ///
+ /// Gets the file name that will be used in the Content-Disposition header of the response.
+ ///
+ public string FileDownloadName
+ {
+ get { return _fileDownloadName ?? string.Empty; }
+ set { _fileDownloadName = value; }
+ }
+
+ ///
+ public override Task ExecuteResultAsync([NotNull] ActionContext context)
+ {
+ var response = context.HttpContext.Response;
+ response.ContentType = ContentType;
+
+ if (!string.IsNullOrEmpty(FileDownloadName))
+ {
+ // From RFC 2183, Sec. 2.3:
+ // The sender may want to suggest a filename to be used if the entity is
+ // detached and stored in a separate file. If the receiving MUA writes
+ // the entity to a file, the suggested filename should be used as a
+ // basis for the actual filename, where possible.
+ var headerValue = ContentDispositionUtil.GetHeaderValue(FileDownloadName);
+ context.HttpContext.Response.Headers.Set("Content-Disposition", headerValue);
+ }
+
+ // We aren't flowing the cancellation token appropiately, see
+ // https://github.com/aspnet/Mvc/issues/743 for details.
+ return WriteFileAsync(response, CancellationToken.None);
+ }
+
+ ///
+ /// Writes the file to the response.
+ ///
+ ///
+ /// The where the file will be written
+ ///
+ /// The to cancel the operation.
+ ///
+ /// A that will complete when the file has been written to the response.
+ ///
+ protected abstract Task WriteFileAsync(HttpResponse response, CancellationToken cancellation);
+
+ // This is a temporary implementation until we have the right abstractions in HttpAbstractions.
+ internal static class ContentDispositionUtil
+ {
+ private const string HexDigits = "0123456789ABCDEF";
+
+ private static void AddByteToStringBuilder(byte b, StringBuilder builder)
+ {
+ builder.Append('%');
+
+ int i = b;
+ AddHexDigitToStringBuilder(i >> 4, builder);
+ AddHexDigitToStringBuilder(i % 16, builder);
+ }
+
+ private static void AddHexDigitToStringBuilder(int digit, StringBuilder builder)
+ {
+ builder.Append(HexDigits[digit]);
+ }
+
+ private static string CreateRfc2231HeaderValue(string filename)
+ {
+ var builder = new StringBuilder("attachment; filename*=UTF-8''");
+
+ var filenameBytes = Encoding.UTF8.GetBytes(filename);
+ foreach (var b in filenameBytes)
+ {
+ if (IsByteValidHeaderValueCharacter(b))
+ {
+ builder.Append((char)b);
+ }
+ else
+ {
+ AddByteToStringBuilder(b, builder);
+ }
+ }
+
+ return builder.ToString();
+ }
+
+ public static string GetHeaderValue(string fileName)
+ {
+ // If fileName contains any Unicode characters, encode according
+ // to RFC 2231 (with clarifications from RFC 5987)
+ foreach (var c in fileName)
+ {
+ if ((int)c > 127)
+ {
+ return CreateRfc2231HeaderValue(fileName);
+ }
+ }
+
+ return CreateNonUnicodeCharactersHeaderValue(fileName);
+ }
+
+ private static string CreateNonUnicodeCharactersHeaderValue(string fileName)
+ {
+ var escapedFileName = EscapeFileName(fileName);
+ return string.Format("attachment; filename={0}", escapedFileName);
+ }
+
+ private static string EscapeFileName(string fileName)
+ {
+ var hasToBeQuoted = false;
+
+ // We can't break the loop earlier because we need to check the
+ // whole name for \n, in which case we need to provide a special
+ // encoding.
+ for (var i = 0; i < fileName.Length; i++)
+ {
+ if (fileName[i] == '\n')
+ {
+ // See RFC 2047 for more details
+ return GetRfc2047Base64EncodedWord(fileName);
+ }
+
+ // Control characters = (octets 0 - 31) and DEL (127)
+ if (char.IsControl(fileName[i]))
+ {
+ hasToBeQuoted = true;
+ }
+
+ switch (fileName[i])
+ {
+ case '(':
+ case ')':
+ case '<':
+ case '>':
+ case '@':
+ case ',':
+ case ';':
+ case ':':
+ case '\\':
+ case '/':
+ case '[':
+ case ']':
+ case '?':
+ case '=':
+ case '{':
+ case '}':
+ case ' ':
+ case '\t':
+ case '"':
+ hasToBeQuoted = true;
+ break;
+ default:
+ break;
+ }
+ }
+
+ return hasToBeQuoted ? QuoteFileName(fileName) : fileName;
+ }
+
+ private static string QuoteFileName(string fileName)
+ {
+ var builder = new StringBuilder();
+ builder.Append("\"");
+
+ for (var i = 0; i < fileName.Length; i++)
+ {
+ switch (fileName[i])
+ {
+ case '\\':
+ // Escape \
+ builder.Append("\\\\");
+ break;
+ case '"':
+ // Escape "
+ builder.Append("\\\"");
+ break;
+ default:
+ builder.Append(fileName[i]);
+ break;
+ }
+ }
+
+ builder.Append("\"");
+ return builder.ToString();
+ }
+
+ private static string GetRfc2047Base64EncodedWord(string fileName)
+ {
+ // See RFC 2047 for details. Section 8 for examples.
+ const string charset = "utf-8";
+ // B means Base64
+ const string encoding = "B";
+
+ var fileNameBytes = Encoding.UTF8.GetBytes(fileName);
+ var base64EncodedFileName = Convert.ToBase64String(fileNameBytes);
+
+ // Encoded words are defined as "=?{charset}?{encoding}?{encpoded value}?="
+ return string.Format("\"=?{0}?{1}?{2}?=\"", charset, encoding, base64EncodedFileName);
+ }
+
+ // Application of RFC 2231 Encoding to Hypertext Transfer Protocol (HTTP) Header Fields, sec. 3.2
+ // http://greenbytes.de/tech/webdav/draft-reschke-rfc2231-in-http-latest.html
+ private static bool IsByteValidHeaderValueCharacter(byte b)
+ {
+ if ((byte)'0' <= b && b <= (byte)'9')
+ {
+ return true; // is digit
+ }
+ if ((byte)'a' <= b && b <= (byte)'z')
+ {
+ return true; // lowercase letter
+ }
+ if ((byte)'A' <= b && b <= (byte)'Z')
+ {
+ return true; // uppercase letter
+ }
+
+ switch (b)
+ {
+ case (byte)'-':
+ case (byte)'.':
+ case (byte)'_':
+ case (byte)'~':
+ case (byte)':':
+ case (byte)'!':
+ case (byte)'$':
+ case (byte)'&':
+ case (byte)'+':
+ return true;
+ }
+
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/FileStreamResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/FileStreamResult.cs
new file mode 100644
index 0000000000..d7e4337790
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/FileStreamResult.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Http;
+
+namespace Microsoft.AspNet.Mvc
+{
+ ///
+ /// Represents an that when executed will
+ /// write a file from a stream to the response.
+ ///
+ public class FileStreamResult : FileResult
+ {
+ // default buffer size as defined in BufferedStream type
+ private const int BufferSize = 0x1000;
+
+ ///
+ /// Creates a new instance with
+ /// the provided and the
+ /// provided .
+ ///
+ /// The stream with the file.
+ /// The Content-Type header of the response.
+ public FileStreamResult([NotNull] Stream fileStream, string contentType)
+ : base(contentType)
+ {
+ FileStream = fileStream;
+ }
+
+ ///
+ /// Gets the stream with the file that will be sent back as the response.
+ ///
+ public Stream FileStream { get; private set; }
+
+ ///
+ protected async override Task WriteFileAsync(HttpResponse response, CancellationToken cancellation)
+ {
+ var outputStream = response.Body;
+
+ using (FileStream)
+ {
+ await FileStream.CopyToAsync(outputStream, BufferSize, cancellation);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Controller.cs b/src/Microsoft.AspNet.Mvc.Core/Controller.cs
index 9b3e79c39a..b9ee828566 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Controller.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Controller.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.IO;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
@@ -424,6 +425,90 @@ namespace Microsoft.AspNet.Mvc
return new RedirectToRouteResult(Url, routeName, routeValues, permanent: true);
}
+ ///
+ /// Returns a file with the specified as content and the
+ /// specified as the Content-Type.
+ ///
+ /// The file contents.
+ /// The Content-Type of the file.
+ /// The created for the response.
+ [NonAction]
+ public virtual FileContentResult File(byte[] fileContents, string contentType)
+ {
+ return File(fileContents, contentType, fileDownloadName: null);
+ }
+
+ ///
+ /// Returns a file with the specified as content, the
+ /// specified as the Content-Type and the
+ /// specified as the suggested file name.
+ ///
+ /// The file contents.
+ /// The Content-Type of the file.
+ /// The suggested file name.
+ /// The created for the response.
+ [NonAction]
+ public virtual FileContentResult File(byte[] fileContents, string contentType, string fileDownloadName)
+ {
+ return new FileContentResult(fileContents, contentType) { FileDownloadName = fileDownloadName };
+ }
+
+ ///
+ /// Returns a file in the specified with the
+ /// specified as the Content-Type.
+ ///
+ /// The with the contents of the file.
+ /// The Content-Type of the file.
+ /// The created for the response.
+ [NonAction]
+ public virtual FileStreamResult File(Stream fileStream, string contentType)
+ {
+ return File(fileStream, contentType, fileDownloadName: null);
+ }
+
+ ///
+ /// Returns a file in the specified with the
+ /// specified as the Content-Type and the
+ /// specified as the suggested file name.
+ ///
+ /// The with the contents of the file.
+ /// The Content-Type of the file.
+ /// The suggested file name.
+ /// The created for the response.
+ [NonAction]
+ public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName)
+ {
+ return new FileStreamResult(fileStream, contentType) { FileDownloadName = fileDownloadName };
+ }
+
+ ///
+ /// Returns the file specified by with the
+ /// specified as the Content-Type.
+ ///
+ /// The with the contents of the file.
+ /// The Content-Type of the file.
+ /// The created for the response.
+ [NonAction]
+ public virtual FilePathResult File(string fileName, string contentType)
+ {
+ return File(fileName, contentType, fileDownloadName: null);
+ }
+
+ ///
+ /// Returns the file specified by with the
+ /// specified as the Content-Type and the
+ /// specified as the suggested file name.
+ ///
+ /// The with the contents of the file.
+ /// The Content-Type of the file.
+ /// The suggested file name.
+ /// The created for the response.
+ [NonAction]
+ public virtual FilePathResult File(string fileName, string contentType, string fileDownloadName)
+ {
+ return new FilePathResult(fileName, contentType) { FileDownloadName = fileDownloadName };
+ }
+
///
/// Creates an that produces a Not Found (404) response.
///
diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
index 72f9f30018..8144638ae8 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
@@ -1482,6 +1482,22 @@ namespace Microsoft.AspNet.Mvc.Core
return GetString("AttributeRoute_NullTemplateRepresentation");
}
+ ///
+ /// "The path to the file must be absolute: {0}"
+ ///
+ internal static string FileResult_InvalidPathType_RelativeOrVirtualPath
+ {
+ get { return GetString("FileResult_InvalidPathType_RelativeOrVirtualPath"); }
+ }
+
+ ///
+ /// "The path to the file must be absolute: {0}"
+ ///
+ internal static string FormatFileResult_InvalidPathType_RelativeOrVirtualPath(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("FileResult_InvalidPathType_RelativeOrVirtualPath"), p0);
+ }
+
///
/// Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1}
///
diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
index 4633517ed2..38e275f72d 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
@@ -406,4 +406,8 @@
Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1}
0 is the newline - 1 is a newline separate list of action display names
+
+ "The path to the file must be absolute: {0}"
+ {0} is the value for the provided path
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FileContentResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FileContentResultTest.cs
new file mode 100644
index 0000000000..5d0837b2b2
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FileContentResultTest.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNet.PipelineCore;
+using Microsoft.AspNet.Routing;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc
+{
+ public class FileContentResultTest
+ {
+ [Fact]
+ public void Constructor_SetsFileContents()
+ {
+ // Arrange
+ var fileContents = new byte[0];
+
+ // Act
+ var result = new FileContentResult(fileContents, "text/plain");
+
+ // Assert
+ Assert.Same(fileContents, result.FileContents);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_CopiesBuffer_ToOutputStream()
+ {
+ // Arrange
+ var buffer = new byte[] { 1, 2, 3, 4, 5 };
+
+ var httpContext = new DefaultHttpContext();
+
+ var outStream = new MemoryStream();
+ httpContext.Response.Body = outStream;
+
+ var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ var result = new FileContentResult(buffer, "text/plain");
+
+ // Act
+ await result.ExecuteResultAsync(context);
+
+ // Assert
+ Assert.Equal(buffer, outStream.ToArray());
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FilePathResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FilePathResultTest.cs
new file mode 100644
index 0000000000..e39866680d
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FilePathResultTest.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNet.HttpFeature;
+using Microsoft.AspNet.PipelineCore;
+using Microsoft.AspNet.Routing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc
+{
+ public class FilePathResultTest
+ {
+ [Fact]
+ public void Constructor_SetsFileName()
+ {
+ // Arrange & Act
+ var path = Path.GetFullPath("helllo.txt");
+ var result = new FilePathResult(path, "text/plain");
+
+ // Act & Assert
+ Assert.Equal(path, result.FileName);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_FallsbackToStreamCopy_IfNoIHttpSendFilePresent()
+ {
+ // Arrange
+ var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
+ var result = new FilePathResult(path, "text/plain");
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Response.Body = new MemoryStream();
+
+ var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(context);
+ httpContext.Response.Body.Position = 0;
+
+ // Assert
+ Assert.NotNull(httpContext.Response.Body);
+ var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
+ Assert.Equal("FilePathResultTestFile contents", contents);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent()
+ {
+ // Arrange
+ var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
+ var result = new FilePathResult(path, "text/plain");
+
+ var sendFileMock = new Mock();
+ sendFileMock
+ .Setup(s => s.SendFileAsync(path, 0, null, CancellationToken.None))
+ .Returns(Task.FromResult(0));
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.SetFeature(sendFileMock.Object);
+
+ var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(context);
+
+ // Assert
+ sendFileMock.Verify();
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FileResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FileResultTest.cs
new file mode 100644
index 0000000000..2ae6021964
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FileResultTest.cs
@@ -0,0 +1,240 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.IO;
+using System.Net.Mime;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Http;
+using Microsoft.AspNet.Routing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc
+{
+ public class FileResultTest
+ {
+ [Fact]
+ public void Constructor_SetsContentType()
+ {
+ // Act
+ var result = new EmptyFileResult("text/plain");
+
+ // Assert
+ Assert.Equal("text/plain", result.ContentType);
+ }
+
+ [Fact]
+ public async Task ContentDispositionHeader_IsEncodedCorrectly()
+ {
+ // See comment in FileResult.cs detailing how the FileDownloadName should be encoded.
+
+ // Arrange
+ var httpContext = new Mock();
+ httpContext.SetupSet(c => c.Response.ContentType = "application/my-type").Verifiable();
+ httpContext
+ .Setup(c => c.Response.Headers.Set("Content-Disposition", @"attachment; filename=""some\\file"""))
+ .Verifiable();
+
+ var actionContext = CreateActionContext(httpContext.Object);
+
+ var result = new EmptyFileResult("application/my-type")
+ {
+ FileDownloadName = @"some\file"
+ };
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ Assert.True(result.WasWriteFileCalled);
+ httpContext.Verify();
+ }
+
+ [Fact]
+ public async Task ContentDispositionHeader_IsEncodedCorrectly_ForUnicodeCharacters()
+ {
+ // Arrange
+ var httpContext = new Mock();
+ httpContext.SetupSet(c => c.Response.ContentType = "application/my-type").Verifiable();
+ httpContext
+ .Setup(c => c.Response.Headers.Set(
+ "Content-Disposition",
+ @"attachment; filename*=UTF-8''ABCXYZabcxyz012789!%40%23$%25%5E&%2A%28%29-%3D_+.:~%CE%94"))
+ .Verifiable();
+
+ var actionContext = CreateActionContext(httpContext.Object);
+
+ var result = new EmptyFileResult("application/my-type")
+ {
+ FileDownloadName = "ABCXYZabcxyz012789!@#$%^&*()-=_+.:~Δ"
+ };
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ Assert.True(result.WasWriteFileCalled);
+ httpContext.Verify();
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_DoesNotSetContentDisposition_IfNotSpecified()
+ {
+ // Arrange
+ var httpContext = new Mock(MockBehavior.Strict);
+ httpContext.SetupSet(c => c.Response.ContentType = "application/my-type").Verifiable();
+ httpContext.Setup(c => c.Response.Body).Returns(Stream.Null);
+
+ var actionContext = CreateActionContext(httpContext.Object);
+
+ var result = new EmptyFileResult("application/my-type");
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ Assert.True(result.WasWriteFileCalled);
+ httpContext.Verify();
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_SetsContentDisposition_IfSpecified()
+ {
+ // Arrange
+ var httpContext = new Mock(MockBehavior.Strict);
+ httpContext.SetupSet(c => c.Response.ContentType = "application/my-type").Verifiable();
+ httpContext
+ .Setup(c => c.Response.Headers.Set("Content-Disposition", "attachment; filename=filename.ext"))
+ .Verifiable();
+
+ var actionContext = CreateActionContext(httpContext.Object);
+
+ var result = new EmptyFileResult("application/my-type")
+ {
+ FileDownloadName = "filename.ext"
+ };
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ Assert.True(result.WasWriteFileCalled);
+ httpContext.Verify();
+ }
+
+ public static TheoryData ContentDispositionData
+ {
+ get
+ {
+ return new TheoryData
+ {
+ // Non quoted values
+ { "09aAzZ", "attachment; filename=09aAzZ" },
+ { "a.b", "attachment; filename=a.b" },
+ { "#", "attachment; filename=#" },
+ { "-", "attachment; filename=-" },
+ { "_", "attachment; filename=_" },
+
+ // Values that need to be quoted
+ { ": :", "attachment; filename=\": :\"" },
+ { "~", "attachment; filename=~" },
+ { "$", "attachment; filename=$" },
+ { "&", "attachment; filename=&" },
+ { "+", "attachment; filename=+" },
+ { "(", "attachment; filename=\"(\"" },
+ { ")", "attachment; filename=\")\"" },
+ { "<", "attachment; filename=\"<\"" },
+ { ">", "attachment; filename=\">\"" },
+ { "@", "attachment; filename=\"@\"" },
+ { ",", "attachment; filename=\",\"" },
+ { ";", "attachment; filename=\";\"" },
+ { ":", "attachment; filename=\":\"" },
+ { "/", "attachment; filename=\"/\"" },
+ { "[", "attachment; filename=\"[\"" },
+ { "]", "attachment; filename=\"]\"" },
+ { "?", "attachment; filename=\"?\"" },
+ { "=", "attachment; filename=\"=\"" },
+ { "{", "attachment; filename=\"{\"" },
+ { "}", "attachment; filename=\"}\"" },
+ { " ", "attachment; filename=\" \"" },
+ { "a\tb", "attachment; filename=\"a\tb\"" },
+ { "a b", "attachment; filename=\"a b\"" },
+
+ // Values that need to be escaped
+ { "\"", "attachment; filename=\"\\\"\"" },
+ { "\\", "attachment; filename=\"\\\\\"" },
+
+ // Values that need to be specially encoded (Base64, see rfc2047)
+ { "a\nb", "attachment; filename=\"=?utf-8?B?YQpi?=\"" },
+
+ // Values with non unicode characters
+ { "résumé.txt", "attachment; filename*=UTF-8''r%C3%A9sum%C3%A9.txt" },
+ { "Δ", "attachment; filename*=UTF-8''%CE%94" },
+ { "Δ\t", "attachment; filename*=UTF-8''%CE%94%09" },
+ { "ABCXYZabcxyz012789!@#$%^&*()-=_+.:~Δ", @"attachment; filename*=UTF-8''ABCXYZabcxyz012789!%40%23$%25%5E&%2A%28%29-%3D_+.:~%CE%94" },
+ };
+ }
+ }
+
+ public static TheoryData ContentDispositionControlCharactersData
+ {
+ get
+ {
+ var data = new TheoryData();
+ for (var i = 0; i < 32; i++)
+ {
+ if (i == 10)
+ {
+ // skip \n as it has a special encoding
+ continue;
+ }
+
+ data.Add(char.ConvertFromUtf32(i), "attachment; filename=\"" + char.ConvertFromUtf32(i) + "\"");
+ }
+
+ data.Add(char.ConvertFromUtf32(127), "attachment; filename=\"" + char.ConvertFromUtf32(127) + "\"");
+
+ return data;
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(ContentDispositionData))]
+ [MemberData(nameof(ContentDispositionControlCharactersData))]
+ public void GetHeaderValue_Produces_Correct_ContentDisposition(string input, string expectedOutput)
+ {
+ // Arrange & Act
+ var actual = FileResult.ContentDispositionUtil.GetHeaderValue(input);
+
+ // Assert
+ Assert.Equal(expectedOutput, actual);
+ }
+
+ private static ActionContext CreateActionContext(HttpContext context)
+ {
+ return new ActionContext(context, new RouteData(), new ActionDescriptor());
+ }
+
+ private class EmptyFileResult : FileResult
+ {
+ public bool WasWriteFileCalled;
+
+ public EmptyFileResult()
+ : this(MediaTypeNames.Application.Octet)
+ {
+ }
+
+ public EmptyFileResult(string contentType)
+ : base(contentType)
+ {
+ }
+
+ protected override Task WriteFileAsync(HttpResponse response, CancellationToken cancellation)
+ {
+ WasWriteFileCalled = true;
+ return Task.FromResult(0);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FileStreamResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FileStreamResultTest.cs
new file mode 100644
index 0000000000..3cac078839
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FileStreamResultTest.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNet.PipelineCore;
+using Microsoft.AspNet.Routing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc
+{
+ public class FileStreamResultTest
+ {
+ [Fact]
+ public void Constructor_SetsFileName()
+ {
+ // Arrange
+ var stream = Stream.Null;
+
+ // Act
+ var result = new FileStreamResult(stream, "text/plain");
+
+ // Assert
+ Assert.Equal(stream, result.FileStream);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_WritesResponse_InChunksOfFourKilobytes()
+ {
+ // Arrange
+ var mockReadStream = new Mock();
+ mockReadStream.SetupSequence(s => s.ReadAsync(It.IsAny(), 0, 0x1000, CancellationToken.None))
+ .Returns(Task.FromResult(0x1000))
+ .Returns(Task.FromResult(0x500))
+ .Returns(Task.FromResult(0));
+
+ var mockBodyStream = new Mock();
+ mockBodyStream
+ .Setup(s => s.WriteAsync(It.IsAny(), 0, 0x1000, CancellationToken.None))
+ .Returns(Task.FromResult(0));
+
+ mockBodyStream
+ .Setup(s => s.WriteAsync(It.IsAny(), 0, 0x500, CancellationToken.None))
+ .Returns(Task.FromResult(0));
+
+ var result = new FileStreamResult(mockReadStream.Object, "text/plain");
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Response.Body = mockBodyStream.Object;
+
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ mockReadStream.Verify();
+ mockBodyStream.Verify();
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_CopiesProvidedStream_ToOutputStream()
+ {
+ // Arrange
+ // Generate an array of bytes with a predictable pattern
+ // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, 10, 11, 12, 13
+ var originalBytes = Enumerable.Range(0, 0x1234)
+ .Select(b => (byte)(b % 20)).ToArray();
+
+ var originalStream = new MemoryStream(originalBytes);
+
+ var httpContext = new DefaultHttpContext();
+ var outStream = new MemoryStream();
+ httpContext.Response.Body = outStream;
+
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ var result = new FileStreamResult(originalStream, "text/plain");
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var outBytes = outStream.ToArray();
+ Assert.True(originalBytes.SequenceEqual(outBytes));
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs
index 80cb2c3e0d..2a2768bc25 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs
@@ -2,10 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
+using System.IO;
using System.Linq;
+using System.Text;
using System.Threading.Tasks;
using System.Reflection;
-using System.Text;
+using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Testing;
@@ -13,7 +15,6 @@ using Microsoft.AspNet.Testing;
using Moq;
#endif
using Xunit;
-using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Mvc.Test
{
@@ -368,6 +369,109 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal(expected, resultPermanent.RouteValues);
}
+ [Fact]
+ public void File_WithContents()
+ {
+ // Arrange
+ var controller = new Controller();
+ var fileContents = new byte[0];
+
+ // Act
+ var result = controller.File(fileContents, "someContentType");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(fileContents, result.FileContents);
+ Assert.Equal("someContentType", result.ContentType);
+ Assert.Equal(string.Empty, result.FileDownloadName);
+ }
+
+ [Fact]
+ public void File_WithContentsAndFileDownloadName()
+ {
+ // Arrange
+ var controller = new Controller();
+ var fileContents = new byte[0];
+
+ // Act
+ var result = controller.File(fileContents, "someContentType", "someDownloadName");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(fileContents, result.FileContents);
+ Assert.Equal("someContentType", result.ContentType);
+ Assert.Equal("someDownloadName", result.FileDownloadName);
+ }
+
+ [Fact]
+ public void File_WithPath()
+ {
+ // Arrange
+ var controller = new Controller();
+ var path = Path.GetFullPath("somepath");
+
+ // Act
+ var result = controller.File(path, "someContentType");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(path, result.FileName);
+ Assert.Equal("someContentType", result.ContentType);
+ Assert.Equal(string.Empty, result.FileDownloadName);
+ }
+
+ [Fact]
+ public void File_WithPathAndFileDownloadName()
+ {
+ // Arrange
+ var controller = new Controller();
+ var path = Path.GetFullPath("somepath");
+
+ // Act
+ var result = controller.File(path, "someContentType", "someDownloadName");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(path, result.FileName);
+ Assert.Equal("someContentType", result.ContentType);
+ Assert.Equal("someDownloadName", result.FileDownloadName);
+ }
+
+ [Fact]
+ public void File_WithStream()
+ {
+ // Arrange
+ var controller = new Controller();
+ var fileStream = Stream.Null;
+
+ // Act
+ var result = controller.File(fileStream, "someContentType");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(fileStream, result.FileStream);
+ Assert.Equal("someContentType", result.ContentType);
+ Assert.Equal(string.Empty, result.FileDownloadName);
+ }
+
+ [Fact]
+ public void File_WithStreamAndFileDownloadName()
+ {
+ // Arrange
+ var controller = new Controller();
+ var fileStream = Stream.Null;
+
+ // Act
+ var result = controller.File(fileStream, "someContentType", "someDownloadName");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(fileStream, result.FileStream);
+ Assert.Equal("someContentType", result.ContentType);
+ Assert.Equal("someDownloadName", result.FileDownloadName);
+ }
+
+
[Fact]
public void HttpNotFound_SetsStatusCode()
{
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/FilePathResultTestFile.txt b/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/FilePathResultTestFile.txt
new file mode 100644
index 0000000000..1ae10032bf
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/FilePathResultTestFile.txt
@@ -0,0 +1 @@
+FilePathResultTestFile contents
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/FileResultTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/FileResultTests.cs
new file mode 100644
index 0000000000..23fc3317f4
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/FileResultTests.cs
@@ -0,0 +1,156 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Builder;
+using Microsoft.AspNet.TestHost;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.FunctionalTests
+{
+ public class FileResultTests
+ {
+ private readonly IServiceProvider _services = TestHelper.CreateServices("FilesWebSite");
+ private readonly Action _app = new FilesWebSite.Startup().Configure;
+
+ [Fact]
+ public async Task FileFromDisk_CanBeEnabled_WithMiddleware()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ // Act
+ var response = await client.GetAsync("http://localhost/DownloadFiles/DowloadFromDisk");
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
+
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.NotNull(body);
+ Assert.Equal("This is a sample text file", body);
+ }
+
+ [Fact]
+ public async Task FileFromDisk_ReturnsFileWithFileName()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ // Act
+ var response = await client.GetAsync("http://localhost/DownloadFiles/DowloadFromDiskWithFileName");
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
+
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.NotNull(body);
+ Assert.Equal("This is a sample text file", body);
+
+ var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
+ Assert.NotNull(contentDisposition);
+ Assert.Equal("attachment; filename=downloadName.txt", contentDisposition);
+ }
+
+ [Fact]
+ public async Task FileFromStream_ReturnsFile()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ // Act
+ var response = await client.GetAsync("http://localhost/DownloadFiles/DowloadFromStream");
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
+
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.NotNull(body);
+ Assert.Equal("This is sample text from a stream", body);
+ }
+
+ [Fact]
+ public async Task FileFromStream_ReturnsFileWithFileName()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ // Act
+ var response = await client.GetAsync("http://localhost/DownloadFiles/DowloadFromStreamWithFileName");
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
+
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.NotNull(body);
+ Assert.Equal("This is sample text from a stream", body);
+
+ var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
+ Assert.NotNull(contentDisposition);
+ Assert.Equal("attachment; filename=downloadName.txt", contentDisposition);
+ }
+
+ [Fact]
+ public async Task FileFromBinaryData_ReturnsFile()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ // Act
+ var response = await client.GetAsync("http://localhost/DownloadFiles/DowloadFromBinaryData");
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
+
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.NotNull(body);
+ Assert.Equal("This is a sample text from a binary array", body);
+ }
+
+ [Fact]
+ public async Task FileFromBinaryData_ReturnsFileWithFileName()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ // Act
+ var response = await client.GetAsync("http://localhost/DownloadFiles/DowloadFromBinaryDataWithFileName");
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
+
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.NotNull(body);
+ Assert.Equal("This is a sample text from a binary array", body);
+
+ var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
+ Assert.NotNull(contentDisposition);
+ Assert.Equal("attachment; filename=downloadName.txt", contentDisposition);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json
index 8acc20ce70..526e766a55 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json
@@ -10,6 +10,7 @@
"BasicWebSite": "",
"CompositeViewEngine": "",
"ConnegWebsite": "",
+ "FilesWebSite": "",
"FiltersWebSite": "",
"FormatterWebSite": "",
"InlineConstraintsWebSite": "",
diff --git a/test/WebSites/FilesWebSite/Controllers/DowloadFilesController.cs b/test/WebSites/FilesWebSite/Controllers/DowloadFilesController.cs
new file mode 100644
index 0000000000..e37af6c0cd
--- /dev/null
+++ b/test/WebSites/FilesWebSite/Controllers/DowloadFilesController.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.IO;
+using System.Text;
+using Microsoft.AspNet.Mvc;
+using Microsoft.Framework.Runtime;
+
+namespace FilesWebSite
+{
+ public class DownloadFilesController : Controller
+ {
+ private readonly IApplicationEnvironment _appEnvironment;
+
+ public DownloadFilesController(IApplicationEnvironment appEnvironment)
+ {
+ _appEnvironment = appEnvironment;
+ }
+
+ public IActionResult DowloadFromDisk()
+ {
+ var path = Path.Combine(_appEnvironment.ApplicationBasePath, "sample.txt");
+ return File(path, "text/plain");
+ }
+
+ public IActionResult DowloadFromDiskWithFileName()
+ {
+ var path = Path.Combine(_appEnvironment.ApplicationBasePath, "sample.txt");
+ return File(path, "text/plain", "downloadName.txt");
+ }
+
+ public IActionResult DowloadFromStream()
+ {
+ var stream = new MemoryStream();
+ var writer = new StreamWriter(stream);
+ writer.Write("This is sample text from a stream");
+ writer.Flush();
+ stream.Seek(0, SeekOrigin.Begin);
+
+ return File(stream, "text/plain");
+ }
+
+ public IActionResult DowloadFromStreamWithFileName()
+ {
+ var stream = new MemoryStream();
+ var writer = new StreamWriter(stream);
+ writer.Write("This is sample text from a stream");
+ writer.Flush();
+ stream.Seek(0, SeekOrigin.Begin);
+
+ return File(stream, "text/plain", "downloadName.txt");
+ }
+
+ public IActionResult DowloadFromBinaryData()
+ {
+ var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
+ return File(data, "text/plain");
+ }
+
+ public IActionResult DowloadFromBinaryDataWithFileName()
+ {
+ var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
+ return File(data, "text/plain", "downloadName.txt");
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/FilesWebSite/FilesWebSite.kproj b/test/WebSites/FilesWebSite/FilesWebSite.kproj
new file mode 100644
index 0000000000..3b17a2f4d7
--- /dev/null
+++ b/test/WebSites/FilesWebSite/FilesWebSite.kproj
@@ -0,0 +1,30 @@
+
+
+
+ 12.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+ Debug
+ AnyCPU
+
+
+
+ 0ef9860b-10d7-452f-b0f4-a405b88bebb3
+ Web
+ FilesWebSite
+
+
+ ConsoleDebugger
+
+
+ WebDebugger
+
+
+
+
+ 2.0
+ 18002
+
+
+
\ No newline at end of file
diff --git a/test/WebSites/FilesWebSite/SendFileMiddleware.cs b/test/WebSites/FilesWebSite/SendFileMiddleware.cs
new file mode 100644
index 0000000000..62e6beb295
--- /dev/null
+++ b/test/WebSites/FilesWebSite/SendFileMiddleware.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Framework.Runtime;
+using Microsoft.AspNet.Builder;
+using Microsoft.AspNet.Http;
+using Microsoft.AspNet.HttpFeature;
+
+namespace FilesWebSite
+{
+ public class SendFileMiddleware
+ {
+ private const int DefaultBufferSize = 0x1000;
+
+ private readonly RequestDelegate _next;
+
+ public SendFileMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ public async Task Invoke(HttpContext context)
+ {
+ var environment = (IApplicationEnvironment)context.RequestServices.GetService(typeof(IApplicationEnvironment));
+
+ if (context.GetFeature() == null)
+ {
+ var sendFile = new SendFileFallBack(context.Response.Body, environment.ApplicationBasePath);
+ context.SetFeature(sendFile);
+ }
+
+ await _next(context);
+ }
+
+ private class SendFileFallBack : IHttpSendFileFeature
+ {
+ private readonly string _appBasePath;
+ private Stream _responseStream;
+
+ public SendFileFallBack(Stream responseStream, string appBasePath)
+ {
+ _responseStream = responseStream;
+ _appBasePath = appBasePath;
+ }
+
+ public async Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
+ {
+ using (var stream = new FileStream(Path.Combine(_appBasePath, path), FileMode.Open))
+ {
+ length = length ?? stream.Length - offset;
+
+ stream.Seek(offset, SeekOrigin.Begin);
+
+ var bufferSize = length < DefaultBufferSize ? length.Value : DefaultBufferSize;
+ var buffer = new byte[bufferSize];
+ var bytesRead = 0;
+
+ do
+ {
+ var bytesToRead = bufferSize < length ? bufferSize : length;
+ bytesRead = await stream.ReadAsync(buffer, 0, (int)bytesToRead);
+ length = length - bytesRead;
+
+ await _responseStream.WriteAsync(buffer, 0, bytesRead);
+ } while (bytesRead > 0 && length > 0);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/FilesWebSite/Startup.cs b/test/WebSites/FilesWebSite/Startup.cs
new file mode 100644
index 0000000000..59c706ca1a
--- /dev/null
+++ b/test/WebSites/FilesWebSite/Startup.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNet.Builder;
+using Microsoft.AspNet.Routing;
+using Microsoft.Framework.DependencyInjection;
+
+namespace FilesWebSite
+{
+ public class Startup
+ {
+ public void Configure(IApplicationBuilder app)
+ {
+ var configuration = app.GetTestConfiguration();
+
+ app.UseServices(services =>
+ {
+ services.AddMvc(configuration);
+ });
+
+ app.UseMiddleware();
+
+ app.UseMvc(routes =>
+ {
+ routes.MapRoute(name: null, template: "{controller}/{action}", defaults: null);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/FilesWebSite/project.json b/test/WebSites/FilesWebSite/project.json
new file mode 100644
index 0000000000..1d2b4c1309
--- /dev/null
+++ b/test/WebSites/FilesWebSite/project.json
@@ -0,0 +1,11 @@
+{
+ "dependencies": {
+ "Microsoft.AspNet.Mvc": "",
+ "Microsoft.AspNet.Mvc.TestConfiguration": "",
+ "Microsoft.AspNet.Server.IIS": "1.0.0-*"
+ },
+ "frameworks": {
+ "aspnet50": { },
+ "aspnetcore50": { }
+ }
+}
diff --git a/test/WebSites/FilesWebSite/sample.txt b/test/WebSites/FilesWebSite/sample.txt
new file mode 100644
index 0000000000..a34f4c4be3
--- /dev/null
+++ b/test/WebSites/FilesWebSite/sample.txt
@@ -0,0 +1 @@
+This is a sample text file
\ No newline at end of file