Skip to content

Add IResult implementations for more IActionResults #32647

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion src/Mvc/Mvc.Core/src/FileContentResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc
/// Represents an <see cref="ActionResult"/> that when executed will
/// write a binary file to the response.
/// </summary>
public class FileContentResult : FileResult
public class FileContentResult : FileResult, IResult
{
private byte[] _fileContents;

Expand Down Expand Up @@ -77,5 +80,43 @@ public override Task ExecuteResultAsync(ActionContext context)
var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<FileContentResult>>();
return executor.ExecuteAsync(context, this);
}

Task IResult.ExecuteAsync(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}

var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<RedirectResult>();

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);
}
}
}
87 changes: 57 additions & 30 deletions src/Mvc/Mvc.Core/src/Infrastructure/FileResultExecutorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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)
{
Expand All @@ -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))
{
Expand All @@ -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();
}
}

Expand All @@ -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;
Expand All @@ -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;
}
}
Expand All @@ -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;
Expand All @@ -234,7 +255,7 @@ internal PreconditionState GetPreconditionState(

if (ifMatchState == PreconditionState.PreconditionFailed)
{
Logger.IfMatchPreconditionFailed(etag);
logger.IfMatchPreconditionFailed(etag);
}
}

Expand Down Expand Up @@ -269,7 +290,7 @@ internal PreconditionState GetPreconditionState(

if (ifUnmodifiedSinceState == PreconditionState.PreconditionFailed)
{
Logger.IfUnmodifiedSincePreconditionFailed(lastModified, ifUnmodifiedSince);
logger.IfUnmodifiedSincePreconditionFailed(lastModified, ifUnmodifiedSince);
}
}

Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -397,6 +419,11 @@ protected static ILogger CreateLogger<T>(ILoggerFactory factory)
/// <param name="rangeLength">The range length.</param>
/// <returns>The async task.</returns>
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,9 +67,19 @@ public virtual Task ExecuteAsync(ActionContext context, PhysicalFileResult resul
/// <inheritdoc/>
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)
Expand All @@ -84,15 +92,15 @@ 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));
}

if (range != null)
{
Logger.WritingRangeToBody();
logger.WritingRangeToBody();
}

if (range != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand All @@ -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);
Expand All @@ -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;
}

Expand Down
Loading