Range support for FileResult(#6150)

Addresses #3702
This commit is contained in:
Jass Bagga 2017-05-19 10:51:46 -07:00 committed by GitHub
parent ece5e748ad
commit 9aff0a67c1
21 changed files with 2657 additions and 86 deletions

View File

@ -1135,8 +1135,10 @@ namespace Microsoft.AspNetCore.Mvc
}
/// <summary>
/// Returns a file with the specified <paramref name="fileContents" /> as content
/// (<see cref="StatusCodes.Status200OK"/>) and the specified <paramref name="contentType" /> as the Content-Type.
/// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>),
/// and the specified <paramref name="contentType" /> as the Content-Type.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="fileContents">The file contents.</param>
/// <param name="contentType">The Content-Type of the file.</param>
@ -1149,8 +1151,9 @@ namespace Microsoft.AspNetCore.Mvc
/// <summary>
/// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>), the
/// specified <paramref name="contentType" /> as the Content-Type and the
/// specified <paramref name="fileDownloadName" /> as the suggested file name.
/// specified <paramref name="contentType" /> as the Content-Type and the specified <paramref name="fileDownloadName" /> as the suggested file name.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="fileContents">The file contents.</param>
/// <param name="contentType">The Content-Type of the file.</param>
@ -1163,8 +1166,54 @@ namespace Microsoft.AspNetCore.Mvc
}
/// <summary>
/// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>)
/// with the specified <paramref name="contentType" /> as the Content-Type.
/// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>),
/// and the specified <paramref name="contentType" /> as the Content-Type.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="fileContents">The file contents.</param>
/// <param name="contentType">The Content-Type of the file.</param>
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
/// <returns>The created <see cref="FileContentResult"/> for the response.</returns>
[NonAction]
public virtual FileContentResult File(byte[] fileContents, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
{
return new FileContentResult(fileContents, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
};
}
/// <summary>
/// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>), the
/// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="fileContents">The file contents.</param>
/// <param name="contentType">The Content-Type of the file.</param>
/// <param name="fileDownloadName">The suggested file name.</param>
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
/// <returns>The created <see cref="FileContentResult"/> for the response.</returns>
[NonAction]
public virtual FileContentResult File(byte[] fileContents, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
{
return new FileContentResult(fileContents, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
FileDownloadName = fileDownloadName,
};
}
/// <summary>
/// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>), with the
/// specified <paramref name="contentType" /> as the Content-Type.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
/// <param name="contentType">The Content-Type of the file.</param>
@ -1179,6 +1228,8 @@ namespace Microsoft.AspNetCore.Mvc
/// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>) with the
/// specified <paramref name="contentType" /> as the Content-Type and the
/// specified <paramref name="fileDownloadName" /> as the suggested file name.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
/// <param name="contentType">The Content-Type of the file.</param>
@ -1190,9 +1241,55 @@ namespace Microsoft.AspNetCore.Mvc
return new FileStreamResult(fileStream, contentType) { FileDownloadName = fileDownloadName };
}
/// <summary>
/// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>),
/// and the specified <paramref name="contentType" /> as the Content-Type.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
/// <param name="contentType">The Content-Type of the file.</param>
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
/// <returns>The created <see cref="FileStreamResult"/> for the response.</returns>
[NonAction]
public virtual FileStreamResult File(Stream fileStream, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
{
return new FileStreamResult(fileStream, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
};
}
/// <summary>
/// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>), the
/// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
/// <param name="contentType">The Content-Type of the file.</param>
/// <param name="fileDownloadName">The suggested file name.</param>
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
/// <returns>The created <see cref="FileStreamResult"/> for the response.</returns>
[NonAction]
public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
{
return new FileStreamResult(fileStream, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
FileDownloadName = fileDownloadName,
};
}
/// <summary>
/// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
/// specified <paramref name="contentType" /> as the Content-Type.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="virtualPath">The virtual path of the file to be returned.</param>
/// <param name="contentType">The Content-Type of the file.</param>
@ -1207,6 +1304,8 @@ namespace Microsoft.AspNetCore.Mvc
/// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
/// specified <paramref name="contentType" /> as the Content-Type and the
/// specified <paramref name="fileDownloadName" /> as the suggested file name.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="virtualPath">The virtual path of the file to be returned.</param>
/// <param name="contentType">The Content-Type of the file.</param>
@ -1218,9 +1317,55 @@ namespace Microsoft.AspNetCore.Mvc
return new VirtualFileResult(virtualPath, contentType) { FileDownloadName = fileDownloadName };
}
/// <summary>
/// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>), and the
/// specified <paramref name="contentType" /> as the Content-Type.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="virtualPath">The virtual path of the file to be returned.</param>
/// <param name="contentType">The Content-Type of the file.</param>
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
/// <returns>The created <see cref="VirtualFileResult"/> for the response.</returns>
[NonAction]
public virtual VirtualFileResult File(string virtualPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
{
return new VirtualFileResult(virtualPath, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
};
}
/// <summary>
/// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>), the
/// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="virtualPath">The virtual path of the file to be returned.</param>
/// <param name="contentType">The Content-Type of the file.</param>
/// <param name="fileDownloadName">The suggested file name.</param>
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
/// <returns>The created <see cref="VirtualFileResult"/> for the response.</returns>
[NonAction]
public virtual VirtualFileResult File(string virtualPath, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
{
return new VirtualFileResult(virtualPath, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
FileDownloadName = fileDownloadName,
};
}
/// <summary>
/// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
/// specified <paramref name="contentType" /> as the Content-Type.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="physicalPath">The physical path of the file to be returned.</param>
/// <param name="contentType">The Content-Type of the file.</param>
@ -1235,6 +1380,8 @@ namespace Microsoft.AspNetCore.Mvc
/// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
/// specified <paramref name="contentType" /> as the Content-Type and the
/// specified <paramref name="fileDownloadName" /> as the suggested file name.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="physicalPath">The physical path of the file to be returned.</param>
/// <param name="contentType">The Content-Type of the file.</param>
@ -1249,6 +1396,50 @@ namespace Microsoft.AspNetCore.Mvc
return new PhysicalFileResult(physicalPath, contentType) { FileDownloadName = fileDownloadName };
}
/// <summary>
/// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>), and
/// the specified <paramref name="contentType" /> as the Content-Type.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="physicalPath">The physical path of the file to be returned.</param>
/// <param name="contentType">The Content-Type of the file.</param>
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
/// <returns>The created <see cref="PhysicalFileResult"/> for the response.</returns>
[NonAction]
public virtual PhysicalFileResult PhysicalFile(string physicalPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
{
return new PhysicalFileResult(physicalPath, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
};
}
/// <summary>
/// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>), the
/// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
/// </summary>
/// <param name="physicalPath">The physical path of the file to be returned.</param>
/// <param name="contentType">The Content-Type of the file.</param>
/// <param name="fileDownloadName">The suggested file name.</param>
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
/// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
/// <returns>The created <see cref="PhysicalFileResult"/> for the response.</returns>
[NonAction]
public virtual PhysicalFileResult PhysicalFile(string physicalPath, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
{
return new PhysicalFileResult(physicalPath, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
FileDownloadName = fileDownloadName,
};
}
/// <summary>
/// Creates an <see cref="UnauthorizedResult"/> that produces an <see cref="StatusCodes.Status401Unauthorized"/> response.
/// </summary>

View File

@ -3,8 +3,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Net.Http.Headers;

View File

@ -2,8 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc
{
@ -43,5 +42,15 @@ namespace Microsoft.AspNetCore.Mvc
get { return _fileDownloadName ?? string.Empty; }
set { _fileDownloadName = value; }
}
/// <summary>
/// Gets or sets the last modified information associated with the <see cref="FileResult"/>.
/// </summary>
public DateTimeOffset? LastModified { get; set; }
/// <summary>
/// Gets or sets the etag associated with the <see cref="FileResult"/>.
/// </summary>
public EntityTagHeaderValue EntityTag { get; set; }
}
}

View File

@ -2,8 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.Internal
{
@ -26,11 +28,22 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
SetHeadersAndLog(context, result);
return WriteFileAsync(context, result);
var (range, rangeLength, serveBody) = SetHeadersAndLog(
context,
result,
result.FileContents.Length,
result.LastModified,
result.EntityTag);
if (!serveBody)
{
return Task.CompletedTask;
}
return WriteFileAsync(context, result, range, rangeLength);
}
protected virtual Task WriteFileAsync(ActionContext context, FileContentResult result)
protected virtual Task WriteFileAsync(ActionContext context, FileContentResult result, RangeItemHeaderValue range, long rangeLength)
{
if (context == null)
{
@ -42,9 +55,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
var response = context.HttpContext.Response;
if (range != null && rangeLength == 0)
{
return Task.CompletedTask;
}
return response.Body.WriteAsync(result.FileContents, offset: 0, count: result.FileContents.Length);
var response = context.HttpContext.Response;
var outputStream = response.Body;
var fileContentsStream = new MemoryStream(result.FileContents);
return WriteFileAsync(context.HttpContext, fileContentsStream, range, rangeLength);
}
}
}

View File

@ -2,6 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
@ -9,20 +17,38 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
public class FileResultExecutorBase
{
private const string AcceptRangeHeaderValue = "bytes";
// default buffer size as defined in BufferedStream type
protected const int BufferSize = 0x1000;
public FileResultExecutorBase(ILogger logger)
{
Logger = logger;
}
internal enum PreconditionState
{
Unspecified,
NotModified,
ShouldProcess,
PreconditionFailed,
IgnoreRangeRequest
}
protected ILogger Logger { get; }
protected virtual void SetHeadersAndLog(ActionContext context, FileResult result)
protected virtual (RangeItemHeaderValue range, long rangeLength, bool serveBody) SetHeadersAndLog(
ActionContext context,
FileResult result, long?
fileLength, DateTimeOffset?
lastModified = null,
EntityTagHeaderValue etag = null)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (result == null)
{
throw new ArgumentNullException(nameof(result));
@ -31,9 +57,56 @@ namespace Microsoft.AspNetCore.Mvc.Internal
SetContentType(context, result);
SetContentDispositionHeader(context, result);
Logger.FileResultExecuting(result.FileDownloadName);
if (fileLength.HasValue)
{
SetAcceptRangeHeader(context);
}
var request = context.HttpContext.Request;
var httpRequestHeaders = request.GetTypedHeaders();
var response = context.HttpContext.Response;
var httpResponseHeaders = response.GetTypedHeaders();
if (lastModified.HasValue)
{
httpResponseHeaders.LastModified = lastModified;
}
if (etag != null)
{
httpResponseHeaders.ETag = etag;
}
var serveBody = !HttpMethods.IsHead(request.Method);
if (HttpMethods.IsHead(request.Method) || HttpMethods.IsGet(request.Method))
{
var preconditionState = GetPreconditionState(context, httpRequestHeaders, lastModified, etag);
if (request.Headers.ContainsKey(HeaderNames.Range) &&
(preconditionState == PreconditionState.Unspecified ||
preconditionState == PreconditionState.ShouldProcess))
{
return SetRangeHeaders(context, httpRequestHeaders, fileLength, lastModified, etag);
}
if (preconditionState == PreconditionState.NotModified)
{
serveBody = false;
response.StatusCode = StatusCodes.Status304NotModified;
}
else if (preconditionState == PreconditionState.PreconditionFailed)
{
serveBody = false;
response.StatusCode = StatusCodes.Status412PreconditionFailed;
}
}
return (range: null, rangeLength: 0, serveBody: serveBody);
}
private void SetContentDispositionHeader(ActionContext context, FileResult result)
private static void SetContentType(ActionContext context, FileResult result)
{
var response = context.HttpContext.Response;
response.ContentType = result.ContentType;
}
private static void SetContentDispositionHeader(ActionContext context, FileResult result)
{
if (!string.IsNullOrEmpty(result.FileDownloadName))
{
@ -48,10 +121,199 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
private void SetContentType(ActionContext context, FileResult result)
private static void SetAcceptRangeHeader(ActionContext context)
{
var response = context.HttpContext.Response;
response.ContentType = result.ContentType;
response.Headers[HeaderNames.AcceptRanges] = AcceptRangeHeaderValue;
}
private static PreconditionState GetEtagMatchState(
IList<EntityTagHeaderValue> etagHeader,
EntityTagHeaderValue etag,
PreconditionState matchFoundState,
PreconditionState matchNotFoundState)
{
if (etagHeader != null && etagHeader.Any())
{
var state = matchNotFoundState;
foreach (var entityTag in etagHeader)
{
if (entityTag.Equals(EntityTagHeaderValue.Any) || entityTag.Compare(etag, useStrongComparison: true))
{
state = matchFoundState;
break;
}
}
return state;
}
return PreconditionState.Unspecified;
}
// Internal for testing
internal static PreconditionState GetPreconditionState(
ActionContext context,
RequestHeaders httpRequestHeaders,
DateTimeOffset? lastModified = null,
EntityTagHeaderValue etag = null)
{
var ifMatchState = PreconditionState.Unspecified;
var ifNoneMatchState = PreconditionState.Unspecified;
var ifModifiedSinceState = PreconditionState.Unspecified;
var ifUnmodifiedSinceState = PreconditionState.Unspecified;
var ifRangeState = PreconditionState.Unspecified;
// 14.24 If-Match
var ifMatch = httpRequestHeaders.IfMatch;
if (etag != null)
{
ifMatchState = GetEtagMatchState(
etagHeader: ifMatch,
etag: etag,
matchFoundState: PreconditionState.ShouldProcess,
matchNotFoundState: PreconditionState.PreconditionFailed);
}
// 14.26 If-None-Match
var ifNoneMatch = httpRequestHeaders.IfNoneMatch;
if (etag != null)
{
ifNoneMatchState = GetEtagMatchState(
etagHeader: ifNoneMatch,
etag: etag,
matchFoundState: PreconditionState.NotModified,
matchNotFoundState: PreconditionState.ShouldProcess);
}
var now = DateTimeOffset.UtcNow;
// 14.25 If-Modified-Since
var ifModifiedSince = httpRequestHeaders.IfModifiedSince;
if (lastModified.HasValue && ifModifiedSince.HasValue && ifModifiedSince <= now)
{
var modified = ifModifiedSince < lastModified;
ifModifiedSinceState = modified ? PreconditionState.ShouldProcess : PreconditionState.NotModified;
}
// 14.28 If-Unmodified-Since
var ifUnmodifiedSince = httpRequestHeaders.IfUnmodifiedSince;
if (lastModified.HasValue && ifUnmodifiedSince.HasValue && ifUnmodifiedSince <= now)
{
var unmodified = ifUnmodifiedSince >= lastModified;
ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed;
}
var ifRange = httpRequestHeaders.IfRange;
if (ifRange != null)
{
// If the validator given in the If-Range header field matches the
// current validator for the selected representation of the target
// resource, then the server SHOULD process the Range header field as
// requested. If the validator does not match, the server MUST ignore
// the Range header field.
if (ifRange.LastModified.HasValue)
{
if (lastModified.HasValue && lastModified > ifRange.LastModified)
{
ifRangeState = PreconditionState.IgnoreRangeRequest;
}
}
else if (etag != null && ifRange.EntityTag != null && !ifRange.EntityTag.Compare(etag, useStrongComparison: true))
{
ifRangeState = PreconditionState.IgnoreRangeRequest;
}
}
var state = GetMaxPreconditionState(ifMatchState, ifNoneMatchState, ifModifiedSinceState, ifUnmodifiedSinceState, ifRangeState);
return state;
}
private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states)
{
var max = PreconditionState.Unspecified;
for (var i = 0; i < states.Length; i++)
{
if (states[i] > max)
{
max = states[i];
}
}
return max;
}
private static (RangeItemHeaderValue range, long rangeLength, bool serveBody) SetRangeHeaders(
ActionContext context,
RequestHeaders httpRequestHeaders,
long? fileLength,
DateTimeOffset? lastModified = null,
EntityTagHeaderValue etag = null)
{
var response = context.HttpContext.Response;
var httpResponseHeaders = response.GetTypedHeaders();
// Checked for presence of Range header explicitly before calling this method.
// Range may be null for parsing errors, multiple ranges and when the file length is missing.
var range = fileLength.HasValue ? ParseRange(context, httpRequestHeaders, fileLength.Value, lastModified, etag) : null;
if (range == null)
{
// 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable)
// SHOULD include a Content-Range field with a byte-range-resp-spec of "*". The instance-length specifies
// the current length of the selected resource. e.g. */length
response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
if (fileLength.HasValue)
{
httpResponseHeaders.ContentRange = new ContentRangeHeaderValue(fileLength.Value);
}
return (range: null, rangeLength: 0, serveBody: false);
}
httpResponseHeaders.ContentRange = new ContentRangeHeaderValue(
range.From.Value,
range.To.Value,
fileLength.Value);
response.StatusCode = StatusCodes.Status206PartialContent;
var rangeLength = SetContentLength(context, range);
return (range, rangeLength, serveBody: true);
}
private static long SetContentLength(ActionContext context, RangeItemHeaderValue range)
{
var start = range.From.Value;
var end = range.To.Value;
var length = end - start + 1;
var response = context.HttpContext.Response;
response.ContentLength = length;
return length;
}
private static RangeItemHeaderValue ParseRange(
ActionContext context,
RequestHeaders httpRequestHeaders,
long fileLength,
DateTimeOffset? lastModified = null,
EntityTagHeaderValue etag = null)
{
var httpContext = context.HttpContext;
var response = httpContext.Response;
var range = RangeHelper.ParseRange(httpContext, httpRequestHeaders, lastModified, etag);
if (range != null)
{
var normalizedRanges = RangeHelper.NormalizeRanges(range, fileLength);
if (normalizedRanges == null || normalizedRanges.Count == 0)
{
return null;
}
return normalizedRanges.Single();
}
return null;
}
protected static ILogger CreateLogger<T>(ILoggerFactory factory)
@ -63,5 +325,31 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return factory.CreateLogger<T>();
}
protected static async Task WriteFileAsync(HttpContext context, Stream fileStream, RangeItemHeaderValue range, long rangeLength)
{
var outputStream = context.Response.Body;
using (fileStream)
{
try
{
if (range == null)
{
await StreamCopyOperation.CopyToAsync(fileStream, outputStream, count: null, bufferSize: BufferSize, cancel: context.RequestAborted);
}
else
{
fileStream.Seek(range.From.Value, SeekOrigin.Begin);
await StreamCopyOperation.CopyToAsync(fileStream, outputStream, rangeLength, BufferSize, context.RequestAborted);
}
}
catch (OperationCanceledException)
{
// Don't throw this exception, it's most likely caused by the client disconnecting.
// However, if it was cancelled for any other reason we need to prevent empty responses.
context.Abort();
}
}
}
}
}

