diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs
index 3a6469783e..457d88393b 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs
@@ -1135,8 +1135,10 @@ namespace Microsoft.AspNetCore.Mvc
}
///
- /// Returns a file with the specified as content
- /// () and the specified as the Content-Type.
+ /// Returns a file with the specified as content (),
+ /// and the specified as the Content-Type.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
///
/// The file contents.
/// The Content-Type of the file.
@@ -1149,8 +1151,9 @@ namespace Microsoft.AspNetCore.Mvc
///
/// Returns a file with the specified as content (), the
- /// specified as the Content-Type and the
- /// specified as the suggested file name.
+ /// specified as the Content-Type and the specified as the suggested file name.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
///
/// The file contents.
/// The Content-Type of the file.
@@ -1163,8 +1166,54 @@ namespace Microsoft.AspNetCore.Mvc
}
///
- /// Returns a file in the specified ()
- /// with the specified as the Content-Type.
+ /// Returns a file with the specified as content (),
+ /// and the specified as the Content-Type.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
+ ///
+ /// The file contents.
+ /// The Content-Type of the file.
+ /// The of when the file was last modified.
+ /// The associated with the file.
+ /// The created for the response.
+ [NonAction]
+ public virtual FileContentResult File(byte[] fileContents, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+ {
+ return new FileContentResult(fileContents, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ };
+ }
+
+ ///
+ /// Returns a file with the specified as content (), the
+ /// specified as the Content-Type, and the specified as the suggested file name.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
+ ///
+ /// The file contents.
+ /// The Content-Type of the file.
+ /// The suggested file name.
+ /// The of when the file was last modified.
+ /// The associated with the file.
+ /// The created for the response.
+ [NonAction]
+ public virtual FileContentResult File(byte[] fileContents, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+ {
+ return new FileContentResult(fileContents, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ FileDownloadName = fileDownloadName,
+ };
+ }
+
+ ///
+ /// Returns a file in the specified (), with the
+ /// specified as the Content-Type.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
///
/// The with the contents of the file.
/// The Content-Type of the file.
@@ -1179,6 +1228,8 @@ namespace Microsoft.AspNetCore.Mvc
/// Returns a file in the specified () with the
/// specified as the Content-Type and the
/// specified as the suggested file name.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
///
/// The with the contents of the file.
/// The Content-Type of the file.
@@ -1190,9 +1241,55 @@ namespace Microsoft.AspNetCore.Mvc
return new FileStreamResult(fileStream, contentType) { FileDownloadName = fileDownloadName };
}
+ ///
+ /// Returns a file in the specified (),
+ /// and the specified as the Content-Type.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
+ ///
+ /// The with the contents of the file.
+ /// The Content-Type of the file.
+ /// The of when the file was last modified.
+ /// The associated with the file.
+ /// The created for the response.
+ [NonAction]
+ public virtual FileStreamResult File(Stream fileStream, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+ {
+ return new FileStreamResult(fileStream, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ };
+ }
+
+ ///
+ /// Returns a file in the specified (), the
+ /// specified as the Content-Type, and the specified as the suggested file name.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
+ ///
+ /// The with the contents of the file.
+ /// The Content-Type of the file.
+ /// The suggested file name.
+ /// The of when the file was last modified.
+ /// The associated with the file.
+ /// The created for the response.
+ [NonAction]
+ public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+ {
+ return new FileStreamResult(fileStream, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ FileDownloadName = fileDownloadName,
+ };
+ }
+
///
/// Returns the file specified by () with the
/// specified as the Content-Type.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
///
/// The virtual path of the file to be returned.
/// The Content-Type of the file.
@@ -1207,6 +1304,8 @@ namespace Microsoft.AspNetCore.Mvc
/// Returns the file specified by () with the
/// specified as the Content-Type and the
/// specified as the suggested file name.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
///
/// The virtual path of the file to be returned.
/// The Content-Type of the file.
@@ -1218,9 +1317,55 @@ namespace Microsoft.AspNetCore.Mvc
return new VirtualFileResult(virtualPath, contentType) { FileDownloadName = fileDownloadName };
}
+ ///
+ /// Returns the file specified by (), and the
+ /// specified as the Content-Type.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
+ ///
+ /// The virtual path of the file to be returned.
+ /// The Content-Type of the file.
+ /// The of when the file was last modified.
+ /// The associated with the file.
+ /// The created for the response.
+ [NonAction]
+ public virtual VirtualFileResult File(string virtualPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+ {
+ return new VirtualFileResult(virtualPath, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ };
+ }
+
+ ///
+ /// Returns the file specified by (), the
+ /// specified as the Content-Type, and the specified as the suggested file name.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
+ ///
+ /// The virtual path of the file to be returned.
+ /// The Content-Type of the file.
+ /// The suggested file name.
+ /// The of when the file was last modified.
+ /// The associated with the file.
+ /// The created for the response.
+ [NonAction]
+ public virtual VirtualFileResult File(string virtualPath, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+ {
+ return new VirtualFileResult(virtualPath, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ FileDownloadName = fileDownloadName,
+ };
+ }
+
///
/// Returns the file specified by () with the
/// specified as the Content-Type.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
///
/// The physical path of the file to be returned.
/// The Content-Type of the file.
@@ -1235,6 +1380,8 @@ namespace Microsoft.AspNetCore.Mvc
/// Returns the file specified by () with the
/// specified as the Content-Type and the
/// specified as the suggested file name.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
///
/// The physical path of the file to be returned.
/// The Content-Type of the file.
@@ -1249,6 +1396,50 @@ namespace Microsoft.AspNetCore.Mvc
return new PhysicalFileResult(physicalPath, contentType) { FileDownloadName = fileDownloadName };
}
+ ///
+ /// Returns the file specified by (), and
+ /// the specified as the Content-Type.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
+ ///
+ /// The physical path of the file to be returned.
+ /// The Content-Type of the file.
+ /// The of when the file was last modified.
+ /// The associated with the file.
+ /// The created for the response.
+ [NonAction]
+ public virtual PhysicalFileResult PhysicalFile(string physicalPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+ {
+ return new PhysicalFileResult(physicalPath, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ };
+ }
+
+ ///
+ /// Returns the file specified by (), the
+ /// specified as the Content-Type, and the specified as the suggested file name.
+ /// This supports range requests ( or
+ /// if the range is not satisfiable).
+ ///
+ /// The physical path of the file to be returned.
+ /// The Content-Type of the file.
+ /// The suggested file name.
+ /// The of when the file was last modified.
+ /// The associated with the file.
+ /// The created for the response.
+ [NonAction]
+ public virtual PhysicalFileResult PhysicalFile(string physicalPath, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+ {
+ return new PhysicalFileResult(physicalPath, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ FileDownloadName = fileDownloadName,
+ };
+ }
+
///
/// Creates an that produces an response.
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/FileContentResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/FileContentResult.cs
index 861909e3e2..30c5a5d213 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/FileContentResult.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/FileContentResult.cs
@@ -3,8 +3,6 @@
using System;
using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Net.Http.Headers;
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/FileResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/FileResult.cs
index bec8d8ec59..dea1dff3e3 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/FileResult.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/FileResult.cs
@@ -2,8 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
+using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc
{
@@ -43,5 +42,15 @@ namespace Microsoft.AspNetCore.Mvc
get { return _fileDownloadName ?? string.Empty; }
set { _fileDownloadName = value; }
}
+
+ ///
+ /// Gets or sets the last modified information associated with the .
+ ///
+ public DateTimeOffset? LastModified { get; set; }
+
+ ///
+ /// Gets or sets the etag associated with the .
+ ///
+ public EntityTagHeaderValue EntityTag { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileContentResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileContentResultExecutor.cs
index 5fd8d05654..9d26a364a2 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileContentResultExecutor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileContentResultExecutor.cs
@@ -2,8 +2,10 @@
// 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.Tasks;
using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.Internal
{
@@ -26,11 +28,22 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
- SetHeadersAndLog(context, result);
- return WriteFileAsync(context, result);
+ var (range, rangeLength, serveBody) = SetHeadersAndLog(
+ context,
+ result,
+ result.FileContents.Length,
+ result.LastModified,
+ result.EntityTag);
+
+ if (!serveBody)
+ {
+ return Task.CompletedTask;
+ }
+
+ return WriteFileAsync(context, result, range, rangeLength);
}
- protected virtual Task WriteFileAsync(ActionContext context, FileContentResult result)
+ protected virtual Task WriteFileAsync(ActionContext context, FileContentResult result, RangeItemHeaderValue range, long rangeLength)
{
if (context == null)
{
@@ -42,9 +55,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
- var response = context.HttpContext.Response;
+ if (range != null && rangeLength == 0)
+ {
+ return Task.CompletedTask;
+ }
- return response.Body.WriteAsync(result.FileContents, offset: 0, count: result.FileContents.Length);
+ var response = context.HttpContext.Response;
+ var outputStream = response.Body;
+ var fileContentsStream = new MemoryStream(result.FileContents);
+ return WriteFileAsync(context.HttpContext, fileContentsStream, range, rangeLength);
}
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileResultExecutorBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileResultExecutorBase.cs
index c6b2e3f148..45f89fb5a2 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileResultExecutorBase.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileResultExecutorBase.cs
@@ -2,6 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.AspNetCore.Http.Headers;
+using Microsoft.AspNetCore.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
@@ -9,20 +17,38 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
public class FileResultExecutorBase
{
+ private const string AcceptRangeHeaderValue = "bytes";
+
+ // default buffer size as defined in BufferedStream type
+ protected const int BufferSize = 0x1000;
+
public FileResultExecutorBase(ILogger logger)
{
Logger = logger;
}
+ internal enum PreconditionState
+ {
+ Unspecified,
+ NotModified,
+ ShouldProcess,
+ PreconditionFailed,
+ IgnoreRangeRequest
+ }
+
protected ILogger Logger { get; }
- protected virtual void SetHeadersAndLog(ActionContext context, FileResult result)
+ protected virtual (RangeItemHeaderValue range, long rangeLength, bool serveBody) SetHeadersAndLog(
+ ActionContext context,
+ FileResult result, long?
+ fileLength, DateTimeOffset?
+ lastModified = null,
+ EntityTagHeaderValue etag = null)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
-
if (result == null)
{
throw new ArgumentNullException(nameof(result));
@@ -31,9 +57,56 @@ namespace Microsoft.AspNetCore.Mvc.Internal
SetContentType(context, result);
SetContentDispositionHeader(context, result);
Logger.FileResultExecuting(result.FileDownloadName);
+ if (fileLength.HasValue)
+ {
+ SetAcceptRangeHeader(context);
+ }
+
+ var request = context.HttpContext.Request;
+ var httpRequestHeaders = request.GetTypedHeaders();
+ var response = context.HttpContext.Response;
+ var httpResponseHeaders = response.GetTypedHeaders();
+ if (lastModified.HasValue)
+ {
+ httpResponseHeaders.LastModified = lastModified;
+ }
+ if (etag != null)
+ {
+ httpResponseHeaders.ETag = etag;
+ }
+
+ var serveBody = !HttpMethods.IsHead(request.Method);
+ if (HttpMethods.IsHead(request.Method) || HttpMethods.IsGet(request.Method))
+ {
+ var preconditionState = GetPreconditionState(context, httpRequestHeaders, lastModified, etag);
+ if (request.Headers.ContainsKey(HeaderNames.Range) &&
+ (preconditionState == PreconditionState.Unspecified ||
+ preconditionState == PreconditionState.ShouldProcess))
+ {
+ return SetRangeHeaders(context, httpRequestHeaders, fileLength, lastModified, etag);
+ }
+ if (preconditionState == PreconditionState.NotModified)
+ {
+ serveBody = false;
+ response.StatusCode = StatusCodes.Status304NotModified;
+ }
+ else if (preconditionState == PreconditionState.PreconditionFailed)
+ {
+ serveBody = false;
+ response.StatusCode = StatusCodes.Status412PreconditionFailed;
+ }
+ }
+
+ return (range: null, rangeLength: 0, serveBody: serveBody);
}
- private void SetContentDispositionHeader(ActionContext context, FileResult result)
+ private static void SetContentType(ActionContext context, FileResult result)
+ {
+ var response = context.HttpContext.Response;
+ response.ContentType = result.ContentType;
+ }
+
+ private static void SetContentDispositionHeader(ActionContext context, FileResult result)
{
if (!string.IsNullOrEmpty(result.FileDownloadName))
{
@@ -48,10 +121,199 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
- private void SetContentType(ActionContext context, FileResult result)
+ private static void SetAcceptRangeHeader(ActionContext context)
{
var response = context.HttpContext.Response;
- response.ContentType = result.ContentType;
+ response.Headers[HeaderNames.AcceptRanges] = AcceptRangeHeaderValue;
+ }
+
+ private static PreconditionState GetEtagMatchState(
+ IList etagHeader,
+ EntityTagHeaderValue etag,
+ PreconditionState matchFoundState,
+ PreconditionState matchNotFoundState)
+ {
+ if (etagHeader != null && etagHeader.Any())
+ {
+ var state = matchNotFoundState;
+ foreach (var entityTag in etagHeader)
+ {
+ if (entityTag.Equals(EntityTagHeaderValue.Any) || entityTag.Compare(etag, useStrongComparison: true))
+ {
+ state = matchFoundState;
+ break;
+ }
+ }
+
+ return state;
+ }
+
+ return PreconditionState.Unspecified;
+ }
+
+ // Internal for testing
+ internal static PreconditionState GetPreconditionState(
+ ActionContext context,
+ RequestHeaders httpRequestHeaders,
+ DateTimeOffset? lastModified = null,
+ EntityTagHeaderValue etag = null)
+ {
+ var ifMatchState = PreconditionState.Unspecified;
+ var ifNoneMatchState = PreconditionState.Unspecified;
+ var ifModifiedSinceState = PreconditionState.Unspecified;
+ var ifUnmodifiedSinceState = PreconditionState.Unspecified;
+ var ifRangeState = PreconditionState.Unspecified;
+
+ // 14.24 If-Match
+ var ifMatch = httpRequestHeaders.IfMatch;
+ if (etag != null)
+ {
+ ifMatchState = GetEtagMatchState(
+ etagHeader: ifMatch,
+ etag: etag,
+ matchFoundState: PreconditionState.ShouldProcess,
+ matchNotFoundState: PreconditionState.PreconditionFailed);
+ }
+
+ // 14.26 If-None-Match
+ var ifNoneMatch = httpRequestHeaders.IfNoneMatch;
+ if (etag != null)
+ {
+ ifNoneMatchState = GetEtagMatchState(
+ etagHeader: ifNoneMatch,
+ etag: etag,
+ matchFoundState: PreconditionState.NotModified,
+ matchNotFoundState: PreconditionState.ShouldProcess);
+ }
+
+ var now = DateTimeOffset.UtcNow;
+
+ // 14.25 If-Modified-Since
+ var ifModifiedSince = httpRequestHeaders.IfModifiedSince;
+ if (lastModified.HasValue && ifModifiedSince.HasValue && ifModifiedSince <= now)
+ {
+ var modified = ifModifiedSince < lastModified;
+ ifModifiedSinceState = modified ? PreconditionState.ShouldProcess : PreconditionState.NotModified;
+ }
+
+ // 14.28 If-Unmodified-Since
+ var ifUnmodifiedSince = httpRequestHeaders.IfUnmodifiedSince;
+ if (lastModified.HasValue && ifUnmodifiedSince.HasValue && ifUnmodifiedSince <= now)
+ {
+ var unmodified = ifUnmodifiedSince >= lastModified;
+ ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed;
+ }
+
+ var ifRange = httpRequestHeaders.IfRange;
+ if (ifRange != null)
+ {
+ // If the validator given in the If-Range header field matches the
+ // current validator for the selected representation of the target
+ // resource, then the server SHOULD process the Range header field as
+ // requested. If the validator does not match, the server MUST ignore
+ // the Range header field.
+ if (ifRange.LastModified.HasValue)
+ {
+ if (lastModified.HasValue && lastModified > ifRange.LastModified)
+ {
+ ifRangeState = PreconditionState.IgnoreRangeRequest;
+ }
+ }
+ else if (etag != null && ifRange.EntityTag != null && !ifRange.EntityTag.Compare(etag, useStrongComparison: true))
+ {
+ ifRangeState = PreconditionState.IgnoreRangeRequest;
+ }
+ }
+
+ var state = GetMaxPreconditionState(ifMatchState, ifNoneMatchState, ifModifiedSinceState, ifUnmodifiedSinceState, ifRangeState);
+ return state;
+ }
+
+ private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states)
+ {
+ var max = PreconditionState.Unspecified;
+ for (var i = 0; i < states.Length; i++)
+ {
+ if (states[i] > max)
+ {
+ max = states[i];
+ }
+ }
+
+ return max;
+ }
+
+ private static (RangeItemHeaderValue range, long rangeLength, bool serveBody) SetRangeHeaders(
+ ActionContext context,
+ RequestHeaders httpRequestHeaders,
+ long? fileLength,
+ DateTimeOffset? lastModified = null,
+ EntityTagHeaderValue etag = null)
+ {
+ var response = context.HttpContext.Response;
+ var httpResponseHeaders = response.GetTypedHeaders();
+
+ // Checked for presence of Range header explicitly before calling this method.
+ // Range may be null for parsing errors, multiple ranges and when the file length is missing.
+ var range = fileLength.HasValue ? ParseRange(context, httpRequestHeaders, fileLength.Value, lastModified, etag) : null;
+ if (range == null)
+ {
+ // 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable)
+ // SHOULD include a Content-Range field with a byte-range-resp-spec of "*". The instance-length specifies
+ // the current length of the selected resource. e.g. */length
+ response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
+ if (fileLength.HasValue)
+ {
+ httpResponseHeaders.ContentRange = new ContentRangeHeaderValue(fileLength.Value);
+ }
+
+ return (range: null, rangeLength: 0, serveBody: false);
+ }
+
+ httpResponseHeaders.ContentRange = new ContentRangeHeaderValue(
+ range.From.Value,
+ range.To.Value,
+ fileLength.Value);
+
+ response.StatusCode = StatusCodes.Status206PartialContent;
+ var rangeLength = SetContentLength(context, range);
+ return (range, rangeLength, serveBody: true);
+ }
+
+ private static long SetContentLength(ActionContext context, RangeItemHeaderValue range)
+ {
+ var start = range.From.Value;
+ var end = range.To.Value;
+ var length = end - start + 1;
+ var response = context.HttpContext.Response;
+ response.ContentLength = length;
+ return length;
+ }
+
+ private static RangeItemHeaderValue ParseRange(
+ ActionContext context,
+ RequestHeaders httpRequestHeaders,
+ long fileLength,
+ DateTimeOffset? lastModified = null,
+ EntityTagHeaderValue etag = null)
+ {
+ var httpContext = context.HttpContext;
+ var response = httpContext.Response;
+
+ var range = RangeHelper.ParseRange(httpContext, httpRequestHeaders, lastModified, etag);
+
+ if (range != null)
+ {
+ var normalizedRanges = RangeHelper.NormalizeRanges(range, fileLength);
+ if (normalizedRanges == null || normalizedRanges.Count == 0)
+ {
+ return null;
+ }
+
+ return normalizedRanges.Single();
+ }
+
+ return null;
}
protected static ILogger CreateLogger(ILoggerFactory factory)
@@ -63,5 +325,31 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return factory.CreateLogger();
}
+
+ protected static async Task WriteFileAsync(HttpContext context, Stream fileStream, RangeItemHeaderValue range, long rangeLength)
+ {
+ var outputStream = context.Response.Body;
+ using (fileStream)
+ {
+ try
+ {
+ if (range == null)
+ {
+ await StreamCopyOperation.CopyToAsync(fileStream, outputStream, count: null, bufferSize: BufferSize, cancel: context.RequestAborted);
+ }
+ else
+ {
+ fileStream.Seek(range.From.Value, SeekOrigin.Begin);
+ await StreamCopyOperation.CopyToAsync(fileStream, outputStream, rangeLength, BufferSize, context.RequestAborted);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't throw this exception, it's most likely caused by the client disconnecting.
+ // However, if it was cancelled for any other reason we need to prevent empty responses.
+ context.Abort();
+ }
+ }
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileStreamResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileStreamResultExecutor.cs
index 76bbde3594..5912d73190 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileStreamResultExecutor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileStreamResultExecutor.cs
@@ -4,14 +4,12 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class FileStreamResultExecutor : FileResultExecutorBase
{
- // default buffer size as defined in BufferedStream type
- private const int BufferSize = 0x1000;
-
public FileStreamResultExecutor(ILoggerFactory loggerFactory)
: base(CreateLogger(loggerFactory))
{
@@ -29,11 +27,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
- SetHeadersAndLog(context, result);
- return WriteFileAsync(context, result);
+ long? fileLength = null;
+ if (result.FileStream.CanSeek)
+ {
+ fileLength = result.FileStream.Length;
+ }
+
+ var (range, rangeLength, serveBody) = SetHeadersAndLog(
+ context,
+ result,
+ fileLength,
+ result.LastModified,
+ result.EntityTag);
+
+ if (!serveBody)
+ {
+ return Task.CompletedTask;
+ }
+
+ return WriteFileAsync(context, result, range, rangeLength);
}
- protected virtual async Task WriteFileAsync(ActionContext context, FileStreamResult result)
+ protected virtual Task WriteFileAsync(ActionContext context, FileStreamResult result, RangeItemHeaderValue range, long rangeLength)
{
if (context == null)
{
@@ -45,13 +60,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
+ if (range != null && rangeLength == 0)
+ {
+ return Task.CompletedTask;
+ }
+
var response = context.HttpContext.Response;
var outputStream = response.Body;
-
- using (result.FileStream)
- {
- await result.FileStream.CopyToAsync(outputStream, BufferSize);
- }
+ return WriteFileAsync(context.HttpContext, result.FileStream, range, rangeLength);
}
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/PhysicalFileResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/PhysicalFileResultExecutor.cs
index c835790048..889630c68e 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/PhysicalFileResultExecutor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/PhysicalFileResultExecutor.cs
@@ -8,13 +8,12 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class PhysicalFileResultExecutor : FileResultExecutorBase
{
- private const int DefaultBufferSize = 0x1000;
-
public PhysicalFileResultExecutor(ILoggerFactory loggerFactory)
: base(CreateLogger(loggerFactory))
{
@@ -32,11 +31,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
- SetHeadersAndLog(context, result);
- return WriteFileAsync(context, result);
+ var fileInfo = GetFileInfo(result.FileName);
+ if (!fileInfo.Exists)
+ {
+ throw new FileNotFoundException(
+ Resources.FormatFileResult_InvalidPath(result.FileName), result.FileName);
+ }
+
+ var lastModified = result.LastModified ?? fileInfo.LastModified;
+ var (range, rangeLength, serveBody) = SetHeadersAndLog(
+ context,
+ result,
+ fileInfo.Length,
+ lastModified,
+ result.EntityTag);
+
+ if (serveBody)
+ {
+ return WriteFileAsync(context, result, range, rangeLength);
+ }
+
+ return Task.CompletedTask;
}
- protected virtual async Task WriteFileAsync(ActionContext context, PhysicalFileResult result)
+ protected virtual Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue range, long rangeLength)
{
if (context == null)
{
@@ -48,8 +66,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
- var response = context.HttpContext.Response;
+ if (range != null && rangeLength == 0)
+ {
+ return Task.CompletedTask;
+ }
+ var response = context.HttpContext.Response;
if (!Path.IsPathRooted(result.FileName))
{
throw new NotSupportedException(Resources.FormatFileResult_PathNotRooted(result.FileName));
@@ -58,21 +80,23 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var sendFile = response.HttpContext.Features.Get();
if (sendFile != null)
{
- await sendFile.SendFileAsync(
+ if (range != null)
+ {
+ return sendFile.SendFileAsync(
+ result.FileName,
+ offset: range.From ?? 0L,
+ count: rangeLength,
+ cancellation: default(CancellationToken));
+ }
+
+ return sendFile.SendFileAsync(
result.FileName,
offset: 0,
count: null,
cancellation: default(CancellationToken));
}
- else
- {
- var fileStream = GetFileStream(result.FileName);
- using (fileStream)
- {
- await fileStream.CopyToAsync(response.Body, DefaultBufferSize);
- }
- }
+ return WriteFileAsync(context.HttpContext, GetFileStream(result.FileName), range, rangeLength);
}
protected virtual Stream GetFileStream(string path)
@@ -87,8 +111,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
- DefaultBufferSize,
+ BufferSize,
FileOptions.Asynchronous | FileOptions.SequentialScan);
}
+
+ protected virtual FileMetadata GetFileInfo(string path)
+ {
+ var fileInfo = new FileInfo(path);
+ return new FileMetadata
+ {
+ Exists = fileInfo.Exists,
+ Length = fileInfo.Length,
+ LastModified = fileInfo.LastWriteTimeUtc,
+ };
+ }
+
+ protected class FileMetadata
+ {
+ public bool Exists { get; set; }
+
+ public long Length { get; set; }
+
+ public DateTimeOffset LastModified { get; set; }
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/VirtualFileResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/VirtualFileResultExecutor.cs
index 770a933b9b..825de2686d 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/VirtualFileResultExecutor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/VirtualFileResultExecutor.cs
@@ -10,12 +10,12 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class VirtualFileResultExecutor : FileResultExecutorBase
{
- private const int DefaultBufferSize = 0x1000;
private readonly IHostingEnvironment _hostingEnvironment;
public VirtualFileResultExecutor(ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment)
@@ -41,11 +41,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
- SetHeadersAndLog(context, result);
- return WriteFileAsync(context, result);
+ var fileInfo = GetFileInformation(result);
+ if (!fileInfo.Exists)
+ {
+ throw new FileNotFoundException(
+ Resources.FormatFileResult_InvalidPath(result.FileName), result.FileName);
+ }
+
+ var lastModified = result.LastModified ?? fileInfo.LastModified;
+ var (range, rangeLength, serveBody) = SetHeadersAndLog(
+ context,
+ result,
+ fileInfo.Length,
+ lastModified,
+ result.EntityTag);
+
+ if (serveBody)
+ {
+ return WriteFileAsync(context, result, fileInfo, range, rangeLength);
+ }
+
+ return Task.CompletedTask;
}
- protected virtual async Task WriteFileAsync(ActionContext context, VirtualFileResult result)
+ protected virtual Task WriteFileAsync(ActionContext context, VirtualFileResult result, IFileInfo fileInfo, RangeItemHeaderValue range, long rangeLength)
{
if (context == null)
{
@@ -57,7 +76,37 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
+ if (range != null && rangeLength == 0)
+ {
+ return Task.CompletedTask;
+ }
+
var response = context.HttpContext.Response;
+ var physicalPath = fileInfo.PhysicalPath;
+ var sendFile = response.HttpContext.Features.Get();
+ if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
+ {
+ if (range != null)
+ {
+ return sendFile.SendFileAsync(
+ physicalPath,
+ offset: range.From ?? 0L,
+ count: rangeLength,
+ cancellation: default(CancellationToken));
+ }
+
+ return sendFile.SendFileAsync(
+ physicalPath,
+ offset: 0,
+ count: null,
+ cancellation: default(CancellationToken));
+ }
+
+ return WriteFileAsync(context.HttpContext, GetFileStream(fileInfo), range, rangeLength);
+ }
+
+ private IFileInfo GetFileInformation(VirtualFileResult result)
+ {
var fileProvider = GetFileProvider(result);
var normalizedPath = result.FileName;
@@ -67,32 +116,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
var fileInfo = fileProvider.GetFileInfo(normalizedPath);
- if (fileInfo.Exists)
- {
- var physicalPath = fileInfo.PhysicalPath;
- var sendFile = response.HttpContext.Features.Get();
- if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
- {
- await sendFile.SendFileAsync(
- physicalPath,
- offset: 0,
- count: null,
- cancellation: default(CancellationToken));
- }
- else
- {
- var fileStream = GetFileStream(fileInfo);
- using (fileStream)
- {
- await fileStream.CopyToAsync(response.Body, DefaultBufferSize);
- }
- }
- }
- else
- {
- throw new FileNotFoundException(
- Resources.FormatFileResult_InvalidPath(result.FileName), result.FileName);
- }
+ return fileInfo;
}
private IFileProvider GetFileProvider(VirtualFileResult result)
@@ -103,7 +127,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
result.FileProvider = _hostingEnvironment.WebRootFileProvider;
-
return result.FileProvider;
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj
index 5acd398017..058082e23f 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj
@@ -25,6 +25,8 @@ Microsoft.AspNetCore.Mvc.RouteAttribute
+
+
@@ -37,6 +39,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute
+
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs
index 0a7de27d3c..49ad6abc6a 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs
@@ -1427,6 +1427,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.Equal(string.Empty, result.FileDownloadName);
}
+ [Theory]
+ [InlineData(null, null)]
+ [InlineData(null, "\"Etag\"")]
+ [InlineData("05/01/2008 +1:00", null)]
+ [InlineData("05/01/2008 +1:00", "\"Etag\"")]
+ public void File_WithContents_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
+ {
+ // Arrange
+ var controller = new TestableController();
+ var fileContents = new byte[0];
+ var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
+ var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
+
+ // Act
+ var result = controller.File(fileContents, "application/pdf", lastModified, entityTag);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(fileContents, result.FileContents);
+ Assert.Equal("application/pdf", result.ContentType.ToString());
+ Assert.Equal(string.Empty, result.FileDownloadName);
+ Assert.Equal(lastModified, result.LastModified);
+ Assert.Equal(entityTag, result.EntityTag);
+ }
+
[Fact]
public void File_WithContentsAndFileDownloadName()
{
@@ -1444,6 +1469,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.Equal("someDownloadName", result.FileDownloadName);
}
+ [Theory]
+ [InlineData(null, null)]
+ [InlineData(null, "\"Etag\"")]
+ [InlineData("05/01/2008 +1:00", null)]
+ [InlineData("05/01/2008 +1:00", "\"Etag\"")]
+ public void File_WithContentsAndFileDownloadName_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
+ {
+ // Arrange
+ var controller = new TestableController();
+ var fileContents = new byte[0];
+ var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
+ var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
+
+ // Act
+ var result = controller.File(fileContents, "application/pdf", "someDownloadName", lastModified, entityTag);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(fileContents, result.FileContents);
+ Assert.Equal("application/pdf", result.ContentType.ToString());
+ Assert.Equal("someDownloadName", result.FileDownloadName);
+ Assert.Equal(lastModified, result.LastModified);
+ Assert.Equal(entityTag, result.EntityTag);
+ }
+
[Fact]
public void File_WithPath()
{
@@ -1461,6 +1511,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.Equal(string.Empty, result.FileDownloadName);
}
+ [Theory]
+ [InlineData(null, null)]
+ [InlineData(null, "\"Etag\"")]
+ [InlineData("05/01/2008 +1:00", null)]
+ [InlineData("05/01/2008 +1:00", "\"Etag\"")]
+ public void File_WithPath_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
+ {
+ // Arrange
+ var controller = new TestableController();
+ var path = Path.GetFullPath("somepath");
+ var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
+ var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
+
+ // Act
+ var result = controller.File(path, "application/pdf", lastModified, entityTag);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(path, result.FileName);
+ Assert.Equal("application/pdf", result.ContentType.ToString());
+ Assert.Equal(string.Empty, result.FileDownloadName);
+ Assert.Equal(lastModified, result.LastModified);
+ Assert.Equal(entityTag, result.EntityTag);
+ }
+
[Fact]
public void File_WithPathAndFileDownloadName()
{
@@ -1478,6 +1553,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.Equal("someDownloadName", result.FileDownloadName);
}
+ [Theory]
+ [InlineData(null, null)]
+ [InlineData(null, "\"Etag\"")]
+ [InlineData("05/01/2008 +1:00", null)]
+ [InlineData("05/01/2008 +1:00", "\"Etag\"")]
+ public void File_WithPathAndFileDownloadName_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
+ {
+ // Arrange
+ var controller = new TestableController();
+ var path = Path.GetFullPath("somepath");
+ var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
+ var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
+
+ // Act
+ var result = controller.File(path, "application/pdf", "someDownloadName", lastModified, entityTag);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(path, result.FileName);
+ Assert.Equal("application/pdf", result.ContentType.ToString());
+ Assert.Equal("someDownloadName", result.FileDownloadName);
+ Assert.Equal(lastModified, result.LastModified);
+ Assert.Equal(entityTag, result.EntityTag);
+ }
+
[Fact]
public void File_WithStream()
{
@@ -1500,6 +1600,36 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.Equal(string.Empty, result.FileDownloadName);
}
+ [Theory]
+ [InlineData(null, null)]
+ [InlineData(null, "\"Etag\"")]
+ [InlineData("05/01/2008 +1:00", null)]
+ [InlineData("05/01/2008 +1:00", "\"Etag\"")]
+ public void File_WithStream_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
+ {
+ // Arrange
+ var mockHttpContext = new Mock();
+ mockHttpContext.Setup(x => x.Response.RegisterForDispose(It.IsAny()));
+
+ var controller = new TestableController();
+ controller.ControllerContext.HttpContext = mockHttpContext.Object;
+
+ var fileStream = Stream.Null;
+ var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
+ var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
+
+ // Act
+ var result = controller.File(fileStream, "application/pdf", lastModified, entityTag);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(fileStream, result.FileStream);
+ Assert.Equal("application/pdf", result.ContentType.ToString());
+ Assert.Equal(string.Empty, result.FileDownloadName);
+ Assert.Equal(lastModified, result.LastModified);
+ Assert.Equal(entityTag, result.EntityTag);
+ }
+
[Fact]
public void File_WithStreamAndFileDownloadName()
{
@@ -1521,6 +1651,35 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.Equal("someDownloadName", result.FileDownloadName);
}
+ [Theory]
+ [InlineData(null, null)]
+ [InlineData(null, "\"Etag\"")]
+ [InlineData("05/01/2008 +1:00", null)]
+ [InlineData("05/01/2008 +1:00", "\"Etag\"")]
+ public void File_WithStreamAndFileDownloadName_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
+ {
+ // Arrange
+ var mockHttpContext = new Mock();
+
+ var controller = new TestableController();
+ controller.ControllerContext.HttpContext = mockHttpContext.Object;
+
+ var fileStream = Stream.Null;
+ var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
+ var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
+
+ // Act
+ var result = controller.File(fileStream, "application/pdf", "someDownloadName", lastModified, entityTag);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(fileStream, result.FileStream);
+ Assert.Equal("application/pdf", result.ContentType.ToString());
+ Assert.Equal("someDownloadName", result.FileDownloadName);
+ Assert.Equal(lastModified, result.LastModified);
+ Assert.Equal(entityTag, result.EntityTag);
+ }
+
[Fact]
public void HttpUnauthorized_SetsStatusCode()
{
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs
index 72bb0c36b5..0c913f5c7e 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs
@@ -1,7 +1,9 @@
// Copyright (c) .NET Foundation. 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.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
@@ -11,6 +13,7 @@ using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNetCore.Mvc
@@ -46,6 +49,29 @@ namespace Microsoft.AspNetCore.Mvc
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
}
+ [Fact]
+ public void Constructor_SetsLastModifiedAndEtag()
+ {
+ // Arrange
+ var fileContents = new byte[0];
+ var contentType = "text/plain";
+ var expectedMediaType = contentType;
+ var lastModified = new DateTimeOffset();
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+
+ // Act
+ var result = new FileContentResult(fileContents, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag
+ };
+
+ // Assert
+ Assert.Equal(lastModified, result.LastModified);
+ Assert.Equal(entityTag, result.EntityTag);
+ MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
+ }
+
[Fact]
public async Task WriteFileAsync_CopiesBuffer_ToOutputStream()
{
@@ -68,6 +94,268 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Equal(buffer, outStream.ToArray());
}
+ [Theory]
+ [InlineData(0, 4, "Hello", 5)]
+ [InlineData(6, 10, "World", 5)]
+ [InlineData(null, 5, "World", 5)]
+ [InlineData(6, null, "World", 5)]
+ public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
+ {
+ // Arrange
+ var contentType = "text/plain";
+ var lastModified = new DateTimeOffset();
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ var byteArray = Encoding.ASCII.GetBytes("Hello World");
+
+ var result = new FileContentResult(byteArray, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag
+ };
+
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.Range = new RangeHeaderValue(start, end);
+ requestHeaders.IfMatch = new[]
+ {
+ new EntityTagHeaderValue("\"Etag\""),
+ };
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ start = start ?? 11 - end;
+ end = start + contentLength - 1;
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, byteArray.Length);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
+ Assert.Equal(contentLength, httpResponse.ContentLength);
+ Assert.Equal(expectedString, body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
+ {
+ // Arrange
+ var contentType = "text/plain";
+ var lastModified = DateTimeOffset.MinValue;
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ var byteArray = Encoding.ASCII.GetBytes("Hello World");
+
+ var result = new FileContentResult(byteArray, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag
+ };
+
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfMatch = new[]
+ {
+ new EntityTagHeaderValue("\"Etag\""),
+ };
+ requestHeaders.Range = new RangeHeaderValue(0, 4);
+ requestHeaders.IfRange = new RangeConditionHeaderValue(DateTimeOffset.MinValue);
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ var contentRange = new ContentRangeHeaderValue(0, 4, byteArray.Length);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
+ Assert.Equal(5, httpResponse.ContentLength);
+ Assert.Equal("Hello", body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
+ {
+ // Arrange
+ var contentType = "text/plain";
+ var lastModified = DateTimeOffset.MinValue.AddDays(1);
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ var byteArray = Encoding.ASCII.GetBytes("Hello World");
+
+ var result = new FileContentResult(byteArray, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag
+ };
+
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfMatch = new[]
+ {
+ new EntityTagHeaderValue("\"Etag\""),
+ };
+ requestHeaders.Range = new RangeHeaderValue(0, 4);
+ requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
+ Assert.Equal("Hello World", body);
+ }
+
+ [Theory]
+ [InlineData("0-5")]
+ [InlineData("bytes = 11-0")]
+ [InlineData("bytes = 1-4, 5-11")]
+ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
+ {
+ // Arrange
+ var contentType = "text/plain";
+ var lastModified = new DateTimeOffset();
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ var byteArray = Encoding.ASCII.GetBytes("Hello World");
+
+ var result = new FileContentResult(byteArray, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag
+ };
+
+ var httpContext = GetHttpContext();
+ httpContext.Request.Headers[HeaderNames.Range] = rangeString;
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ var contentRange = new ContentRangeHeaderValue(byteArray.Length);
+ Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
+ Assert.Empty(body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
+ {
+ // Arrange
+ var contentType = "text/plain";
+ var lastModified = new DateTimeOffset();
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ var byteArray = Encoding.ASCII.GetBytes("Hello World");
+
+ var result = new FileContentResult(byteArray, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag
+ };
+
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfMatch = new[]
+ {
+ new EntityTagHeaderValue("\"NotEtag\""),
+ };
+ httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Null(httpResponse.ContentLength);
+ Assert.Empty(body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeRequested_NotModified()
+ {
+ // Arrange
+ var contentType = "text/plain";
+ var lastModified = new DateTimeOffset();
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ var byteArray = Encoding.ASCII.GetBytes("Hello World");
+
+ var result = new FileContentResult(byteArray, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag
+ };
+
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfNoneMatch = new[]
+ {
+ new EntityTagHeaderValue("\"Etag\""),
+ };
+ httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Null(httpResponse.ContentLength);
+ Assert.Empty(body);
+ }
+
[Fact]
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
{
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs
index dbab62b669..aea1c3dab1 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs
@@ -250,6 +250,160 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Equal(expectedOutput, actual);
}
+ [Fact]
+ public async Task SetsAcceptRangeHeader()
+ {
+ // Arrange
+ var httpContext = GetHttpContext();
+ var actionContext = CreateActionContext(httpContext);
+
+ var result = new EmptyFileResult("application/my-type");
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ Assert.Equal("bytes", httpContext.Response.Headers[HeaderNames.AcceptRanges]);
+ }
+
+ [Theory]
+ [InlineData("\"Etag\"", "\"NotEtag\"", "\"Etag\"")]
+ [InlineData("\"Etag\"", null, null)]
+ [InlineData(null, "\"NotEtag\"", "\"Etag\"")]
+ public void GetPreconditionState_ShouldProcess(string ifMatch, string ifNoneMatch, string ifRange)
+ {
+ // Arrange
+ var actionContext = new ActionContext();
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Method = HttpMethods.Get;
+ var httpRequestHeaders = httpContext.Request.GetTypedHeaders();
+ var lastModified = DateTimeOffset.MinValue;
+ lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
+ var etag = new EntityTagHeaderValue("\"Etag\"");
+ httpRequestHeaders.IfMatch = ifMatch == null ? null : new[]
+ {
+ new EntityTagHeaderValue(ifMatch),
+ };
+
+ httpRequestHeaders.IfNoneMatch = ifNoneMatch == null ? null : new[]
+ {
+ new EntityTagHeaderValue(ifNoneMatch),
+ };
+ httpRequestHeaders.IfRange = ifRange == null ? null : new RangeConditionHeaderValue(ifRange);
+ httpRequestHeaders.IfUnmodifiedSince = lastModified;
+ httpRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1);
+ actionContext.HttpContext = httpContext;
+
+ // Act
+ var state = FileResultExecutorBase.GetPreconditionState(
+ actionContext,
+ httpRequestHeaders,
+ lastModified,
+ etag);
+
+ // Assert
+ Assert.Equal(FileResultExecutorBase.PreconditionState.ShouldProcess, state);
+ }
+
+ [Theory]
+ [InlineData("\"NotEtag\"", null)]
+ [InlineData("\"Etag\"", "\"Etag\"")]
+ [InlineData(null, null)]
+ public void GetPreconditionState_ShouldNotProcess_PreconditionFailed(string ifMatch, string ifNoneMatch)
+ {
+ // Arrange
+ var actionContext = new ActionContext();
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Method = HttpMethods.Delete;
+ var httpRequestHeaders = httpContext.Request.GetTypedHeaders();
+ var lastModified = DateTimeOffset.MinValue.AddDays(1);
+ var etag = new EntityTagHeaderValue("\"Etag\"");
+ httpRequestHeaders.IfMatch = ifMatch == null ? null : new[]
+ {
+ new EntityTagHeaderValue(ifMatch),
+ };
+
+ httpRequestHeaders.IfNoneMatch = ifNoneMatch == null ? null : new[]
+ {
+ new EntityTagHeaderValue(ifNoneMatch),
+ };
+ httpRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue;
+ httpRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(2);
+ actionContext.HttpContext = httpContext;
+
+ // Act
+ var state = FileResultExecutorBase.GetPreconditionState(
+ actionContext,
+ httpRequestHeaders,
+ lastModified,
+ etag);
+
+ // Assert
+ Assert.Equal(FileResultExecutorBase.PreconditionState.PreconditionFailed, state);
+ }
+
+ [Theory]
+ [InlineData(null, "\"Etag\"")]
+ [InlineData(null, null)]
+ public void GetPreconditionState_ShouldNotProcess_NotModified(string ifMatch, string ifNoneMatch)
+ {
+ // Arrange
+ var actionContext = new ActionContext();
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Method = HttpMethods.Get;
+ var httpRequestHeaders = httpContext.Request.GetTypedHeaders();
+ var lastModified = DateTimeOffset.MinValue;
+ lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
+ var etag = new EntityTagHeaderValue("\"Etag\"");
+ httpRequestHeaders.IfMatch = ifMatch == null ? null : new[]
+ {
+ new EntityTagHeaderValue(ifMatch),
+ };
+
+ httpRequestHeaders.IfNoneMatch = ifNoneMatch == null ? null : new[]
+ {
+ new EntityTagHeaderValue(ifNoneMatch),
+ };
+ httpRequestHeaders.IfModifiedSince = lastModified;
+ actionContext.HttpContext = httpContext;
+
+ // Act
+ var state = FileResultExecutorBase.GetPreconditionState(
+ actionContext,
+ httpRequestHeaders,
+ lastModified,
+ etag);
+
+ // Assert
+ Assert.Equal(FileResultExecutorBase.PreconditionState.NotModified, state);
+ }
+
+ [Fact]
+ public void GetPreconditionState_ShouldNotProcess_IgnoreRangeRequest()
+ {
+ // Arrange
+ var actionContext = new ActionContext();
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Method = HttpMethods.Get;
+ var httpRequestHeaders = httpContext.Request.GetTypedHeaders();
+ var lastModified = DateTimeOffset.MinValue;
+ lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
+ var etag = new EntityTagHeaderValue("\"Etag\"");
+ httpRequestHeaders.IfRange = new RangeConditionHeaderValue("\"NotEtag\"");
+ httpRequestHeaders.IfModifiedSince = lastModified;
+ actionContext.HttpContext = httpContext;
+
+ // Act
+ var state = FileResultExecutorBase.GetPreconditionState(
+ actionContext,
+ httpRequestHeaders,
+ lastModified,
+ etag);
+
+ // Assert
+ Assert.Equal(FileResultExecutorBase.PreconditionState.IgnoreRangeRequest, state);
+ }
+
private static IServiceCollection CreateServices()
{
var services = new ServiceCollection();
@@ -297,13 +451,13 @@ namespace Microsoft.AspNetCore.Mvc
private class EmptyFileResultExecutor : FileResultExecutorBase
{
public EmptyFileResultExecutor(ILoggerFactory loggerFactory)
- :base(CreateLogger(loggerFactory))
+ : base(CreateLogger(loggerFactory))
{
}
public Task ExecuteAsync(ActionContext context, EmptyFileResult result)
{
- SetHeadersAndLog(context, result);
+ SetHeadersAndLog(context, result, 0L);
result.WasWriteFileCalled = true;
return Task.FromResult(0);
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs
index 937b81b53d..cb8ab453ce 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs
@@ -1,8 +1,10 @@
// Copyright (c) .NET Foundation. 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.Linq;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@@ -13,6 +15,7 @@ using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
@@ -49,6 +52,348 @@ namespace Microsoft.AspNetCore.Mvc
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
}
+ [Fact]
+ public void Constructor_SetsLastModifiedAndEtag()
+ {
+ // Arrange
+ var stream = Stream.Null;
+ var contentType = "text/plain";
+ var expectedMediaType = contentType;
+ var lastModified = new DateTimeOffset();
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+
+ // Act
+ var result = new FileStreamResult(stream, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ };
+
+ // Assert
+ Assert.Equal(lastModified, result.LastModified);
+ Assert.Equal(entityTag, result.EntityTag);
+ MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
+ }
+
+ [Theory]
+ [InlineData(0, 4, "Hello", 5)]
+ [InlineData(6, 10, "World", 5)]
+ [InlineData(null, 5, "World", 5)]
+ [InlineData(6, null, "World", 5)]
+ public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
+ {
+ // Arrange
+ var contentType = "text/plain";
+ var lastModified = new DateTimeOffset();
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ var byteArray = Encoding.ASCII.GetBytes("Hello World");
+ var readStream = new MemoryStream(byteArray);
+ readStream.SetLength(11);
+
+ var result = new FileStreamResult(readStream, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ };
+
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.Range = new RangeHeaderValue(start, end);
+ requestHeaders.IfMatch = new[]
+ {
+ new EntityTagHeaderValue("\"Etag\""),
+ };
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ start = start ?? 11 - end;
+ end = start + contentLength - 1;
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, byteArray.Length);
+ Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
+ Assert.Equal(contentLength, httpResponse.ContentLength);
+ Assert.Equal(expectedString, body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
+ {
+ // Arrange
+ var contentType = "text/plain";
+ var lastModified = DateTimeOffset.MinValue;
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ var byteArray = Encoding.ASCII.GetBytes("Hello World");
+ var readStream = new MemoryStream(byteArray);
+ readStream.SetLength(11);
+
+ var result = new FileStreamResult(readStream, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ };
+
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfMatch = new[]
+ {
+ new EntityTagHeaderValue("\"Etag\""),
+ };
+ requestHeaders.Range = new RangeHeaderValue(0, 4);
+ requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ var contentRange = new ContentRangeHeaderValue(0, 4, byteArray.Length);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
+ Assert.Equal(5, httpResponse.ContentLength);
+ Assert.Equal("Hello", body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
+ {
+ // Arrange
+ var contentType = "text/plain";
+ var lastModified = DateTimeOffset.MinValue.AddDays(1);
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ var byteArray = Encoding.ASCII.GetBytes("Hello World");
+ var readStream = new MemoryStream(byteArray);
+ readStream.SetLength(11);
+
+ var result = new FileStreamResult(readStream, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ };
+
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfMatch = new[]
+ {
+ new EntityTagHeaderValue("\"Etag\""),
+ };
+ requestHeaders.Range = new RangeHeaderValue(0, 4);
+ requestHeaders.IfRange = new RangeConditionHeaderValue(DateTimeOffset.MinValue);
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
+ Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
+ Assert.Equal("Hello World", body);
+ }
+
+ [Theory]
+ [InlineData("0-5")]
+ [InlineData("bytes = 11-0")]
+ [InlineData("bytes = 1-4, 5-11")]
+ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
+ {
+ // Arrange
+ var contentType = "text/plain";
+ var lastModified = new DateTimeOffset();
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ var byteArray = Encoding.ASCII.GetBytes("Hello World");
+ var readStream = new MemoryStream(byteArray);
+
+ var result = new FileStreamResult(readStream, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ };
+
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ httpContext.Request.Headers[HeaderNames.Range] = rangeString;
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ var contentRange = new ContentRangeHeaderValue(byteArray.Length);
+ Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
+ Assert.Empty(body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
+ {
+ // Arrange
+ var contentType = "text/plain";
+ var lastModified = new DateTimeOffset();
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ var byteArray = Encoding.ASCII.GetBytes("Hello World");
+ var readStream = new MemoryStream(byteArray);
+
+ var result = new FileStreamResult(readStream, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ };
+
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfMatch = new[]
+ {
+ new EntityTagHeaderValue("\"NotEtag\""),
+ };
+ httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Null(httpResponse.ContentLength);
+ Assert.Empty(body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeRequested_NotModified()
+ {
+ // Arrange
+ var contentType = "text/plain";
+ var lastModified = new DateTimeOffset();
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ var byteArray = Encoding.ASCII.GetBytes("Hello World");
+ var readStream = new MemoryStream(byteArray);
+
+ var result = new FileStreamResult(readStream, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ };
+
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfNoneMatch = new[]
+ {
+ new EntityTagHeaderValue("\"Etag\""),
+ };
+ httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Null(httpResponse.ContentLength);
+ Assert.Empty(body);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(null)]
+ public async Task WriteFileAsync_RangeRequested_FileLengthZeroOrNull(long? fileLength)
+ {
+ // Arrange
+ var contentType = "text/plain";
+ var lastModified = new DateTimeOffset();
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ var byteArray = Encoding.ASCII.GetBytes("");
+ var readStream = new MemoryStream(byteArray);
+ fileLength = fileLength ?? 0L;
+ readStream.SetLength(fileLength.Value);
+ var result = new FileStreamResult(readStream, contentType)
+ {
+ LastModified = lastModified,
+ EntityTag = entityTag,
+ };
+
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.Range = new RangeHeaderValue(0, 5);
+ requestHeaders.IfMatch = new[]
+ {
+ new EntityTagHeaderValue("\"Etag\""),
+ };
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+
+ var contentRange = new ContentRangeHeaderValue(byteArray.Length);
+ Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
+ Assert.Empty(body);
+ }
+
[Fact]
public async Task WriteFileAsync_WritesResponse_InChunksOfFourKilobytes()
{
@@ -153,7 +498,6 @@ namespace Microsoft.AspNetCore.Mvc
var httpContext = new DefaultHttpContext();
httpContext.RequestServices = services.BuildServiceProvider();
-
return httpContext;
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs
index 8731208531..210b64c6a6 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs
@@ -6,7 +6,6 @@ using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Abstractions;
@@ -16,6 +15,7 @@ using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
@@ -52,6 +52,198 @@ namespace Microsoft.AspNetCore.Mvc
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
}
+ [Theory]
+ [InlineData(0, 3, "File", 4)]
+ [InlineData(8, 13, "Result", 6)]
+ [InlineData(null, 5, "ts�", 5)]
+ [InlineData(8, null, "ResultTestFile contents�", 26)]
+ public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
+ {
+ // Arrange
+ var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
+ var result = new TestPhysicalFileResult(path, "text/plain");
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
+ requestHeaders.Range = new RangeHeaderValue(start, end);
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ start = start ?? 34 - end;
+ end = start + contentLength - 1;
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 34);
+ Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Equal(contentLength, httpResponse.ContentLength);
+ Assert.Equal(expectedString, body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
+ {
+ // Arrange
+ var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
+ var result = new TestPhysicalFileResult(path, "text/plain");
+ var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
+ requestHeaders.Range = new RangeHeaderValue(0, 3);
+ requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ var contentRange = new ContentRangeHeaderValue(0, 3, 34);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
+ Assert.Equal(4, httpResponse.ContentLength);
+ Assert.Equal("File", body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
+ {
+ // Arrange
+ var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
+ var result = new TestPhysicalFileResult(path, "text/plain");
+ var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
+ requestHeaders.Range = new RangeHeaderValue(0, 3);
+ requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Equal("FilePathResultTestFile contents�", body);
+ }
+
+ [Theory]
+ [InlineData("0-5")]
+ [InlineData("bytes = 11-0")]
+ [InlineData("bytes = 1-4, 5-11")]
+ public async Task WriteFileAsync_RangeRequested_NotSatisfiable(string rangeString)
+ {
+ // Arrange
+ var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
+ var result = new TestPhysicalFileResult(path, "text/plain");
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
+ httpContext.Request.Headers[HeaderNames.Range] = rangeString;
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ var contentRange = new ContentRangeHeaderValue(34);
+ Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Empty(body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
+ {
+ // Arrange
+ var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
+ var result = new TestPhysicalFileResult(path, "text/plain");
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue;
+ httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Null(httpResponse.ContentLength);
+ Assert.Empty(body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeRequested_NotModified()
+ {
+ // Arrange
+ var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
+ var result = new TestPhysicalFileResult(path, "text/plain");
+ var httpContext = GetHttpContext();
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1);
+ httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Null(httpResponse.ContentLength);
+ Assert.Empty(body);
+ }
+
[Fact]
public async Task ExecuteResultAsync_FallsbackToStreamCopy_IfNoIHttpSendFilePresent()
{
@@ -94,6 +286,46 @@ namespace Microsoft.AspNetCore.Mvc
sendFileMock.Verify();
}
+ [Theory]
+ [InlineData(0, 3, "File", 4)]
+ [InlineData(8, 13, "Result", 6)]
+ [InlineData(null, 3, "ts¡", 3)]
+ [InlineData(8, null, "ResultTestFile contents¡", 26)]
+ public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, string expectedString, long contentLength)
+ {
+ // Arrange
+ var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
+ var result = new TestPhysicalFileResult(path, "text/plain");
+
+ var sendFile = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFile);
+ var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.Range = new RangeHeaderValue(start, end);
+ requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
+ httpContext.Request.Method = HttpMethods.Get;
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ start = start ?? 34 - end;
+ end = start + contentLength - 1;
+ var httpResponse = actionContext.HttpContext.Response;
+ Assert.Equal(Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")), sendFile.Name);
+ Assert.Equal(start, sendFile.Offset);
+ Assert.Equal(contentLength, sendFile.Length);
+ Assert.Equal(CancellationToken.None, sendFile.Token);
+ var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 34);
+ Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Equal(contentLength, httpResponse.ContentLength);
+ }
+
[Fact]
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
{
@@ -236,6 +468,35 @@ namespace Microsoft.AspNetCore.Mvc
return new MemoryStream(Encoding.UTF8.GetBytes("FilePathResultTestFile contents�"));
}
}
+
+ protected override FileMetadata GetFileInfo(string path)
+ {
+ var lastModified = DateTimeOffset.MinValue.AddDays(1);
+ return new FileMetadata
+ {
+ Exists = true,
+ Length = 34,
+ LastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0))
+ };
+ }
+ }
+
+ private class TestSendFileFeature : IHttpSendFileFeature
+ {
+ public string Name { get; set; }
+ public long Offset { get; set; }
+ public long? Length { get; set; }
+ public CancellationToken Token { get; set; }
+
+ public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
+ {
+ Name = path;
+ Offset = offset;
+ Length = length;
+ Token = cancellation;
+
+ return Task.FromResult(0);
+ }
}
private static IServiceCollection CreateServices()
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs
index 2cb6e5a0fe..4034bbb0c1 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs
@@ -17,6 +17,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
@@ -51,8 +52,272 @@ namespace Microsoft.AspNetCore.Mvc
// Assert
Assert.Equal(path, result.FileName);
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
-
}
+
+ [Theory]
+ [InlineData(0, 3, "File", 4)]
+ [InlineData(8, 13, "Result", 6)]
+ [InlineData(null, 4, "ts¡", 4)]
+ [InlineData(8, null, "ResultTestFile contents¡", 25)]
+ public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var result = new TestVirtualFileResult(path, contentType);
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var httpContext = GetHttpContext();
+ httpContext.Response.Body = new MemoryStream();
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.Range = new RangeHeaderValue(start, end);
+ requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
+ httpContext.Request.Method = HttpMethods.Get;
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ start = start ?? 33 - end;
+ end = start + contentLength - 1;
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 33);
+ Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Equal(contentLength, httpResponse.ContentLength);
+ Assert.Equal(expectedString, body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var result = new TestVirtualFileResult(path, contentType);
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var httpContext = GetHttpContext();
+ httpContext.Response.Body = new MemoryStream();
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
+ requestHeaders.Range = new RangeHeaderValue(0, 3);
+ requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ var contentRange = new ContentRangeHeaderValue(0, 3, 33);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
+ Assert.Equal(4, httpResponse.ContentLength);
+ Assert.Equal("File", body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var result = new TestVirtualFileResult(path, contentType);
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var httpContext = GetHttpContext();
+ httpContext.Response.Body = new MemoryStream();
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
+ requestHeaders.Range = new RangeHeaderValue(0, 3);
+ requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Equal("FilePathResultTestFile contents¡", body);
+ }
+
+ [Theory]
+ [InlineData("0-5")]
+ [InlineData("bytes = 11-0")]
+ [InlineData("bytes = 1-4, 5-11")]
+ public async Task WriteFileAsync_RangeRequested_NotSatisfiable(string rangeString)
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var result = new TestVirtualFileResult(path, contentType);
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var httpContext = GetHttpContext();
+ httpContext.Response.Body = new MemoryStream();
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ httpContext.Request.Headers[HeaderNames.Range] = rangeString;
+ requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ var contentRange = new ContentRangeHeaderValue(33);
+ Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Empty(body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var result = new TestVirtualFileResult(path, contentType);
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var httpContext = GetHttpContext();
+ httpContext.Response.Body = new MemoryStream();
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue;
+ httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Null(httpResponse.ContentLength);
+ Assert.Empty(body);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeRequested_NotModified()
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var result = new TestVirtualFileResult(path, contentType);
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var httpContext = GetHttpContext();
+ httpContext.Response.Body = new MemoryStream();
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1);
+ httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
+ httpContext.Request.Method = HttpMethods.Get;
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ httpResponse.Body.Seek(0, SeekOrigin.Begin);
+ var streamReader = new StreamReader(httpResponse.Body);
+ var body = streamReader.ReadToEndAsync().Result;
+ Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Null(httpResponse.ContentLength);
+ Assert.Empty(body);
+ }
+
[Fact]
public async Task ExecuteResultAsync_FallsBackToWebRootFileProvider_IfNoFileProviderIsPresent()
{
@@ -133,6 +398,58 @@ namespace Microsoft.AspNetCore.Mvc
sendFileMock.Verify();
}
+ [Theory]
+ [InlineData(0, 3, "File", 4)]
+ [InlineData(8, 13, "Result", 6)]
+ [InlineData(null, 3, "ts¡", 3)]
+ [InlineData(8, null, "ResultTestFile contents¡", 25)]
+ public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, string expectedString, long contentLength)
+ {
+ // Arrange
+ var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt");
+ var result = new TestVirtualFileResult(path, "text/plain")
+ {
+ FileProvider = GetFileProvider(path),
+ };
+
+ var sendFile = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFile);
+ var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.Range = new RangeHeaderValue(start, end);
+ requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
+ httpContext.Request.Method = HttpMethods.Get;
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ start = start ?? 33 - end;
+ end = start + contentLength - 1;
+ var httpResponse = actionContext.HttpContext.Response;
+ Assert.Equal(Path.Combine("TestFiles", "FilePathResultTestFile.txt"), sendFile.Name);
+ Assert.Equal(start, sendFile.Offset);
+ Assert.Equal(contentLength, sendFile.Length);
+ Assert.Equal(CancellationToken.None, sendFile.Token);
+ var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 33);
+ Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
+ Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
+ Assert.Equal(contentLength, httpResponse.ContentLength);
+ }
+
[Fact]
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
{
@@ -325,7 +642,11 @@ namespace Microsoft.AspNetCore.Mvc
private static IFileProvider GetFileProvider(string path)
{
var fileInfo = new Mock();
+ fileInfo.SetupGet(fi => fi.Length).Returns(33);
fileInfo.SetupGet(fi => fi.Exists).Returns(true);
+ var lastModified = DateTimeOffset.MinValue.AddDays(1);
+ lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
+ fileInfo.SetupGet(fi => fi.LastModified).Returns(lastModified);
fileInfo.SetupGet(fi => fi.PhysicalPath).Returns(path);
var fileProvider = new Mock();
fileProvider.Setup(fp => fp.GetFileInfo(path))
@@ -355,7 +676,7 @@ namespace Microsoft.AspNetCore.Mvc
private class TestVirtualFileResultExecutor : VirtualFileResultExecutor
{
public TestVirtualFileResultExecutor(ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment)
- : base(loggerFactory,hostingEnvironment)
+ : base(loggerFactory, hostingEnvironment)
{
}
@@ -373,5 +694,23 @@ namespace Microsoft.AspNetCore.Mvc
}
}
}
+
+ private class TestSendFileFeature : IHttpSendFileFeature
+ {
+ public string Name { get; set; }
+ public long Offset { get; set; }
+ public long? Length { get; set; }
+ public CancellationToken Token { get; set; }
+
+ public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
+ {
+ Name = path;
+ Offset = offset;
+ Length = length;
+ Token = cancellation;
+
+ return Task.FromResult(0);
+ }
+ }
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs
index a37acbfd6b..a87f20754d 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs
@@ -1,8 +1,11 @@
// Copyright (c) .NET Foundation. 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.Net;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
@@ -37,6 +40,92 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("This is a sample text file", body);
}
+ [Theory]
+ [InlineData(0, 6, "This is")]
+ [InlineData(17, 25, "text file")]
+ [InlineData(0, 50, "This is a sample text file")]
+ public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequest(long start, long end, string expectedBody)
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDisk");
+ httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.PartialContent, 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(expectedBody, body);
+ }
+
+ [Theory]
+ [InlineData("0-6")]
+ [InlineData("bytes = 11-6")]
+ [InlineData("bytes = 1-4, 5-11")]
+ public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestNotSatisfiable(string rangeString)
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDisk");
+ httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.Empty(body);
+ }
+
+ [Theory]
+ [InlineData(0, 6, "This is")]
+ [InlineData(17, 25, "text file")]
+ [InlineData(0, 50, "This is a sample text file")]
+ public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequest_WithLastModifiedAndEtag(long start, long end, string expectedBody)
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDisk_WithLastModifiedAndEtag");
+ httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.PartialContent, 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(expectedBody, body);
+ }
+
+ [Theory]
+ [InlineData("0-6")]
+ [InlineData("bytes = 11-6")]
+ [InlineData("bytes = 1-4, 5-11")]
+ public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestNotSatisfiable_WithLastModifiedAndEtag(string rangeString)
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName_WithLastModifiedAndEtag");
+ httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.Empty(body);
+ }
+
[ConditionalFact]
// https://github.com/aspnet/Mvc/issues/2727
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
@@ -60,6 +149,45 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}
+ [Fact]
+ public async Task FileFromDisk_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest_WithLastModifiedAndEtag()
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName_WithLastModifiedAndEtag");
+ httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
+ httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.PartialContent, 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", body);
+ }
+
+ [Fact]
+ public async Task FileFromDisk_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestIgnored_WithLastModifiedAndEtag()
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName_WithLastModifiedAndEtag");
+ httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
+ httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // 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.Equal("This is a sample text file", body);
+ }
+
[Fact]
public async Task FileFromStream_ReturnsFile()
{
@@ -77,6 +205,49 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("This is sample text from a stream", body);
}
+ [Theory]
+ [InlineData(0, 6, "This is")]
+ [InlineData(25, 32, "a stream")]
+ [InlineData(0, 50, "This is sample text from a stream")]
+ public async Task FileFromStream_ReturnsFile_RangeRequest(long start, long end, string expectedBody)
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStream");
+ httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.PartialContent, 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(expectedBody, body);
+ }
+
+ [Theory]
+ [InlineData("0-6")]
+ [InlineData("bytes = 11-6")]
+ [InlineData("bytes = 1-4, 5-11")]
+ public async Task FileFromStream_ReturnsFile_RangeRequestNotSatisfiable(string rangeString)
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStream");
+ httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.Empty(body);
+ }
+
[Fact]
public async Task FileFromStream_ReturnsFileWithFileName()
{
@@ -98,6 +269,45 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}
+ [Fact]
+ public async Task FileFromStream_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest()
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStreamWithFileName_WithEtag");
+ httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
+ httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.PartialContent, 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", body);
+ }
+
+ [Fact]
+ public async Task FileFromStream_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestNotSatisfiable()
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStreamWithFileName_WithEtag");
+ httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
+ httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // 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.Equal("This is sample text from a stream", body);
+ }
+
[Fact]
public async Task FileFromBinaryData_ReturnsFile()
{
@@ -115,6 +325,49 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("This is a sample text from a binary array", body);
}
+ [Theory]
+ [InlineData(0, 6, "This is")]
+ [InlineData(29, 40, "binary array")]
+ [InlineData(0, 50, "This is a sample text from a binary array")]
+ public async Task FileFromBinaryData_ReturnsFile_RangeRequest(long start, long end, string expectedBody)
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryData");
+ httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.PartialContent, 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(expectedBody, body);
+ }
+
+ [Theory]
+ [InlineData("0-6")]
+ [InlineData("bytes = 11-6")]
+ [InlineData("bytes = 1-4, 5-11")]
+ public async Task FileFromBinaryData_ReturnsFile_RangeRequestNotSatisfiable(string rangeString)
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryData");
+ httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.Empty(body);
+ }
+
[Fact]
public async Task FileFromBinaryData_ReturnsFileWithFileName()
{
@@ -136,6 +389,45 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}
+ [Fact]
+ public async Task FileFromBinaryData_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest()
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryDataWithFileName_WithEtag");
+ httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
+ httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.PartialContent, 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", body);
+ }
+
+ [Fact]
+ public async Task FileFromBinaryData_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestIgnored()
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryDataWithFileName_WithEtag");
+ httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
+ httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // 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.Equal("This is a sample text from a binary array", body);
+ }
+
[Fact]
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName()
{
@@ -159,5 +451,99 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.NotNull(contentDisposition);
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}
+
+ [Theory]
+ [InlineData(0, 6, "Sample ")]
+ [InlineData(20, 37, "embedded resource.")]
+ [InlineData(7, 50, "text file as embedded resource.")]
+ public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequest(long start, long end, string expectedBody)
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName");
+ httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.PartialContent, 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(expectedBody, body);
+ var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
+ Assert.NotNull(contentDisposition);
+ Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
+ }
+
+ [Fact]
+ public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest()
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName_WithEtag");
+ httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
+ httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.PartialContent, 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("Sample ", body);
+ var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
+ Assert.NotNull(contentDisposition);
+ Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
+ }
+
+ [Fact]
+ public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestIgnored()
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName_WithEtag");
+ httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
+ httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // 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.Equal("Sample text file as embedded resource.", body);
+ var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
+ Assert.NotNull(contentDisposition);
+ Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
+ }
+
+ [Theory]
+ [InlineData("0-6")]
+ [InlineData("bytes = 11-6")]
+ [InlineData("bytes = 1-4, 5-11")]
+ public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequestNotSatisfiable(string rangeString)
+ {
+ // Arrange
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName");
+ httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
+
+ // Act
+ var response = await Client.SendAsync(httpRequestMessage);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.Empty(body);
+ var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
+ Assert.NotNull(contentDisposition);
+ Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
index d51c5d91f4..17b6030ae7 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
@@ -48,6 +48,7 @@
+
diff --git a/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs b/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs
index 2446d4afcb..e86e211932 100644
--- a/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs
+++ b/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs
@@ -1,10 +1,12 @@
// Copyright (c) .NET Foundation. 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.Text;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Net.Http.Headers;
namespace FilesWebSite
{
@@ -23,12 +25,28 @@ namespace FilesWebSite
return PhysicalFile(path, "text/plain");
}
+ public IActionResult DownloadFromDisk_WithLastModifiedAndEtag()
+ {
+ var path = Path.Combine(_hostingEnvironment.ContentRootPath, "sample.txt");
+ var lastModified = new DateTimeOffset(year: 1999, month: 11, day: 04, hour: 3, minute: 0, second: 0, offset: new TimeSpan(0));
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ return PhysicalFile(path, "text/plain", lastModified, entityTag);
+ }
+
public IActionResult DownloadFromDiskWithFileName()
{
var path = Path.Combine(_hostingEnvironment.ContentRootPath, "sample.txt");
return PhysicalFile(path, "text/plain", "downloadName.txt");
}
+ public IActionResult DownloadFromDiskWithFileName_WithLastModifiedAndEtag()
+ {
+ var path = Path.Combine(_hostingEnvironment.ContentRootPath, "sample.txt");
+ var lastModified = new DateTimeOffset(year: 1999, month: 11, day: 04, hour: 3, minute: 0, second: 0, offset: new TimeSpan(0));
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ return PhysicalFile(path, "text/plain", "downloadName.txt", lastModified, entityTag);
+ }
+
public IActionResult DownloadFromStream()
{
var stream = new MemoryStream();
@@ -51,6 +69,17 @@ namespace FilesWebSite
return File(stream, "text/plain", "downloadName.txt");
}
+ public IActionResult DownloadFromStreamWithFileName_WithEtag()
+ {
+ 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);
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ return File(stream, "text/plain", "downloadName.txt", lastModified: null, entityTag: entityTag);
+ }
+
public IActionResult DownloadFromBinaryData()
{
var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
@@ -62,5 +91,12 @@ namespace FilesWebSite
var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
return File(data, "text/plain", "downloadName.txt");
}
+
+ public IActionResult DownloadFromBinaryDataWithFileName_WithEtag()
+ {
+ var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
+ var entityTag = new EntityTagHeaderValue("\"Etag\"");
+ return File(data, "text/plain", "downloadName.txt", lastModified: null, entityTag: entityTag);
+ }
}
}
diff --git a/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs b/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs
index 5b471886b7..758aea7b7d 100644
--- a/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs
+++ b/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs
@@ -17,5 +17,17 @@ namespace FilesWebSite
FileDownloadName = "downloadName.txt"
};
}
+
+ public IActionResult DownloadFileWithFileName_WithEtag()
+ {
+ var file = new VirtualFileResult("/Greetings.txt", "text/plain")
+ {
+ FileProvider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, "FilesWebSite.EmbeddedResources"),
+ FileDownloadName = "downloadName.txt"
+ };
+
+ file.EntityTag = new Microsoft.Net.Http.Headers.EntityTagHeaderValue("\"Etag\"");
+ return file;
+ }
}
}
\ No newline at end of file
diff --git a/test/WebSites/FilesWebSite/EmbeddedResources/Greetings.txt b/test/WebSites/FilesWebSite/EmbeddedResources/Greetings.txt
index 6b5d08138b..9531546f3d 100644
--- a/test/WebSites/FilesWebSite/EmbeddedResources/Greetings.txt
+++ b/test/WebSites/FilesWebSite/EmbeddedResources/Greetings.txt
@@ -1 +1 @@
-Sample text file as embedded resource.
\ No newline at end of file
+Sample text file as embedded resource.
\ No newline at end of file
diff --git a/test/WebSites/FilesWebSite/sample.txt b/test/WebSites/FilesWebSite/sample.txt
index a34f4c4be3..0f8c58ac58 100644
--- a/test/WebSites/FilesWebSite/sample.txt
+++ b/test/WebSites/FilesWebSite/sample.txt
@@ -1 +1 @@
-This is a sample text file
\ No newline at end of file
+This is a sample text file
\ No newline at end of file