From 0a21d046749ca7ec173504ba694269b446b53324 Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Thu, 28 Sep 2017 13:27:20 -0700 Subject: [PATCH] Add EnableRangeProcessing --- .../ControllerBase.cs | 331 ++++++++++++++++++ .../FileResult.cs | 5 + .../FileContentResultExecutor.cs | 1 + .../Infrastructure/FileResultExecutorBase.cs | 113 +++--- .../FileStreamResultExecutor.cs | 1 + .../PhysicalFileResultExecutor.cs | 1 + .../VirtualFileResultExecutor.cs | 3 +- .../ControllerBaseTest.cs | 104 ++++-- .../FileContentResultTest.cs | 103 ++++-- .../FileResultTest.cs | 6 +- .../FileStreamResultTest.cs | 77 +++- .../PhysicalFileResultTest.cs | 56 ++- .../VirtualFileResultTest.cs | 56 ++- .../FileResultTests.cs | 103 +++++- .../Controllers/DownloadFilesController.cs | 14 +- .../Controllers/EmbeddedFilesController.cs | 15 +- 16 files changed, 813 insertions(+), 176 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs index ec3f58a6e8..faac451c28 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs @@ -1088,6 +1088,20 @@ public virtual RedirectToPageResult RedirectToPagePermanentPreserveMethod( public virtual FileContentResult File(byte[] fileContents, string contentType) => File(fileContents, contentType, fileDownloadName: null); + /// + /// 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. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual FileContentResult File(byte[] fileContents, string contentType, bool enableRangeProcessing) + => File(fileContents, contentType, fileDownloadName: null, enableRangeProcessing: enableRangeProcessing); + /// /// Returns a file with the specified as content (), the /// specified as the Content-Type and the specified as the suggested file name. @@ -1102,6 +1116,25 @@ public virtual FileContentResult File(byte[] fileContents, string contentType) public virtual FileContentResult File(byte[] fileContents, string contentType, string fileDownloadName) => new FileContentResult(fileContents, contentType) { FileDownloadName = fileDownloadName }; + /// + /// 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. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual FileContentResult File(byte[] fileContents, string contentType, string fileDownloadName, bool enableRangeProcessing) + => new FileContentResult(fileContents, contentType) + { + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + }; + /// /// Returns a file with the specified as content (), /// and the specified as the Content-Type. @@ -1123,6 +1156,29 @@ public virtual FileContentResult File(byte[] fileContents, string contentType, D }; } + /// + /// 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. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual FileContentResult File(byte[] fileContents, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing) + { + return new FileContentResult(fileContents, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = enableRangeProcessing, + }; + } + /// /// Returns a file with the specified as content (), the /// specified as the Content-Type, and the specified as the suggested file name. @@ -1146,6 +1202,31 @@ public virtual FileContentResult File(byte[] fileContents, string contentType, s }; } + /// + /// 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. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual FileContentResult File(byte[] fileContents, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing) + { + return new FileContentResult(fileContents, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + }; + } + /// /// Returns a file in the specified (), with the /// specified as the Content-Type. @@ -1159,6 +1240,20 @@ public virtual FileContentResult File(byte[] fileContents, string contentType, s public virtual FileStreamResult File(Stream fileStream, string contentType) => File(fileStream, contentType, fileDownloadName: null); + /// + /// 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. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual FileStreamResult File(Stream fileStream, string contentType, bool enableRangeProcessing) + => File(fileStream, contentType, fileDownloadName: null, enableRangeProcessing: enableRangeProcessing); + /// /// Returns a file in the specified () with the /// specified as the Content-Type and the @@ -1174,6 +1269,26 @@ public virtual FileStreamResult File(Stream fileStream, string contentType) public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName) => new FileStreamResult(fileStream, contentType) { FileDownloadName = fileDownloadName }; + /// + /// 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. + /// The suggested file name. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName, bool enableRangeProcessing) + => new FileStreamResult(fileStream, contentType) + { + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + }; + /// /// Returns a file in the specified (), /// and the specified as the Content-Type. @@ -1195,6 +1310,29 @@ public virtual FileStreamResult File(Stream fileStream, string contentType, Date }; } + /// + /// 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. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual FileStreamResult File(Stream fileStream, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing) + { + return new FileStreamResult(fileStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = enableRangeProcessing, + }; + } + /// /// Returns a file in the specified (), the /// specified as the Content-Type, and the specified as the suggested file name. @@ -1218,6 +1356,31 @@ public virtual FileStreamResult File(Stream fileStream, string contentType, stri }; } + /// + /// 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. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing) + { + return new FileStreamResult(fileStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + }; + } + /// /// Returns the file specified by () with the /// specified as the Content-Type. @@ -1231,6 +1394,20 @@ public virtual FileStreamResult File(Stream fileStream, string contentType, stri public virtual VirtualFileResult File(string virtualPath, string contentType) => File(virtualPath, contentType, fileDownloadName: null); + /// + /// 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. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual VirtualFileResult File(string virtualPath, string contentType, bool enableRangeProcessing) + => File(virtualPath, contentType, fileDownloadName: null, enableRangeProcessing: enableRangeProcessing); + /// /// Returns the file specified by () with the /// specified as the Content-Type and the @@ -1246,6 +1423,26 @@ public virtual VirtualFileResult File(string virtualPath, string contentType) public virtual VirtualFileResult File(string virtualPath, string contentType, string fileDownloadName) => new VirtualFileResult(virtualPath, contentType) { FileDownloadName = fileDownloadName }; + /// + /// 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. + /// The suggested file name. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual VirtualFileResult File(string virtualPath, string contentType, string fileDownloadName, bool enableRangeProcessing) + => new VirtualFileResult(virtualPath, contentType) + { + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + }; + /// /// Returns the file specified by (), and the /// specified as the Content-Type. @@ -1267,6 +1464,29 @@ public virtual VirtualFileResult File(string virtualPath, string contentType, Da }; } + /// + /// 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. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual VirtualFileResult File(string virtualPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing) + { + return new VirtualFileResult(virtualPath, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = enableRangeProcessing, + }; + } + /// /// Returns the file specified by (), the /// specified as the Content-Type, and the specified as the suggested file name. @@ -1290,6 +1510,31 @@ public virtual VirtualFileResult File(string virtualPath, string contentType, st }; } + /// + /// 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. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual VirtualFileResult File(string virtualPath, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing) + { + return new VirtualFileResult(virtualPath, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + }; + } + /// /// Returns the file specified by () with the /// specified as the Content-Type. @@ -1303,6 +1548,20 @@ public virtual VirtualFileResult File(string virtualPath, string contentType, st public virtual PhysicalFileResult PhysicalFile(string physicalPath, string contentType) => PhysicalFile(physicalPath, contentType, fileDownloadName: null); + /// + /// 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. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual PhysicalFileResult PhysicalFile(string physicalPath, string contentType, bool enableRangeProcessing) + => PhysicalFile(physicalPath, contentType, fileDownloadName: null, enableRangeProcessing: enableRangeProcessing); + /// /// Returns the file specified by () with the /// specified as the Content-Type and the @@ -1321,6 +1580,30 @@ public virtual PhysicalFileResult PhysicalFile( string fileDownloadName) => new PhysicalFileResult(physicalPath, contentType) { FileDownloadName = fileDownloadName }; + /// + /// 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. + /// The suggested file name. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual PhysicalFileResult PhysicalFile( + string physicalPath, + string contentType, + string fileDownloadName, + bool enableRangeProcessing) + => new PhysicalFileResult(physicalPath, contentType) + { + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + }; + /// /// Returns the file specified by (), and /// the specified as the Content-Type. @@ -1342,6 +1625,29 @@ public virtual PhysicalFileResult PhysicalFile(string physicalPath, string conte }; } + /// + /// 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. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual PhysicalFileResult PhysicalFile(string physicalPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing) + { + return new PhysicalFileResult(physicalPath, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = enableRangeProcessing, + }; + } + /// /// Returns the file specified by (), the /// specified as the Content-Type, and the specified as the suggested file name. @@ -1365,6 +1671,31 @@ public virtual PhysicalFileResult PhysicalFile(string physicalPath, string conte }; } + /// + /// 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. + /// Set to true to enable range requests processing. + /// The created for the response. + [NonAction] + public virtual PhysicalFileResult PhysicalFile(string physicalPath, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing) + { + return new PhysicalFileResult(physicalPath, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + }; + } + /// /// Creates an that produces an response. /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/FileResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/FileResult.cs index dea1dff3e3..6b3b7743f2 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/FileResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/FileResult.cs @@ -52,5 +52,10 @@ public string FileDownloadName /// Gets or sets the etag associated with the . /// public EntityTagHeaderValue EntityTag { get; set; } + + /// + /// Gets or sets the value that enables range processing for the . + /// + public bool EnableRangeProcessing { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileContentResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileContentResultExecutor.cs index af8cfe17cf..0739c522d4 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileContentResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileContentResultExecutor.cs @@ -33,6 +33,7 @@ public virtual Task ExecuteAsync(ActionContext context, FileContentResult result context, result, result.FileContents.Length, + result.EnableRangeProcessing, result.LastModified, result.EntityTag); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileResultExecutorBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileResultExecutorBase.cs index 8e2a9e6b80..6bd8939bc7 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileResultExecutorBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileResultExecutorBase.cs @@ -41,6 +41,7 @@ protected virtual (RangeItemHeaderValue range, long rangeLength, bool serveBody) ActionContext context, FileResult result, long? fileLength, + bool enableRangeProcessing, DateTimeOffset? lastModified = null, EntityTagHeaderValue etag = null) { @@ -59,54 +60,55 @@ protected virtual (RangeItemHeaderValue range, long rangeLength, bool serveBody) var request = context.HttpContext.Request; var httpRequestHeaders = request.GetTypedHeaders(); + var preconditionState = GetPreconditionState(httpRequestHeaders, lastModified, etag); + var response = context.HttpContext.Response; - var httpResponseHeaders = response.GetTypedHeaders(); - if (lastModified.HasValue) - { - httpResponseHeaders.LastModified = lastModified; - } - if (etag != null) - { - httpResponseHeaders.ETag = etag; - } + SetLastModifiedAndEtagHeaders(response, lastModified, etag); var serveBody = !HttpMethods.IsHead(request.Method); - var preconditionState = GetPreconditionState(context, httpRequestHeaders, lastModified, etag); + + // Short circuit if the preconditional headers process to 304 (NotModified) or 412 (PreconditionFailed) if (preconditionState == PreconditionState.NotModified) { serveBody = false; response.StatusCode = StatusCodes.Status304NotModified; + return (range: null, rangeLength: 0, serveBody); } else if (preconditionState == PreconditionState.PreconditionFailed) { serveBody = false; response.StatusCode = StatusCodes.Status412PreconditionFailed; + return (range: null, rangeLength: 0, serveBody); } if (fileLength.HasValue) { - SetAcceptRangeHeader(context); - // Assuming the request is not a range request, the Content-Length header is set to the length of the entire file. + // Assuming the request is not a range request, and the response body is not empty, the Content-Length header is set to + // the length of the entire file. // If the request is a valid range request, this header is overwritten with the length of the range as part of the // range processing (see method SetContentLength). if (serveBody) { response.ContentLength = fileLength.Value; } - if (HttpMethods.IsHead(request.Method) || HttpMethods.IsGet(request.Method)) + + // Handle range request + if (enableRangeProcessing) { - if ((preconditionState == PreconditionState.Unspecified || - preconditionState == PreconditionState.ShouldProcess)) + SetAcceptRangeHeader(response); + + // If the request method is HEAD or GET, PreconditionState is Unspecified or ShouldProcess, and IfRange header is valid, + // range should be processed and Range headers should be set + if ((HttpMethods.IsHead(request.Method) || HttpMethods.IsGet(request.Method)) + && (preconditionState == PreconditionState.Unspecified || preconditionState == PreconditionState.ShouldProcess) + && (IfRangeValid(httpRequestHeaders, lastModified, etag))) { - if (IfRangeValid(context, httpRequestHeaders, lastModified, etag)) - { - return SetRangeHeaders(context, httpRequestHeaders, fileLength.Value); - } + return SetRangeHeaders(context, httpRequestHeaders, fileLength.Value); } } } - return (range: null, rangeLength: 0, serveBody: serveBody); + return (range: null, rangeLength: 0, serveBody); } private static void SetContentType(ActionContext context, FileResult result) @@ -130,38 +132,25 @@ private static void SetContentDispositionHeader(ActionContext context, FileResul } } - private static void SetAcceptRangeHeader(ActionContext context) - { - var response = context.HttpContext.Response; - response.Headers[HeaderNames.AcceptRanges] = AcceptRangeHeaderValue; - } - - private static PreconditionState GetEtagMatchState( - IList etagHeader, - EntityTagHeaderValue etag, - PreconditionState matchFoundState, - PreconditionState matchNotFoundState) + private static void SetLastModifiedAndEtagHeaders(HttpResponse response, DateTimeOffset? lastModified, EntityTagHeaderValue etag) { - if (etagHeader != null && etagHeader.Any()) + var httpResponseHeaders = response.GetTypedHeaders(); + if (lastModified.HasValue) { - var state = matchNotFoundState; - foreach (var entityTag in etagHeader) - { - if (entityTag.Equals(EntityTagHeaderValue.Any) || entityTag.Compare(etag, useStrongComparison: true)) - { - state = matchFoundState; - break; - } - } - - return state; + httpResponseHeaders.LastModified = lastModified; } + if (etag != null) + { + httpResponseHeaders.ETag = etag; + } + } - return PreconditionState.Unspecified; + private static void SetAcceptRangeHeader(HttpResponse response) + { + response.Headers[HeaderNames.AcceptRanges] = AcceptRangeHeaderValue; } internal static bool IfRangeValid( - ActionContext context, RequestHeaders httpRequestHeaders, DateTimeOffset? lastModified = null, EntityTagHeaderValue etag = null) @@ -193,7 +182,6 @@ internal static bool IfRangeValid( // Internal for testing internal static PreconditionState GetPreconditionState( - ActionContext context, RequestHeaders httpRequestHeaders, DateTimeOffset? lastModified = null, EntityTagHeaderValue etag = null) @@ -247,6 +235,30 @@ internal static PreconditionState GetPreconditionState( return state; } + 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; + } + private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states) { var max = PreconditionState.Unspecified; @@ -281,6 +293,7 @@ private static (RangeItemHeaderValue range, long rangeLength, bool serveBody) Se return (range: null, rangeLength: 0, serveBody: true); } + // Requested range is not satisfiable if (range == null) { // 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable) @@ -292,23 +305,23 @@ private static (RangeItemHeaderValue range, long rangeLength, bool serveBody) Se return (range: null, rangeLength: 0, serveBody: false); } + response.StatusCode = StatusCodes.Status206PartialContent; httpResponseHeaders.ContentRange = new ContentRangeHeaderValue( range.From.Value, range.To.Value, fileLength); - response.StatusCode = StatusCodes.Status206PartialContent; // Overwrite the Content-Length header for valid range requests with the range length. - var rangeLength = SetContentLength(context, range); + var rangeLength = SetContentLength(response, range); + return (range, rangeLength, serveBody: true); } - private static long SetContentLength(ActionContext context, RangeItemHeaderValue range) + private static long SetContentLength(HttpResponse response, 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; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileStreamResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileStreamResultExecutor.cs index 97bda5988f..72a4d53cb5 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileStreamResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileStreamResultExecutor.cs @@ -38,6 +38,7 @@ public virtual Task ExecuteAsync(ActionContext context, FileStreamResult result) context, result, fileLength, + result.EnableRangeProcessing, result.LastModified, result.EntityTag); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/PhysicalFileResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/PhysicalFileResultExecutor.cs index c2ea1a624d..e36d22a3d9 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/PhysicalFileResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/PhysicalFileResultExecutor.cs @@ -44,6 +44,7 @@ public virtual Task ExecuteAsync(ActionContext context, PhysicalFileResult resul context, result, fileInfo.Length, + result.EnableRangeProcessing, lastModified, result.EntityTag); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/VirtualFileResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/VirtualFileResultExecutor.cs index 694b58e84a..42b0b2a669 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/VirtualFileResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/VirtualFileResultExecutor.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure { - public class VirtualFileResultExecutor : FileResultExecutorBase , IActionResultExecutor + public class VirtualFileResultExecutor : FileResultExecutorBase, IActionResultExecutor { private readonly IHostingEnvironment _hostingEnvironment; @@ -54,6 +54,7 @@ public virtual Task ExecuteAsync(ActionContext context, VirtualFileResult result context, result, fileInfo.Length, + result.EnableRangeProcessing, lastModified, result.EntityTag); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs index c2f10b2fc0..48afdc739f 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs @@ -1632,14 +1632,33 @@ public void File_WithContents() Assert.Same(fileContents, result.FileContents); Assert.Equal("application/pdf", result.ContentType.ToString()); Assert.Equal(string.Empty, result.FileDownloadName); + Assert.False(result.EnableRangeProcessing); + } + + [Fact] + public void File_WithContents_EnableRangeProcessing() + { + // Arrange + var controller = new TestableController(); + var fileContents = new byte[0]; + + // Act + var result = controller.File(fileContents, "application/pdf", true); + + // Assert + Assert.NotNull(result); + Assert.Same(fileContents, result.FileContents); + Assert.Equal("application/pdf", result.ContentType.ToString()); + Assert.Equal(string.Empty, result.FileDownloadName); + Assert.True(result.EnableRangeProcessing); } [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) + [InlineData(null, null, false)] + [InlineData(null, "\"Etag\"", false)] + [InlineData("05/01/2008 +1:00", null, true)] + [InlineData("05/01/2008 +1:00", "\"Etag\"", true)] + public void File_WithContents_LastModifiedAndEtag(string lastModifiedString, string entityTagString, bool enableRangeProcessing) { // Arrange var controller = new TestableController(); @@ -1648,7 +1667,7 @@ public void File_WithContents_LastModifiedAndEtag(string lastModifiedString, str var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString); // Act - var result = controller.File(fileContents, "application/pdf", lastModified, entityTag); + var result = controller.File(fileContents, "application/pdf", lastModified, entityTag, enableRangeProcessing); // Assert Assert.NotNull(result); @@ -1657,6 +1676,7 @@ public void File_WithContents_LastModifiedAndEtag(string lastModifiedString, str Assert.Equal(string.Empty, result.FileDownloadName); Assert.Equal(lastModified, result.LastModified); Assert.Equal(entityTag, result.EntityTag); + Assert.Equal(enableRangeProcessing, result.EnableRangeProcessing); } [Fact] @@ -1674,14 +1694,15 @@ public void File_WithContentsAndFileDownloadName() Assert.Same(fileContents, result.FileContents); Assert.Equal("application/pdf", result.ContentType.ToString()); Assert.Equal("someDownloadName", result.FileDownloadName); + Assert.False(result.EnableRangeProcessing); } [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) + [InlineData(null, null, false)] + [InlineData(null, "\"Etag\"", false)] + [InlineData("05/01/2008 +1:00", null, true)] + [InlineData("05/01/2008 +1:00", "\"Etag\"", true)] + public void File_WithContentsAndFileDownloadName_LastModifiedAndEtag(string lastModifiedString, string entityTagString, bool enableRangeProcessing) { // Arrange var controller = new TestableController(); @@ -1690,7 +1711,7 @@ public void File_WithContentsAndFileDownloadName_LastModifiedAndEtag(string last var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString); // Act - var result = controller.File(fileContents, "application/pdf", "someDownloadName", lastModified, entityTag); + var result = controller.File(fileContents, "application/pdf", "someDownloadName", lastModified, entityTag, enableRangeProcessing); // Assert Assert.NotNull(result); @@ -1699,6 +1720,7 @@ public void File_WithContentsAndFileDownloadName_LastModifiedAndEtag(string last Assert.Equal("someDownloadName", result.FileDownloadName); Assert.Equal(lastModified, result.LastModified); Assert.Equal(entityTag, result.EntityTag); + Assert.Equal(enableRangeProcessing, result.EnableRangeProcessing); } [Fact] @@ -1716,14 +1738,15 @@ public void File_WithPath() Assert.Equal(path, result.FileName); Assert.Equal("application/pdf", result.ContentType.ToString()); Assert.Equal(string.Empty, result.FileDownloadName); + Assert.False(result.EnableRangeProcessing); } [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) + [InlineData(null, null, false)] + [InlineData(null, "\"Etag\"", false)] + [InlineData("05/01/2008 +1:00", null, true)] + [InlineData("05/01/2008 +1:00", "\"Etag\"", true)] + public void File_WithPath_LastModifiedAndEtag(string lastModifiedString, string entityTagString, bool enableRangeProcessing) { // Arrange var controller = new TestableController(); @@ -1732,7 +1755,7 @@ public void File_WithPath_LastModifiedAndEtag(string lastModifiedString, string var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString); // Act - var result = controller.File(path, "application/pdf", lastModified, entityTag); + var result = controller.File(path, "application/pdf", lastModified, entityTag, enableRangeProcessing); // Assert Assert.NotNull(result); @@ -1741,6 +1764,7 @@ public void File_WithPath_LastModifiedAndEtag(string lastModifiedString, string Assert.Equal(string.Empty, result.FileDownloadName); Assert.Equal(lastModified, result.LastModified); Assert.Equal(entityTag, result.EntityTag); + Assert.Equal(enableRangeProcessing, result.EnableRangeProcessing); } [Fact] @@ -1758,14 +1782,15 @@ public void File_WithPathAndFileDownloadName() Assert.Equal(path, result.FileName); Assert.Equal("application/pdf", result.ContentType.ToString()); Assert.Equal("someDownloadName", result.FileDownloadName); + Assert.False(result.EnableRangeProcessing); } [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) + [InlineData(null, null, false)] + [InlineData(null, "\"Etag\"", false)] + [InlineData("05/01/2008 +1:00", null, true)] + [InlineData("05/01/2008 +1:00", "\"Etag\"", true)] + public void File_WithPathAndFileDownloadName_LastModifiedAndEtag(string lastModifiedString, string entityTagString, bool enableRangeProcessing) { // Arrange var controller = new TestableController(); @@ -1774,7 +1799,7 @@ public void File_WithPathAndFileDownloadName_LastModifiedAndEtag(string lastModi var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString); // Act - var result = controller.File(path, "application/pdf", "someDownloadName", lastModified, entityTag); + var result = controller.File(path, "application/pdf", "someDownloadName", lastModified, entityTag, enableRangeProcessing); // Assert Assert.NotNull(result); @@ -1783,6 +1808,7 @@ public void File_WithPathAndFileDownloadName_LastModifiedAndEtag(string lastModi Assert.Equal("someDownloadName", result.FileDownloadName); Assert.Equal(lastModified, result.LastModified); Assert.Equal(entityTag, result.EntityTag); + Assert.Equal(enableRangeProcessing, result.EnableRangeProcessing); } [Fact] @@ -1805,14 +1831,15 @@ public void File_WithStream() Assert.Same(fileStream, result.FileStream); Assert.Equal("application/pdf", result.ContentType.ToString()); Assert.Equal(string.Empty, result.FileDownloadName); + Assert.False(result.EnableRangeProcessing); } [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) + [InlineData(null, null, false)] + [InlineData(null, "\"Etag\"", false)] + [InlineData("05/01/2008 +1:00", null, true)] + [InlineData("05/01/2008 +1:00", "\"Etag\"", true)] + public void File_WithStream_LastModifiedAndEtag(string lastModifiedString, string entityTagString, bool enableRangeProcessing) { // Arrange var mockHttpContext = new Mock(); @@ -1826,7 +1853,7 @@ public void File_WithStream_LastModifiedAndEtag(string lastModifiedString, strin var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString); // Act - var result = controller.File(fileStream, "application/pdf", lastModified, entityTag); + var result = controller.File(fileStream, "application/pdf", lastModified, entityTag, enableRangeProcessing); // Assert Assert.NotNull(result); @@ -1835,6 +1862,7 @@ public void File_WithStream_LastModifiedAndEtag(string lastModifiedString, strin Assert.Equal(string.Empty, result.FileDownloadName); Assert.Equal(lastModified, result.LastModified); Assert.Equal(entityTag, result.EntityTag); + Assert.Equal(enableRangeProcessing, result.EnableRangeProcessing); } [Fact] @@ -1856,14 +1884,15 @@ public void File_WithStreamAndFileDownloadName() Assert.Same(fileStream, result.FileStream); Assert.Equal("application/pdf", result.ContentType.ToString()); Assert.Equal("someDownloadName", result.FileDownloadName); + Assert.False(result.EnableRangeProcessing); } [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) + [InlineData(null, null, false)] + [InlineData(null, "\"Etag\"", false)] + [InlineData("05/01/2008 +1:00", null, true)] + [InlineData("05/01/2008 +1:00", "\"Etag\"", true)] + public void File_WithStreamAndFileDownloadName_LastModifiedAndEtag(string lastModifiedString, string entityTagString, bool enableRangeProcessing) { // Arrange var mockHttpContext = new Mock(); @@ -1876,7 +1905,7 @@ public void File_WithStreamAndFileDownloadName_LastModifiedAndEtag(string lastMo var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString); // Act - var result = controller.File(fileStream, "application/pdf", "someDownloadName", lastModified, entityTag); + var result = controller.File(fileStream, "application/pdf", "someDownloadName", lastModified, entityTag, enableRangeProcessing); // Assert Assert.NotNull(result); @@ -1885,6 +1914,7 @@ public void File_WithStreamAndFileDownloadName_LastModifiedAndEtag(string lastMo Assert.Equal("someDownloadName", result.FileDownloadName); Assert.Equal(lastModified, result.LastModified); Assert.Equal(entityTag, result.EntityTag); + Assert.Equal(enableRangeProcessing, result.EnableRangeProcessing); } [Fact] @@ -2738,7 +2768,7 @@ public void RedirectToPage_WithPageName_Handler_AndRouteValues() var controller = new TestableController(); // Act - var result = controller.RedirectToPage("page", "handler", new { test = "value"}); + var result = controller.RedirectToPage("page", "handler", new { test = "value" }); // Assert Assert.Equal("page", result.PageName); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs index ff0ee343cf..e2d0dd087d 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.TestCommon; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -111,7 +110,8 @@ public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeReque var result = new FileContentResult(byteArray, contentType) { LastModified = lastModified, - EntityTag = entityTag + EntityTag = entityTag, + EnableRangeProcessing = true, }; var httpContext = GetHttpContext(); @@ -135,18 +135,18 @@ public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeReque httpResponse.Body.Seek(0, SeekOrigin.Begin); var streamReader = new StreamReader(httpResponse.Body); var body = streamReader.ReadToEndAsync().Result; + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); 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() + public async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest() { // Arrange var contentType = "text/plain"; @@ -157,7 +157,8 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() var result = new FileContentResult(byteArray, contentType) { LastModified = lastModified, - EntityTag = entityTag + EntityTag = entityTag, + EnableRangeProcessing = true, }; var httpContext = GetHttpContext(); @@ -180,18 +181,28 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() 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); + + if (result.EnableRangeProcessing) + { + 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(5, httpResponse.ContentLength); + Assert.Equal("Hello", body); + } + else + { + Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); + Assert.Equal(11, httpResponse.ContentLength); + Assert.Equal("Hello World", body); + } } [Fact] - public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() + public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored() { // Arrange var contentType = "text/plain"; @@ -205,6 +216,48 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() 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.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); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored() + { + // 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, + EnableRangeProcessing = true, + }; + var httpContext = GetHttpContext(); var requestHeaders = httpContext.Request.GetTypedHeaders(); requestHeaders.IfMatch = new[] @@ -226,7 +279,6 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() 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); @@ -247,7 +299,8 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnore var result = new FileContentResult(byteArray, contentType) { LastModified = lastModified, - EntityTag = entityTag + EntityTag = entityTag, + EnableRangeProcessing = true, }; var httpContext = GetHttpContext(); @@ -266,7 +319,6 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnore var body = streamReader.ReadToEndAsync().Result; Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]); 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); @@ -286,7 +338,8 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotS var result = new FileContentResult(byteArray, contentType) { LastModified = lastModified, - EntityTag = entityTag + EntityTag = entityTag, + EnableRangeProcessing = true, }; var httpContext = GetHttpContext(); @@ -304,16 +357,16 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotS var streamReader = new StreamReader(httpResponse.Body); var body = streamReader.ReadToEndAsync().Result; var contentRange = new ContentRangeHeaderValue(byteArray.Length); + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); 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() + public async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored() { // Arrange var contentType = "text/plain"; @@ -324,7 +377,8 @@ public async Task WriteFileAsync_RangeRequested_PreconditionFailed() var result = new FileContentResult(byteArray, contentType) { LastModified = lastModified, - EntityTag = entityTag + EntityTag = entityTag, + EnableRangeProcessing = true, }; var httpContext = GetHttpContext(); @@ -347,7 +401,6 @@ public async Task WriteFileAsync_RangeRequested_PreconditionFailed() 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.Null(httpResponse.ContentLength); Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]); Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); @@ -355,7 +408,7 @@ public async Task WriteFileAsync_RangeRequested_PreconditionFailed() } [Fact] - public async Task WriteFileAsync_RangeRequested_NotModified() + public async Task WriteFileAsync_NotModified_RangeRequestedIgnored() { // Arrange var contentType = "text/plain"; @@ -366,7 +419,8 @@ public async Task WriteFileAsync_RangeRequested_NotModified() var result = new FileContentResult(byteArray, contentType) { LastModified = lastModified, - EntityTag = entityTag + EntityTag = entityTag, + EnableRangeProcessing = true, }; var httpContext = GetHttpContext(); @@ -389,7 +443,6 @@ public async Task WriteFileAsync_RangeRequested_NotModified() 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.Null(httpResponse.ContentLength); Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]); Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs index a520b04c38..ce8db7449e 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs @@ -297,7 +297,6 @@ public void GetPreconditionState_ShouldProcess(string ifMatch, string ifNoneMatc // Act var state = FileResultExecutorBase.GetPreconditionState( - actionContext, httpRequestHeaders, lastModified, etag); @@ -334,7 +333,6 @@ public void GetPreconditionState_ShouldNotProcess_PreconditionFailed(string ifMa // Act var state = FileResultExecutorBase.GetPreconditionState( - actionContext, httpRequestHeaders, lastModified, etag); @@ -370,7 +368,6 @@ public void GetPreconditionState_ShouldNotProcess_NotModified(string ifMatch, st // Act var state = FileResultExecutorBase.GetPreconditionState( - actionContext, httpRequestHeaders, lastModified, etag); @@ -398,7 +395,6 @@ public void IfRangeValid_IgnoreRangeRequest(string ifRangeString, bool expected) // Act var ifRangeIsValid = FileResultExecutorBase.IfRangeValid( - actionContext, httpRequestHeaders, lastModified, etag); @@ -460,7 +456,7 @@ public EmptyFileResultExecutor(ILoggerFactory loggerFactory) public Task ExecuteAsync(ActionContext context, EmptyFileResult result) { - SetHeadersAndLog(context, result, 0L); + SetHeadersAndLog(context, result, 0L, true); 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 48bd5574fa..eea79e044f 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.TestCommon; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -95,6 +94,7 @@ public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeReque { LastModified = lastModified, EntityTag = entityTag, + EnableRangeProcessing = true, }; var httpContext = GetHttpContext(); @@ -119,11 +119,11 @@ public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeReque var streamReader = new StreamReader(httpResponse.Body); var body = streamReader.ReadToEndAsync().Result; var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, byteArray.Length); + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); 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); } @@ -143,6 +143,7 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() { LastModified = lastModified, EntityTag = entityTag, + EnableRangeProcessing = true, }; var httpContext = GetHttpContext(); @@ -165,16 +166,59 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() httpResponse.Body.Seek(0, SeekOrigin.Begin); var streamReader = new StreamReader(httpResponse.Body); var body = streamReader.ReadToEndAsync().Result; + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); + var contentRange = new ContentRangeHeaderValue(0, 4, byteArray.Length); 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_RangeProcessingNotEnabled_RangeRequestedIgnored() + { + // 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(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); + } + [Fact] public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() { @@ -190,6 +234,7 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() { LastModified = lastModified, EntityTag = entityTag, + EnableRangeProcessing = true, }; var httpContext = GetHttpContext(); @@ -235,6 +280,7 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnore { LastModified = lastModified, EntityTag = entityTag, + EnableRangeProcessing = true, }; var httpContext = GetHttpContext(); @@ -254,7 +300,6 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnore var body = streamReader.ReadToEndAsync().Result; Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]); 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); @@ -276,6 +321,7 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotS { LastModified = lastModified, EntityTag = entityTag, + EnableRangeProcessing = true, }; var httpContext = GetHttpContext(); @@ -293,11 +339,12 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotS var streamReader = new StreamReader(httpResponse.Body); var body = streamReader.ReadToEndAsync().Result; var contentRange = new ContentRangeHeaderValue(byteArray.Length); + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); 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.Equal(11, httpResponse.ContentLength); Assert.Empty(body); } @@ -315,6 +362,7 @@ public async Task WriteFileAsync_RangeRequested_PreconditionFailed() { LastModified = lastModified, EntityTag = entityTag, + EnableRangeProcessing = true, }; var httpContext = GetHttpContext(); @@ -337,7 +385,6 @@ public async Task WriteFileAsync_RangeRequested_PreconditionFailed() 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.Null(httpResponse.ContentLength); Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]); Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); @@ -345,7 +392,7 @@ public async Task WriteFileAsync_RangeRequested_PreconditionFailed() } [Fact] - public async Task WriteFileAsync_RangeRequested_NotModified() + public async Task WriteFileAsync_NotModified_RangeRequestedIgnored() { // Arrange var contentType = "text/plain"; @@ -358,6 +405,7 @@ public async Task WriteFileAsync_RangeRequested_NotModified() { LastModified = lastModified, EntityTag = entityTag, + EnableRangeProcessing = true, }; var httpContext = GetHttpContext(); @@ -380,7 +428,6 @@ public async Task WriteFileAsync_RangeRequested_NotModified() 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.Null(httpResponse.ContentLength); Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]); Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); @@ -404,6 +451,7 @@ public async Task WriteFileAsync_RangeRequested_FileLengthZeroOrNull(long? fileL { LastModified = lastModified, EntityTag = entityTag, + EnableRangeProcessing = true, }; var httpContext = GetHttpContext(); @@ -425,13 +473,12 @@ public async Task WriteFileAsync_RangeRequested_FileLengthZeroOrNull(long? fileL httpResponse.Body.Seek(0, SeekOrigin.Begin); var streamReader = new StreamReader(httpResponse.Body); var body = streamReader.ReadToEndAsync().Result; - + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); 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); } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs index 683f11bf00..69dd1a4550 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.TestCommon; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -63,6 +62,7 @@ public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, st // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); var result = new TestPhysicalFileResult(path, "text/plain"); + result.EnableRangeProcessing = true; var httpContext = GetHttpContext(); var requestHeaders = httpContext.Request.GetTypedHeaders(); requestHeaders.IfModifiedSince = DateTimeOffset.MinValue; @@ -97,6 +97,7 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); var result = new TestPhysicalFileResult(path, "text/plain"); var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\""); + result.EnableRangeProcessing = true; var httpContext = GetHttpContext(); var requestHeaders = httpContext.Request.GetTypedHeaders(); requestHeaders.IfModifiedSince = DateTimeOffset.MinValue; @@ -124,12 +125,42 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() Assert.Equal("File", body); } + [Fact] + public async Task WriteFileAsync_RangeProcessingNotEnabled_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("\"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.Status200OK, httpResponse.StatusCode); + Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal("FilePathResultTestFile contents�", 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"); + result.EnableRangeProcessing = true; var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\""); var httpContext = GetHttpContext(); var requestHeaders = httpContext.Request.GetTypedHeaders(); @@ -149,7 +180,6 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() 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.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); Assert.Equal("FilePathResultTestFile contents�", body); } @@ -158,7 +188,7 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() [InlineData("0-5")] [InlineData("bytes = ")] [InlineData("bytes = 1-4, 5-11")] - public async Task WriteFileAsync_RangeRequestIgnored(string rangeString) + public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string rangeString) { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); @@ -180,7 +210,6 @@ public async Task WriteFileAsync_RangeRequestIgnored(string rangeString) 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.Empty(httpResponse.Headers[HeaderNames.ContentRange]); Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); Assert.Equal("FilePathResultTestFile contents�", body); @@ -194,6 +223,7 @@ public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); var result = new TestPhysicalFileResult(path, "text/plain"); + result.EnableRangeProcessing = true; var httpContext = GetHttpContext(); var requestHeaders = httpContext.Request.GetTypedHeaders(); requestHeaders.IfModifiedSince = DateTimeOffset.MinValue; @@ -224,6 +254,7 @@ public async Task WriteFileAsync_RangeRequested_PreconditionFailed() // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); var result = new TestPhysicalFileResult(path, "text/plain"); + result.EnableRangeProcessing = true; var httpContext = GetHttpContext(); var requestHeaders = httpContext.Request.GetTypedHeaders(); requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; @@ -241,7 +272,6 @@ public async Task WriteFileAsync_RangeRequested_PreconditionFailed() 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.Null(httpResponse.ContentLength); Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]); Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); @@ -254,6 +284,7 @@ public async Task WriteFileAsync_RangeRequested_NotModified() // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); var result = new TestPhysicalFileResult(path, "text/plain"); + result.EnableRangeProcessing = true; var httpContext = GetHttpContext(); var requestHeaders = httpContext.Request.GetTypedHeaders(); requestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1); @@ -271,7 +302,6 @@ public async Task WriteFileAsync_RangeRequested_NotModified() 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.Null(httpResponse.ContentLength); Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]); Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); @@ -321,16 +351,16 @@ public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent() } [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) + [InlineData(0, 3, 4)] + [InlineData(8, 13, 6)] + [InlineData(null, 3, 3)] + [InlineData(8, null, 26)] + public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, long contentLength) { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); var result = new TestPhysicalFileResult(path, "text/plain"); - + result.EnableRangeProcessing = true; var sendFile = new TestSendFileFeature(); var httpContext = GetHttpContext(); httpContext.Features.Set(sendFile); @@ -351,7 +381,7 @@ public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHtt 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); + 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]); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs index aaf10c7cb4..21a20fb637 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs @@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.TestCommon; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -66,6 +65,7 @@ public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, st var path = Path.GetFullPath("helllo.txt"); var contentType = "text/plain; charset=us-ascii; p1=p1-value"; var result = new TestVirtualFileResult(path, contentType); + result.EnableRangeProcessing = true; var appEnvironment = new Mock(); appEnvironment.Setup(app => app.WebRootFileProvider) .Returns(GetFileProvider(path)); @@ -110,6 +110,7 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() var path = Path.GetFullPath("helllo.txt"); var contentType = "text/plain; charset=us-ascii; p1=p1-value"; var result = new TestVirtualFileResult(path, contentType); + result.EnableRangeProcessing = true; var appEnvironment = new Mock(); appEnvironment.Setup(app => app.WebRootFileProvider) .Returns(GetFileProvider(path)); @@ -148,6 +149,47 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() Assert.Equal("File", body); } + [Fact] + public async Task WriteFileAsync_RangeProcessingNotEnabled_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, TestVirtualFileResultExecutor>() + .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.Status200OK, httpResponse.StatusCode); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); + Assert.Equal("FilePathResultTestFile contents¡", body); + } + [Fact] public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() { @@ -155,6 +197,7 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() var path = Path.GetFullPath("helllo.txt"); var contentType = "text/plain; charset=us-ascii; p1=p1-value"; var result = new TestVirtualFileResult(path, contentType); + result.EnableRangeProcessing = true; var appEnvironment = new Mock(); appEnvironment.Setup(app => app.WebRootFileProvider) .Returns(GetFileProvider(path)); @@ -186,7 +229,6 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() 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); } @@ -194,12 +236,13 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() [InlineData("0-5")] [InlineData("bytes = ")] [InlineData("bytes = 1-4, 5-11")] - public async Task WriteFileAsync_RangeRequestIgnored(string rangeString) + public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(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); + result.EnableRangeProcessing = true; var appEnvironment = new Mock(); appEnvironment.Setup(app => app.WebRootFileProvider) .Returns(GetFileProvider(path)); @@ -228,7 +271,6 @@ public async Task WriteFileAsync_RangeRequestIgnored(string rangeString) 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.Empty(httpResponse.Headers[HeaderNames.ContentRange]); Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); Assert.Equal("FilePathResultTestFile contents¡", body); @@ -243,6 +285,7 @@ public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString var path = Path.GetFullPath("helllo.txt"); var contentType = "text/plain; charset=us-ascii; p1=p1-value"; var result = new TestVirtualFileResult(path, contentType); + result.EnableRangeProcessing = true; var appEnvironment = new Mock(); appEnvironment.Setup(app => app.WebRootFileProvider) .Returns(GetFileProvider(path)); @@ -285,6 +328,7 @@ public async Task WriteFileAsync_RangeRequested_PreconditionFailed() var path = Path.GetFullPath("helllo.txt"); var contentType = "text/plain; charset=us-ascii; p1=p1-value"; var result = new TestVirtualFileResult(path, contentType); + result.EnableRangeProcessing = true; var appEnvironment = new Mock(); appEnvironment.Setup(app => app.WebRootFileProvider) .Returns(GetFileProvider(path)); @@ -313,7 +357,6 @@ public async Task WriteFileAsync_RangeRequested_PreconditionFailed() 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.Null(httpResponse.ContentLength); Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]); Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); @@ -327,6 +370,7 @@ public async Task WriteFileAsync_RangeRequested_NotModified() var path = Path.GetFullPath("helllo.txt"); var contentType = "text/plain; charset=us-ascii; p1=p1-value"; var result = new TestVirtualFileResult(path, contentType); + result.EnableRangeProcessing = true; var appEnvironment = new Mock(); appEnvironment.Setup(app => app.WebRootFileProvider) .Returns(GetFileProvider(path)); @@ -355,7 +399,6 @@ public async Task WriteFileAsync_RangeRequested_NotModified() 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.Null(httpResponse.ContentLength); Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]); Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); @@ -454,6 +497,7 @@ public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHtt var result = new TestVirtualFileResult(path, "text/plain") { FileProvider = GetFileProvider(path), + EnableRangeProcessing = true, }; var sendFile = new TestSendFileFeature(); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs index b631f22490..e5733a558e 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs @@ -2,11 +2,11 @@ // 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.Mvc.Infrastructure; using Microsoft.AspNetCore.Testing.xunit; using Xunit; @@ -189,6 +189,24 @@ public async Task FileFromDisk_ReturnsFileWithFileName() Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition); } + [Fact] + public async Task FileFromDisk_ReturnsFileWithFileName_RangeProcessingNotEnabled_RangeRequestedIgnored() + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName"); + httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6); + + // 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 FileFromDisk_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest_WithLastModifiedAndEtag() { @@ -259,11 +277,11 @@ public async Task FileFromStream_ReturnsFile_RangeRequest(long start, long end, 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(HttpStatusCode.PartialContent, response.StatusCode); Assert.Equal(expectedBody, body); } @@ -299,12 +317,12 @@ public async Task FileFromStream_ReturnsFile_RangeRequestNotSatisfiable(string r // Act var response = await Client.SendAsync(httpRequestMessage); + var body = await response.Content.ReadAsStringAsync(); // 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); } @@ -329,6 +347,24 @@ public async Task FileFromStream_ReturnsFileWithFileName() Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition); } + [Fact] + public async Task FileFromStream_ReturnsFileWithFileName_RangeProcessingNotEnabled_RangeRequestedIgnored() + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStreamWithFileName"); + httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6); + + // 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 FileFromStream_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest() { @@ -339,13 +375,13 @@ public async Task FileFromStream_ReturnsFileWithFileName_IfRangeHeaderValid_Rang // Act var response = await Client.SendAsync(httpRequestMessage); + var body = await response.Content.ReadAsStringAsync(); // 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(HttpStatusCode.PartialContent, response.StatusCode); Assert.Equal("This is", body); } @@ -361,10 +397,8 @@ public async Task FileFromStream_ReturnsFileWithFileName_IfRangeHeaderInvalid_Ra 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(HttpStatusCode.OK, response.StatusCode); Assert.Equal("This is sample text from a stream", body); } @@ -397,13 +431,13 @@ public async Task FileFromBinaryData_ReturnsFile_RangeRequest(long start, long e // Act var response = await Client.SendAsync(httpRequestMessage); + var body = await response.Content.ReadAsStringAsync(); // 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(HttpStatusCode.PartialContent, response.StatusCode); Assert.Equal(expectedBody, body); } @@ -439,12 +473,15 @@ public async Task FileFromBinaryData_ReturnsFile_RangeRequestNotSatisfiable(stri // Act var response = await Client.SendAsync(httpRequestMessage); + var body = await response.Content.ReadAsStringAsync(); // Assert + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + Assert.NotNull(body); 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); } @@ -470,22 +507,40 @@ public async Task FileFromBinaryData_ReturnsFileWithFileName() } [Fact] - public async Task FileFromBinaryData_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest() + public async Task FileFromBinaryData_ReturnsFileWithFileName_RangeProcessingNotEnabled_RangeRequestedIgnored() { // Arrange - var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryDataWithFileName_WithEtag"); + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryDataWithFileName"); 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.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 FileFromBinaryData_ReturnsFileWithFileName_IfRangeHeaderValid() + { + // 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); + var body = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); Assert.NotNull(body); + Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode); Assert.Equal("This is", body); } @@ -557,6 +612,24 @@ public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequest Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition); } + [Fact] + public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeProcessingNotEnabled_RangeRequestedIgnored() + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName_RangeProcessingNotEnabled"); + httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6); + + // 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); + } + [Fact] public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest() { diff --git a/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs b/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs index e86e211932..7b23292272 100644 --- a/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs +++ b/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs @@ -22,7 +22,7 @@ public DownloadFilesController(IHostingEnvironment hostingEnvironment) public IActionResult DownloadFromDisk() { var path = Path.Combine(_hostingEnvironment.ContentRootPath, "sample.txt"); - return PhysicalFile(path, "text/plain"); + return PhysicalFile(path, "text/plain", true); } public IActionResult DownloadFromDisk_WithLastModifiedAndEtag() @@ -30,7 +30,7 @@ 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); + return PhysicalFile(path, "text/plain", lastModified, entityTag, true); } public IActionResult DownloadFromDiskWithFileName() @@ -44,7 +44,7 @@ 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); + return PhysicalFile(path, "text/plain", "downloadName.txt", lastModified, entityTag, true); } public IActionResult DownloadFromStream() @@ -55,7 +55,7 @@ public IActionResult DownloadFromStream() writer.Flush(); stream.Seek(0, SeekOrigin.Begin); - return File(stream, "text/plain"); + return File(stream, "text/plain", true); } public IActionResult DownloadFromStreamWithFileName() @@ -77,13 +77,13 @@ public IActionResult DownloadFromStreamWithFileName_WithEtag() writer.Flush(); stream.Seek(0, SeekOrigin.Begin); var entityTag = new EntityTagHeaderValue("\"Etag\""); - return File(stream, "text/plain", "downloadName.txt", lastModified: null, entityTag: entityTag); + return File(stream, "text/plain", "downloadName.txt", lastModified: null, entityTag: entityTag, enableRangeProcessing: true); } public IActionResult DownloadFromBinaryData() { var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array"); - return File(data, "text/plain"); + return File(data, "text/plain", true); } public IActionResult DownloadFromBinaryDataWithFileName() @@ -96,7 +96,7 @@ 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); + return File(data, "text/plain", "downloadName.txt", lastModified: null, entityTag: entityTag, enableRangeProcessing: true); } } } diff --git a/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs b/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs index 758aea7b7d..64e834742d 100644 --- a/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs +++ b/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs @@ -14,7 +14,17 @@ public IActionResult DownloadFileWithFileName() return new VirtualFileResult("/Greetings.txt", "text/plain") { FileProvider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, "FilesWebSite.EmbeddedResources"), - FileDownloadName = "downloadName.txt" + FileDownloadName = "downloadName.txt", + EnableRangeProcessing = true, + }; + } + + public IActionResult DownloadFileWithFileName_RangeProcessingNotEnabled() + { + return new VirtualFileResult("/Greetings.txt", "text/plain") + { + FileProvider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, "FilesWebSite.EmbeddedResources"), + FileDownloadName = "downloadName.txt", }; } @@ -23,7 +33,8 @@ public IActionResult DownloadFileWithFileName_WithEtag() var file = new VirtualFileResult("/Greetings.txt", "text/plain") { FileProvider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, "FilesWebSite.EmbeddedResources"), - FileDownloadName = "downloadName.txt" + FileDownloadName = "downloadName.txt", + EnableRangeProcessing = true, }; file.EntityTag = new Microsoft.Net.Http.Headers.EntityTagHeaderValue("\"Etag\"");