View File

@ -4,14 +4,12 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class FileStreamResultExecutor : FileResultExecutorBase
{
// default buffer size as defined in BufferedStream type
private const int BufferSize = 0x1000;
public FileStreamResultExecutor(ILoggerFactory loggerFactory)
: base(CreateLogger<VirtualFileResultExecutor>(loggerFactory))
{
@ -29,11 +27,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
SetHeadersAndLog(context, result);
return WriteFileAsync(context, result);
long? fileLength = null;
if (result.FileStream.CanSeek)
{
fileLength = result.FileStream.Length;
}
var (range, rangeLength, serveBody) = SetHeadersAndLog(
context,
result,
fileLength,
result.LastModified,
result.EntityTag);
if (!serveBody)
{
return Task.CompletedTask;
}
return WriteFileAsync(context, result, range, rangeLength);
}
protected virtual async Task WriteFileAsync(ActionContext context, FileStreamResult result)
protected virtual Task WriteFileAsync(ActionContext context, FileStreamResult result, RangeItemHeaderValue range, long rangeLength)
{
if (context == null)
{
@ -45,13 +60,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
if (range != null && rangeLength == 0)
{
return Task.CompletedTask;
}
var response = context.HttpContext.Response;
var outputStream = response.Body;
using (result.FileStream)
{
await result.FileStream.CopyToAsync(outputStream, BufferSize);
}
return WriteFileAsync(context.HttpContext, result.FileStream, range, rangeLength);
}
}
}

