diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileStreamResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileStreamResultExecutor.cs
index 46e10e036c..6cb300eff1 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileStreamResultExecutor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileStreamResultExecutor.cs
@@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
}
///
- public virtual Task ExecuteAsync(ActionContext context, FileStreamResult result)
+ public virtual async Task ExecuteAsync(ActionContext context, FileStreamResult result)
{
if (context == null)
{
@@ -29,31 +29,38 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
throw new ArgumentNullException(nameof(result));
}
- Logger.ExecutingFileResult(result);
-
- long? fileLength = null;
- if (result.FileStream.CanSeek)
+ using (result.FileStream)
{
- fileLength = result.FileStream.Length;
+ Logger.ExecutingFileResult(result);
+
+ long? fileLength = null;
+ if (result.FileStream.CanSeek)
+ {
+ fileLength = result.FileStream.Length;
+ }
+
+ var (range, rangeLength, serveBody) = SetHeadersAndLog(
+ context,
+ result,
+ fileLength,
+ result.EnableRangeProcessing,
+ result.LastModified,
+ result.EntityTag);
+
+ if (!serveBody)
+ {
+ return;
+ }
+
+ await WriteFileAsync(context, result, range, rangeLength);
}
-
- var (range, rangeLength, serveBody) = SetHeadersAndLog(
- context,
- result,
- fileLength,
- result.EnableRangeProcessing,
- result.LastModified,
- result.EntityTag);
-
- if (!serveBody)
- {
- return Task.CompletedTask;
- }
-
- return WriteFileAsync(context, result, range, rangeLength);
}
- protected virtual Task WriteFileAsync(ActionContext context, FileStreamResult result, RangeItemHeaderValue range, long rangeLength)
+ protected virtual Task WriteFileAsync(
+ ActionContext context,
+ FileStreamResult result,
+ RangeItemHeaderValue range,
+ long rangeLength)
{
if (context == null)
{
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IAsyncPageFilter.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IAsyncPageFilter.cs
index a4534e7dc2..1c6a0503df 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IAsyncPageFilter.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IAsyncPageFilter.cs
@@ -6,7 +6,8 @@ using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Mvc.Filters
{
///
- /// A filter that asynchronously surrounds execution of the page handler method.
+ /// A filter that asynchronously surrounds execution of a page handler method. This filter is executed only when
+ /// decorated on a handler's type and not on individual handler methods.
///
public interface IAsyncPageFilter : IFilterMetadata
{
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IPageFilter.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IPageFilter.cs
index 101317c94f..6fde59efb1 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IPageFilter.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IPageFilter.cs
@@ -4,7 +4,8 @@
namespace Microsoft.AspNetCore.Mvc.Filters
{
///
- /// A filter that surrounds execution of a page handler method.
+ /// A filter that surrounds execution of a page handler method. This filter is executed only when decorated on a
+ /// handler's type and not on individual handler methods.
///
public interface IPageFilter : IFilterMetadata
{
@@ -21,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.Filters
void OnPageHandlerExecuting(PageHandlerExecutingContext context);
///
- /// Called after the handler method executes, before the action method is invoked.
+ /// Called after the handler method executes, before the action result executes.
///
/// The .
void OnPageHandlerExecuted(PageHandlerExecutedContext context);
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs
index cd21e58430..9684c711f0 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs
@@ -128,7 +128,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
_page = (PageBase)CacheEntry.PageFactory(_pageContext, _viewContext);
}
-
pageResult.Page = _page;
pageResult.ViewData = pageResult.ViewData ?? _pageContext.ViewData;
}
@@ -278,6 +277,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
_result = new PageResult();
}
+
+ // Ensure ViewData is set on PageResult for backwards compatibility (For example, Identity UI accesses
+ // ViewData in a PageFilter's PageHandlerExecutedMethod)
+ if (_result is PageResult pageResult)
+ {
+ pageResult.ViewData = pageResult.ViewData ?? _pageContext.ViewData;
+ }
}
private Task Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs
index ba9062bfe3..ce5a76af27 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs
@@ -1675,7 +1675,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
}
///
- /// Called after the handler method executes, before the action method is invoked.
+ /// Called after the handler method executes, before the action result executes.
///
/// The .
public virtual void OnPageHandlerExecuted(PageHandlerExecutedContext context)
diff --git a/src/Microsoft.AspNetCore.Mvc.Testing/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Testing.targets b/src/Microsoft.AspNetCore.Mvc.Testing/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Testing.targets
index f90a0792b3..6cd039b00b 100644
--- a/src/Microsoft.AspNetCore.Mvc.Testing/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Testing.targets
+++ b/src/Microsoft.AspNetCore.Mvc.Testing/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Testing.targets
@@ -17,7 +17,9 @@
- <_ContentRootProjectReferences Include="@(ReferencePath)" Condition="'%(ReferencePath.ReferenceSourceTarget)' == 'ProjectReference'" />
+ <_ContentRootProjectReferences
+ Include="@(ReferencePath)"
+ Condition="'%(ReferencePath.ReferenceSourceTarget)' == 'ProjectReference' and '%(ReferencePath.TargetFrameworkIdentifier)' != '.NETStandard'" />
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs
index eea79e044f..081eaf7b81 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs
@@ -126,6 +126,7 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.Equal(contentLength, httpResponse.ContentLength);
Assert.Equal(expectedString, body);
+ Assert.False(readStream.CanSeek);
}
[Fact]
@@ -174,6 +175,7 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.Equal(5, httpResponse.ContentLength);
Assert.Equal("Hello", body);
+ Assert.False(readStream.CanSeek);
}
[Fact]
@@ -217,6 +219,7 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal("Hello World", body);
+ Assert.False(readStream.CanSeek);
}
[Fact]
@@ -261,6 +264,7 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal("Hello World", body);
+ Assert.False(readStream.CanSeek);
}
[Theory]
@@ -303,6 +307,7 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal("Hello World", body);
+ Assert.False(readStream.CanSeek);
}
[Theory]
@@ -346,6 +351,7 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.Equal(11, httpResponse.ContentLength);
Assert.Empty(body);
+ Assert.False(readStream.CanSeek);
}
[Fact]
@@ -389,6 +395,7 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Empty(body);
+ Assert.False(readStream.CanSeek);
}
[Fact]
@@ -432,6 +439,7 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Empty(body);
+ Assert.False(readStream.CanSeek);
}
[Theory]
@@ -480,6 +488,7 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.Empty(body);
+ Assert.False(readStream.CanSeek);
}
[Fact]
@@ -541,6 +550,7 @@ namespace Microsoft.AspNetCore.Mvc
// Assert
var outBytes = outStream.ToArray();
Assert.True(originalBytes.SequenceEqual(outBytes));
+ Assert.False(originalStream.CanSeek);
}
[Fact]
@@ -570,6 +580,31 @@ namespace Microsoft.AspNetCore.Mvc
var outBytes = outStream.ToArray();
Assert.True(originalBytes.SequenceEqual(outBytes));
Assert.Equal(expectedContentType, httpContext.Response.ContentType);
+ Assert.False(originalStream.CanSeek);
+ }
+
+ [Fact]
+ public async Task HeadRequest_DoesNotWriteToBody_AndClosesReadStream()
+ {
+ // Arrange
+ var readStream = new MemoryStream(Encoding.UTF8.GetBytes("Hello, World!"));
+
+ var httpContext = GetHttpContext();
+ httpContext.Request.Method = "HEAD";
+ var outStream = new MemoryStream();
+ httpContext.Response.Body = outStream;
+
+ var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ var result = new FileStreamResult(readStream, "text/plain");
+
+ // Act
+ await result.ExecuteResultAsync(actionContext);
+
+ // Assert
+ Assert.False(readStream.CanSeek);
+ Assert.Equal(200, httpContext.Response.StatusCode);
+ Assert.Equal(0, httpContext.Response.Body.Length);
}
private static IServiceCollection CreateServices()
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs
index 1a0cdb4ac7..9076b9d344 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs
@@ -1295,6 +1295,16 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore.InjectedPa
Assert.Equal("From ShortCircuitPageAtPageFilter.cshtml", content);
}
+ [Fact]
+ public async Task ViewDataAvaialableInPageFilter_AfterHandlerMethod_ReturnsPageResult()
+ {
+ // Act
+ var content = await Client.GetStringAsync("http://localhost/Pages/ViewDataAvailableAfterHandlerExecuted");
+
+ // Assert
+ Assert.Equal("ViewData: Bar", content);
+ }
+
private async Task AddAntiforgeryHeaders(HttpRequestMessage request)
{
var getResponse = await Client.GetAsync(request.RequestUri);
diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs
index f90da88615..5b7899ac3e 100644
--- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs
@@ -473,6 +473,33 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
#region Page Filters
+ [Fact]
+ public async Task ViewDataIsSet_AfterHandlerMethodIsExecuted()
+ {
+ // Arrange
+ var pageHandlerExecutedCalled = false;
+ var pageFilter = new Mock();
+ AllowSelector(pageFilter);
+ pageFilter
+ .Setup(f => f.OnPageHandlerExecuted(It.IsAny()))
+ .Callback(c =>
+ {
+ pageHandlerExecutedCalled = true;
+ var result = c.Result;
+ var pageResult = Assert.IsType(result);
+ Assert.IsType>(pageResult.ViewData);
+ Assert.IsType(pageResult.Model);
+ Assert.Null(pageResult.Page);
+ });
+ var invoker = CreateInvoker(new IFilterMetadata[] { pageFilter.Object }, result: new PageResult());
+
+ // Act
+ await invoker.InvokeAsync();
+
+ // Assert
+ Assert.True(pageHandlerExecutedCalled);
+ }
+
[Fact]
public async Task InvokeAction_InvokesPageFilter()
{
diff --git a/test/WebSites/RazorPagesWebSite/Pages/ViewDataAvailableAfterHandlerExecuted.cshtml b/test/WebSites/RazorPagesWebSite/Pages/ViewDataAvailableAfterHandlerExecuted.cshtml
new file mode 100644
index 0000000000..a9c5e2d2e6
--- /dev/null
+++ b/test/WebSites/RazorPagesWebSite/Pages/ViewDataAvailableAfterHandlerExecuted.cshtml
@@ -0,0 +1,5 @@
+@page
+@model ViewDataAvailableAfterHandlerExecutedModel
+@{
+}
+ViewData: @ViewData["Foo"]
\ No newline at end of file
diff --git a/test/WebSites/RazorPagesWebSite/Pages/ViewDataAvailableAfterHandlerExecuted.cshtml.cs b/test/WebSites/RazorPagesWebSite/Pages/ViewDataAvailableAfterHandlerExecuted.cshtml.cs
new file mode 100644
index 0000000000..548623d5f9
--- /dev/null
+++ b/test/WebSites/RazorPagesWebSite/Pages/ViewDataAvailableAfterHandlerExecuted.cshtml.cs
@@ -0,0 +1,40 @@
+// 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 Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace RazorPagesWebSite.Pages
+{
+ [TestPageFilter]
+ public class ViewDataAvailableAfterHandlerExecutedModel : PageModel
+ {
+ public IActionResult OnGet()
+ {
+ return Page();
+ }
+
+ private class TestPageFilterAttribute : Attribute, IPageFilter
+ {
+ public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
+ {
+ // This usage mimics Identity UI where it sets data into ViewData in a PageFilters's
+ // PageHandlerExecuted method.
+ if (context.Result is PageResult pageResult)
+ {
+ pageResult.ViewData["Foo"] = "Bar";
+ }
+ }
+
+ public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
+ {
+ }
+
+ public void OnPageHandlerSelected(PageHandlerSelectedContext context)
+ {
+ }
+ }
+ }
+}