diff --git a/src/Mvc/Mvc.Core/src/FileContentResult.cs b/src/Mvc/Mvc.Core/src/FileContentResult.cs
index 7fcb8729112f..f9bc761110b6 100644
--- a/src/Mvc/Mvc.Core/src/FileContentResult.cs
+++ b/src/Mvc/Mvc.Core/src/FileContentResult.cs
@@ -3,9 +3,12 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using System.IO;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc
@@ -14,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc
/// Represents an that when executed will
/// write a binary file to the response.
///
- public class FileContentResult : FileResult
+ public class FileContentResult : FileResult, IResult
{
private byte[] _fileContents;
@@ -77,5 +80,43 @@ public override Task ExecuteResultAsync(ActionContext context)
var executor = context.HttpContext.RequestServices.GetRequiredService>();
return executor.ExecuteAsync(context, this);
}
+
+ Task IResult.ExecuteAsync(HttpContext httpContext)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ var loggerFactory = httpContext.RequestServices.GetRequiredService();
+ var logger = loggerFactory.CreateLogger();
+
+ var (range, rangeLength, serveBody) = FileResultExecutorBase.SetHeadersAndLog(
+ httpContext,
+ this,
+ FileContents.Length,
+ EnableRangeProcessing,
+ LastModified,
+ EntityTag,
+ logger);
+
+ if (!serveBody)
+ {
+ return Task.CompletedTask;
+ }
+
+ if (range != null && rangeLength == 0)
+ {
+ return Task.CompletedTask;
+ }
+
+ if (range != null)
+ {
+ logger.WritingRangeToBody();
+ }
+
+ var fileContentStream = new MemoryStream(FileContents);
+ return FileResultExecutorBase.WriteFileAsyncInternal(httpContext, fileContentStream, range, rangeLength);
+ }
}
}
diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/FileResultExecutorBase.cs b/src/Mvc/Mvc.Core/src/Infrastructure/FileResultExecutorBase.cs
index c97c01cbf2ff..72dca6e2c2b5 100644
--- a/src/Mvc/Mvc.Core/src/Infrastructure/FileResultExecutorBase.cs
+++ b/src/Mvc/Mvc.Core/src/Infrastructure/FileResultExecutorBase.cs
@@ -68,16 +68,35 @@ protected virtual (RangeItemHeaderValue? range, long rangeLength, bool serveBody
DateTimeOffset? lastModified = null,
EntityTagHeaderValue? etag = null)
{
- if (context == null)
+ return SetHeadersAndLog(
+ context.HttpContext,
+ result,
+ fileLength,
+ enableRangeProcessing,
+ lastModified,
+ etag,
+ Logger);
+ }
+
+ internal static (RangeItemHeaderValue? range, long rangeLength, bool serveBody) SetHeadersAndLog(
+ HttpContext httpContext,
+ FileResult result,
+ long? fileLength,
+ bool enableRangeProcessing,
+ DateTimeOffset? lastModified,
+ EntityTagHeaderValue? etag,
+ ILogger logger)
+ {
+ if (httpContext == null)
{
- throw new ArgumentNullException(nameof(context));
+ throw new ArgumentNullException(nameof(httpContext));
}
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
-
- var request = context.HttpContext.Request;
+
+ var request = httpContext.Request;
var httpRequestHeaders = request.GetTypedHeaders();
// Since the 'Last-Modified' and other similar http date headers are rounded down to whole seconds,
@@ -87,9 +106,9 @@ protected virtual (RangeItemHeaderValue? range, long rangeLength, bool serveBody
lastModified = RoundDownToWholeSeconds(lastModified.Value);
}
- var preconditionState = GetPreconditionState(httpRequestHeaders, lastModified, etag);
+ var preconditionState = GetPreconditionState(httpRequestHeaders, lastModified, etag, logger);
- var response = context.HttpContext.Response;
+ var response = httpContext.Response;
SetLastModifiedAndEtagHeaders(response, lastModified, etag);
// Short circuit if the preconditional headers process to 304 (NotModified) or 412 (PreconditionFailed)
@@ -104,8 +123,8 @@ protected virtual (RangeItemHeaderValue? range, long rangeLength, bool serveBody
return (range: null, rangeLength: 0, serveBody: false);
}
- SetContentType(context, result);
- SetContentDispositionHeader(context, result);
+ SetContentType(httpContext, result);
+ SetContentDispositionHeader(httpContext, result);
if (fileLength.HasValue)
{
@@ -125,27 +144,27 @@ protected virtual (RangeItemHeaderValue? range, long rangeLength, bool serveBody
// range should be processed and Range headers should be set
if ((HttpMethods.IsHead(request.Method) || HttpMethods.IsGet(request.Method))
&& (preconditionState == PreconditionState.Unspecified || preconditionState == PreconditionState.ShouldProcess)
- && (IfRangeValid(httpRequestHeaders, lastModified, etag)))
+ && (IfRangeValid(httpRequestHeaders, lastModified, etag, logger)))
{
- return SetRangeHeaders(context, httpRequestHeaders, fileLength.Value);
+ return SetRangeHeaders(httpContext, httpRequestHeaders, fileLength.Value, logger);
}
}
else
{
- Logger.NotEnabledForRangeProcessing();
+ logger.NotEnabledForRangeProcessing();
}
}
return (range: null, rangeLength: 0, serveBody: !HttpMethods.IsHead(request.Method));
}
- private static void SetContentType(ActionContext context, FileResult result)
+ private static void SetContentType(HttpContext httpContext, FileResult result)
{
- var response = context.HttpContext.Response;
+ var response = httpContext.Response;
response.ContentType = result.ContentType;
}
- private static void SetContentDispositionHeader(ActionContext context, FileResult result)
+ private static void SetContentDispositionHeader(HttpContext httpContext, FileResult result)
{
if (!string.IsNullOrEmpty(result.FileDownloadName))
{
@@ -156,7 +175,7 @@ private static void SetContentDispositionHeader(ActionContext context, FileResul
// basis for the actual filename, where possible.
var contentDisposition = new ContentDispositionHeaderValue("attachment");
contentDisposition.SetHttpFileName(result.FileDownloadName);
- context.HttpContext.Response.Headers.ContentDisposition = contentDisposition.ToString();
+ httpContext.Response.Headers.ContentDisposition = contentDisposition.ToString();
}
}
@@ -178,10 +197,11 @@ private static void SetAcceptRangeHeader(HttpResponse response)
response.Headers.AcceptRanges = AcceptRangeHeaderValue;
}
- internal bool IfRangeValid(
+ internal static bool IfRangeValid(
RequestHeaders httpRequestHeaders,
DateTimeOffset? lastModified,
- EntityTagHeaderValue? etag)
+ EntityTagHeaderValue? etag,
+ ILogger logger)
{
// 14.27 If-Range
var ifRange = httpRequestHeaders.IfRange;
@@ -196,13 +216,13 @@ internal bool IfRangeValid(
{
if (lastModified.HasValue && lastModified > ifRange.LastModified)
{
- Logger.IfRangeLastModifiedPreconditionFailed(lastModified, ifRange.LastModified);
+ logger.IfRangeLastModifiedPreconditionFailed(lastModified, ifRange.LastModified);
return false;
}
}
else if (etag != null && ifRange.EntityTag != null && !ifRange.EntityTag.Compare(etag, useStrongComparison: true))
{
- Logger.IfRangeETagPreconditionFailed(etag, ifRange.EntityTag);
+ logger.IfRangeETagPreconditionFailed(etag, ifRange.EntityTag);
return false;
}
}
@@ -211,10 +231,11 @@ internal bool IfRangeValid(
}
// Internal for testing
- internal PreconditionState GetPreconditionState(
+ internal static PreconditionState GetPreconditionState(
RequestHeaders httpRequestHeaders,
DateTimeOffset? lastModified,
- EntityTagHeaderValue? etag)
+ EntityTagHeaderValue? etag,
+ ILogger logger)
{
var ifMatchState = PreconditionState.Unspecified;
var ifNoneMatchState = PreconditionState.Unspecified;
@@ -234,7 +255,7 @@ internal PreconditionState GetPreconditionState(
if (ifMatchState == PreconditionState.PreconditionFailed)
{
- Logger.IfMatchPreconditionFailed(etag);
+ logger.IfMatchPreconditionFailed(etag);
}
}
@@ -269,7 +290,7 @@ internal PreconditionState GetPreconditionState(
if (ifUnmodifiedSinceState == PreconditionState.PreconditionFailed)
{
- Logger.IfUnmodifiedSincePreconditionFailed(lastModified, ifUnmodifiedSince);
+ logger.IfUnmodifiedSincePreconditionFailed(lastModified, ifUnmodifiedSince);
}
}
@@ -316,22 +337,23 @@ private static PreconditionState GetMaxPreconditionState(params PreconditionStat
return max;
}
- private (RangeItemHeaderValue? range, long rangeLength, bool serveBody) SetRangeHeaders(
- ActionContext context,
+ private static (RangeItemHeaderValue? range, long rangeLength, bool serveBody) SetRangeHeaders(
+ HttpContext httpContext,
RequestHeaders httpRequestHeaders,
- long fileLength)
+ long fileLength,
+ ILogger logger)
{
- var response = context.HttpContext.Response;
+ var response = httpContext.Response;
var httpResponseHeaders = response.GetTypedHeaders();
- var serveBody = !HttpMethods.IsHead(context.HttpContext.Request.Method);
+ var serveBody = !HttpMethods.IsHead(httpContext.Request.Method);
// Range may be null for empty range header, invalid ranges, parsing errors, multiple ranges
// and when the file length is zero.
var (isRangeRequest, range) = RangeHelper.ParseRange(
- context.HttpContext,
+ httpContext,
httpRequestHeaders,
fileLength,
- Logger);
+ logger);
if (!isRangeRequest)
{
@@ -397,6 +419,11 @@ protected static ILogger CreateLogger(ILoggerFactory factory)
/// The range length.
/// The async task.
protected static async Task WriteFileAsync(HttpContext context, Stream fileStream, RangeItemHeaderValue? range, long rangeLength)
+ {
+ await WriteFileAsyncInternal(context, fileStream, range, rangeLength);
+ }
+
+ internal static async Task WriteFileAsyncInternal(HttpContext context, Stream fileStream, RangeItemHeaderValue? range, long rangeLength)
{
var outputStream = context.Response.Body;
using (fileStream)
diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/PhysicalFileResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/PhysicalFileResultExecutor.cs
index 7d1b5cfa161d..f295d2769f77 100644
--- a/src/Mvc/Mvc.Core/src/Infrastructure/PhysicalFileResultExecutor.cs
+++ b/src/Mvc/Mvc.Core/src/Infrastructure/PhysicalFileResultExecutor.cs
@@ -3,10 +3,8 @@
using System;
using System.IO;
-using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
@@ -69,9 +67,19 @@ public virtual Task ExecuteAsync(ActionContext context, PhysicalFileResult resul
///
protected virtual Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
{
- if (context == null)
+ return WriteFileAsyncInternal(context.HttpContext, result, range, rangeLength, Logger);
+ }
+
+ internal static Task WriteFileAsyncInternal(
+ HttpContext httpContext,
+ PhysicalFileResult result,
+ RangeItemHeaderValue? range,
+ long rangeLength,
+ ILogger logger)
+ {
+ if (httpContext == null)
{
- throw new ArgumentNullException(nameof(context));
+ throw new ArgumentNullException(nameof(httpContext));
}
if (result == null)
@@ -84,7 +92,7 @@ protected virtual Task WriteFileAsync(ActionContext context, PhysicalFileResult
return Task.CompletedTask;
}
- var response = context.HttpContext.Response;
+ var response = httpContext.Response;
if (!Path.IsPathRooted(result.FileName))
{
throw new NotSupportedException(Resources.FormatFileResult_PathNotRooted(result.FileName));
@@ -92,7 +100,7 @@ protected virtual Task WriteFileAsync(ActionContext context, PhysicalFileResult
if (range != null)
{
- Logger.WritingRangeToBody();
+ logger.WritingRangeToBody();
}
if (range != null)
diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/VirtualFileResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/VirtualFileResultExecutor.cs
index 68e5da333b4c..3ec1178cda2d 100644
--- a/src/Mvc/Mvc.Core/src/Infrastructure/VirtualFileResultExecutor.cs
+++ b/src/Mvc/Mvc.Core/src/Infrastructure/VirtualFileResultExecutor.cs
@@ -50,7 +50,7 @@ public virtual Task ExecuteAsync(ActionContext context, VirtualFileResult result
throw new ArgumentNullException(nameof(result));
}
- var fileInfo = GetFileInformation(result);
+ var fileInfo = GetFileInformation(result, _hostingEnvironment);
if (!fileInfo.Exists)
{
throw new FileNotFoundException(
@@ -89,16 +89,26 @@ protected virtual Task WriteFileAsync(ActionContext context, VirtualFileResult r
throw new ArgumentNullException(nameof(result));
}
+ return WriteFileAsyncInternal(context.HttpContext, fileInfo, range, rangeLength, Logger);
+ }
+
+ internal static Task WriteFileAsyncInternal(
+ HttpContext httpContext,
+ IFileInfo fileInfo,
+ RangeItemHeaderValue? range,
+ long rangeLength,
+ ILogger logger)
+ {
if (range != null && rangeLength == 0)
{
return Task.CompletedTask;
}
- var response = context.HttpContext.Response;
+ var response = httpContext.Response;
if (range != null)
{
- Logger.WritingRangeToBody();
+ logger.WritingRangeToBody();
}
if (range != null)
@@ -113,9 +123,9 @@ protected virtual Task WriteFileAsync(ActionContext context, VirtualFileResult r
count: null);
}
- private IFileInfo GetFileInformation(VirtualFileResult result)
+ internal static IFileInfo GetFileInformation(VirtualFileResult result, IWebHostEnvironment hostingEnvironment)
{
- var fileProvider = GetFileProvider(result);
+ var fileProvider = GetFileProvider(result, hostingEnvironment);
if (fileProvider is NullFileProvider)
{
throw new InvalidOperationException(Resources.VirtualFileResultExecutor_NoFileProviderConfigured);
@@ -131,14 +141,14 @@ private IFileInfo GetFileInformation(VirtualFileResult result)
return fileInfo;
}
- private IFileProvider GetFileProvider(VirtualFileResult result)
+ internal static IFileProvider GetFileProvider(VirtualFileResult result, IWebHostEnvironment hostingEnvironment)
{
if (result.FileProvider != null)
{
return result.FileProvider;
}
- result.FileProvider = _hostingEnvironment.WebRootFileProvider;
+ result.FileProvider = hostingEnvironment.WebRootFileProvider;
return result.FileProvider;
}
diff --git a/src/Mvc/Mvc.Core/src/PhysicalFileResult.cs b/src/Mvc/Mvc.Core/src/PhysicalFileResult.cs
index def6a2b65408..9734ddbacdfe 100644
--- a/src/Mvc/Mvc.Core/src/PhysicalFileResult.cs
+++ b/src/Mvc/Mvc.Core/src/PhysicalFileResult.cs
@@ -3,9 +3,13 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using System.IO;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc
@@ -14,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc
/// A on execution will write a file from disk to the response
/// using mechanisms provided by the host.
///
- public class PhysicalFileResult : FileResult
+ public class PhysicalFileResult : FileResult, IResult
{
private string _fileName;
@@ -66,5 +70,51 @@ public override Task ExecuteResultAsync(ActionContext context)
var executor = context.HttpContext.RequestServices.GetRequiredService>();
return executor.ExecuteAsync(context, this);
}
+
+ Task IResult.ExecuteAsync(HttpContext httpContext)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ var fileInfo = new FileInfo(FileName);
+ if (!fileInfo.Exists)
+ {
+ throw new FileNotFoundException(
+ Resources.FormatFileResult_InvalidPath(FileName), FileName);
+ }
+
+ return ExecuteAsyncInternal(httpContext, this, fileInfo.LastWriteTimeUtc, fileInfo.Length);
+ }
+
+ internal static Task ExecuteAsyncInternal(
+ HttpContext httpContext,
+ PhysicalFileResult result,
+ DateTimeOffset fileLastModified,
+ long fileLength)
+ {
+ var loggerFactory = httpContext.RequestServices.GetRequiredService();
+ var logger = loggerFactory.CreateLogger();
+
+ logger.ExecutingFileResult(result, result.FileName);
+
+ var lastModified = result.LastModified ?? fileLastModified;
+ var (range, rangeLength, serveBody) = FileResultExecutorBase.SetHeadersAndLog(
+ httpContext,
+ result,
+ fileLength,
+ result.EnableRangeProcessing,
+ lastModified,
+ result.EntityTag,
+ logger);
+
+ if (serveBody)
+ {
+ return PhysicalFileResultExecutor.WriteFileAsyncInternal(httpContext, result, range, rangeLength, logger);
+ }
+
+ return Task.CompletedTask;
+ }
}
}
diff --git a/src/Mvc/Mvc.Core/src/RedirectResult.cs b/src/Mvc/Mvc.Core/src/RedirectResult.cs
index 22db7fc57ed1..8ce2beb6f6b4 100644
--- a/src/Mvc/Mvc.Core/src/RedirectResult.cs
+++ b/src/Mvc/Mvc.Core/src/RedirectResult.cs
@@ -4,10 +4,13 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc
{
@@ -15,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc
/// An that returns a Found (302), Moved Permanently (301), Temporary Redirect (307),
/// or Permanent Redirect (308) response with a Location header to the supplied URL.
///
- public class RedirectResult : ActionResult, IKeepTempDataResult
+ public class RedirectResult : ActionResult, IResult, IKeepTempDataResult
{
private string _url;
@@ -112,5 +115,36 @@ public override Task ExecuteResultAsync(ActionContext context)
var executor = context.HttpContext.RequestServices.GetRequiredService>();
return executor.ExecuteAsync(context, this);
}
+
+ ///
+ Task IResult.ExecuteAsync(HttpContext httpContext)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ var loggerFactory = httpContext.RequestServices.GetRequiredService();
+ var logger = loggerFactory.CreateLogger();
+
+ // IsLocalUrl is called to handle URLs starting with '~/'.
+ var destinationUrl = UrlHelperBase.CheckIsLocalUrl(_url) ? UrlHelperBase.Content(httpContext, _url) : _url;
+
+ logger.RedirectResultExecuting(destinationUrl);
+
+ if (PreserveMethod)
+ {
+ httpContext.Response.StatusCode = Permanent
+ ? StatusCodes.Status308PermanentRedirect
+ : StatusCodes.Status307TemporaryRedirect;
+ httpContext.Response.Headers.Location = destinationUrl;
+ }
+ else
+ {
+ httpContext.Response.Redirect(destinationUrl, Permanent);
+ }
+
+ return Task.CompletedTask;
+ }
}
}
diff --git a/src/Mvc/Mvc.Core/src/Routing/UrlHelperBase.cs b/src/Mvc/Mvc.Core/src/Routing/UrlHelperBase.cs
index 92a4280446ef..44717c66a27f 100644
--- a/src/Mvc/Mvc.Core/src/Routing/UrlHelperBase.cs
+++ b/src/Mvc/Mvc.Core/src/Routing/UrlHelperBase.cs
@@ -49,86 +49,11 @@ protected UrlHelperBase(ActionContext actionContext)
public ActionContext ActionContext { get; }
///
- public virtual bool IsLocalUrl([NotNullWhen(true)] string? url)
- {
- if (string.IsNullOrEmpty(url))
- {
- return false;
- }
-
- // Allows "/" or "/foo" but not "//" or "/\".
- if (url[0] == '/')
- {
- // url is exactly "/"
- if (url.Length == 1)
- {
- return true;
- }
-
- // url doesn't start with "//" or "/\"
- if (url[1] != '/' && url[1] != '\\')
- {
- return !HasControlCharacter(url.AsSpan(1));
- }
-
- return false;
- }
-
- // Allows "~/" or "~/foo" but not "~//" or "~/\".
- if (url[0] == '~' && url.Length > 1 && url[1] == '/')
- {
- // url is exactly "~/"
- if (url.Length == 2)
- {
- return true;
- }
-
- // url doesn't start with "~//" or "~/\"
- if (url[2] != '/' && url[2] != '\\')
- {
- return !HasControlCharacter(url.AsSpan(2));
- }
-
- return false;
- }
-
- return false;
-
- static bool HasControlCharacter(ReadOnlySpan readOnlySpan)
- {
- // URLs may not contain ASCII control characters.
- for (var i = 0; i < readOnlySpan.Length; i++)
- {
- if (char.IsControl(readOnlySpan[i]))
- {
- return true;
- }
- }
-
- return false;
- }
- }
+ public virtual bool IsLocalUrl([NotNullWhen(true)] string? url) => CheckIsLocalUrl(url);
///
[return: NotNullIfNotNull("contentPath")]
- public virtual string? Content(string? contentPath)
- {
- if (string.IsNullOrEmpty(contentPath))
- {
- return null;
- }
- else if (contentPath[0] == '~')
- {
- var segment = new PathString(contentPath.Substring(1));
- var applicationPath = ActionContext.HttpContext.Request.PathBase;
-
- var path = applicationPath.Add(segment);
- Debug.Assert(path.HasValue);
- return path.Value;
- }
-
- return contentPath;
- }
+ public virtual string? Content(string? contentPath) => Content(ActionContext.HttpContext, contentPath);
///
public virtual string? Link(string? routeName, object? values)
@@ -372,6 +297,86 @@ internal static void NormalizeRouteValuesForPage(
}
}
+ [return: NotNullIfNotNull("contentPath")]
+ internal static string? Content(HttpContext httpContext, string? contentPath)
+ {
+ if (string.IsNullOrEmpty(contentPath))
+ {
+ return null;
+ }
+ else if (contentPath[0] == '~')
+ {
+ var segment = new PathString(contentPath.Substring(1));
+ var applicationPath = httpContext.Request.PathBase;
+
+ var path = applicationPath.Add(segment);
+ Debug.Assert(path.HasValue);
+ return path.Value;
+ }
+
+ return contentPath;
+ }
+
+ internal static bool CheckIsLocalUrl([NotNullWhen(true)] string? url)
+ {
+ if (string.IsNullOrEmpty(url))
+ {
+ return false;
+ }
+
+ // Allows "/" or "/foo" but not "//" or "/\".
+ if (url[0] == '/')
+ {
+ // url is exactly "/"
+ if (url.Length == 1)
+ {
+ return true;
+ }
+
+ // url doesn't start with "//" or "/\"
+ if (url[1] != '/' && url[1] != '\\')
+ {
+ return !HasControlCharacter(url.AsSpan(1));
+ }
+
+ return false;
+ }
+
+ // Allows "~/" or "~/foo" but not "~//" or "~/\".
+ if (url[0] == '~' && url.Length > 1 && url[1] == '/')
+ {
+ // url is exactly "~/"
+ if (url.Length == 2)
+ {
+ return true;
+ }
+
+ // url doesn't start with "~//" or "~/\"
+ if (url[2] != '/' && url[2] != '\\')
+ {
+ return !HasControlCharacter(url.AsSpan(2));
+ }
+
+ return false;
+ }
+
+ return false;
+
+ static bool HasControlCharacter(ReadOnlySpan readOnlySpan)
+ {
+ // URLs may not contain ASCII control characters.
+ for (var i = 0; i < readOnlySpan.Length; i++)
+ {
+ if (char.IsControl(readOnlySpan[i]))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
private static object CalculatePageName(ActionContext? context, RouteValueDictionary? ambientValues, string pageName)
{
Debug.Assert(pageName.Length > 0);
diff --git a/src/Mvc/Mvc.Core/src/VirtualFileResult.cs b/src/Mvc/Mvc.Core/src/VirtualFileResult.cs
index 7d49d9de41de..ac95c84a07eb 100644
--- a/src/Mvc/Mvc.Core/src/VirtualFileResult.cs
+++ b/src/Mvc/Mvc.Core/src/VirtualFileResult.cs
@@ -3,10 +3,15 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using System.IO;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc
@@ -15,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc
/// A that on execution writes the file specified using a virtual path to the response
/// using mechanisms provided by the host.
///
- public class VirtualFileResult : FileResult
+ public class VirtualFileResult : FileResult, IResult
{
private string _fileName;
@@ -69,5 +74,42 @@ public override Task ExecuteResultAsync(ActionContext context)
var executor = context.HttpContext.RequestServices.GetRequiredService>();
return executor.ExecuteAsync(context, this);
}
+
+ Task IResult.ExecuteAsync(HttpContext httpContext)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ var hostingEnvironment = httpContext.RequestServices.GetRequiredService();
+
+ var fileInfo = VirtualFileResultExecutor.GetFileInformation(this, hostingEnvironment);
+ if (!fileInfo.Exists)
+ {
+ throw new FileNotFoundException(
+ Resources.FormatFileResult_InvalidPath(FileName), FileName);
+ }
+
+ var loggerFactory = httpContext.RequestServices.GetRequiredService();
+ var logger = loggerFactory.CreateLogger();
+
+ var lastModified = LastModified ?? fileInfo.LastModified;
+ var (range, rangeLength, serveBody) = FileResultExecutorBase.SetHeadersAndLog(
+ httpContext,
+ this,
+ fileInfo.Length,
+ EnableRangeProcessing,
+ lastModified,
+ EntityTag,
+ logger);
+
+ if (serveBody)
+ {
+ return VirtualFileResultExecutor.WriteFileAsyncInternal(httpContext, fileInfo, range, rangeLength, logger);
+ }
+
+ return Task.CompletedTask;
+ }
}
}
diff --git a/src/Mvc/Mvc.Core/test/FileContentResultTest.cs b/src/Mvc/Mvc.Core/test/BaseFileContentResultTest.cs
similarity index 80%
rename from src/Mvc/Mvc.Core/test/FileContentResultTest.cs
rename to src/Mvc/Mvc.Core/test/BaseFileContentResultTest.cs
index 77380a55755c..cb397e816a81 100644
--- a/src/Mvc/Mvc.Core/test/FileContentResultTest.cs
+++ b/src/Mvc/Mvc.Core/test/BaseFileContentResultTest.cs
@@ -17,62 +17,10 @@
namespace Microsoft.AspNetCore.Mvc
{
- public class FileContentResultTest
+ public class BaseFileContentResultTest
{
- [Fact]
- public void Constructor_SetsFileContents()
- {
- // Arrange
- var fileContents = new byte[0];
-
- // Act
- var result = new FileContentResult(fileContents, "text/plain");
-
- // Assert
- Assert.Same(fileContents, result.FileContents);
- }
-
- [Fact]
- public void Constructor_SetsContentTypeAndParameters()
- {
- // Arrange
- var fileContents = new byte[0];
- var contentType = "text/plain; charset=us-ascii; p1=p1-value";
- var expectedMediaType = contentType;
-
- // Act
- var result = new FileContentResult(fileContents, contentType);
-
- // Assert
- Assert.Same(fileContents, result.FileContents);
- 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()
+ public static async Task WriteFileAsync_CopiesBuffer_ToOutputStream(
+ Func function)
{
// Arrange
var buffer = new byte[] { 1, 2, 3, 4, 5 };
@@ -82,23 +30,24 @@ public async Task WriteFileAsync_CopiesBuffer_ToOutputStream()
var outStream = new MemoryStream();
httpContext.Response.Body = outStream;
- var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var result = new FileContentResult(buffer, "text/plain");
// Act
- await result.ExecuteResultAsync(context);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
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)
+ public static async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(
+ long? start,
+ long? end,
+ string expectedString,
+ long contentLength,
+ Func function)
{
// Arrange
var contentType = "text/plain";
@@ -125,7 +74,8 @@ public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeReque
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
start = start ?? 11 - end;
@@ -144,8 +94,8 @@ public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeReque
Assert.Equal(expectedString, body);
}
- [Fact]
- public async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest()
+ public static async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest(
+ Func function)
{
// Arrange
var contentType = "text/plain";
@@ -173,7 +123,8 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest()
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var httpResponse = actionContext.HttpContext.Response;
@@ -200,8 +151,8 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest()
}
}
- [Fact]
- public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored()
+ public static async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored(
+ Func function)
{
// Arrange
var contentType = "text/plain";
@@ -228,7 +179,8 @@ public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored()
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var httpResponse = actionContext.HttpContext.Response;
@@ -241,8 +193,8 @@ public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored()
Assert.Equal("Hello World", body);
}
- [Fact]
- public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored()
+ public static async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored(
+ Func function)
{
// Arrange
var contentType = "text/plain";
@@ -270,7 +222,8 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored()
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var httpResponse = actionContext.HttpContext.Response;
@@ -283,11 +236,9 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored()
Assert.Equal("Hello World", body);
}
- [Theory]
- [InlineData("0-5")]
- [InlineData("bytes = ")]
- [InlineData("bytes = 1-4, 5-11")]
- public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(string rangeString)
+ public static async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(
+ string rangeString,
+ Func function)
{
// Arrange
var contentType = "text/plain";
@@ -309,7 +260,8 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnore
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var httpResponse = actionContext.HttpContext.Response;
@@ -323,10 +275,9 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnore
Assert.Equal("Hello World", body);
}
- [Theory]
- [InlineData("bytes = 12-13")]
- [InlineData("bytes = -0")]
- public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
+ public static async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(
+ string rangeString,
+ Func function)
{
// Arrange
var contentType = "text/plain";
@@ -348,7 +299,8 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotS
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var httpResponse = actionContext.HttpContext.Response;
@@ -365,8 +317,8 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotS
Assert.Empty(body);
}
- [Fact]
- public async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored()
+ public static async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored(
+ Func function)
{
// Arrange
var contentType = "text/plain";
@@ -393,7 +345,8 @@ public async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored()
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var httpResponse = actionContext.HttpContext.Response;
@@ -407,8 +360,8 @@ public async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored()
Assert.Empty(body);
}
- [Fact]
- public async Task WriteFileAsync_NotModified_RangeRequestedIgnored()
+ public static async Task WriteFileAsync_NotModified_RangeRequestedIgnored(
+ Func function)
{
// Arrange
var contentType = "text/plain";
@@ -435,7 +388,8 @@ public async Task WriteFileAsync_NotModified_RangeRequestedIgnored()
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var httpResponse = actionContext.HttpContext.Response;
@@ -450,8 +404,8 @@ public async Task WriteFileAsync_NotModified_RangeRequestedIgnored()
Assert.Empty(body);
}
- [Fact]
- public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
+ public static async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(
+ Func function)
{
// Arrange
var expectedContentType = "text/foo; charset=us-ascii";
@@ -462,12 +416,13 @@ public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
var outStream = new MemoryStream();
httpContext.Response.Body = outStream;
- var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var result = new FileContentResult(buffer, expectedContentType);
// Act
- await result.ExecuteResultAsync(context);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
Assert.Equal(buffer, outStream.ToArray());
@@ -493,4 +448,4 @@ private static HttpContext GetHttpContext()
return httpContext;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Mvc/Mvc.Core/test/PhysicalFileResultTest.cs b/src/Mvc/Mvc.Core/test/BasePhysicalFileResultTest.cs
similarity index 75%
rename from src/Mvc/Mvc.Core/test/PhysicalFileResultTest.cs
rename to src/Mvc/Mvc.Core/test/BasePhysicalFileResultTest.cs
index 8d29080dd204..c4d8960e879e 100644
--- a/src/Mvc/Mvc.Core/test/PhysicalFileResultTest.cs
+++ b/src/Mvc/Mvc.Core/test/BasePhysicalFileResultTest.cs
@@ -2,9 +2,9 @@
// 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.IO.Pipelines;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@@ -21,43 +21,14 @@
namespace Microsoft.AspNetCore.Mvc
{
- public class PhysicalFileResultTest
+ public class BasePhysicalFileResultTest
{
- [Fact]
- public void Constructor_SetsFileName()
- {
- // Arrange
- var path = Path.GetFullPath("helllo.txt");
-
- // Act
- var result = new TestPhysicalFileResult(path, "text/plain");
-
- // Assert
- Assert.Equal(path, result.FileName);
- }
-
- [Fact]
- public void Constructor_SetsContentTypeAndParameters()
- {
- // Arrange
- var path = Path.GetFullPath("helllo.txt");
- var contentType = "text/plain; charset=us-ascii; p1=p1-value";
- var expectedMediaType = contentType;
-
- // Act
- var result = new TestPhysicalFileResult(path, contentType);
-
- // Assert
- Assert.Equal(path, result.FileName);
- MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
- }
-
- [Theory]
- [InlineData(0, 3, "File", 4)]
- [InlineData(8, 13, "Result", 6)]
- [InlineData(null, 5, "ts�", 5)]
- [InlineData(8, null, "ResultTestFile contents�", 26)]
- public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
+ public static async Task WriteFileAsync_WritesRangeRequested(
+ long? start,
+ long? end,
+ string expectedString,
+ long contentLength,
+ Func function)
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
@@ -73,7 +44,8 @@ public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, st
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var startResult = start ?? 34 - end;
@@ -90,8 +62,8 @@ public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, st
Assert.Equal((long?)contentLength, sendFile.Length);
}
- [Fact]
- public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
+ public static async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange(
+ Func function)
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
@@ -109,7 +81,8 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var httpResponse = actionContext.HttpContext.Response;
@@ -125,8 +98,8 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
Assert.Equal(4, sendFile.Length);
}
- [Fact]
- public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored()
+ public static async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored(
+ Func function)
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
@@ -143,7 +116,8 @@ public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var httpResponse = actionContext.HttpContext.Response;
@@ -154,8 +128,8 @@ public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored
Assert.Null(sendFile.Length);
}
- [Fact]
- public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
+ public static async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored(
+ Func function)
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
@@ -173,7 +147,8 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var httpResponse = actionContext.HttpContext.Response;
@@ -184,11 +159,9 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
Assert.Null(sendFile.Length);
}
- [Theory]
- [InlineData("0-5")]
- [InlineData("bytes = ")]
- [InlineData("bytes = 1-4, 5-11")]
- public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string rangeString)
+ public static async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(
+ string rangeString,
+ Func function)
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
@@ -203,7 +176,8 @@ public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var httpResponse = actionContext.HttpContext.Response;
@@ -215,10 +189,9 @@ public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string
Assert.Null(sendFile.Length);
}
- [Theory]
- [InlineData("bytes = 35-36")]
- [InlineData("bytes = -0")]
- public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString)
+ public static async Task WriteFileAsync_RangeRequestedNotSatisfiable(
+ string rangeString,
+ Func function)
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
@@ -233,7 +206,8 @@ public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var httpResponse = actionContext.HttpContext.Response;
@@ -249,8 +223,8 @@ public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString
Assert.Empty(body);
}
- [Fact]
- public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
+ public static async Task WriteFileAsync_RangeRequested_PreconditionFailed(
+ Func function)
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
@@ -265,7 +239,8 @@ public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var httpResponse = actionContext.HttpContext.Response;
@@ -279,8 +254,8 @@ public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
Assert.Empty(body);
}
- [Fact]
- public async Task WriteFileAsync_RangeRequested_NotModified()
+ public static async Task WriteFileAsync_RangeRequested_NotModified(
+ Func function)
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
@@ -295,7 +270,8 @@ public async Task WriteFileAsync_RangeRequested_NotModified()
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
var httpResponse = actionContext.HttpContext.Response;
@@ -310,8 +286,8 @@ public async Task WriteFileAsync_RangeRequested_NotModified()
Assert.Empty(body);
}
- [Fact]
- public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent()
+ public static async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent(
+ Func function)
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
@@ -323,21 +299,21 @@ public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent()
var httpContext = GetHttpContext();
httpContext.Features.Set(sendFileMock.Object);
- var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(context);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
sendFileMock.Verify();
}
- [Theory]
- [InlineData(0, 3, 4)]
- [InlineData(8, 13, 6)]
- [InlineData(null, 3, 3)]
- [InlineData(8, null, 26)]
- public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, long contentLength)
+ public static async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(
+ long? start,
+ long? end,
+ long contentLength,
+ Func function)
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
@@ -354,7 +330,8 @@ public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHtt
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(actionContext);
+ object functionContext = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)functionContext);
// Assert
start = start ?? 34 - end;
@@ -372,8 +349,8 @@ public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHtt
Assert.Equal(contentLength, httpResponse.ContentLength);
}
- [Fact]
- public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
+ public static async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(
+ Func function)
{
// Arrange
var expectedContentType = "text/foo; charset=us-ascii";
@@ -382,10 +359,11 @@ public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
var sendFile = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Features.Set(sendFile);
- var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(context);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
Assert.Equal(expectedContentType, httpContext.Response.ContentType);
@@ -395,8 +373,8 @@ public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
Assert.Equal(CancellationToken.None, sendFile.Token);
}
- [Fact]
- public async Task ExecuteResultAsync_WorksWithAbsolutePaths()
+ public static async Task ExecuteResultAsync_WorksWithAbsolutePaths(
+ Func function)
{
// Arrange
var path = Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile.txt"));
@@ -406,10 +384,11 @@ public async Task ExecuteResultAsync_WorksWithAbsolutePaths()
var httpContext = GetHttpContext();
httpContext.Features.Set(sendFile);
- var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
- await result.ExecuteResultAsync(context);
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
// Assert
Assert.Equal(Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile.txt")), sendFile.Name);
@@ -418,66 +397,53 @@ public async Task ExecuteResultAsync_WorksWithAbsolutePaths()
Assert.Equal(CancellationToken.None, sendFile.Token);
}
- [Theory]
- [InlineData("FilePathResultTestFile.txt")]
- [InlineData("./FilePathResultTestFile.txt")]
- [InlineData(".\\FilePathResultTestFile.txt")]
- [InlineData("~/FilePathResultTestFile.txt")]
- [InlineData("..\\TestFiles/FilePathResultTestFile.txt")]
- [InlineData("..\\TestFiles\\FilePathResultTestFile.txt")]
- [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt")]
- [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt")]
- [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt")]
- [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt")]
- [InlineData("~/SubFolder/SubFolderTestFile.txt")]
- [InlineData("~/SubFolder\\SubFolderTestFile.txt")]
- public async Task ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths(string path)
+ public static async Task ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths(
+ string path,
+ Func function)
{
// Arrange
var result = new TestPhysicalFileResult(path, "text/plain");
- var context = new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor());
+ var actionContext = new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor());
var expectedMessage = $"Path '{path}' was not rooted.";
// Act
- var ex = await Assert.ThrowsAsync(() => result.ExecuteResultAsync(context));
+ object context = typeof(TContext) == typeof(HttpContext) ? actionContext.HttpContext : actionContext;
+ var ex = await Assert.ThrowsAsync(
+ () => function(result, (TContext)context));
// Assert
Assert.Equal(expectedMessage, ex.Message);
}
- [Theory]
- [InlineData("/SubFolder/SubFolderTestFile.txt")]
- [InlineData("\\SubFolder\\SubFolderTestFile.txt")]
- [InlineData("/SubFolder\\SubFolderTestFile.txt")]
- [InlineData("\\SubFolder/SubFolderTestFile.txt")]
- [InlineData("./SubFolder/SubFolderTestFile.txt")]
- [InlineData(".\\SubFolder\\SubFolderTestFile.txt")]
- [InlineData("./SubFolder\\SubFolderTestFile.txt")]
- [InlineData(".\\SubFolder/SubFolderTestFile.txt")]
- public void ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(string path)
+ public static void ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(
+ string path,
+ Func function)
{
// Arrange
var result = new TestPhysicalFileResult(path, "text/plain");
- var context = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
+ var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
// Act & Assert
- Assert.ThrowsAsync(() => result.ExecuteResultAsync(context));
+ object context = typeof(TContext) == typeof(HttpContext) ? actionContext.HttpContext : actionContext;
+ Assert.ThrowsAsync(
+ () => function(result, (TContext)context));
}
- [Theory]
- [InlineData("/FilePathResultTestFile.txt")]
- [InlineData("\\FilePathResultTestFile.txt")]
- public void ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(string path)
+ public static void ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(
+ string path,
+ Func function)
{
// Arrange
var result = new TestPhysicalFileResult(path, "text/plain");
- var context = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
+ var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
// Act & Assert
- Assert.ThrowsAsync(() => result.ExecuteResultAsync(context));
+ object context = typeof(TContext) == typeof(HttpContext) ? actionContext.HttpContext : actionContext;
+ Assert.ThrowsAsync(
+ () => function(result, (TContext)context));
}
- private class TestPhysicalFileResult : PhysicalFileResult
+ private class TestPhysicalFileResult : PhysicalFileResult, IResult
{
public TestPhysicalFileResult(string filePath, string contentType)
: base(filePath, contentType)
@@ -489,6 +455,13 @@ public override Task ExecuteResultAsync(ActionContext context)
var executor = context.HttpContext.RequestServices.GetRequiredService();
return executor.ExecuteAsync(context, this);
}
+
+ Task IResult.ExecuteAsync(HttpContext httpContext)
+ {
+ var lastModified = DateTimeOffset.MinValue.AddDays(1);
+ var fileLastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
+ return ExecuteAsyncInternal(httpContext, this, fileLastModified, 34);
+ }
}
private class TestPhysicalFileResultExecutor : PhysicalFileResultExecutor
@@ -564,4 +537,4 @@ private static HttpContext GetHttpContext()
return httpContext;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Mvc/Mvc.Core/test/BaseRedirectResultTest.cs b/src/Mvc/Mvc.Core/test/BaseRedirectResultTest.cs
new file mode 100644
index 000000000000..6097a09263ec
--- /dev/null
+++ b/src/Mvc/Mvc.Core/test/BaseRedirectResultTest.cs
@@ -0,0 +1,91 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.AspNetCore.Mvc.Routing;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ public class BaseRedirectResultTest
+ {
+ public static async Task Execute_ReturnsContentPath_WhenItDoesNotStartWithTilde(
+ string appRoot,
+ string contentPath,
+ string expectedPath,
+ Func function)
+ {
+ // Arrange
+ var httpContext = GetHttpContext(appRoot);
+ var actionContext = GetActionContext(httpContext);
+ var result = new RedirectResult(contentPath);
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext) context);
+
+ // Assert
+ // Verifying if Redirect was called with the specific Url and parameter flag.
+ Assert.Equal(expectedPath, httpContext.Response.Headers.Location.ToString());
+ Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode);
+ }
+
+ public static async Task Execute_ReturnsAppRelativePath_WhenItStartsWithTilde(
+ string appRoot,
+ string contentPath,
+ string expectedPath,
+ Func function)
+ {
+ // Arrange
+ var httpContext = GetHttpContext(appRoot);
+ var actionContext = GetActionContext(httpContext);
+ var result = new RedirectResult(contentPath);
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // Assert
+ // Verifying if Redirect was called with the specific Url and parameter flag.
+ Assert.Equal(expectedPath, httpContext.Response.Headers.Location.ToString());
+ Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode);
+ }
+
+ private static ActionContext GetActionContext(HttpContext httpContext)
+ {
+ var routeData = new RouteData();
+ routeData.Routers.Add(Mock.Of());
+
+ return new ActionContext(
+ httpContext,
+ routeData,
+ new ActionDescriptor());
+ }
+
+ private static IServiceProvider GetServiceProvider()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddSingleton, RedirectResultExecutor>();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddTransient();
+ return serviceCollection.BuildServiceProvider();
+ }
+
+ private static HttpContext GetHttpContext(
+ string appRoot)
+ {
+ var httpContext = new DefaultHttpContext();
+ httpContext.RequestServices = GetServiceProvider();
+ httpContext.Request.PathBase = new PathString(appRoot);
+ return httpContext;
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/test/BaseVirtualFileResultTest.cs b/src/Mvc/Mvc.Core/test/BaseVirtualFileResultTest.cs
new file mode 100644
index 000000000000..727d18eadb8f
--- /dev/null
+++ b/src/Mvc/Mvc.Core/test/BaseVirtualFileResultTest.cs
@@ -0,0 +1,748 @@
+// 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.IO.Pipelines;
+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;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.AspNetCore.Routing;
+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;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ public class BaseVirtualFileResultTest
+ {
+ public static async Task WriteFileAsync_WritesRangeRequested(
+ long? start,
+ long? end,
+ string expectedString,
+ long contentLength,
+ Func function)
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var result = new TestVirtualFileResult(path, contentType);
+ result.EnableRangeProcessing = true;
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var sendFileFeature = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFileFeature);
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient, TestVirtualFileResultExecutor>()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.Range = new RangeHeaderValue(start, end);
+ requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
+ httpContext.Request.Method = HttpMethods.Get;
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // Assert
+ var startResult = start ?? 33 - end;
+ var endResult = startResult + contentLength - 1;
+ var httpResponse = actionContext.HttpContext.Response;
+ var contentRange = new ContentRangeHeaderValue(startResult.Value, endResult.Value, 33);
+ Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers.AcceptRanges);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange);
+ Assert.NotEmpty(httpResponse.Headers.LastModified);
+ Assert.Equal(contentLength, httpResponse.ContentLength);
+ Assert.Equal(path, sendFileFeature.Name);
+ Assert.Equal(startResult, sendFileFeature.Offset);
+ Assert.Equal((long?)contentLength, sendFileFeature.Length);
+ }
+
+ public static async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange(
+ Func function)
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var result = new TestVirtualFileResult(path, contentType);
+ result.EnableRangeProcessing = true;
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var sendFileFeature = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFileFeature);
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient, TestVirtualFileResultExecutor>()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
+ requestHeaders.Range = new RangeHeaderValue(0, 3);
+ requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
+ httpContext.Request.Method = HttpMethods.Get;
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
+ Assert.Equal("bytes", httpResponse.Headers.AcceptRanges);
+ var contentRange = new ContentRangeHeaderValue(0, 3, 33);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag);
+ Assert.Equal(4, httpResponse.ContentLength);
+ Assert.Equal(path, sendFileFeature.Name);
+ Assert.Equal(0, sendFileFeature.Offset);
+ Assert.Equal(4, sendFileFeature.Length);
+ }
+
+ public static async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored(
+ Func function)
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var result = new TestVirtualFileResult(path, contentType);
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var sendFileFeature = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFileFeature);
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient, TestVirtualFileResultExecutor>()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
+ requestHeaders.Range = new RangeHeaderValue(0, 3);
+ requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
+ httpContext.Request.Method = HttpMethods.Get;
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag);
+ Assert.Equal(path, sendFileFeature.Name);
+ Assert.Equal(0, sendFileFeature.Offset);
+ Assert.Null(sendFileFeature.Length);
+ }
+
+ public static async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored(
+ Func function)
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var result = new TestVirtualFileResult(path, contentType);
+ result.EnableRangeProcessing = true;
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var sendFileFeature = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFileFeature);
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient, TestVirtualFileResultExecutor>()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
+ requestHeaders.Range = new RangeHeaderValue(0, 3);
+ requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
+ httpContext.Request.Method = HttpMethods.Get;
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
+ Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag);
+ Assert.Equal(path, sendFileFeature.Name);
+ Assert.Equal(0, sendFileFeature.Offset);
+ Assert.Null(sendFileFeature.Length);
+ }
+
+ public static async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(
+ string rangeString,
+ Func function)
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var result = new TestVirtualFileResult(path, contentType);
+ result.EnableRangeProcessing = true;
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var sendFileFeature = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFileFeature);
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient, TestVirtualFileResultExecutor>()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ httpContext.Request.Headers.Range = rangeString;
+ requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
+ httpContext.Request.Method = HttpMethods.Get;
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
+ Assert.Empty(httpResponse.Headers.ContentRange);
+ Assert.NotEmpty(httpResponse.Headers.LastModified);
+ Assert.Equal(path, sendFileFeature.Name);
+ Assert.Equal(0, sendFileFeature.Offset);
+ Assert.Null(sendFileFeature.Length);
+ }
+
+ public static async Task WriteFileAsync_RangeRequestedNotSatisfiable(
+ string rangeString,
+ Func function)
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var result = new TestVirtualFileResult(path, contentType);
+ result.EnableRangeProcessing = true;
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var httpContext = GetHttpContext();
+ httpContext.Response.Body = new MemoryStream();
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient, TestVirtualFileResultExecutor>()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ httpContext.Request.Headers.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
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // 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.AcceptRanges);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange);
+ Assert.NotEmpty(httpResponse.Headers.LastModified);
+ Assert.Equal(0, httpResponse.ContentLength);
+ Assert.Empty(body);
+ }
+
+ public static async Task WriteFileAsync_RangeRequested_PreconditionFailed(
+ Func function)
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var result = new TestVirtualFileResult(path, contentType);
+ result.EnableRangeProcessing = true;
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var sendFileFeature = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFileFeature);
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient, TestVirtualFileResultExecutor>()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue;
+ httpContext.Request.Headers.Range = "bytes = 0-6";
+ httpContext.Request.Method = HttpMethods.Get;
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
+ Assert.Null(httpResponse.ContentLength);
+ Assert.Empty(httpResponse.Headers.ContentRange);
+ Assert.NotEmpty(httpResponse.Headers.LastModified);
+ Assert.Null(sendFileFeature.Name); // Not called
+ }
+
+ public static async Task WriteFileAsync_RangeRequested_NotModified(
+ Func function)
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var result = new TestVirtualFileResult(path, contentType);
+ result.EnableRangeProcessing = true;
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var sendFileFeature = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFileFeature);
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient, TestVirtualFileResultExecutor>()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1);
+ httpContext.Request.Headers.Range = "bytes = 0-6";
+ httpContext.Request.Method = HttpMethods.Get;
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // Assert
+ var httpResponse = actionContext.HttpContext.Response;
+ Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
+ Assert.Null(httpResponse.ContentLength);
+ Assert.Empty(httpResponse.Headers.ContentRange);
+ Assert.NotEmpty(httpResponse.Headers.LastModified);
+ Assert.False(httpResponse.Headers.ContainsKey(HeaderNames.ContentType));
+ Assert.Null(sendFileFeature.Name); // Not called
+ }
+
+ public static async Task ExecuteResultAsync_FallsBackToWebRootFileProvider_IfNoFileProviderIsPresent(
+ Func function)
+ {
+ // Arrange
+ var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt");
+ var result = new TestVirtualFileResult(path, "text/plain");
+
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+
+ var sendFileFeature = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFileFeature);
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient, TestVirtualFileResultExecutor>()
+ .AddTransient()
+ .BuildServiceProvider();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // Assert
+ Assert.Equal(path, sendFileFeature.Name);
+ Assert.Equal(0, sendFileFeature.Offset);
+ Assert.Null(sendFileFeature.Length);
+ }
+
+ public static async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent(
+ Func function)
+ {
+ // Arrange
+ var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt");
+ var result = new TestVirtualFileResult(path, "text/plain")
+ {
+ FileProvider = GetFileProvider(path),
+ };
+
+ var sendFileMock = new Mock();
+ sendFileMock
+ .Setup(s => s.SendFileAsync(path, 0, null, CancellationToken.None))
+ .Returns(Task.FromResult(0));
+
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFileMock.Object);
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // Assert
+ sendFileMock.Verify();
+ }
+ public static async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(
+ long? start,
+ long? end,
+ string expectedString,
+ long contentLength,
+ Func function)
+ {
+ // Arrange
+ var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt");
+ var result = new TestVirtualFileResult(path, "text/plain")
+ {
+ FileProvider = GetFileProvider(path),
+ EnableRangeProcessing = true,
+ };
+
+ var sendFile = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFile);
+ var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRootFileProvider)
+ .Returns(GetFileProvider(path));
+ httpContext.RequestServices = new ServiceCollection()
+ .AddSingleton(appEnvironment.Object)
+ .AddTransient, TestVirtualFileResultExecutor>()
+ .AddTransient()
+ .BuildServiceProvider();
+
+ var requestHeaders = httpContext.Request.GetTypedHeaders();
+ requestHeaders.Range = new RangeHeaderValue(start, end);
+ requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
+ httpContext.Request.Method = HttpMethods.Get;
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ object functionContext = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext) functionContext);
+
+ // 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.AcceptRanges);
+ Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange);
+ Assert.NotEmpty(httpResponse.Headers.LastModified);
+ Assert.Equal(contentLength, httpResponse.ContentLength);
+ }
+
+ public static async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(
+ Func function)
+ {
+ // Arrange
+ var expectedContentType = "text/foo; charset=us-ascii";
+ var result = new TestVirtualFileResult(
+ "FilePathResultTestFile_ASCII.txt", expectedContentType)
+ {
+ FileProvider = GetFileProvider("FilePathResultTestFile_ASCII.txt"),
+ };
+
+ var sendFileFeature = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFileFeature);
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // Assert
+ Assert.Equal(expectedContentType, httpContext.Response.ContentType);
+ Assert.Equal("FilePathResultTestFile_ASCII.txt", sendFileFeature.Name);
+ }
+
+ public static async Task ExecuteResultAsync_ReturnsFileContentsForRelativePaths(
+ Func function)
+ {
+ // Arrange
+ var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt");
+ var result = new TestVirtualFileResult(path, "text/plain")
+ {
+ FileProvider = GetFileProvider(path),
+ };
+
+ var sendFileFeature = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFileFeature);
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // Assert
+ Assert.Equal(path, sendFileFeature.Name);
+ }
+
+ public static async Task ExecuteResultAsync_ReturnsFiles_ForDifferentPaths(
+ string path,
+ Func function)
+ {
+ // Arrange
+ var result = new TestVirtualFileResult(path, "text/plain")
+ {
+ FileProvider = GetFileProvider(path),
+ };
+
+ var sendFileFeature = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFileFeature);
+
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // Assert
+ Mock.Get(result.FileProvider).Verify();
+ Assert.Equal(path, sendFileFeature.Name);
+ }
+
+ public static async Task ExecuteResultAsync_TrimsTilde_BeforeInvokingFileProvider(
+ string path,
+ Func function)
+ {
+ // Arrange
+ var expectedPath = path.Substring(1);
+ var result = new TestVirtualFileResult(path, "text/plain")
+ {
+ FileProvider = GetFileProvider(expectedPath),
+ };
+
+ var sendFileFeature = new TestSendFileFeature();
+ var httpContext = GetHttpContext();
+ httpContext.Features.Set(sendFileFeature);
+
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(result, (TContext)context);
+
+ // Assert
+ Mock.Get(result.FileProvider).Verify();
+ Assert.Equal(expectedPath, sendFileFeature.Name);
+ }
+
+ public static async Task ExecuteResultAsync_WorksWithNonDiskBasedFiles(
+ Func function)
+ {
+ // Arrange
+ var httpContext = GetHttpContext(typeof(VirtualFileResultExecutor));
+ httpContext.Response.Body = new MemoryStream();
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+ var expectedData = "This is an embedded resource";
+ var sourceStream = new MemoryStream(Encoding.UTF8.GetBytes(expectedData));
+
+ var nonDiskFileInfo = new Mock();
+ nonDiskFileInfo.SetupGet(fi => fi.Exists).Returns(true);
+ nonDiskFileInfo.SetupGet(fi => fi.PhysicalPath).Returns(() => null); // set null to indicate non-disk file
+ nonDiskFileInfo.Setup(fi => fi.CreateReadStream()).Returns(sourceStream);
+ var nonDiskFileProvider = new Mock();
+ nonDiskFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny())).Returns(nonDiskFileInfo.Object);
+
+ var filePathResult = new VirtualFileResult("/SampleEmbeddedFile.txt", "text/plain")
+ {
+ FileProvider = nonDiskFileProvider.Object
+ };
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext;
+ await function(filePathResult, (TContext)context);
+
+ // Assert
+ httpContext.Response.Body.Position = 0;
+ var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
+ Assert.Equal(expectedData, contents);
+ }
+
+ public static async Task ExecuteResultAsync_ThrowsFileNotFound_IfFileProviderCanNotFindTheFile(
+ Func function)
+ {
+ // Arrange
+ var path = "TestPath.txt";
+ var fileInfo = new Mock();
+ fileInfo.SetupGet(f => f.Exists).Returns(false);
+ var fileProvider = new Mock();
+ fileProvider.Setup(f => f.GetFileInfo(path)).Returns(fileInfo.Object);
+ var filePathResult = new TestVirtualFileResult(path, "text/plain")
+ {
+ FileProvider = fileProvider.Object,
+ };
+
+ var expectedMessage = "Could not find file: " + path;
+ var actionContext = new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor());
+
+ // Act
+ object context = typeof(TContext) == typeof(HttpContext) ? actionContext.HttpContext : actionContext;
+ var ex = await Assert.ThrowsAsync(() => function(filePathResult, (TContext)context));
+
+ // Assert
+ Assert.Equal(expectedMessage, ex.Message);
+ Assert.Equal(path, ex.FileName);
+ }
+
+ private static IServiceCollection CreateServices(Type executorType)
+ {
+ var services = new ServiceCollection();
+
+ var hostingEnvironment = new Mock();
+
+ services.AddSingleton, TestVirtualFileResultExecutor>();
+ if (executorType != null)
+ {
+ services.AddSingleton(typeof(IActionResultExecutor), executorType);
+ }
+
+ services.AddSingleton(hostingEnvironment.Object);
+ services.AddSingleton(NullLoggerFactory.Instance);
+
+ return services;
+ }
+
+ private static HttpContext GetHttpContext(Type executorType = null)
+ {
+ var services = CreateServices(executorType);
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.RequestServices = services.BuildServiceProvider();
+
+ return httpContext;
+ }
+
+ private static IFileProvider GetFileProvider(string path)
+ {
+ var fileInfo = new Mock();
+ fileInfo.SetupGet(fi => fi.Length).Returns(33);
+ fileInfo.SetupGet(fi => fi.Exists).Returns(true);
+ var lastModified = DateTimeOffset.MinValue.AddDays(1);
+ lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0));
+ fileInfo.SetupGet(fi => fi.LastModified).Returns(lastModified);
+ fileInfo.SetupGet(fi => fi.PhysicalPath).Returns(path);
+ var fileProvider = new Mock();
+ fileProvider.Setup(fp => fp.GetFileInfo(path))
+ .Returns(fileInfo.Object)
+ .Verifiable();
+
+ return fileProvider.Object;
+ }
+
+ private class TestVirtualFileResult : VirtualFileResult
+ {
+ public TestVirtualFileResult(string filePath, string contentType)
+ : base(filePath, contentType)
+ {
+ }
+
+ public override Task ExecuteResultAsync(ActionContext context)
+ {
+ var executor = (TestVirtualFileResultExecutor)context.HttpContext.RequestServices.GetRequiredService>();
+ return executor.ExecuteAsync(context, this);
+ }
+ }
+
+ private class TestVirtualFileResultExecutor : VirtualFileResultExecutor
+ {
+ public TestVirtualFileResultExecutor(ILoggerFactory loggerFactory, IWebHostEnvironment hostingEnvironment)
+ : base(loggerFactory, hostingEnvironment)
+ {
+ }
+ }
+
+ private class TestSendFileFeature : IHttpResponseBodyFeature
+ {
+ public string Name { get; set; }
+ public long Offset { get; set; }
+ public long? Length { get; set; }
+ public CancellationToken Token { get; set; }
+
+ public Stream Stream => throw new NotImplementedException();
+
+ public PipeWriter Writer => throw new NotImplementedException();
+
+ public Task CompleteAsync()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void DisableBuffering()
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
+ {
+ Name = path;
+ Offset = offset;
+ Length = length;
+ Token = cancellation;
+
+ return Task.FromResult(0);
+ }
+
+ public Task StartAsync(CancellationToken cancellation = default)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/test/FileContentActionResultTest.cs b/src/Mvc/Mvc.Core/test/FileContentActionResultTest.cs
new file mode 100644
index 000000000000..a8e91fddd761
--- /dev/null
+++ b/src/Mvc/Mvc.Core/test/FileContentActionResultTest.cs
@@ -0,0 +1,155 @@
+// 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.Threading.Tasks;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ public class FileContentActionResultTest
+ {
+ [Fact]
+ public void Constructor_SetsFileContents()
+ {
+ // Arrange
+ var fileContents = new byte[0];
+
+ // Act
+ var result = new FileContentResult(fileContents, "text/plain");
+
+ // Assert
+ Assert.Same(fileContents, result.FileContents);
+ }
+
+ [Fact]
+ public void Constructor_SetsContentTypeAndParameters()
+ {
+ // Arrange
+ var fileContents = new byte[0];
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var expectedMediaType = contentType;
+
+ // Act
+ var result = new FileContentResult(fileContents, contentType);
+
+ // Assert
+ Assert.Same(fileContents, result.FileContents);
+ 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()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_CopiesBuffer_ToOutputStream(action);
+ }
+
+ [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)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseFileContentResultTest
+ .WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(start, end, expectedString, contentLength, action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored(action);
+ }
+
+ [Theory]
+ [InlineData("0-5")]
+ [InlineData("bytes = ")]
+ [InlineData("bytes = 1-4, 5-11")]
+ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(string rangeString)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(rangeString, action);
+ }
+
+ [Theory]
+ [InlineData("bytes = 12-13")]
+ [InlineData("bytes = -0")]
+ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(rangeString, action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_PreconditionFailed_RangeRequestedIgnored(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_NotModified_RangeRequestedIgnored()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_NotModified_RangeRequestedIgnored(action);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseFileContentResultTest.ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(action);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/test/FileContentResult.cs b/src/Mvc/Mvc.Core/test/FileContentResult.cs
new file mode 100644
index 000000000000..7be596c7ee6a
--- /dev/null
+++ b/src/Mvc/Mvc.Core/test/FileContentResult.cs
@@ -0,0 +1,101 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ public class FileContentResultTest
+ {
+ [Fact]
+ public async Task WriteFileAsync_CopiesBuffer_ToOutputStream()
+ {var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+ await BaseFileContentResultTest.WriteFileAsync_CopiesBuffer_ToOutputStream(action);
+ }
+
+ [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)
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BaseFileContentResultTest
+ .WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(start, end, expectedString, contentLength, action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest()
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored()
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored()
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored(action);
+ }
+
+ [Theory]
+ [InlineData("0-5")]
+ [InlineData("bytes = ")]
+ [InlineData("bytes = 1-4, 5-11")]
+ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(string rangeString)
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(rangeString, action);
+ }
+
+ [Theory]
+ [InlineData("bytes = 12-13")]
+ [InlineData("bytes = -0")]
+ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(rangeString, action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored()
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_PreconditionFailed_RangeRequestedIgnored(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_NotModified_RangeRequestedIgnored()
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BaseFileContentResultTest.WriteFileAsync_NotModified_RangeRequestedIgnored(action);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BaseFileContentResultTest.ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(action);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/test/FileResultTest.cs b/src/Mvc/Mvc.Core/test/FileResultTest.cs
index 07967adc1eda..25e6d6f3634e 100644
--- a/src/Mvc/Mvc.Core/test/FileResultTest.cs
+++ b/src/Mvc/Mvc.Core/test/FileResultTest.cs
@@ -268,13 +268,13 @@ public void GetPreconditionState_ShouldProcess(string ifMatch, string ifNoneMatc
httpRequestHeaders.IfUnmodifiedSince = lastModified;
httpRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1);
actionContext.HttpContext = httpContext;
- var fileResult = (new Mock(NullLogger.Instance)).Object;
// Act
- var state = fileResult.GetPreconditionState(
+ var state = FileResultExecutorBase.GetPreconditionState(
httpRequestHeaders,
lastModified,
- etag);
+ etag,
+ NullLogger.Instance);
// Assert
Assert.Equal(FileResultExecutorBase.PreconditionState.ShouldProcess, state);
@@ -307,13 +307,13 @@ public void GetPreconditionState_ShouldNotProcess_PreconditionFailed(string ifMa
httpRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue;
httpRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(2);
actionContext.HttpContext = httpContext;
- var fileResult = (new Mock(NullLogger.Instance)).Object;
// Act
- var state = fileResult.GetPreconditionState(
+ var state = FileResultExecutorBase.GetPreconditionState(
httpRequestHeaders,
lastModified,
- etag);
+ etag,
+ NullLogger.Instance);
// Assert
Assert.Equal(FileResultExecutorBase.PreconditionState.PreconditionFailed, state);
@@ -344,13 +344,13 @@ public void GetPreconditionState_ShouldNotProcess_NotModified(string ifMatch, st
};
httpRequestHeaders.IfModifiedSince = lastModified;
actionContext.HttpContext = httpContext;
- var fileResult = (new Mock(NullLogger.Instance)).Object;
// Act
- var state = fileResult.GetPreconditionState(
+ var state = FileResultExecutorBase.GetPreconditionState(
httpRequestHeaders,
lastModified,
- etag);
+ etag,
+ NullLogger.Instance);
// Assert
Assert.Equal(FileResultExecutorBase.PreconditionState.NotModified, state);
@@ -372,13 +372,13 @@ public void IfRangeValid_IgnoreRangeRequest(string ifRangeString, bool expected)
httpRequestHeaders.IfRange = new RangeConditionHeaderValue(ifRangeString);
httpRequestHeaders.IfModifiedSince = lastModified;
actionContext.HttpContext = httpContext;
- var fileResult = (new Mock(NullLogger.Instance)).Object;
// Act
- var ifRangeIsValid = fileResult.IfRangeValid(
+ var ifRangeIsValid = FileResultExecutorBase.IfRangeValid(
httpRequestHeaders,
lastModified,
- etag);
+ etag,
+ NullLogger.Instance);
// Assert
Assert.Equal(expected, ifRangeIsValid);
diff --git a/src/Mvc/Mvc.Core/test/PhysicalFileActionResultTest.cs b/src/Mvc/Mvc.Core/test/PhysicalFileActionResultTest.cs
new file mode 100644
index 000000000000..7677af32f434
--- /dev/null
+++ b/src/Mvc/Mvc.Core/test/PhysicalFileActionResultTest.cs
@@ -0,0 +1,204 @@
+// 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.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ public class PhysicalFileActionResultTest
+ {
+ [Fact]
+ public void Constructor_SetsFileName()
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+
+ // Act
+ var result = new PhysicalFileResult(path, "text/plain");
+
+ // Assert
+ Assert.Equal(path, result.FileName);
+ }
+
+ [Fact]
+ public void Constructor_SetsContentTypeAndParameters()
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var expectedMediaType = contentType;
+
+ // Act
+ var result = new PhysicalFileResult(path, contentType);
+
+ // Assert
+ Assert.Equal(path, result.FileName);
+ MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
+ }
+
+ [Theory]
+ [InlineData(0, 3, "File", 4)]
+ [InlineData(8, 13, "Result", 6)]
+ [InlineData(null, 5, "ts�", 5)]
+ [InlineData(8, null, "ResultTestFile contents�", 26)]
+ public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_WritesRangeRequested(
+ start,
+ end,
+ expectedString,
+ contentLength,
+ action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored(action);
+ }
+
+ [Theory]
+ [InlineData("0-5")]
+ [InlineData("bytes = ")]
+ [InlineData("bytes = 1-4, 5-11")]
+ public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string rangeString)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(rangeString, action);
+ }
+
+ [Theory]
+ [InlineData("bytes = 35-36")]
+ [InlineData("bytes = -0")]
+ public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_RangeRequestedNotSatisfiable(rangeString, action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_RangeRequested_PreconditionFailed(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeRequested_NotModified()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_RangeRequested_NotModified(action);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BasePhysicalFileResultTest.ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent(action);
+ }
+
+ [Theory]
+ [InlineData(0, 3, 4)]
+ [InlineData(8, 13, 6)]
+ [InlineData(null, 3, 3)]
+ [InlineData(8, null, 26)]
+ public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, long contentLength)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BasePhysicalFileResultTest.ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(start, end, contentLength, action);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BasePhysicalFileResultTest.ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(action);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_WorksWithAbsolutePaths()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BasePhysicalFileResultTest.ExecuteResultAsync_WorksWithAbsolutePaths(action);
+ }
+
+ [Theory]
+ [InlineData("FilePathResultTestFile.txt")]
+ [InlineData("./FilePathResultTestFile.txt")]
+ [InlineData(".\\FilePathResultTestFile.txt")]
+ [InlineData("~/FilePathResultTestFile.txt")]
+ [InlineData("..\\TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("..\\TestFiles\\FilePathResultTestFile.txt")]
+ [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt")]
+ [InlineData("~/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("~/SubFolder\\SubFolderTestFile.txt")]
+ public async Task ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths(string path)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BasePhysicalFileResultTest.ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths(path, action);
+ }
+
+ [Theory]
+ [InlineData("/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("\\SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("/SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("\\SubFolder/SubFolderTestFile.txt")]
+ [InlineData("./SubFolder/SubFolderTestFile.txt")]
+ [InlineData(".\\SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("./SubFolder\\SubFolderTestFile.txt")]
+ [InlineData(".\\SubFolder/SubFolderTestFile.txt")]
+ public void ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(string path)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ BasePhysicalFileResultTest
+ .ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(path, action);
+ }
+
+ [Theory]
+ [InlineData("/FilePathResultTestFile.txt")]
+ [InlineData("\\FilePathResultTestFile.txt")]
+ public void ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(string path)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ BasePhysicalFileResultTest
+ .ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(path, action);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/test/PhysicalFileResult.cs b/src/Mvc/Mvc.Core/test/PhysicalFileResult.cs
new file mode 100644
index 000000000000..5cadbb3db739
--- /dev/null
+++ b/src/Mvc/Mvc.Core/test/PhysicalFileResult.cs
@@ -0,0 +1,173 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ public class PhysicalFileResultTest
+ {
+ [Theory]
+ [InlineData(0, 3, "File", 4)]
+ [InlineData(8, 13, "Result", 6)]
+ [InlineData(null, 5, "ts�", 5)]
+ [InlineData(8, null, "ResultTestFile contents�", 26)]
+ public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_WritesRangeRequested(
+ start,
+ end,
+ expectedString,
+ contentLength,
+ action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored()
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored(action);
+ }
+
+ [Theory]
+ [InlineData("0-5")]
+ [InlineData("bytes = ")]
+ [InlineData("bytes = 1-4, 5-11")]
+ public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string rangeString)
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(rangeString, action);
+ }
+
+ [Theory]
+ [InlineData("bytes = 35-36")]
+ [InlineData("bytes = -0")]
+ public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString)
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_RangeRequestedNotSatisfiable(rangeString, action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
+ {var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+ await BasePhysicalFileResultTest.WriteFileAsync_RangeRequested_PreconditionFailed(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeRequested_NotModified()
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BasePhysicalFileResultTest.WriteFileAsync_RangeRequested_NotModified(action);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent()
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BasePhysicalFileResultTest.ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent(action);
+ }
+
+ [Theory]
+ [InlineData(0, 3, 4)]
+ [InlineData(8, 13, 6)]
+ [InlineData(null, 3, 3)]
+ [InlineData(8, null, 26)]
+ public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, long contentLength)
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BasePhysicalFileResultTest.ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(start, end, contentLength, action);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BasePhysicalFileResultTest.ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(action);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_WorksWithAbsolutePaths()
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BasePhysicalFileResultTest.ExecuteResultAsync_WorksWithAbsolutePaths(action);
+ }
+
+ [Theory]
+ [InlineData("FilePathResultTestFile.txt")]
+ [InlineData("./FilePathResultTestFile.txt")]
+ [InlineData(".\\FilePathResultTestFile.txt")]
+ [InlineData("~/FilePathResultTestFile.txt")]
+ [InlineData("..\\TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("..\\TestFiles\\FilePathResultTestFile.txt")]
+ [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt")]
+ [InlineData("~/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("~/SubFolder\\SubFolderTestFile.txt")]
+ public async Task ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths(string path)
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ await BasePhysicalFileResultTest.ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths(path, action);
+ }
+
+ [Theory]
+ [InlineData("/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("\\SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("/SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("\\SubFolder/SubFolderTestFile.txt")]
+ [InlineData("./SubFolder/SubFolderTestFile.txt")]
+ [InlineData(".\\SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("./SubFolder\\SubFolderTestFile.txt")]
+ [InlineData(".\\SubFolder/SubFolderTestFile.txt")]
+ public void ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(string path)
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ BasePhysicalFileResultTest
+ .ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(path, action);
+ }
+
+ [Theory]
+ [InlineData("/FilePathResultTestFile.txt")]
+ [InlineData("\\FilePathResultTestFile.txt")]
+ public void ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(string path)
+ {
+ var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
+
+ BasePhysicalFileResultTest
+ .ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(path, action);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/test/RedirectActionResultTest.cs b/src/Mvc/Mvc.Core/test/RedirectActionResultTest.cs
new file mode 100644
index 000000000000..9b139431fd88
--- /dev/null
+++ b/src/Mvc/Mvc.Core/test/RedirectActionResultTest.cs
@@ -0,0 +1,96 @@
+// 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.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ public class RedirectActionResultTest
+ {
+ [Fact]
+ public void RedirectResult_Constructor_WithParameterUrl_SetsResultUrlAndNotPermanentOrPreserveMethod()
+ {
+ // Arrange
+ var url = "/test/url";
+
+ // Act
+ var result = new RedirectResult(url);
+
+ // Assert
+ Assert.False(result.PreserveMethod);
+ Assert.False(result.Permanent);
+ Assert.Same(url, result.Url);
+ }
+
+ [Fact]
+ public void RedirectResult_Constructor_WithParameterUrlAndPermanent_SetsResultUrlAndPermanentNotPreserveMethod()
+ {
+ // Arrange
+ var url = "/test/url";
+
+ // Act
+ var result = new RedirectResult(url, permanent: true);
+
+ // Assert
+ Assert.False(result.PreserveMethod);
+ Assert.True(result.Permanent);
+ Assert.Same(url, result.Url);
+ }
+
+ [Fact]
+ public void RedirectResult_Constructor_WithParameterUrlPermanentAndPreservesMethod_SetsResultUrlPermanentAndPreservesMethod()
+ {
+ // Arrange
+ var url = "/test/url";
+
+ // Act
+ var result = new RedirectResult(url, permanent: true, preserveMethod: true);
+
+ // Assert
+ Assert.True(result.PreserveMethod);
+ Assert.True(result.Permanent);
+ Assert.Same(url, result.Url);
+ }
+
+ [Theory]
+ [InlineData("", "/Home/About", "/Home/About")]
+ [InlineData("/myapproot", "/test", "/test")]
+ public async Task Execute_ReturnsContentPath_WhenItDoesNotStartWithTilde(
+ string appRoot,
+ string contentPath,
+ string expectedPath)
+ {
+ var action
+ = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseRedirectResultTest.Execute_ReturnsContentPath_WhenItDoesNotStartWithTilde(
+ appRoot,
+ contentPath,
+ expectedPath,
+ action);
+ }
+
+ [Theory]
+ [InlineData(null, "~/Home/About", "/Home/About")]
+ [InlineData("/", "~/Home/About", "/Home/About")]
+ [InlineData("/", "~/", "/")]
+ [InlineData("", "~/Home/About", "/Home/About")]
+ [InlineData("/myapproot", "~/", "/myapproot/")]
+ public async Task Execute_ReturnsAppRelativePath_WhenItStartsWithTilde(
+ string appRoot,
+ string contentPath,
+ string expectedPath)
+ {
+ var action =
+ new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseRedirectResultTest.Execute_ReturnsAppRelativePath_WhenItStartsWithTilde(
+ appRoot,
+ contentPath,
+ expectedPath,
+ action);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/test/RedirectResultTest.cs b/src/Mvc/Mvc.Core/test/RedirectResultTest.cs
index 481245fee89d..68e310a82aac 100644
--- a/src/Mvc/Mvc.Core/test/RedirectResultTest.cs
+++ b/src/Mvc/Mvc.Core/test/RedirectResultTest.cs
@@ -4,66 +4,12 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Features;
-using Microsoft.AspNetCore.Mvc.Abstractions;
-using Microsoft.AspNetCore.Mvc.Infrastructure;
-using Microsoft.AspNetCore.Mvc.Routing;
-using Microsoft.AspNetCore.Routing;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc
{
public class RedirectResultTest
{
- [Fact]
- public void RedirectResult_Constructor_WithParameterUrl_SetsResultUrlAndNotPermanentOrPreserveMethod()
- {
- // Arrange
- var url = "/test/url";
-
- // Act
- var result = new RedirectResult(url);
-
- // Assert
- Assert.False(result.PreserveMethod);
- Assert.False(result.Permanent);
- Assert.Same(url, result.Url);
- }
-
- [Fact]
- public void RedirectResult_Constructor_WithParameterUrlAndPermanent_SetsResultUrlAndPermanentNotPreserveMethod()
- {
- // Arrange
- var url = "/test/url";
-
- // Act
- var result = new RedirectResult(url, permanent: true);
-
- // Assert
- Assert.False(result.PreserveMethod);
- Assert.True(result.Permanent);
- Assert.Same(url, result.Url);
- }
-
- [Fact]
- public void RedirectResult_Constructor_WithParameterUrlPermanentAndPreservesMethod_SetsResultUrlPermanentAndPreservesMethod()
- {
- // Arrange
- var url = "/test/url";
-
- // Act
- var result = new RedirectResult(url, permanent: true, preserveMethod: true);
-
- // Assert
- Assert.True(result.PreserveMethod);
- Assert.True(result.Permanent);
- Assert.Same(url, result.Url);
- }
-
[Theory]
[InlineData("", "/Home/About", "/Home/About")]
[InlineData("/myapproot", "/test", "/test")]
@@ -72,18 +18,14 @@ public async Task Execute_ReturnsContentPath_WhenItDoesNotStartWithTilde(
string contentPath,
string expectedPath)
{
- // Arrange
- var httpContext = GetHttpContext(appRoot);
- var actionContext = GetActionContext(httpContext);
- var result = new RedirectResult(contentPath);
-
- // Act
- await result.ExecuteResultAsync(actionContext);
+ var action
+ = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
- // Assert
- // Verifying if Redirect was called with the specific Url and parameter flag.
- Assert.Equal(expectedPath, httpContext.Response.Headers.Location.ToString());
- Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode);
+ await BaseRedirectResultTest.Execute_ReturnsContentPath_WhenItDoesNotStartWithTilde(
+ appRoot,
+ contentPath,
+ expectedPath,
+ action);
}
[Theory]
@@ -97,46 +39,14 @@ public async Task Execute_ReturnsAppRelativePath_WhenItStartsWithTilde(
string contentPath,
string expectedPath)
{
- // Arrange
- var httpContext = GetHttpContext(appRoot);
- var actionContext = GetActionContext(httpContext);
- var result = new RedirectResult(contentPath);
-
- // Act
- await result.ExecuteResultAsync(actionContext);
-
- // Assert
- // Verifying if Redirect was called with the specific Url and parameter flag.
- Assert.Equal(expectedPath, httpContext.Response.Headers.Location.ToString());
- Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode);
- }
-
- private static ActionContext GetActionContext(HttpContext httpContext)
- {
- var routeData = new RouteData();
- routeData.Routers.Add(new Mock().Object);
+ var action
+ = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context));
- return new ActionContext(httpContext,
- routeData,
- new ActionDescriptor());
- }
-
- private static IServiceProvider GetServiceProvider()
- {
- var serviceCollection = new ServiceCollection();
- serviceCollection.AddSingleton, RedirectResultExecutor>();
- serviceCollection.AddSingleton();
- serviceCollection.AddTransient();
- return serviceCollection.BuildServiceProvider();
- }
-
- private static HttpContext GetHttpContext(
- string appRoot)
- {
- var httpContext = new DefaultHttpContext();
- httpContext.RequestServices = GetServiceProvider();
- httpContext.Request.PathBase = new PathString(appRoot);
- return httpContext;
+ await BaseRedirectResultTest.Execute_ReturnsAppRelativePath_WhenItStartsWithTilde(
+ appRoot,
+ contentPath,
+ expectedPath,
+ action);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Mvc/Mvc.Core/test/VirtualFileActionResultTest .cs b/src/Mvc/Mvc.Core/test/VirtualFileActionResultTest .cs
new file mode 100644
index 000000000000..ebc4f89f903e
--- /dev/null
+++ b/src/Mvc/Mvc.Core/test/VirtualFileActionResultTest .cs
@@ -0,0 +1,213 @@
+// 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.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ public class VirtualFileActionResultTest
+ {
+ [Fact]
+ public void Constructor_SetsFileName()
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+
+ // Act
+ var result = new VirtualFileResult(path, "text/plain");
+
+ // Assert
+ Assert.Equal(path, result.FileName);
+ }
+
+ [Fact]
+ public void Constructor_SetsContentTypeAndParameters()
+ {
+ // Arrange
+ var path = Path.GetFullPath("helllo.txt");
+ var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+ var expectedMediaType = contentType;
+
+ // Act
+ var result = new VirtualFileResult(path, contentType);
+
+ // 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)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseVirtualFileResultTest.WriteFileAsync_WritesRangeRequested(
+ start,
+ end,
+ expectedString,
+ contentLength,
+ action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseVirtualFileResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseVirtualFileResultTest
+ .WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+ await BaseVirtualFileResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored(action);
+ }
+
+ [Theory]
+ [InlineData("0-5")]
+ [InlineData("bytes = ")]
+ [InlineData("bytes = 1-4, 5-11")]
+ public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string rangeString)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+ await BaseVirtualFileResultTest.WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(rangeString, action);
+ }
+
+ [Theory]
+ [InlineData("bytes = 35-36")]
+ [InlineData("bytes = -0")]
+ public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+ await BaseVirtualFileResultTest.WriteFileAsync_RangeRequestedNotSatisfiable(rangeString, action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+ await BaseVirtualFileResultTest.WriteFileAsync_RangeRequested_PreconditionFailed(action);
+ }
+
+ [Fact]
+ public async Task WriteFileAsync_RangeRequested_NotModified()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseVirtualFileResultTest.WriteFileAsync_RangeRequested_NotModified(action);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_FallsBackToWebRootFileProvider_IfNoFileProviderIsPresent()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseVirtualFileResultTest
+ .ExecuteResultAsync_FallsBackToWebRootFileProvider_IfNoFileProviderIsPresent(action);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseVirtualFileResultTest
+ .ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent(action);
+ }
+
+ [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)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseVirtualFileResultTest.ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(
+ start,
+ end,
+ expectedString,
+ contentLength,
+ action);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseVirtualFileResultTest.ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(action);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_ReturnsFileContentsForRelativePaths()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseVirtualFileResultTest.ExecuteResultAsync_ReturnsFileContentsForRelativePaths(action);
+ }
+
+ [Theory]
+ [InlineData("FilePathResultTestFile.txt")]
+ [InlineData("TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("TestFiles/../FilePathResultTestFile.txt")]
+ [InlineData("TestFiles\\FilePathResultTestFile.txt")]
+ [InlineData("TestFiles\\..\\FilePathResultTestFile.txt")]
+ [InlineData(@"\\..//?><|""&@#\c:\..\? /..txt")]
+ public async Task ExecuteResultAsync_ReturnsFiles_ForDifferentPaths(string path)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseVirtualFileResultTest
+ .ExecuteResultAsync_ReturnsFiles_ForDifferentPaths(path, action);
+ }
+
+ [Theory]
+ [InlineData("~/FilePathResultTestFile.txt")]
+ [InlineData("~/TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("~/TestFiles/../FilePathResultTestFile.txt")]
+ [InlineData("~/TestFiles\\..\\FilePathResultTestFile.txt")]
+ [InlineData(@"~~~~\\..//?>~<|""&@#\c:\..\? /..txt~~~")]
+ public async Task ExecuteResultAsync_TrimsTilde_BeforeInvokingFileProvider(string path)
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseVirtualFileResultTest
+ .ExecuteResultAsync_TrimsTilde_BeforeInvokingFileProvider(path, action);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_WorksWithNonDiskBasedFiles()
+ {
+ var action = new Func(async (result, context) => await result.ExecuteResultAsync(context));
+
+ await BaseVirtualFileResultTest.ExecuteResultAsync_WorksWithNonDiskBasedFiles(action);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_ThrowsFileNotFound_IfFileProviderCanNotFindTheFile()
+ {
+ var action = new Func