View File

@ -8,13 +8,12 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class PhysicalFileResultExecutor : FileResultExecutorBase
{
private const int DefaultBufferSize = 0x1000;
public PhysicalFileResultExecutor(ILoggerFactory loggerFactory)
: base(CreateLogger<PhysicalFileResultExecutor>(loggerFactory))
{
@ -32,11 +31,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
SetHeadersAndLog(context, result);
return WriteFileAsync(context, result);
var fileInfo = GetFileInfo(result.FileName);
if (!fileInfo.Exists)
{
throw new FileNotFoundException(
Resources.FormatFileResult_InvalidPath(result.FileName), result.FileName);
}
var lastModified = result.LastModified ?? fileInfo.LastModified;
var (range, rangeLength, serveBody) = SetHeadersAndLog(
context,
result,
fileInfo.Length,
lastModified,
result.EntityTag);
if (serveBody)
{
return WriteFileAsync(context, result, range, rangeLength);
}
return Task.CompletedTask;
}
protected virtual async Task WriteFileAsync(ActionContext context, PhysicalFileResult result)
protected virtual Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue range, long rangeLength)
{
if (context == null)
{
@ -48,8 +66,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
var response = context.HttpContext.Response;
if (range != null && rangeLength == 0)
{
return Task.CompletedTask;
}
var response = context.HttpContext.Response;
if (!Path.IsPathRooted(result.FileName))
{
throw new NotSupportedException(Resources.FormatFileResult_PathNotRooted(result.FileName));
@ -58,21 +80,23 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var sendFile = response.HttpContext.Features.Get<IHttpSendFileFeature>();
if (sendFile != null)
{
await sendFile.SendFileAsync(
if (range != null)
{
return sendFile.SendFileAsync(
result.FileName,
offset: range.From ?? 0L,
count: rangeLength,
cancellation: default(CancellationToken));
}
return sendFile.SendFileAsync(
result.FileName,
offset: 0,
count: null,
cancellation: default(CancellationToken));
}
else
{
var fileStream = GetFileStream(result.FileName);
using (fileStream)
{
await fileStream.CopyToAsync(response.Body, DefaultBufferSize);
}
}
return WriteFileAsync(context.HttpContext, GetFileStream(result.FileName), range, rangeLength);
}
protected virtual Stream GetFileStream(string path)
@ -87,8 +111,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
DefaultBufferSize,
BufferSize,
FileOptions.Asynchronous | FileOptions.SequentialScan);
}
protected virtual FileMetadata GetFileInfo(string path)
{
var fileInfo = new FileInfo(path);
return new FileMetadata
{
Exists = fileInfo.Exists,
Length = fileInfo.Length,
LastModified = fileInfo.LastWriteTimeUtc,
};
}
protected class FileMetadata
{
public bool Exists { get; set; }
public long Length { get; set; }
public DateTimeOffset LastModified { get; set; }
}
}
}

View File

@ -10,12 +10,12 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class VirtualFileResultExecutor : FileResultExecutorBase
{
private const int DefaultBufferSize = 0x1000;
private readonly IHostingEnvironment _hostingEnvironment;
public VirtualFileResultExecutor(ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment)
@ -41,11 +41,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
SetHeadersAndLog(context, result);
return WriteFileAsync(context, result);
var fileInfo = GetFileInformation(result);
if (!fileInfo.Exists)
{
throw new FileNotFoundException(
Resources.FormatFileResult_InvalidPath(result.FileName), result.FileName);
}
var lastModified = result.LastModified ?? fileInfo.LastModified;
var (range, rangeLength, serveBody) = SetHeadersAndLog(
context,
result,
fileInfo.Length,
lastModified,
result.EntityTag);
if (serveBody)
{
return WriteFileAsync(context, result, fileInfo, range, rangeLength);
}
return Task.CompletedTask;
}
protected virtual async Task WriteFileAsync(ActionContext context, VirtualFileResult result)
protected virtual Task WriteFileAsync(ActionContext context, VirtualFileResult result, IFileInfo fileInfo, RangeItemHeaderValue range, long rangeLength)
{
if (context == null)
{
@ -57,7 +76,37 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(result));
}
if (range != null && rangeLength == 0)
{
return Task.CompletedTask;
}
var response = context.HttpContext.Response;
var physicalPath = fileInfo.PhysicalPath;
var sendFile = response.HttpContext.Features.Get<IHttpSendFileFeature>();
if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
{
if (range != null)
{
return sendFile.SendFileAsync(
physicalPath,
offset: range.From ?? 0L,
count: rangeLength,
cancellation: default(CancellationToken));
}
return sendFile.SendFileAsync(
physicalPath,
offset: 0,
count: null,
cancellation: default(CancellationToken));
}
return WriteFileAsync(context.HttpContext, GetFileStream(fileInfo), range, rangeLength);
}
private IFileInfo GetFileInformation(VirtualFileResult result)
{
var fileProvider = GetFileProvider(result);
var normalizedPath = result.FileName;
@ -67,32 +116,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
var fileInfo = fileProvider.GetFileInfo(normalizedPath);
if (fileInfo.Exists)
{
var physicalPath = fileInfo.PhysicalPath;
var sendFile = response.HttpContext.Features.Get<IHttpSendFileFeature>();
if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
{
await sendFile.SendFileAsync(
physicalPath,
offset: 0,
count: null,
cancellation: default(CancellationToken));
}
else
{
var fileStream = GetFileStream(fileInfo);
using (fileStream)
{
await fileStream.CopyToAsync(response.Body, DefaultBufferSize);
}
}
}
else
{
throw new FileNotFoundException(
Resources.FormatFileResult_InvalidPath(result.FileName), result.FileName);
}
return fileInfo;
}
private IFileProvider GetFileProvider(VirtualFileResult result)
@ -103,7 +127,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
result.FileProvider = _hostingEnvironment.WebRootFileProvider;
return result.FileProvider;
}

