diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs index e9f6642963..f926e21dc8 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs @@ -6,19 +6,16 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; -using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { @@ -34,7 +31,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private Page _page; private object _model; private ViewContext _viewContext; - private ExceptionContext _exceptionContext; public PageActionInvoker( IPageHandlerMethodSelector handlerMethodSelector, @@ -75,8 +71,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal /// protected override async Task InvokeInnerFilterAsync() { - var next = State.ResourceInnerBegin; - var scope = Scope.Resource; + var next = State.PageBegin; + var scope = Scope.Invoker; var state = (object)null; var isCompleted = false; @@ -106,186 +102,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal switch (next) { - case State.ResourceInnerBegin: - { - goto case State.ExceptionBegin; - } - - case State.ExceptionBegin: - { - _cursor.Reset(); - goto case State.ExceptionNext; - } - - case State.ExceptionNext: - { - var current = _cursor.GetNextFilter(); - if (current.FilterAsync != null) - { - state = current.FilterAsync; - goto case State.ExceptionAsyncBegin; - } - else if (current.Filter != null) - { - state = current.Filter; - goto case State.ExceptionSyncBegin; - } - else if (scope == Scope.Exception) - { - // All exception filters are on the stack already - so execute the 'inside'. - goto case State.ExceptionInside; - } - else - { - // There are no exception filters - so jump right to 'inside'. - Debug.Assert(scope == Scope.Resource); - goto case State.PageBegin; - } - } - - case State.ExceptionAsyncBegin: - { - var task = InvokeNextExceptionFilterAsync(); - if (task.Status != TaskStatus.RanToCompletion) - { - next = State.ExceptionAsyncResume; - return task; - } - - goto case State.ExceptionAsyncResume; - } - - case State.ExceptionAsyncResume: - { - Debug.Assert(state != null); - - var filter = (IAsyncExceptionFilter)state; - var exceptionContext = _exceptionContext; - - // When we get here we're 'unwinding' the stack of exception filters. If we have an unhandled exception, - // we'll call the filter. Otherwise there's nothing to do. - if (exceptionContext?.Exception != null && !exceptionContext.ExceptionHandled) - { - _diagnosticSource.BeforeOnExceptionAsync(exceptionContext, filter); - - var task = filter.OnExceptionAsync(exceptionContext); - if (task.Status != TaskStatus.RanToCompletion) - { - next = State.ExceptionAsyncEnd; - return task; - } - - goto case State.ExceptionAsyncEnd; - } - - goto case State.ExceptionEnd; - } - - case State.ExceptionAsyncEnd: - { - Debug.Assert(state != null); - Debug.Assert(_exceptionContext != null); - - var filter = (IAsyncExceptionFilter)state; - var exceptionContext = _exceptionContext; - - _diagnosticSource.AfterOnExceptionAsync(exceptionContext, filter); - - if (exceptionContext.Exception == null || exceptionContext.ExceptionHandled) - { - _logger.ExceptionFilterShortCircuited(filter); - } - - goto case State.ExceptionEnd; - } - - case State.ExceptionSyncBegin: - { - var task = InvokeNextExceptionFilterAsync(); - if (task.Status != TaskStatus.RanToCompletion) - { - next = State.ExceptionSyncEnd; - return task; - } - - goto case State.ExceptionSyncEnd; - } - - case State.ExceptionSyncEnd: - { - Debug.Assert(state != null); - - var filter = (IExceptionFilter)state; - var exceptionContext = _exceptionContext; - - // When we get here we're 'unwinding' the stack of exception filters. If we have an unhandled exception, - // we'll call the filter. Otherwise there's nothing to do. - if (exceptionContext?.Exception != null && !exceptionContext.ExceptionHandled) - { - _diagnosticSource.BeforeOnException(exceptionContext, filter); - - filter.OnException(exceptionContext); - - _diagnosticSource.AfterOnException(exceptionContext, filter); - - if (exceptionContext.Exception == null || exceptionContext.ExceptionHandled) - { - _logger.ExceptionFilterShortCircuited(filter); - } - } - - goto case State.ExceptionEnd; - } - - case State.ExceptionInside: - { - goto case State.PageBegin; - } - - case State.ExceptionShortCircuit: - { - Debug.Assert(state != null); - Debug.Assert(_exceptionContext != null); - - if (scope == Scope.Resource) - { - Debug.Assert(_exceptionContext.Result != null); - _result = _exceptionContext.Result; - } - - var task = InvokeResultAsync(_exceptionContext.Result); - if (task.Status != TaskStatus.RanToCompletion) - { - next = State.ResourceInnerEnd; - return task; - } - - goto case State.ResourceInnerEnd; - } - - case State.ExceptionEnd: - { - var exceptionContext = _exceptionContext; - - if (scope == Scope.Exception) - { - isCompleted = true; - return TaskCache.CompletedTask; - } - - if (exceptionContext != null) - { - if (exceptionContext.Result != null && !exceptionContext.ExceptionHandled) - { - goto case State.ExceptionShortCircuit; - } - - Rethrow(exceptionContext); - } - - goto case State.ResourceInnerEnd; - } - case State.PageBegin: { var pageContext = _pageContext; @@ -297,20 +113,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } case State.PageEnd: - { - if (scope == Scope.Exception) - { - // If we're inside an exception filter, let's allow those filters to 'unwind' before - // the result. - isCompleted = true; - return TaskCache.CompletedTask; - } - - Debug.Assert(scope == Scope.Resource); - goto case State.ResourceInnerEnd; - } - - case State.ResourceInnerEnd: { isCompleted = true; return TaskCache.CompletedTask; @@ -348,6 +150,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal _model = CacheEntry.ModelFactory(_pageContext); _pageContext.ViewData.Model = _model; + // Flow the PageModel in places where the result filters would flow the controller. + _instance = _model; + if (CacheEntry.PropertyBinder != null) { await CacheEntry.PropertyBinder(_pageContext, _model); @@ -388,8 +193,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal pageResult.Page = _page; pageResult.ViewData = pageResult.ViewData ?? _pageContext.ViewData; } - - await _result.ExecuteResultAsync(_pageContext); } private async Task ExecutePageWithoutPageModelAsync() @@ -405,6 +208,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal _page = (Page)CacheEntry.PageFactory(_pageContext, _viewContext); + // Flow the Page in places where the result filters would flow the controller. + _instance = _page; + if (_actionDescriptor.ModelTypeInfo == _actionDescriptor.PageTypeInfo) { _model = _page; @@ -440,8 +246,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal pageResult.Page = _page; pageResult.ViewData = pageResult.ViewData ?? _pageContext.ViewData; } - - await _result.ExecuteResultAsync(_pageContext); } private async Task GetArguments(HandlerMethodDescriptor handler) @@ -506,74 +310,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal return result; } - private async Task InvokeNextExceptionFilterAsync() - { - try - { - var next = State.ExceptionNext; - var state = (object)null; - var scope = Scope.Exception; - var isCompleted = false; - while (!isCompleted) - { - await Next(ref next, ref scope, ref state, ref isCompleted); - } - } - catch (Exception exception) - { - _exceptionContext = new ExceptionContext(_actionContext, _filters) - { - ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception), - }; - } - } - - private static void Rethrow(ExceptionContext context) - { - if (context == null) - { - return; - } - - if (context.ExceptionHandled) - { - return; - } - - if (context.ExceptionDispatchInfo != null) - { - context.ExceptionDispatchInfo.Throw(); - } - - if (context.Exception != null) - { - throw context.Exception; - } - } - private enum Scope { - Resource, - Exception, + Invoker, Page, } private enum State { - ResourceInnerBegin, - ExceptionBegin, - ExceptionNext, - ExceptionAsyncBegin, - ExceptionAsyncResume, - ExceptionAsyncEnd, - ExceptionSyncBegin, - ExceptionSyncEnd, - ExceptionInside, - ExceptionShortCircuit, - ExceptionEnd, PageBegin, PageEnd, - ResourceInnerEnd, } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs index 818ff1dcee..fdc3002df2 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs @@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal Assert.Equal(4, sink.Writes.Count); Assert.Equal($"Executing action {displayName}", sink.Writes[0].State?.ToString()); Assert.Equal($"Executing action method {displayName} with arguments ((null)) - ModelState is Valid", sink.Writes[1].State?.ToString()); - Assert.Equal($"Executed action method {displayName}, returned result Microsoft.AspNetCore.Mvc.ContentResult.", sink.Writes[2].State?.ToString()); + Assert.Equal($"Executed action method {displayName}, returned result {Result.GetType().FullName}.", sink.Writes[2].State?.ToString()); // This message has the execution time embedded, which we don't want to verify. Assert.StartsWith($"Executed action {displayName} ", sink.Writes[3].State?.ToString()); } @@ -226,7 +226,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal .Callback(c => result = c.Result) .Verifiable(); - var invoker = CreateInvoker(filter.Object); + var invoker = CreateInvoker(filter.Object, result: Result); // Act await invoker.InvokeAsync(); @@ -254,7 +254,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal }) .Verifiable(); - var invoker = CreateInvoker(filter.Object); + var invoker = CreateInvoker(filter.Object, result: Result); // Act await invoker.InvokeAsync(); @@ -1397,7 +1397,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal protected override ResourceInvoker CreateInvoker( IFilterMetadata[] filters, Exception exception = null, - List valueProviderFactories = null) + IActionResult result = null, + IList valueProviderFactories = null) { var actionDescriptor = new ControllerActionDescriptor() { @@ -1407,7 +1408,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal BoundProperties = new List(), }; - if (exception == Exception) + if (result == Result) + { + actionDescriptor.MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ActionMethod)); + } + else if (result != null) + { + throw new InvalidOperationException($"Unexpected action result {result}."); + } + else if (exception == Exception) { actionDescriptor.MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ThrowingActionMethod)); } @@ -1501,10 +1510,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); - services.AddSingleton(new ContentResultExecutor( - NullLogger.Instance, - new MemoryPoolHttpResponseStreamWriterFactory(ArrayPool.Shared, ArrayPool.Shared))); - httpContext.Response.Body = new MemoryStream(); httpContext.RequestServices = services.BuildServiceProvider(); @@ -1740,47 +1745,5 @@ namespace Microsoft.AspNetCore.Mvc.Internal actionDescriptor.ControllerTypeInfo, ParameterDefaultValues.GetParameterDefaultValues(actionDescriptor.MethodInfo)); } - - private class MockAuthorizationFilter : IAuthorizationFilter - { - int _expectedMaxAllowedErrors; - - public MockAuthorizationFilter(int maxAllowedErrors) - { - _expectedMaxAllowedErrors = maxAllowedErrors; - } - - public void OnAuthorization(AuthorizationFilterContext context) - { - Assert.Equal(_expectedMaxAllowedErrors, context.ModelState.MaxAllowedErrors); - } - } - - private class TestParameterBinder : ParameterBinder - { - private readonly IDictionary _actionParameters; - public TestParameterBinder(IDictionary actionParameters) - : base( - new EmptyModelMetadataProvider(), - TestModelBinderFactory.CreateDefault(), - Mock.Of()) - { - _actionParameters = actionParameters; - } - - public override Task BindModelAsync( - ActionContext actionContext, - IValueProvider valueProvider, - ParameterDescriptor parameter, - object value) - { - if (_actionParameters.TryGetValue(parameter.Name, out var result)) - { - return Task.FromResult(ModelBindingResult.Success(result)); - } - - return Task.FromResult(ModelBindingResult.Failed()); - } - } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs index 7dbca6e257..c74d590e56 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs @@ -2,8 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Linq; using System.Reflection; using System.Text.Encodings.Web; using System.Threading.Tasks; @@ -22,544 +25,66 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { - public class PageActionInvokerTest + public class PageActionInvokerTest : CommonResourceInvokerTest { - private readonly DivideByZeroException _pageException = new DivideByZeroException(); - - [Fact] - public async Task InvokeAsync_DoesNotInvokeExceptionFilter_WhenPageDoesNotThrow() - { - // Arrange - var filter = new Mock(MockBehavior.Strict); - filter - .Setup(f => f.OnException(It.IsAny())) - .Verifiable(); - - var invoker = CreateInvoker(new[] { filter.Object }, pageThrows: false); - - // Act - await invoker.InvokeAsync(); - - // Assert - filter.Verify(f => f.OnException(It.IsAny()), Times.Never()); - } - - [Fact] - public async Task InvokeAsync_DoesNotAsyncInvokeExceptionFilter_WhenPageDoesNotThrow() - { - // Arrange - var filter = new Mock(MockBehavior.Strict); - filter - .Setup(f => f.OnExceptionAsync(It.IsAny())) - .Returns((context) => Task.FromResult(true)) - .Verifiable(); - - var invoker = CreateInvoker(new[] { filter.Object }, pageThrows: false); - - // Act - await invoker.InvokeAsync(); - - // Assert - filter.Verify( - f => f.OnExceptionAsync(It.IsAny()), - Times.Never()); - } - - [Fact] - public async Task InvokeAsync_InvokesExceptionFilter_WhenPageThrows() - { - // Arrange - Exception exception = null; - IActionResult pageAction = null; - var expected = new Mock(MockBehavior.Strict); - expected - .Setup(r => r.ExecuteResultAsync(It.IsAny())) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var filter1 = new Mock(MockBehavior.Strict); - filter1 - .Setup(f => f.OnException(It.IsAny())) - .Verifiable(); - var filter2 = new Mock(MockBehavior.Strict); - filter2 - .Setup(f => f.OnException(It.IsAny())) - .Callback(context => - { - exception = context.Exception; - pageAction = context.Result; - - // Handle the exception - context.Result = expected.Object; - }) - .Verifiable(); - - var invoker = CreateInvoker(new[] { filter1.Object, filter2.Object }, pageThrows: true); - - // Act - await invoker.InvokeAsync(); - - // Assert - expected.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); - filter2.Verify(f => f.OnException(It.IsAny()), Times.Once()); - - Assert.Same(_pageException, exception); - Assert.Null(pageAction); - } - - [Fact] - public async Task InvokeAsync_InvokesAsyncExceptionFilter_WhenPageThrows() - { - // Arrange - Exception exception = null; - IActionResult pageAction = null; - var expected = new Mock(MockBehavior.Strict); - expected - .Setup(r => r.ExecuteResultAsync(It.IsAny())) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var filter1 = new Mock(MockBehavior.Strict); - filter1 - .Setup(f => f.OnExceptionAsync(It.IsAny())) - .Returns((context) => Task.FromResult(true)) - .Verifiable(); - var filter2 = new Mock(MockBehavior.Strict); - filter2 - .Setup(f => f.OnExceptionAsync(It.IsAny())) - .Callback(context => - { - exception = context.Exception; - pageAction = context.Result; - - // Handle the exception - context.Result = expected.Object; - }) - .Returns((context) => Task.FromResult(true)) - .Verifiable(); - - var invoker = CreateInvoker(new[] { filter1.Object, filter2.Object }, pageThrows: true); - - // Act - await invoker.InvokeAsync(); - - // Assert - expected.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); - filter2.Verify( - f => f.OnExceptionAsync(It.IsAny()), - Times.Once()); - - Assert.Same(_pageException, exception); - Assert.Null(pageAction); - } - - [Fact] - public async Task InvokeAsync_InvokesExceptionFilter_ShortCircuit_ExceptionNull() - { - // Arrange - var filter1 = new Mock(MockBehavior.Strict); - - var filter2 = new Mock(MockBehavior.Strict); - filter2 - .Setup(f => f.OnException(It.IsAny())) - .Callback(context => - { - filter2.ToString(); - context.Exception = null; - }) - .Verifiable(); - - var invoker = CreateInvoker(new[] { filter1.Object, filter2.Object }, pageThrows: true); - - // Act - await invoker.InvokeAsync(); - - // Assert - filter2.Verify( - f => f.OnException(It.IsAny()), - Times.Once()); - } - - [Fact] - public async Task InvokeAsync_InvokesExceptionFilter_ShortCircuit_ExceptionHandled() - { - // Arrange - var filter1 = new Mock(MockBehavior.Strict); - - var filter2 = new Mock(MockBehavior.Strict); - filter2 - .Setup(f => f.OnException(It.IsAny())) - .Callback(context => - { - context.ExceptionHandled = true; - }) - .Verifiable(); - - var invoker = CreateInvoker(new[] { filter1.Object, filter2.Object }, pageThrows: true); - - // Act - await invoker.InvokeAsync(); - - // Assert - filter2.Verify( - f => f.OnException(It.IsAny()), - Times.Once()); - } - - [Fact] - public async Task InvokeAsync_InvokesAsyncExceptionFilter_ShortCircuit_ExceptionNull() - { - // Arrange - var filter1 = new Mock(MockBehavior.Strict); - var filter2 = new Mock(MockBehavior.Strict); - - filter2 - .Setup(f => f.OnExceptionAsync(It.IsAny())) - .Callback(context => - { - filter2.ToString(); - context.Exception = null; - }) - .Returns((context) => Task.FromResult(true)) - .Verifiable(); - - var filterMetadata = new IFilterMetadata[] { filter1.Object, filter2.Object }; - var invoker = CreateInvoker(filterMetadata, pageThrows: true); - - // Act - await invoker.InvokeAsync(); - - // Assert - filter2.Verify( - f => f.OnExceptionAsync(It.IsAny()), - Times.Once()); - } - - [Fact] - public async Task InvokeAsync_InvokesAsyncExceptionFilter_ShortCircuit_ExceptionHandled() - { - // Arrange - var filter1 = new Mock(MockBehavior.Strict); - - var filter2 = new Mock(MockBehavior.Strict); - filter2 - .Setup(f => f.OnExceptionAsync(It.IsAny())) - .Callback(context => - { - context.ExceptionHandled = true; - }) - .Returns((context) => Task.FromResult(true)) - .Verifiable(); - - var invoker = CreateInvoker(new IFilterMetadata[] { filter1.Object, filter2.Object }, pageThrows: true); - - // Act - await invoker.InvokeAsync(); - - // Assert - filter2.Verify( - f => f.OnExceptionAsync(It.IsAny()), - Times.Once()); - } - - [Fact] - public async Task InvokeAsync_InvokesExceptionFilter_UnhandledExceptionIsThrown() - { - // Arrange - var filter = new Mock(MockBehavior.Strict); - filter - .Setup(f => f.OnException(It.IsAny())) - .Verifiable(); - - var invoker = CreateInvoker(new[] { filter.Object }, pageThrows: true); - - // Act - await Assert.ThrowsAsync(_pageException.GetType(), invoker.InvokeAsync); - - // Assert - filter.Verify(f => f.OnException(It.IsAny()), Times.Once()); - } - - [Fact] - public async Task InvokeAsync_InvokesAuthorizationFilter() - { - // Arrange - var filter = new Mock(MockBehavior.Strict); - filter.Setup(f => f.OnAuthorization(It.IsAny())).Verifiable(); - - var invoker = CreateInvoker(new[] { filter.Object }); - - // Act - await invoker.InvokeAsync(); - - // Assert - filter.Verify(f => f.OnAuthorization(It.IsAny()), Times.Once()); - } - - [Fact] - public async Task InvokeAsync_InvokesAsyncAuthorizationFilter() - { - // Arrange - var filter = new Mock(MockBehavior.Strict); - filter - .Setup(f => f.OnAuthorizationAsync(It.IsAny())) - .Returns(context => Task.FromResult(true)) - .Verifiable(); - - var invoker = CreateInvoker(new[] { filter.Object }); - - // Act - await invoker.InvokeAsync(); - - // Assert - filter.Verify( - f => f.OnAuthorizationAsync(It.IsAny()), - Times.Once()); - } - - [Fact] - public async Task InvokeAsync_InvokesAuthorizationFilter_ShortCircuit() - { - // Arrange - var createCalled = false; - var challenge = new Mock(MockBehavior.Strict); - challenge - .Setup(r => r.ExecuteResultAsync(It.IsAny())) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var filter1 = new Mock(MockBehavior.Strict); - filter1 - .Setup(f => f.OnAuthorization(It.IsAny())) - .Callback(c => Task.FromResult(true)) - .Verifiable(); - - var filter2 = new Mock(MockBehavior.Strict); - filter2 - .Setup(f => f.OnAuthorization(It.IsAny())) - .Callback(c => c.Result = challenge.Object) - .Verifiable(); - - var filter3 = new Mock(MockBehavior.Strict); - - var actionDescriptor = new CompiledPageActionDescriptor() - { - HandlerTypeInfo = typeof(TestPage).GetTypeInfo(), - ModelTypeInfo = typeof(TestPage).GetTypeInfo(), - PageTypeInfo = typeof(TestPage).GetTypeInfo(), - }; - - var cacheEntry = new PageActionInvokerCacheEntry( - actionDescriptor, - null, - (context, viewContext) => createCalled = true, - null, - (context) => null, - null, - null, - null, - null, - new FilterItem[0]); - var invoker = CreateInvoker( - new[] { filter1.Object, filter2.Object, filter3.Object }, - actionDescriptor, - cacheEntry: cacheEntry); - - // Act - await invoker.InvokeAsync(); - - // Assert - challenge.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); - filter1.Verify(f => f.OnAuthorization(It.IsAny()), Times.Once()); - Assert.False(createCalled); - } - - [Fact] - public async Task InvokeAsync_InvokesAsyncAuthorizationFilter_ShortCircuit() - { - // Arrange - var createCalled = false; - var challenge = new Mock(MockBehavior.Strict); - challenge - .Setup(r => r.ExecuteResultAsync(It.IsAny())) - .Returns(Task.FromResult(true)) - .Verifiable(); - - var filter1 = new Mock(MockBehavior.Strict); - filter1 - .Setup(f => f.OnAuthorizationAsync(It.IsAny())) - .Returns((context) => - { - return Task.FromResult(true); - }) - .Verifiable(); - - var filter2 = new Mock(MockBehavior.Strict); - filter2 - .Setup(f => f.OnAuthorizationAsync(It.IsAny())) - .Returns((context) => - { - context.Result = challenge.Object; - return Task.FromResult(true); - }); - - var filter3 = new Mock(MockBehavior.Strict); - - var actionDescriptor = new CompiledPageActionDescriptor() - { - HandlerTypeInfo = typeof(TestPage).GetTypeInfo(), - ModelTypeInfo = typeof(TestPage).GetTypeInfo(), - PageTypeInfo = typeof(TestPage).GetTypeInfo(), - }; - - var cacheEntry = new PageActionInvokerCacheEntry( - actionDescriptor, - null, - (context, viewContext) => createCalled = true, - null, - (context) => null, - null, - null, - null, - null, - new FilterItem[0]); - var invoker = CreateInvoker( - new IFilterMetadata[] { filter1.Object, filter2.Object, filter3.Object }, - actionDescriptor, - cacheEntry: cacheEntry); - - // Act - await invoker.InvokeAsync(); - - // Assert - challenge.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); - filter1.Verify( - f => f.OnAuthorizationAsync(It.IsAny()), - Times.Once()); - - Assert.False(createCalled); - } - - [Fact] - public async Task InvokeAsync_ExceptionInAuthorizationFilter_CannotBeHandledByOtherFilters() - { - // Arrange - var expected = new InvalidCastException(); - - var exceptionFilter = new Mock(MockBehavior.Strict); - exceptionFilter - .Setup(f => f.OnException(It.IsAny())) - .Callback(context => - { - // Mark as handled - context.Result = new EmptyResult(); - }) - .Verifiable(); - - var authorizationFilter1 = new Mock(MockBehavior.Strict); - authorizationFilter1 - .Setup(f => f.OnAuthorization(It.IsAny())) - .Callback(c => { throw expected; }) - .Verifiable(); - - // None of these filters should run - var authorizationFilter2 = new Mock(MockBehavior.Strict); - var resourceFilter = new Mock(MockBehavior.Strict); - var actionFilter = new Mock(MockBehavior.Strict); - var resultFilter = new Mock(MockBehavior.Strict); - - var invoker = CreateInvoker(new IFilterMetadata[] - { - exceptionFilter.Object, - authorizationFilter1.Object, - authorizationFilter2.Object, - resourceFilter.Object, - actionFilter.Object, - resultFilter.Object, - }); - - // Act - var thrown = await Assert.ThrowsAsync(invoker.InvokeAsync); - - // Assert - Assert.Same(expected, thrown); - exceptionFilter.Verify(f => f.OnException(It.IsAny()), Times.Never()); - authorizationFilter1.Verify(f => f.OnAuthorization(It.IsAny()), Times.Once()); - } - - [Fact] - public async Task InvokeAsync_InvokesAuthorizationFilter_ChallengeNotSeenByResultFilters() - { - // Arrange - var challenge = new Mock(MockBehavior.Strict); - challenge - .Setup(r => r.ExecuteResultAsync(It.IsAny())) - .Returns((context) => Task.FromResult(true)) - .Verifiable(); - - var authorizationFilter = new Mock(MockBehavior.Strict); - authorizationFilter - .Setup(f => f.OnAuthorization(It.IsAny())) - .Callback(c => c.Result = challenge.Object) - .Verifiable(); - - var resultFilter = new Mock(MockBehavior.Strict); - - var invoker = CreateInvoker(new IFilterMetadata[] { authorizationFilter.Object, resultFilter.Object }); - - // Act - await invoker.InvokeAsync(); - - // Assert - authorizationFilter.Verify(f => f.OnAuthorization(It.IsAny()), Times.Once()); - challenge.Verify(c => c.ExecuteResultAsync(It.IsAny()), Times.Once()); - } - - private PageActionInvoker CreateInvoker( + protected override ResourceInvoker CreateInvoker( IFilterMetadata[] filters, - bool pageThrows = false, - int maxAllowedErrorsInModelState = 200, - List valueProviderFactories = null) + Exception exception = null, + IActionResult result = null, + IList valueProviderFactories = null) { - Func executeAction; - if (pageThrows) - { - executeAction = _ => { throw _pageException; }; - } - else - { - executeAction = context => context.HttpContext.Response.WriteAsync("Hello"); - } - var executor = new TestPageResultExecutor(executeAction); var actionDescriptor = new CompiledPageActionDescriptor { ViewEnginePath = "/Index.cshtml", RelativePath = "/Index.cshtml", + HandlerMethods = new List(), HandlerTypeInfo = typeof(TestPage).GetTypeInfo(), ModelTypeInfo = typeof(TestPage).GetTypeInfo(), PageTypeInfo = typeof(TestPage).GetTypeInfo(), }; + var handlers = new List>>(); + if (result != null) + { + handlers.Add((obj, args) => Task.FromResult(result)); + actionDescriptor.HandlerMethods.Add(new HandlerMethodDescriptor() + { + HttpMethod = "GET", + Parameters = new List(), + }); + } + else if (exception != null) + { + handlers.Add((obj, args) => Task.FromException(exception)); + actionDescriptor.HandlerMethods.Add(new HandlerMethodDescriptor() + { + HttpMethod = "GET", + Parameters = new List(), + }); + } + + var executor = new TestPageResultExecutor(); return CreateInvoker( filters, actionDescriptor, - executor); + executor, + handlers: handlers.ToArray()); } private PageActionInvoker CreateInvoker( IFilterMetadata[] filters, CompiledPageActionDescriptor actionDescriptor, PageResultExecutor executor = null, - IPageHandlerMethodSelector selector = null, PageActionInvokerCacheEntry cacheEntry = null, ITempDataDictionaryFactory tempDataFactory = null, - int maxAllowedErrorsInModelState = 200, - List valueProviderFactories = null, + IList valueProviderFactories = null, + Func>[] handlers = null, RouteData routeData = null, ILogger logger = null) { @@ -578,6 +103,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal HtmlEncoder.Default); } + var mvcOptionsAccessor = new TestOptionsManager(); + serviceCollection.AddSingleton(NullLoggerFactory.Instance); + serviceCollection.AddSingleton>(mvcOptionsAccessor); + serviceCollection.AddSingleton(new ObjectResultExecutor( + mvcOptionsAccessor, + new TestHttpResponseStreamWriterFactory(), + NullLoggerFactory.Instance)); + + httpContext.Response.Body = new MemoryStream(); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + serviceCollection.AddSingleton(executor ?? executor); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); @@ -597,11 +133,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var viewDataFactory = ViewDataDictionaryFactory.CreateFactory(actionDescriptor.ModelTypeInfo); pageContext.ViewData = viewDataFactory(new EmptyModelMetadataProvider(), pageContext.ModelState); - - if (selector == null) - { - selector = Mock.Of(); - } if (valueProviderFactories == null) { @@ -633,17 +164,23 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal _ => Activator.CreateInstance(actionDescriptor.ModelTypeInfo.AsType()), (c, model) => { (model as IDisposable)?.Dispose(); }, null, - null, + handlers, null, new FilterItem[0]); + + // Always just select the first one. + var selector = new Mock(); + selector + .Setup(s => s.Select(It.IsAny())) + .Returns(c => c.ActionDescriptor.HandlerMethods.FirstOrDefault()); var invoker = new PageActionInvoker( - selector, + selector.Object, diagnosticSource, logger, pageContext, filters, - valueProviderFactories.AsReadOnly(), + valueProviderFactories.ToArray(), cacheEntry, GetParameterBinder(), tempDataFactory, @@ -687,6 +224,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { private readonly Func _executeAction; + public TestPageResultExecutor() + : this(null) + { + } + public TestPageResultExecutor(Func executeAction) : base( Mock.Of(), @@ -700,7 +242,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } public override Task ExecuteAsync(PageContext pageContext, PageResult result) - => _executeAction(pageContext); + { + return _executeAction?.Invoke(pageContext) ?? Task.CompletedTask; + } } private class TestPage : Page diff --git a/test/Microsoft.AspNetCore.Mvc.TestCommon/CommonResourceInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.TestCommon/CommonResourceInvokerTest.cs index d04ff0ae35..ae17e2ec30 100644 --- a/test/Microsoft.AspNetCore.Mvc.TestCommon/CommonResourceInvokerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TestCommon/CommonResourceInvokerTest.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc { public abstract class CommonResourceInvokerTest { - protected static readonly ContentResult Result = new ContentResult() { Content = "Hello, world!" }; + protected static readonly TestResult Result = new TestResult(); // Intentionally choosing an uncommon exception type. protected static readonly Exception Exception = new DivideByZeroException(); @@ -23,15 +23,17 @@ namespace Microsoft.AspNetCore.Mvc protected ResourceInvoker CreateInvoker( IFilterMetadata filter, Exception exception = null, - List valueProviderFactories = null) + IActionResult result = null, + IList valueProviderFactories = null) { - return CreateInvoker(new IFilterMetadata[] { filter }, exception, valueProviderFactories); + return CreateInvoker(new IFilterMetadata[] { filter }, exception, result, valueProviderFactories); } protected abstract ResourceInvoker CreateInvoker( IFilterMetadata[] filters, Exception exception = null, - List valueProviderFactories = null); + IActionResult result = null, + IList valueProviderFactories = null); [Fact] public async Task InvokeAction_DoesNotInvokeExceptionFilter_WhenActionDoesNotThrow() @@ -845,7 +847,7 @@ namespace Microsoft.AspNetCore.Mvc var filter3 = new Mock(MockBehavior.Strict); - var invoker = CreateInvoker(new IFilterMetadata[] { filter1.Object, filter2.Object, filter3.Object }); + var invoker = CreateInvoker(new IFilterMetadata[] { filter1.Object, filter2.Object, filter3.Object }, result: Result); // Act await invoker.InvokeAsync(); @@ -859,7 +861,7 @@ namespace Microsoft.AspNetCore.Mvc Times.Once()); Assert.True(context.Canceled); - Assert.IsType(context.Result); + Assert.Same(Result, context.Result); } [Fact] @@ -887,7 +889,7 @@ namespace Microsoft.AspNetCore.Mvc var filter3 = new Mock(MockBehavior.Strict); - var invoker = CreateInvoker(new IFilterMetadata[] { filter1.Object, filter2.Object, filter3.Object }); + var invoker = CreateInvoker(new IFilterMetadata[] { filter1.Object, filter2.Object, filter3.Object }, result: Result); // Act await invoker.InvokeAsync(); @@ -901,7 +903,7 @@ namespace Microsoft.AspNetCore.Mvc Times.Once()); Assert.True(context.Canceled); - Assert.IsType(context.Result); + Assert.Same(Result, context.Result); } [Fact] @@ -1203,7 +1205,7 @@ namespace Microsoft.AspNetCore.Mvc }) .Verifiable(); - var invoker = CreateInvoker(resourceFilter.Object); + var invoker = CreateInvoker(resourceFilter.Object, result: Result); // Act await invoker.InvokeAsync(); @@ -1710,5 +1712,9 @@ namespace Microsoft.AspNetCore.Mvc f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny()), Times.Never()); } + + public class TestResult : ActionResult + { + } } }