-
Notifications
You must be signed in to change notification settings - Fork 10.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement the Mvc PushFileStreamResult API
Fixes #39383
- Loading branch information
Showing
7 changed files
with
412 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
src/Mvc/Mvc.Core/src/Infrastructure/PushFileStreamResultExecutor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Diagnostics; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Net.Http.Headers; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.Infrastructure; | ||
|
||
/// <summary> | ||
/// A <see cref="IActionResultExecutor{PushFileStreamResult}"/> for <see cref="PushFileStreamResult"/>. | ||
/// </summary> | ||
public partial class PushFileStreamResultExecutor : FileResultExecutorBase, IActionResultExecutor<PushFileStreamResult> | ||
{ | ||
/// <summary> | ||
/// Initializes a new <see cref="PushFileStreamResultExecutor"/>. | ||
/// </summary> | ||
/// <param name="loggerFactory">The factory used to create loggers.</param> | ||
public PushFileStreamResultExecutor(ILoggerFactory loggerFactory) | ||
: base(CreateLogger<PushFileStreamResultExecutor>(loggerFactory)) | ||
{ | ||
} | ||
|
||
/// <inheritdoc /> | ||
public virtual async Task ExecuteAsync(ActionContext context, PushFileStreamResult result) | ||
{ | ||
ArgumentNullException.ThrowIfNull(context); | ||
ArgumentNullException.ThrowIfNull(result); | ||
|
||
Log.ExecutingFileResult(Logger, result); | ||
|
||
var (range, rangeLength, serveBody) = SetHeadersAndLog( | ||
context, | ||
result, | ||
fileLength: null, | ||
result.EnableRangeProcessing, | ||
result.LastModified, | ||
result.EntityTag); | ||
|
||
if (!serveBody) | ||
{ | ||
return; | ||
} | ||
|
||
await WriteFileAsync(context, result, range, rangeLength); | ||
} | ||
|
||
/// <summary> | ||
/// Write the contents of the PushFileStreamResult to the response body. | ||
/// </summary> | ||
/// <param name="context">The <see cref="ActionContext"/>.</param> | ||
/// <param name="result">The PushFileStreamResult to write.</param> | ||
/// <param name="range">The <see cref="RangeItemHeaderValue"/>.</param> | ||
/// <param name="rangeLength">The range length.</param> | ||
protected virtual async Task WriteFileAsync( | ||
ActionContext context, | ||
PushFileStreamResult result, | ||
RangeItemHeaderValue? range, | ||
long rangeLength) | ||
{ | ||
ArgumentNullException.ThrowIfNull(context); | ||
ArgumentNullException.ThrowIfNull(result); | ||
|
||
Debug.Assert(range == null); | ||
Debug.Assert(rangeLength == 0); | ||
|
||
await result.StreamWriterCallback(context.HttpContext.Response.Body); | ||
} | ||
|
||
private static partial class Log | ||
{ | ||
public static void ExecutingFileResult(ILogger logger, FileResult fileResult) | ||
{ | ||
if (logger.IsEnabled(LogLevel.Information)) | ||
{ | ||
var fileResultType = fileResult.GetType().Name; | ||
ExecutingFileResultWithNoFileName(logger, fileResultType, fileResult.FileDownloadName); | ||
} | ||
} | ||
|
||
[LoggerMessage(1, LogLevel.Information, "Executing {FileResultType}, sending file with download name '{FileDownloadName}' ...", EventName = "ExecutingFileResultWithNoFileName", SkipEnabledCheck = true)] | ||
private static partial void ExecutingFileResultWithNoFileName(ILogger logger, string fileResultType, string fileDownloadName); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Microsoft.AspNetCore.Mvc.Infrastructure; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace Microsoft.AspNetCore.Mvc; | ||
|
||
/// <summary> | ||
/// A <see cref="FileResult" /> that on execution writes the file using the specified stream writer callback. | ||
/// </summary> | ||
public class PushFileStreamResult : FileResult | ||
{ | ||
/// <summary> | ||
/// The callback that writes the file to the provided stream. | ||
/// </summary> | ||
public Func<Stream, Task> StreamWriterCallback { get; set; } | ||
|
||
/// <summary> | ||
/// Creates a new <see cref="PushFileStreamResult"/> instance with | ||
/// the provided <paramref name="streamWriterCallback"/> and the provided <paramref name="contentType"/>. | ||
/// </summary> | ||
/// <param name="streamWriterCallback">The callback that writes the file to the provided stream.</param> | ||
/// <param name="contentType">The Content-Type header of the response.</param> | ||
public PushFileStreamResult(Func<Stream, Task> streamWriterCallback, string contentType) : base(contentType) | ||
{ | ||
ArgumentNullException.ThrowIfNull(streamWriterCallback); | ||
|
||
StreamWriterCallback = streamWriterCallback; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public override Task ExecuteResultAsync(ActionContext context) | ||
{ | ||
ArgumentNullException.ThrowIfNull(context); | ||
|
||
var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<PushFileStreamResult>>(); | ||
return executor.ExecuteAsync(context, this); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Internal; | ||
using Microsoft.AspNetCore.Mvc.Infrastructure; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Logging.Abstractions; | ||
using Microsoft.Net.Http.Headers; | ||
|
||
namespace Microsoft.AspNetCore.Mvc; | ||
|
||
public class PushFileStreamResultTest : PushFileStreamResultTestBase | ||
{ | ||
protected override Task ExecuteAsync( | ||
HttpContext httpContext, | ||
Func<Stream, Task> streamWriterCallback, | ||
string contentType, | ||
DateTimeOffset? lastModified = null, | ||
EntityTagHeaderValue entityTag = null) | ||
{ | ||
httpContext.RequestServices = new ServiceCollection() | ||
.AddSingleton<ILoggerFactory, NullLoggerFactory>() | ||
.AddSingleton<IActionResultExecutor<PushFileStreamResult>, PushFileStreamResultExecutor>() | ||
.BuildServiceProvider(); | ||
|
||
var actionContext = new ActionContext(httpContext, new(), new()); | ||
var fileStreamResult = new PushFileStreamResult(streamWriterCallback, contentType) | ||
{ | ||
LastModified = lastModified, | ||
EntityTag = entityTag, | ||
}; | ||
|
||
return fileStreamResult.ExecuteResultAsync(actionContext); | ||
} | ||
|
||
[Fact] | ||
public void Constructor_SetsContentType() | ||
{ | ||
// Arrange | ||
var streamWriterCallback = (Stream _) => Task.CompletedTask; | ||
var contentType = "text/plain; charset=us-ascii; p1=p1-value"; | ||
var expectedMediaType = contentType; | ||
|
||
// Act | ||
var result = new PushFileStreamResult(streamWriterCallback, contentType); | ||
|
||
// Assert | ||
Assert.Equal(expectedMediaType, result.ContentType); | ||
} | ||
|
||
[Fact] | ||
public void Constructor_SetsLastModifiedAndEtag() | ||
{ | ||
// Arrange | ||
var streamWriterCallback = (Stream _) => Task.CompletedTask; | ||
var contentType = "text/plain"; | ||
var expectedMediaType = contentType; | ||
var lastModified = new DateTimeOffset(); | ||
var entityTag = new EntityTagHeaderValue("\"Etag\""); | ||
|
||
// Act | ||
var result = new PushFileStreamResult(streamWriterCallback, contentType) | ||
{ | ||
LastModified = lastModified, | ||
EntityTag = entityTag, | ||
}; | ||
|
||
// Assert | ||
Assert.Equal(lastModified, result.LastModified); | ||
Assert.Equal(entityTag, result.EntityTag); | ||
Assert.Equal(expectedMediaType, result.ContentType); | ||
} | ||
} |
Oops, something went wrong.