View File

@ -25,6 +25,8 @@ Microsoft.AspNetCore.Mvc.RouteAttribute</Description>
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.RangeHelper.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.ResponseCaching.Abstractions" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Routing" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.ClosedGenericMatcher.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
@ -37,6 +39,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute</Description>
<PackageReference Include="Microsoft.Extensions.PropertyActivator.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.PropertyHelper.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.SecurityHelper.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
<PackageReference Include="System.ValueTuple" Version="$(CoreFxVersion)" />
</ItemGroup>
</Project>

View File

@ -1427,6 +1427,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.Equal(string.Empty, result.FileDownloadName);
}
[Theory]
[InlineData(null, null)]
[InlineData(null, "\"Etag\"")]
[InlineData("05/01/2008 +1:00", null)]
[InlineData("05/01/2008 +1:00", "\"Etag\"")]
public void File_WithContents_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
{
// Arrange
var controller = new TestableController();
var fileContents = new byte[0];
var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
// Act
var result = controller.File(fileContents, "application/pdf", lastModified, entityTag);
// Assert
Assert.NotNull(result);
Assert.Same(fileContents, result.FileContents);
Assert.Equal("application/pdf", result.ContentType.ToString());
Assert.Equal(string.Empty, result.FileDownloadName);
Assert.Equal(lastModified, result.LastModified);
Assert.Equal(entityTag, result.EntityTag);
}
[Fact]
public void File_WithContentsAndFileDownloadName()
{
@ -1444,6 +1469,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.Equal("someDownloadName", result.FileDownloadName);
}
[Theory]
[InlineData(null, null)]
[InlineData(null, "\"Etag\"")]
[InlineData("05/01/2008 +1:00", null)]
[InlineData("05/01/2008 +1:00", "\"Etag\"")]
public void File_WithContentsAndFileDownloadName_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
{
// Arrange
var controller = new TestableController();
var fileContents = new byte[0];
var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
// Act
var result = controller.File(fileContents, "application/pdf", "someDownloadName", lastModified, entityTag);
// Assert
Assert.NotNull(result);
Assert.Same(fileContents, result.FileContents);
Assert.Equal("application/pdf", result.ContentType.ToString());
Assert.Equal("someDownloadName", result.FileDownloadName);
Assert.Equal(lastModified, result.LastModified);
Assert.Equal(entityTag, result.EntityTag);
}
[Fact]
public void File_WithPath()
{
@ -1461,6 +1511,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.Equal(string.Empty, result.FileDownloadName);
}
[Theory]
[InlineData(null, null)]
[InlineData(null, "\"Etag\"")]
[InlineData("05/01/2008 +1:00", null)]
[InlineData("05/01/2008 +1:00", "\"Etag\"")]
public void File_WithPath_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
{
// Arrange
var controller = new TestableController();
var path = Path.GetFullPath("somepath");
var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
// Act
var result = controller.File(path, "application/pdf", lastModified, entityTag);
// Assert
Assert.NotNull(result);
Assert.Equal(path, result.FileName);
Assert.Equal("application/pdf", result.ContentType.ToString());
Assert.Equal(string.Empty, result.FileDownloadName);
Assert.Equal(lastModified, result.LastModified);
Assert.Equal(entityTag, result.EntityTag);
}
[Fact]
public void File_WithPathAndFileDownloadName()
{
@ -1478,6 +1553,31 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.Equal("someDownloadName", result.FileDownloadName);
}
[Theory]
[InlineData(null, null)]
[InlineData(null, "\"Etag\"")]
[InlineData("05/01/2008 +1:00", null)]
[InlineData("05/01/2008 +1:00", "\"Etag\"")]
public void File_WithPathAndFileDownloadName_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
{
// Arrange
var controller = new TestableController();
var path = Path.GetFullPath("somepath");
var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
// Act
var result = controller.File(path, "application/pdf", "someDownloadName", lastModified, entityTag);
// Assert
Assert.NotNull(result);
Assert.Equal(path, result.FileName);
Assert.Equal("application/pdf", result.ContentType.ToString());
Assert.Equal("someDownloadName", result.FileDownloadName);
Assert.Equal(lastModified, result.LastModified);
Assert.Equal(entityTag, result.EntityTag);
}
[Fact]
public void File_WithStream()
{
@ -1500,6 +1600,36 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.Equal(string.Empty, result.FileDownloadName);
}
[Theory]
[InlineData(null, null)]
[InlineData(null, "\"Etag\"")]
[InlineData("05/01/2008 +1:00", null)]
[InlineData("05/01/2008 +1:00", "\"Etag\"")]
public void File_WithStream_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
{
// Arrange
var mockHttpContext = new Mock<HttpContext>();
mockHttpContext.Setup(x => x.Response.RegisterForDispose(It.IsAny<IDisposable>()));
var controller = new TestableController();
controller.ControllerContext.HttpContext = mockHttpContext.Object;
var fileStream = Stream.Null;
var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
// Act
var result = controller.File(fileStream, "application/pdf", lastModified, entityTag);
// Assert
Assert.NotNull(result);
Assert.Same(fileStream, result.FileStream);
Assert.Equal("application/pdf", result.ContentType.ToString());
Assert.Equal(string.Empty, result.FileDownloadName);
Assert.Equal(lastModified, result.LastModified);
Assert.Equal(entityTag, result.EntityTag);
}
[Fact]
public void File_WithStreamAndFileDownloadName()
{
@ -1521,6 +1651,35 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.Equal("someDownloadName", result.FileDownloadName);
}
[Theory]
[InlineData(null, null)]
[InlineData(null, "\"Etag\"")]
[InlineData("05/01/2008 +1:00", null)]
[InlineData("05/01/2008 +1:00", "\"Etag\"")]
public void File_WithStreamAndFileDownloadName_LastModifiedAndEtag(string lastModifiedString, string entityTagString)
{
// Arrange
var mockHttpContext = new Mock<HttpContext>();
var controller = new TestableController();
controller.ControllerContext.HttpContext = mockHttpContext.Object;
var fileStream = Stream.Null;
var lastModified = (lastModifiedString == null) ? (DateTimeOffset?)null : DateTimeOffset.Parse(lastModifiedString);
var entityTag = (entityTagString == null) ? null : new EntityTagHeaderValue(entityTagString);
// Act
var result = controller.File(fileStream, "application/pdf", "someDownloadName", lastModified, entityTag);
// Assert
Assert.NotNull(result);
Assert.Same(fileStream, result.FileStream);
Assert.Equal("application/pdf", result.ContentType.ToString());
Assert.Equal("someDownloadName", result.FileDownloadName);
Assert.Equal(lastModified, result.LastModified);
Assert.Equal(entityTag, result.EntityTag);
}
[Fact]
public void HttpUnauthorized_SetsStatusCode()
{

View File

@ -1,7 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
@ -11,6 +13,7 @@ using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNetCore.Mvc
@ -46,6 +49,29 @@ namespace Microsoft.AspNetCore.Mvc
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
}
[Fact]
public void Constructor_SetsLastModifiedAndEtag()
{
// Arrange
var fileContents = new byte[0];
var contentType = "text/plain";
var expectedMediaType = contentType;
var lastModified = new DateTimeOffset();
var entityTag = new EntityTagHeaderValue("\"Etag\"");
// Act
var result = new FileContentResult(fileContents, contentType)
{
LastModified = lastModified,
EntityTag = entityTag
};
// Assert
Assert.Equal(lastModified, result.LastModified);
Assert.Equal(entityTag, result.EntityTag);
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
}
[Fact]
public async Task WriteFileAsync_CopiesBuffer_ToOutputStream()
{
@ -68,6 +94,268 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Equal(buffer, outStream.ToArray());
}
[Theory]
[InlineData(0, 4, "Hello", 5)]
[InlineData(6, 10, "World", 5)]
[InlineData(null, 5, "World", 5)]
[InlineData(6, null, "World", 5)]
public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
{
// Arrange
var contentType = "text/plain";
var lastModified = new DateTimeOffset();
var entityTag = new EntityTagHeaderValue("\"Etag\"");
var byteArray = Encoding.ASCII.GetBytes("Hello World");
var result = new FileContentResult(byteArray, contentType)
{
LastModified = lastModified,
EntityTag = entityTag
};
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.Range = new RangeHeaderValue(start, end);
requestHeaders.IfMatch = new[]
{
new EntityTagHeaderValue("\"Etag\""),
};
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
start = start ?? 11 - end;
end = start + contentLength - 1;
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, byteArray.Length);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal(contentLength, httpResponse.ContentLength);
Assert.Equal(expectedString, body);
}
[Fact]
public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
{
// Arrange
var contentType = "text/plain";
var lastModified = DateTimeOffset.MinValue;
var entityTag = new EntityTagHeaderValue("\"Etag\"");
var byteArray = Encoding.ASCII.GetBytes("Hello World");
var result = new FileContentResult(byteArray, contentType)
{
LastModified = lastModified,
EntityTag = entityTag
};
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfMatch = new[]
{
new EntityTagHeaderValue("\"Etag\""),
};
requestHeaders.Range = new RangeHeaderValue(0, 4);
requestHeaders.IfRange = new RangeConditionHeaderValue(DateTimeOffset.MinValue);
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
var contentRange = new ContentRangeHeaderValue(0, 4, byteArray.Length);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal(5, httpResponse.ContentLength);
Assert.Equal("Hello", body);
}
[Fact]
public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
{
// Arrange
var contentType = "text/plain";
var lastModified = DateTimeOffset.MinValue.AddDays(1);
var entityTag = new EntityTagHeaderValue("\"Etag\"");
var byteArray = Encoding.ASCII.GetBytes("Hello World");
var result = new FileContentResult(byteArray, contentType)
{
LastModified = lastModified,
EntityTag = entityTag
};
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfMatch = new[]
{
new EntityTagHeaderValue("\"Etag\""),
};
requestHeaders.Range = new RangeHeaderValue(0, 4);
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal("Hello World", body);
}
[Theory]
[InlineData("0-5")]
[InlineData("bytes = 11-0")]
[InlineData("bytes = 1-4, 5-11")]
public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
{
// Arrange
var contentType = "text/plain";
var lastModified = new DateTimeOffset();
var entityTag = new EntityTagHeaderValue("\"Etag\"");
var byteArray = Encoding.ASCII.GetBytes("Hello World");
var result = new FileContentResult(byteArray, contentType)
{
LastModified = lastModified,
EntityTag = entityTag
};
var httpContext = GetHttpContext();
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
var contentRange = new ContentRangeHeaderValue(byteArray.Length);
Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Empty(body);
}
[Fact]
public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
{
// Arrange
var contentType = "text/plain";
var lastModified = new DateTimeOffset();
var entityTag = new EntityTagHeaderValue("\"Etag\"");
var byteArray = Encoding.ASCII.GetBytes("Hello World");
var result = new FileContentResult(byteArray, contentType)
{
LastModified = lastModified,
EntityTag = entityTag
};
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfMatch = new[]
{
new EntityTagHeaderValue("\"NotEtag\""),
};
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Null(httpResponse.ContentLength);
Assert.Empty(body);
}
[Fact]
public async Task WriteFileAsync_RangeRequested_NotModified()
{
// Arrange
var contentType = "text/plain";
var lastModified = new DateTimeOffset();
var entityTag = new EntityTagHeaderValue("\"Etag\"");
var byteArray = Encoding.ASCII.GetBytes("Hello World");
var result = new FileContentResult(byteArray, contentType)
{
LastModified = lastModified,
EntityTag = entityTag
};
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfNoneMatch = new[]
{
new EntityTagHeaderValue("\"Etag\""),
};
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Null(httpResponse.ContentLength);
Assert.Empty(body);
}
[Fact]
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
{

View File

@ -250,6 +250,160 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Equal(expectedOutput, actual);
}
[Fact]
public async Task SetsAcceptRangeHeader()
{
// Arrange
var httpContext = GetHttpContext();
var actionContext = CreateActionContext(httpContext);
var result = new EmptyFileResult("application/my-type");
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
Assert.Equal("bytes", httpContext.Response.Headers[HeaderNames.AcceptRanges]);
}
[Theory]
[InlineData("\"Etag\"", "\"NotEtag\"", "\"Etag\"")]
[InlineData("\"Etag\"", null, null)]
[InlineData(null, "\"NotEtag\"", "\"Etag\"")]
public void GetPreconditionState_ShouldProcess(string ifMatch, string ifNoneMatch, string ifRange)
{
// Arrange
var actionContext = new ActionContext();
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = HttpMethods.Get;
var httpRequestHeaders = httpContext.Request.GetTypedHeaders();
var lastModified = DateTimeOffset.MinValue;
lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
var etag = new EntityTagHeaderValue("\"Etag\"");
httpRequestHeaders.IfMatch = ifMatch == null ? null : new[]
{
new EntityTagHeaderValue(ifMatch),
};
httpRequestHeaders.IfNoneMatch = ifNoneMatch == null ? null : new[]
{
new EntityTagHeaderValue(ifNoneMatch),
};
httpRequestHeaders.IfRange = ifRange == null ? null : new RangeConditionHeaderValue(ifRange);
httpRequestHeaders.IfUnmodifiedSince = lastModified;
httpRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1);
actionContext.HttpContext = httpContext;
// Act
var state = FileResultExecutorBase.GetPreconditionState(
actionContext,
httpRequestHeaders,
lastModified,
etag);
// Assert
Assert.Equal(FileResultExecutorBase.PreconditionState.ShouldProcess, state);
}
[Theory]
[InlineData("\"NotEtag\"", null)]
[InlineData("\"Etag\"", "\"Etag\"")]
[InlineData(null, null)]
public void GetPreconditionState_ShouldNotProcess_PreconditionFailed(string ifMatch, string ifNoneMatch)
{
// Arrange
var actionContext = new ActionContext();
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = HttpMethods.Delete;
var httpRequestHeaders = httpContext.Request.GetTypedHeaders();
var lastModified = DateTimeOffset.MinValue.AddDays(1);
var etag = new EntityTagHeaderValue("\"Etag\"");
httpRequestHeaders.IfMatch = ifMatch == null ? null : new[]
{
new EntityTagHeaderValue(ifMatch),
};
httpRequestHeaders.IfNoneMatch = ifNoneMatch == null ? null : new[]
{
new EntityTagHeaderValue(ifNoneMatch),
};
httpRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue;
httpRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(2);
actionContext.HttpContext = httpContext;
// Act
var state = FileResultExecutorBase.GetPreconditionState(
actionContext,
httpRequestHeaders,
lastModified,
etag);
// Assert
Assert.Equal(FileResultExecutorBase.PreconditionState.PreconditionFailed, state);
}
[Theory]
[InlineData(null, "\"Etag\"")]
[InlineData(null, null)]
public void GetPreconditionState_ShouldNotProcess_NotModified(string ifMatch, string ifNoneMatch)
{
// Arrange
var actionContext = new ActionContext();
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = HttpMethods.Get;
var httpRequestHeaders = httpContext.Request.GetTypedHeaders();
var lastModified = DateTimeOffset.MinValue;
lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
var etag = new EntityTagHeaderValue("\"Etag\"");
httpRequestHeaders.IfMatch = ifMatch == null ? null : new[]
{
new EntityTagHeaderValue(ifMatch),
};
httpRequestHeaders.IfNoneMatch = ifNoneMatch == null ? null : new[]
{
new EntityTagHeaderValue(ifNoneMatch),
};
httpRequestHeaders.IfModifiedSince = lastModified;
actionContext.HttpContext = httpContext;
// Act
var state = FileResultExecutorBase.GetPreconditionState(
actionContext,
httpRequestHeaders,
lastModified,
etag);
// Assert
Assert.Equal(FileResultExecutorBase.PreconditionState.NotModified, state);
}
[Fact]
public void GetPreconditionState_ShouldNotProcess_IgnoreRangeRequest()
{
// Arrange
var actionContext = new ActionContext();
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = HttpMethods.Get;
var httpRequestHeaders = httpContext.Request.GetTypedHeaders();
var lastModified = DateTimeOffset.MinValue;
lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
var etag = new EntityTagHeaderValue("\"Etag\"");
httpRequestHeaders.IfRange = new RangeConditionHeaderValue("\"NotEtag\"");
httpRequestHeaders.IfModifiedSince = lastModified;
actionContext.HttpContext = httpContext;
// Act
var state = FileResultExecutorBase.GetPreconditionState(
actionContext,
httpRequestHeaders,
lastModified,
etag);
// Assert
Assert.Equal(FileResultExecutorBase.PreconditionState.IgnoreRangeRequest, state);
}
private static IServiceCollection CreateServices()
{
var services = new ServiceCollection();
@ -297,13 +451,13 @@ namespace Microsoft.AspNetCore.Mvc
private class EmptyFileResultExecutor : FileResultExecutorBase
{
public EmptyFileResultExecutor(ILoggerFactory loggerFactory)
:base(CreateLogger<EmptyFileResultExecutor>(loggerFactory))
: base(CreateLogger<EmptyFileResultExecutor>(loggerFactory))
{
}
public Task ExecuteAsync(ActionContext context, EmptyFileResult result)
{
SetHeadersAndLog(context, result);
SetHeadersAndLog(context, result, 0L);
result.WasWriteFileCalled = true;
return Task.FromResult(0);
}

View File

@ -1,8 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@ -13,6 +15,7 @@ using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
@ -49,6 +52,348 @@ namespace Microsoft.AspNetCore.Mvc
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
}
[Fact]
public void Constructor_SetsLastModifiedAndEtag()
{
// Arrange
var stream = Stream.Null;
var contentType = "text/plain";
var expectedMediaType = contentType;
var lastModified = new DateTimeOffset();
var entityTag = new EntityTagHeaderValue("\"Etag\"");
// Act
var result = new FileStreamResult(stream, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
};
// Assert
Assert.Equal(lastModified, result.LastModified);
Assert.Equal(entityTag, result.EntityTag);
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
}
[Theory]
[InlineData(0, 4, "Hello", 5)]
[InlineData(6, 10, "World", 5)]
[InlineData(null, 5, "World", 5)]
[InlineData(6, null, "World", 5)]
public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
{
// Arrange
var contentType = "text/plain";
var lastModified = new DateTimeOffset();
var entityTag = new EntityTagHeaderValue("\"Etag\"");
var byteArray = Encoding.ASCII.GetBytes("Hello World");
var readStream = new MemoryStream(byteArray);
readStream.SetLength(11);
var result = new FileStreamResult(readStream, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
};
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.Range = new RangeHeaderValue(start, end);
requestHeaders.IfMatch = new[]
{
new EntityTagHeaderValue("\"Etag\""),
};
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
start = start ?? 11 - end;
end = start + contentLength - 1;
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, byteArray.Length);
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal(contentLength, httpResponse.ContentLength);
Assert.Equal(expectedString, body);
}
[Fact]
public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
{
// Arrange
var contentType = "text/plain";
var lastModified = DateTimeOffset.MinValue;
var entityTag = new EntityTagHeaderValue("\"Etag\"");
var byteArray = Encoding.ASCII.GetBytes("Hello World");
var readStream = new MemoryStream(byteArray);
readStream.SetLength(11);
var result = new FileStreamResult(readStream, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
};
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfMatch = new[]
{
new EntityTagHeaderValue("\"Etag\""),
};
requestHeaders.Range = new RangeHeaderValue(0, 4);
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
var contentRange = new ContentRangeHeaderValue(0, 4, byteArray.Length);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal(5, httpResponse.ContentLength);
Assert.Equal("Hello", body);
}
[Fact]
public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
{
// Arrange
var contentType = "text/plain";
var lastModified = DateTimeOffset.MinValue.AddDays(1);
var entityTag = new EntityTagHeaderValue("\"Etag\"");
var byteArray = Encoding.ASCII.GetBytes("Hello World");
var readStream = new MemoryStream(byteArray);
readStream.SetLength(11);
var result = new FileStreamResult(readStream, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
};
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfMatch = new[]
{
new EntityTagHeaderValue("\"Etag\""),
};
requestHeaders.Range = new RangeHeaderValue(0, 4);
requestHeaders.IfRange = new RangeConditionHeaderValue(DateTimeOffset.MinValue);
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal("Hello World", body);
}
[Theory]
[InlineData("0-5")]
[InlineData("bytes = 11-0")]
[InlineData("bytes = 1-4, 5-11")]
public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
{
// Arrange
var contentType = "text/plain";
var lastModified = new DateTimeOffset();
var entityTag = new EntityTagHeaderValue("\"Etag\"");
var byteArray = Encoding.ASCII.GetBytes("Hello World");
var readStream = new MemoryStream(byteArray);
var result = new FileStreamResult(readStream, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
};
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
var contentRange = new ContentRangeHeaderValue(byteArray.Length);
Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Empty(body);
}
[Fact]
public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
{
// Arrange
var contentType = "text/plain";
var lastModified = new DateTimeOffset();
var entityTag = new EntityTagHeaderValue("\"Etag\"");
var byteArray = Encoding.ASCII.GetBytes("Hello World");
var readStream = new MemoryStream(byteArray);
var result = new FileStreamResult(readStream, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
};
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfMatch = new[]
{
new EntityTagHeaderValue("\"NotEtag\""),
};
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Null(httpResponse.ContentLength);
Assert.Empty(body);
}
[Fact]
public async Task WriteFileAsync_RangeRequested_NotModified()
{
// Arrange
var contentType = "text/plain";
var lastModified = new DateTimeOffset();
var entityTag = new EntityTagHeaderValue("\"Etag\"");
var byteArray = Encoding.ASCII.GetBytes("Hello World");
var readStream = new MemoryStream(byteArray);
var result = new FileStreamResult(readStream, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
};
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfNoneMatch = new[]
{
new EntityTagHeaderValue("\"Etag\""),
};
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Null(httpResponse.ContentLength);
Assert.Empty(body);
}
[Theory]
[InlineData(0)]
[InlineData(null)]
public async Task WriteFileAsync_RangeRequested_FileLengthZeroOrNull(long? fileLength)
{
// Arrange
var contentType = "text/plain";
var lastModified = new DateTimeOffset();
var entityTag = new EntityTagHeaderValue("\"Etag\"");
var byteArray = Encoding.ASCII.GetBytes("");
var readStream = new MemoryStream(byteArray);
fileLength = fileLength ?? 0L;
readStream.SetLength(fileLength.Value);
var result = new FileStreamResult(readStream, contentType)
{
LastModified = lastModified,
EntityTag = entityTag,
};
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.Range = new RangeHeaderValue(0, 5);
requestHeaders.IfMatch = new[]
{
new EntityTagHeaderValue("\"Etag\""),
};
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
var contentRange = new ContentRangeHeaderValue(byteArray.Length);
Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Empty(body);
}
[Fact]
public async Task WriteFileAsync_WritesResponse_InChunksOfFourKilobytes()
{
@ -153,7 +498,6 @@ namespace Microsoft.AspNetCore.Mvc
var httpContext = new DefaultHttpContext();
httpContext.RequestServices = services.BuildServiceProvider();
return httpContext;
}
}

