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) + { + } + } + } +}