View File

@ -6,7 +6,6 @@ using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Abstractions;
@ -16,6 +15,7 @@ using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
@ -52,6 +52,198 @@ namespace Microsoft.AspNetCore.Mvc
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
}
[Theory]
[InlineData(0, 3, "File", 4)]
[InlineData(8, 13, "Result", 6)]
[InlineData(null, 5, "ts<74>", 5)]
[InlineData(8, null, "ResultTestFile contents<74>", 26)]
public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
var result = new TestPhysicalFileResult(path, "text/plain");
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
requestHeaders.Range = new RangeHeaderValue(start, end);
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
start = start ?? 34 - end;
end = start + contentLength - 1;
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 34);
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(contentLength, httpResponse.ContentLength);
Assert.Equal(expectedString, body);
}
[Fact]
public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
var result = new TestPhysicalFileResult(path, "text/plain");
var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
requestHeaders.Range = new RangeHeaderValue(0, 3);
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
var contentRange = new ContentRangeHeaderValue(0, 3, 34);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal(4, httpResponse.ContentLength);
Assert.Equal("File", body);
}
[Fact]
public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
var result = new TestPhysicalFileResult(path, "text/plain");
var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
requestHeaders.Range = new RangeHeaderValue(0, 3);
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal("FilePathResultTestFile contents<74>", body);
}
[Theory]
[InlineData("0-5")]
[InlineData("bytes = 11-0")]
[InlineData("bytes = 1-4, 5-11")]
public async Task WriteFileAsync_RangeRequested_NotSatisfiable(string rangeString)
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
var result = new TestPhysicalFileResult(path, "text/plain");
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
var contentRange = new ContentRangeHeaderValue(34);
Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Empty(body);
}
[Fact]
public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
var result = new TestPhysicalFileResult(path, "text/plain");
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue;
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Null(httpResponse.ContentLength);
Assert.Empty(body);
}
[Fact]
public async Task WriteFileAsync_RangeRequested_NotModified()
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
var result = new TestPhysicalFileResult(path, "text/plain");
var httpContext = GetHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1);
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Null(httpResponse.ContentLength);
Assert.Empty(body);
}
[Fact]
public async Task ExecuteResultAsync_FallsbackToStreamCopy_IfNoIHttpSendFilePresent()
{
@ -94,6 +286,46 @@ namespace Microsoft.AspNetCore.Mvc
sendFileMock.Verify();
}
[Theory]
[InlineData(0, 3, "File", 4)]
[InlineData(8, 13, "Result", 6)]
[InlineData(null, 3, "ts¡", 3)]
[InlineData(8, null, "ResultTestFile contents¡", 26)]
public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, string expectedString, long contentLength)
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
var result = new TestPhysicalFileResult(path, "text/plain");
var sendFile = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Features.Set<IHttpSendFileFeature>(sendFile);
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.Range = new RangeHeaderValue(start, end);
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
httpContext.Request.Method = HttpMethods.Get;
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
start = start ?? 34 - end;
end = start + contentLength - 1;
var httpResponse = actionContext.HttpContext.Response;
Assert.Equal(Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")), sendFile.Name);
Assert.Equal(start, sendFile.Offset);
Assert.Equal(contentLength, sendFile.Length);
Assert.Equal(CancellationToken.None, sendFile.Token);
var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 34);
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(contentLength, httpResponse.ContentLength);
}
[Fact]
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
{
@ -236,6 +468,35 @@ namespace Microsoft.AspNetCore.Mvc
return new MemoryStream(Encoding.UTF8.GetBytes("FilePathResultTestFile contents<74>"));
}
}
protected override FileMetadata GetFileInfo(string path)
{
var lastModified = DateTimeOffset.MinValue.AddDays(1);
return new FileMetadata
{
Exists = true,
Length = 34,
LastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0))
};
}
}
private class TestSendFileFeature : IHttpSendFileFeature
{
public string Name { get; set; }
public long Offset { get; set; }
public long? Length { get; set; }
public CancellationToken Token { get; set; }
public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
{
Name = path;
Offset = offset;
Length = length;
Token = cancellation;
return Task.FromResult(0);
}
}
private static IServiceCollection CreateServices()

View File

@ -17,6 +17,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
@ -51,8 +52,272 @@ namespace Microsoft.AspNetCore.Mvc
// Assert
Assert.Equal(path, result.FileName);
MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
}
[Theory]
[InlineData(0, 3, "File", 4)]
[InlineData(8, 13, "Result", 6)]
[InlineData(null, 4, "ts¡", 4)]
[InlineData(8, null, "ResultTestFile contents¡", 25)]
public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
{
// Arrange
var path = Path.GetFullPath("helllo.txt");
var contentType = "text/plain; charset=us-ascii; p1=p1-value";
var result = new TestVirtualFileResult(path, contentType);
var appEnvironment = new Mock<IHostingEnvironment>();
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<ILoggerFactory, LoggerFactory>()
.BuildServiceProvider();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.Range = new RangeHeaderValue(start, end);
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
httpContext.Request.Method = HttpMethods.Get;
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
start = start ?? 33 - end;
end = start + contentLength - 1;
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 33);
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(contentLength, httpResponse.ContentLength);
Assert.Equal(expectedString, body);
}
[Fact]
public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
{
// Arrange
var path = Path.GetFullPath("helllo.txt");
var contentType = "text/plain; charset=us-ascii; p1=p1-value";
var result = new TestVirtualFileResult(path, contentType);
var appEnvironment = new Mock<IHostingEnvironment>();
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<ILoggerFactory, LoggerFactory>()
.BuildServiceProvider();
var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
requestHeaders.Range = new RangeHeaderValue(0, 3);
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
var contentRange = new ContentRangeHeaderValue(0, 3, 33);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal(4, httpResponse.ContentLength);
Assert.Equal("File", body);
}
[Fact]
public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
{
// Arrange
var path = Path.GetFullPath("helllo.txt");
var contentType = "text/plain; charset=us-ascii; p1=p1-value";
var result = new TestVirtualFileResult(path, contentType);
var appEnvironment = new Mock<IHostingEnvironment>();
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<ILoggerFactory, LoggerFactory>()
.BuildServiceProvider();
var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
requestHeaders.Range = new RangeHeaderValue(0, 3);
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal("FilePathResultTestFile contents¡", body);
}
[Theory]
[InlineData("0-5")]
[InlineData("bytes = 11-0")]
[InlineData("bytes = 1-4, 5-11")]
public async Task WriteFileAsync_RangeRequested_NotSatisfiable(string rangeString)
{
// Arrange
var path = Path.GetFullPath("helllo.txt");
var contentType = "text/plain; charset=us-ascii; p1=p1-value";
var result = new TestVirtualFileResult(path, contentType);
var appEnvironment = new Mock<IHostingEnvironment>();
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<ILoggerFactory, LoggerFactory>()
.BuildServiceProvider();
var requestHeaders = httpContext.Request.GetTypedHeaders();
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
var contentRange = new ContentRangeHeaderValue(33);
Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Empty(body);
}
[Fact]
public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
{
// Arrange
var path = Path.GetFullPath("helllo.txt");
var contentType = "text/plain; charset=us-ascii; p1=p1-value";
var result = new TestVirtualFileResult(path, contentType);
var appEnvironment = new Mock<IHostingEnvironment>();
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<ILoggerFactory, LoggerFactory>()
.BuildServiceProvider();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue;
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Null(httpResponse.ContentLength);
Assert.Empty(body);
}
[Fact]
public async Task WriteFileAsync_RangeRequested_NotModified()
{
// Arrange
var path = Path.GetFullPath("helllo.txt");
var contentType = "text/plain; charset=us-ascii; p1=p1-value";
var result = new TestVirtualFileResult(path, contentType);
var appEnvironment = new Mock<IHostingEnvironment>();
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<ILoggerFactory, LoggerFactory>()
.BuildServiceProvider();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1);
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Null(httpResponse.ContentLength);
Assert.Empty(body);
}
[Fact]
public async Task ExecuteResultAsync_FallsBackToWebRootFileProvider_IfNoFileProviderIsPresent()
{
@ -133,6 +398,58 @@ namespace Microsoft.AspNetCore.Mvc
sendFileMock.Verify();
}
[Theory]
[InlineData(0, 3, "File", 4)]
[InlineData(8, 13, "Result", 6)]
[InlineData(null, 3, "ts¡", 3)]
[InlineData(8, null, "ResultTestFile contents¡", 25)]
public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, string expectedString, long contentLength)
{
// Arrange
var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt");
var result = new TestVirtualFileResult(path, "text/plain")
{
FileProvider = GetFileProvider(path),
};
var sendFile = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Features.Set<IHttpSendFileFeature>(sendFile);
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var appEnvironment = new Mock<IHostingEnvironment>();
appEnvironment.Setup(app => app.WebRootFileProvider)
.Returns(GetFileProvider(path));
httpContext.RequestServices = new ServiceCollection()
.AddSingleton(appEnvironment.Object)
.AddTransient<TestVirtualFileResultExecutor>()
.AddTransient<ILoggerFactory, LoggerFactory>()
.BuildServiceProvider();
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.Range = new RangeHeaderValue(start, end);
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
httpContext.Request.Method = HttpMethods.Get;
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
start = start ?? 33 - end;
end = start + contentLength - 1;
var httpResponse = actionContext.HttpContext.Response;
Assert.Equal(Path.Combine("TestFiles", "FilePathResultTestFile.txt"), sendFile.Name);
Assert.Equal(start, sendFile.Offset);
Assert.Equal(contentLength, sendFile.Length);
Assert.Equal(CancellationToken.None, sendFile.Token);
var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 33);
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(contentLength, httpResponse.ContentLength);
}
[Fact]
public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
{
@ -325,7 +642,11 @@ namespace Microsoft.AspNetCore.Mvc
private static IFileProvider GetFileProvider(string path)
{
var fileInfo = new Mock<IFileInfo>();
fileInfo.SetupGet(fi => fi.Length).Returns(33);
fileInfo.SetupGet(fi => fi.Exists).Returns(true);
var lastModified = DateTimeOffset.MinValue.AddDays(1);
lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
fileInfo.SetupGet(fi => fi.LastModified).Returns(lastModified);
fileInfo.SetupGet(fi => fi.PhysicalPath).Returns(path);
var fileProvider = new Mock<IFileProvider>();
fileProvider.Setup(fp => fp.GetFileInfo(path))
@ -355,7 +676,7 @@ namespace Microsoft.AspNetCore.Mvc
private class TestVirtualFileResultExecutor : VirtualFileResultExecutor
{
public TestVirtualFileResultExecutor(ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment)
: base(loggerFactory,hostingEnvironment)
: base(loggerFactory, hostingEnvironment)
{
}
@ -373,5 +694,23 @@ namespace Microsoft.AspNetCore.Mvc
}
}
}
private class TestSendFileFeature : IHttpSendFileFeature
{
public string Name { get; set; }
public long Offset { get; set; }
public long? Length { get; set; }
public CancellationToken Token { get; set; }
public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
{
Name = path;
Offset = offset;
Length = length;
Token = cancellation;
return Task.FromResult(0);
}
}
}
}

View File

@ -1,8 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
@ -37,6 +40,92 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("This is a sample text file", body);
}
[Theory]
[InlineData(0, 6, "This is")]
[InlineData(17, 25, "text file")]
[InlineData(0, 50, "This is a sample text file")]
public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequest(long start, long end, string expectedBody)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDisk");
httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.NotNull(body);
Assert.Equal(expectedBody, body);
}
[Theory]
[InlineData("0-6")]
[InlineData("bytes = 11-6")]
[InlineData("bytes = 1-4, 5-11")]
public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestNotSatisfiable(string rangeString)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDisk");
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.Empty(body);
}
[Theory]
[InlineData(0, 6, "This is")]
[InlineData(17, 25, "text file")]
[InlineData(0, 50, "This is a sample text file")]
public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequest_WithLastModifiedAndEtag(long start, long end, string expectedBody)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDisk_WithLastModifiedAndEtag");
httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.NotNull(body);
Assert.Equal(expectedBody, body);
}
[Theory]
[InlineData("0-6")]
[InlineData("bytes = 11-6")]
[InlineData("bytes = 1-4, 5-11")]
public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestNotSatisfiable_WithLastModifiedAndEtag(string rangeString)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName_WithLastModifiedAndEtag");
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.Empty(body);
}
[ConditionalFact]
// https://github.com/aspnet/Mvc/issues/2727
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
@ -60,6 +149,45 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}
[Fact]
public async Task FileFromDisk_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest_WithLastModifiedAndEtag()
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName_WithLastModifiedAndEtag");
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.NotNull(body);
Assert.Equal("This is", body);
}
[Fact]
public async Task FileFromDisk_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestIgnored_WithLastModifiedAndEtag()
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName_WithLastModifiedAndEtag");
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.Equal("This is a sample text file", body);
}
[Fact]
public async Task FileFromStream_ReturnsFile()
{
@ -77,6 +205,49 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("This is sample text from a stream", body);
}
[Theory]
[InlineData(0, 6, "This is")]
[InlineData(25, 32, "a stream")]
[InlineData(0, 50, "This is sample text from a stream")]
public async Task FileFromStream_ReturnsFile_RangeRequest(long start, long end, string expectedBody)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStream");
httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.NotNull(body);
Assert.Equal(expectedBody, body);
}
[Theory]
[InlineData("0-6")]
[InlineData("bytes = 11-6")]
[InlineData("bytes = 1-4, 5-11")]
public async Task FileFromStream_ReturnsFile_RangeRequestNotSatisfiable(string rangeString)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStream");
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.Empty(body);
}
[Fact]
public async Task FileFromStream_ReturnsFileWithFileName()
{
@ -98,6 +269,45 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}
[Fact]
public async Task FileFromStream_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest()
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStreamWithFileName_WithEtag");
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.NotNull(body);
Assert.Equal("This is", body);
}
[Fact]
public async Task FileFromStream_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestNotSatisfiable()
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStreamWithFileName_WithEtag");
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.Equal("This is sample text from a stream", body);
}
[Fact]
public async Task FileFromBinaryData_ReturnsFile()
{
@ -115,6 +325,49 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("This is a sample text from a binary array", body);
}
[Theory]
[InlineData(0, 6, "This is")]
[InlineData(29, 40, "binary array")]
[InlineData(0, 50, "This is a sample text from a binary array")]
public async Task FileFromBinaryData_ReturnsFile_RangeRequest(long start, long end, string expectedBody)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryData");
httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.NotNull(body);
Assert.Equal(expectedBody, body);
}
[Theory]
[InlineData("0-6")]
[InlineData("bytes = 11-6")]
[InlineData("bytes = 1-4, 5-11")]
public async Task FileFromBinaryData_ReturnsFile_RangeRequestNotSatisfiable(string rangeString)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryData");
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.Empty(body);
}
[Fact]
public async Task FileFromBinaryData_ReturnsFileWithFileName()
{
@ -136,6 +389,45 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}
[Fact]
public async Task FileFromBinaryData_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest()
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryDataWithFileName_WithEtag");
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.NotNull(body);
Assert.Equal("This is", body);
}
[Fact]
public async Task FileFromBinaryData_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestIgnored()
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryDataWithFileName_WithEtag");
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.Equal("This is a sample text from a binary array", body);
}
[Fact]
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName()
{
@ -159,5 +451,99 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.NotNull(contentDisposition);
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}
[Theory]
[InlineData(0, 6, "Sample ")]
[InlineData(20, 37, "embedded resource.")]
[InlineData(7, 50, "text file as embedded resource.")]
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequest(long start, long end, string expectedBody)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName");
httpRequestMessage.Headers.Range = new RangeHeaderValue(start, end);
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.NotNull(body);
Assert.Equal(expectedBody, body);
var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
Assert.NotNull(contentDisposition);
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}
[Fact]
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest()
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName_WithEtag");
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.NotNull(body);
Assert.Equal("Sample ", body);
var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
Assert.NotNull(contentDisposition);
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}
[Fact]
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestIgnored()
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName_WithEtag");
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.Equal("Sample text file as embedded resource.", body);
var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
Assert.NotNull(contentDisposition);
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}
[Theory]
[InlineData("0-6")]
[InlineData("bytes = 11-6")]
[InlineData("bytes = 1-4, 5-11")]
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequestNotSatisfiable(string rangeString)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName");
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
// Act
var response = await Client.SendAsync(httpRequestMessage);
// Assert
Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.Empty(body);
var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
Assert.NotNull(contentDisposition);
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}
}
}

View File

@ -48,6 +48,7 @@
<PackageReference Include="FSharp.Core" Version="$(FSharpCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.ChunkingCookieManager.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(AspNetCoreVersion)" />

View File

@ -1,10 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
namespace FilesWebSite
{
@ -23,12 +25,28 @@ namespace FilesWebSite
return PhysicalFile(path, "text/plain");
}
public IActionResult DownloadFromDisk_WithLastModifiedAndEtag()
{
var path = Path.Combine(_hostingEnvironment.ContentRootPath, "sample.txt");
var lastModified = new DateTimeOffset(year: 1999, month: 11, day: 04, hour: 3, minute: 0, second: 0, offset: new TimeSpan(0));
var entityTag = new EntityTagHeaderValue("\"Etag\"");
return PhysicalFile(path, "text/plain", lastModified, entityTag);
}
public IActionResult DownloadFromDiskWithFileName()
{
var path = Path.Combine(_hostingEnvironment.ContentRootPath, "sample.txt");
return PhysicalFile(path, "text/plain", "downloadName.txt");
}
public IActionResult DownloadFromDiskWithFileName_WithLastModifiedAndEtag()
{
var path = Path.Combine(_hostingEnvironment.ContentRootPath, "sample.txt");
var lastModified = new DateTimeOffset(year: 1999, month: 11, day: 04, hour: 3, minute: 0, second: 0, offset: new TimeSpan(0));
var entityTag = new EntityTagHeaderValue("\"Etag\"");
return PhysicalFile(path, "text/plain", "downloadName.txt", lastModified, entityTag);
}
public IActionResult DownloadFromStream()
{
var stream = new MemoryStream();
@ -51,6 +69,17 @@ namespace FilesWebSite
return File(stream, "text/plain", "downloadName.txt");
}
public IActionResult DownloadFromStreamWithFileName_WithEtag()
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write("This is sample text from a stream");
writer.Flush();
stream.Seek(0, SeekOrigin.Begin);
var entityTag = new EntityTagHeaderValue("\"Etag\"");
return File(stream, "text/plain", "downloadName.txt", lastModified: null, entityTag: entityTag);
}
public IActionResult DownloadFromBinaryData()
{
var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
@ -62,5 +91,12 @@ namespace FilesWebSite
var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
return File(data, "text/plain", "downloadName.txt");
}
public IActionResult DownloadFromBinaryDataWithFileName_WithEtag()
{
var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
var entityTag = new EntityTagHeaderValue("\"Etag\"");
return File(data, "text/plain", "downloadName.txt", lastModified: null, entityTag: entityTag);
}
}
}

View File

@ -17,5 +17,17 @@ namespace FilesWebSite
FileDownloadName = "downloadName.txt"
};
}
public IActionResult DownloadFileWithFileName_WithEtag()
{
var file = new VirtualFileResult("/Greetings.txt", "text/plain")
{
FileProvider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, "FilesWebSite.EmbeddedResources"),
FileDownloadName = "downloadName.txt"
};
file.EntityTag = new Microsoft.Net.Http.Headers.EntityTagHeaderValue("\"Etag\"");
return file;
}
}
}

View File

@ -1 +1 @@
Sample text file as embedded resource.
Sample text file as embedded resource.

View File

@ -1 +1 @@
This is a sample text file
This is a sample text file