diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs index 3c0bcd640f..02f464d8f6 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs @@ -209,7 +209,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal filter.OnActionExecuted(actionExecutedContext); - _diagnosticSource.BeforeOnActionExecuted(actionExecutedContext, filter); + _diagnosticSource.AfterOnActionExecuted(actionExecutedContext, filter); goto case State.ActionEnd; } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IAsyncPageFilter.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IAsyncPageFilter.cs new file mode 100644 index 0000000000..a4534e7dc2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IAsyncPageFilter.cs @@ -0,0 +1,30 @@ +// 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.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc.Filters +{ + /// + /// A filter that asynchronously surrounds execution of the page handler method. + /// + public interface IAsyncPageFilter : IFilterMetadata + { + /// + /// Called asynchronously after the handler method has been selected, but before model binding occurs. + /// + /// The . + /// A that on completion indicates the filter has executed. + Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context); + + /// + /// Called asynchronously before the handler method is invoked, after model binding is complete. + /// + /// The . + /// + /// The . Invoked to execute the next page filter or the handler method itself. + /// + /// A that on completion indicates the filter has executed. + Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IPageFilter.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IPageFilter.cs new file mode 100644 index 0000000000..8c9b45ea58 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/IPageFilter.cs @@ -0,0 +1,29 @@ +// 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. + +namespace Microsoft.AspNetCore.Mvc.Filters +{ + /// + /// A filter that surrounds execution of a page handler method. + /// + public interface IPageFilter : IFilterMetadata + { + /// + /// Called after a handler method has been selected, but before model binding occurs. + /// + /// The . + void OnPageHandlerSelected(PageHandlerSelectedContext context); + + /// + /// Called before the handler method executes, after model binding is complete. + /// + /// The . + void OnPageHandlerExecuting(PageHandlerExecutingContext context); + + /// + /// Called after the handler method executes, before the action result. + /// + /// The . + void OnPageHandlerExecuted(PageHandlerExecutedContext context); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/PageHandlerExecutedContext.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/PageHandlerExecutedContext.cs new file mode 100644 index 0000000000..084b44a825 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/PageHandlerExecutedContext.cs @@ -0,0 +1,124 @@ +// 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 System.Collections.Generic; +using System.Runtime.ExceptionServices; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc.Filters +{ + /// + /// A context for page filters, used specifically in + /// and + /// . + /// + public class PageHandlerExecutedContext : FilterContext + { + private Exception _exception; + private ExceptionDispatchInfo _exceptionDispatchInfo; + + /// + /// Creates a new instance of . + /// + /// The associated with the current request. + /// The set of filters associated with the page. + /// The handler method to be invoked, may be null. + /// The handler instance associated with the page. + public PageHandlerExecutedContext( + PageContext pageContext, + IList filters, + HandlerMethodDescriptor handlerMethod, + object handlerInstance) + : base(pageContext, filters) + { + if (handlerInstance == null) + { + throw new ArgumentNullException(nameof(handlerInstance)); + } + + HandlerMethod = handlerMethod; + HandlerInstance = handlerInstance; + } + + /// + /// Gets the descriptor associated with the current page. + /// + public new virtual CompiledPageActionDescriptor ActionDescriptor + { + get + { + return (CompiledPageActionDescriptor)base.ActionDescriptor; + } + } + + /// + /// Gets or sets an indication that an page filter short-circuited the action and the page filter pipeline. + /// + public virtual bool Canceled { get; set; } + + /// + /// Gets the handler instance containing the handler method. + /// + public virtual object HandlerInstance { get; } + + /// + /// Gets the descriptor for the handler method that was invoked. + /// + public virtual HandlerMethodDescriptor HandlerMethod { get; } + + /// + /// Gets or sets the caught while executing the action or action filters, if + /// any. + /// + public virtual Exception Exception + { + get + { + if (_exception == null && _exceptionDispatchInfo != null) + { + return _exceptionDispatchInfo.SourceException; + } + else + { + return _exception; + } + } + + set + { + _exceptionDispatchInfo = null; + _exception = value; + } + } + + /// + /// Gets or sets the for the + /// , if an was caught and this information captured. + /// + public virtual ExceptionDispatchInfo ExceptionDispatchInfo + { + get + { + return _exceptionDispatchInfo; + } + + set + { + _exception = null; + _exceptionDispatchInfo = value; + } + } + + /// + /// Gets or sets an indication that the has been handled. + /// + public virtual bool ExceptionHandled { get; set; } + + /// + /// Gets or sets the . + /// + public virtual IActionResult Result { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/PageHandlerExecutingContext.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/PageHandlerExecutingContext.cs new file mode 100644 index 0000000000..0f712502b9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/PageHandlerExecutingContext.cs @@ -0,0 +1,81 @@ +// 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 System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc.Filters +{ + /// + /// A context for page filters, used specifically in + /// and + /// . + /// + public class PageHandlerExecutingContext : FilterContext + { + /// + /// Creates a new instance of . + /// + /// The associated with the current request. + /// The set of filters associated with the page. + /// The handler method to be invoked, may be null. + /// The arguments to provide to the handler method. + /// The handler instance associated with the page. + public PageHandlerExecutingContext( + PageContext pageContext, + IList filters, + HandlerMethodDescriptor handlerMethod, + IDictionary handlerArguments, + object handlerInstance) + : base(pageContext, filters) + { + if (handlerArguments == null) + { + throw new ArgumentNullException(nameof(handlerArguments)); + } + + if (handlerInstance == null) + { + throw new ArgumentNullException(nameof(handlerInstance)); + } + + HandlerMethod = handlerMethod; + HandlerArguments = handlerArguments; + HandlerInstance = handlerInstance; + } + + /// + /// Gets the descriptor associated with the current page. + /// + public new virtual CompiledPageActionDescriptor ActionDescriptor + { + get + { + return (CompiledPageActionDescriptor)base.ActionDescriptor; + } + } + + /// + /// Gets or sets the to execute. Setting to a non-null + /// value inside a page filter will short-circuit the page and any remaining page filters. + /// + public virtual IActionResult Result { get; set; } + + /// + /// Gets the arguments to pass when invoking the handler method. Keys are parameter names. + /// + public virtual IDictionary HandlerArguments { get; } + + /// + /// Gets the descriptor for the handler method about to be invoked. + /// + public virtual HandlerMethodDescriptor HandlerMethod { get; } + + /// + /// Gets the object instance containing the handler method. + /// + public virtual object HandlerInstance { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/PageHandlerExecutionDelegate.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/PageHandlerExecutionDelegate.cs new file mode 100644 index 0000000000..7bf9708b20 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/PageHandlerExecutionDelegate.cs @@ -0,0 +1,16 @@ +// 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.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc.Filters +{ + /// + /// A delegate that asynchronously returns a indicating the page or the next + /// page filter has executed. + /// + /// + /// A that on completion returns an . + /// + public delegate Task PageHandlerExecutionDelegate(); +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/PageHandlerSelectedContext.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/PageHandlerSelectedContext.cs new file mode 100644 index 0000000000..49d43e3dfa --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Filters/PageHandlerSelectedContext.cs @@ -0,0 +1,59 @@ +// 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 System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc.Filters +{ + /// + /// A context for page filters, used specifically in + /// and + /// . + /// + public class PageHandlerSelectedContext : FilterContext + { + /// + /// Creates a new instance of . + /// + /// The associated with the current request. + /// The set of filters associated with the page. + /// The handler instance associated with the page. + public PageHandlerSelectedContext( + PageContext pageContext, + IList filters, + object handlerInstance) + : base(pageContext, filters) + { + if (handlerInstance == null) + { + throw new ArgumentNullException(nameof(handlerInstance)); + } + + HandlerInstance = handlerInstance; + } + + /// + /// Gets the descriptor associated with the current page. + /// + public new virtual CompiledPageActionDescriptor ActionDescriptor + { + get + { + return (CompiledPageActionDescriptor)base.ActionDescriptor; + } + } + + /// + /// Gets or sets the descriptor for the handler method about to be invoked. + /// + public virtual HandlerMethodDescriptor HandlerMethod { get; set; } + + /// + /// Gets the object instance containing the handler method. + /// + public virtual object HandlerInstance { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/MvcRazorPagesDiagnosticSourceExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/MvcRazorPagesDiagnosticSourceExtensions.cs new file mode 100644 index 0000000000..212006004e --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/MvcRazorPagesDiagnosticSourceExtensions.cs @@ -0,0 +1,290 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc.Internal +{ + public static class MvcRazorPagesDiagnosticSourceExtensions + { + public static void BeforeHandlerMethod( + this DiagnosticSource diagnosticSource, + ActionContext actionContext, + HandlerMethodDescriptor handlerMethodDescriptor, + IDictionary arguments, + object instance) + { + Debug.Assert(diagnosticSource != null); + Debug.Assert(actionContext != null); + Debug.Assert(handlerMethodDescriptor != null); + Debug.Assert(arguments != null); + Debug.Assert(instance != null); + + if (diagnosticSource.IsEnabled("Microsoft.AspNetCore.Mvc.BeforeHandlerMethod")) + { + diagnosticSource.Write( + "Microsoft.AspNetCore.Mvc.BeforeHandlerMethod", + new + { + actionContext = actionContext, + arguments = arguments, + handlerMethodDescriptor = handlerMethodDescriptor, + instance = instance, + }); + } + } + + public static void AfterHandlerMethod( + this DiagnosticSource diagnosticSource, + ActionContext actionContext, + HandlerMethodDescriptor handlerMethodDescriptor, + IDictionary arguments, + object instance, + IActionResult result) + { + Debug.Assert(diagnosticSource != null); + Debug.Assert(actionContext != null); + Debug.Assert(handlerMethodDescriptor != null); + Debug.Assert(arguments != null); + Debug.Assert(instance != null); + + if (diagnosticSource.IsEnabled("Microsoft.AspNetCore.Mvc.AfterHandlerMethod")) + { + diagnosticSource.Write( + "Microsoft.AspNetCore.Mvc.AfterHandlerMethod", + new + { + actionContext = actionContext, + arguments = arguments, + handlerMethodDescriptor = handlerMethodDescriptor, + instance = instance, + result = result + }); + } + } + + public static void BeforeOnPageHandlerExecution( + this DiagnosticSource diagnosticSource, + PageHandlerExecutingContext handlerExecutionContext, + IAsyncPageFilter filter) + { + Debug.Assert(diagnosticSource != null); + Debug.Assert(handlerExecutionContext != null); + Debug.Assert(filter != null); + + if (diagnosticSource.IsEnabled("Microsoft.AspNetCore.Mvc.BeforeOnPageHandlerExecution")) + { + diagnosticSource.Write( + "Microsoft.AspNetCore.Mvc.BeforeOnPageHandlerExecution", + new + { + actionDescriptor = handlerExecutionContext.ActionDescriptor, + handlerExecutionContext = handlerExecutionContext, + filter = filter + }); + } + } + + public static void AfterOnPageHandlerExecution( + this DiagnosticSource diagnosticSource, + PageHandlerExecutedContext handlerExecutedContext, + IAsyncPageFilter filter) + { + Debug.Assert(diagnosticSource != null); + Debug.Assert(handlerExecutedContext != null); + Debug.Assert(filter != null); + + if (diagnosticSource.IsEnabled("Microsoft.AspNetCore.Mvc.AfterOnPageHandlerExecution")) + { + diagnosticSource.Write( + "Microsoft.AspNetCore.Mvc.AfterOnPageHandlerExecution", + new + { + actionDescriptor = handlerExecutedContext.ActionDescriptor, + handlerExecutedContext = handlerExecutedContext, + filter = filter + }); + } + } + + public static void BeforeOnPageHandlerExecuting( + this DiagnosticSource diagnosticSource, + PageHandlerExecutingContext handlerExecutingContext, + IPageFilter filter) + { + Debug.Assert(diagnosticSource != null); + Debug.Assert(handlerExecutingContext != null); + Debug.Assert(filter != null); + + if (diagnosticSource.IsEnabled("Microsoft.AspNetCore.Mvc.BeforeOnPageHandlerExecuting")) + { + diagnosticSource.Write( + "Microsoft.AspNetCore.Mvc.BeforeOnPageHandlerExecuting", + new + { + actionDescriptor = handlerExecutingContext.ActionDescriptor, + handlerExecutingContext = handlerExecutingContext, + filter = filter + }); + } + } + + public static void AfterOnPageHandlerExecuting( + this DiagnosticSource diagnosticSource, + PageHandlerExecutingContext handlerExecutingContext, + IPageFilter filter) + { + Debug.Assert(diagnosticSource != null); + Debug.Assert(handlerExecutingContext != null); + Debug.Assert(filter != null); + + if (diagnosticSource.IsEnabled("Microsoft.AspNetCore.Mvc.AfterOnPageHandlerExecuting")) + { + diagnosticSource.Write( + "Microsoft.AspNetCore.Mvc.AfterOnPageHandlerExecuting", + new + { + actionDescriptor = handlerExecutingContext.ActionDescriptor, + handlerExecutingContext = handlerExecutingContext, + filter = filter + }); + } + } + + public static void BeforeOnPageHandlerExecuted( + this DiagnosticSource diagnosticSource, + PageHandlerExecutedContext handlerExecutedContext, + IPageFilter filter) + { + Debug.Assert(diagnosticSource != null); + Debug.Assert(handlerExecutedContext != null); + Debug.Assert(filter != null); + + if (diagnosticSource.IsEnabled("Microsoft.AspNetCore.Mvc.BeforeOnPageHandlerExecuted")) + { + diagnosticSource.Write( + "Microsoft.AspNetCore.Mvc.BeforeOnPageHandlerExecuted", + new + { + actionDescriptor = handlerExecutedContext.ActionDescriptor, + handlerExecutedContext = handlerExecutedContext, + filter = filter + }); + } + } + + public static void AfterOnPageHandlerExecuted( + this DiagnosticSource diagnosticSource, + PageHandlerExecutedContext handlerExecutedContext, + IPageFilter filter) + { + Debug.Assert(diagnosticSource != null); + Debug.Assert(handlerExecutedContext != null); + Debug.Assert(filter != null); + + if (diagnosticSource.IsEnabled("Microsoft.AspNetCore.Mvc.AfterOnPageHandlerExecuted")) + { + diagnosticSource.Write( + "Microsoft.AspNetCore.Mvc.AfterOnPageHandlerExecuted", + new + { + actionDescriptor = handlerExecutedContext.ActionDescriptor, + handlerExecutedContext = handlerExecutedContext, + filter = filter + }); + } + } + + public static void BeforeOnPageHandlerSelection( + this DiagnosticSource diagnosticSource, + PageHandlerSelectedContext handlerSelectedContext, + IAsyncPageFilter filter) + { + Debug.Assert(diagnosticSource != null); + Debug.Assert(handlerSelectedContext != null); + Debug.Assert(filter != null); + + if (diagnosticSource.IsEnabled("Microsoft.AspNetCore.Mvc.BeforeOnPageHandlerSelection")) + { + diagnosticSource.Write( + "Microsoft.AspNetCore.Mvc.BeforeOnPageHandlerSelection", + new + { + actionDescriptor = handlerSelectedContext.ActionDescriptor, + handlerSelectedContext = handlerSelectedContext, + filter = filter + }); + } + } + + public static void AfterOnPageHandlerSelection( + this DiagnosticSource diagnosticSource, + PageHandlerSelectedContext handlerSelectedContext, + IAsyncPageFilter filter) + { + Debug.Assert(diagnosticSource != null); + Debug.Assert(handlerSelectedContext != null); + Debug.Assert(filter != null); + + if (diagnosticSource.IsEnabled("Microsoft.AspNetCore.Mvc.AfterOnPageHandlerSelection")) + { + diagnosticSource.Write( + "Microsoft.AspNetCore.Mvc.AfterOnPageHandlerSelection", + new + { + actionDescriptor = handlerSelectedContext.ActionDescriptor, + handlerSelectedContext = handlerSelectedContext, + filter = filter + }); + } + } + + public static void BeforeOnPageHandlerSelected( + this DiagnosticSource diagnosticSource, + PageHandlerSelectedContext handlerSelectedContext, + IPageFilter filter) + { + Debug.Assert(diagnosticSource != null); + Debug.Assert(handlerSelectedContext != null); + Debug.Assert(filter != null); + + if (diagnosticSource.IsEnabled("Microsoft.AspNetCore.Mvc.BeforeOnPageHandlerSelected")) + { + diagnosticSource.Write( + "Microsoft.AspNetCore.Mvc.BeforeOnPageHandlerSelected", + new + { + actionDescriptor = handlerSelectedContext.ActionDescriptor, + handlerSelectedContext = handlerSelectedContext, + filter = filter + }); + } + } + + public static void AfterOnPageHandlerSelected( + this DiagnosticSource diagnosticSource, + PageHandlerSelectedContext handlerSelectedContext, + IPageFilter filter) + { + Debug.Assert(diagnosticSource != null); + Debug.Assert(handlerSelectedContext != null); + Debug.Assert(filter != null); + + if (diagnosticSource.IsEnabled("Microsoft.AspNetCore.Mvc.AfterOnPageHandlerSelected")) + { + diagnosticSource.Write( + "Microsoft.AspNetCore.Mvc.AfterOnPageHandlerSelected", + new + { + actionDescriptor = handlerSelectedContext.ActionDescriptor, + handlerSelectedContext = handlerSelectedContext, + filter = filter + }); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs index f926e21dc8..66c01029da 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs @@ -5,7 +5,7 @@ using System; 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; @@ -26,19 +26,24 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private readonly ParameterBinder _parameterBinder; private readonly ITempDataDictionaryFactory _tempDataFactory; private readonly HtmlHelperOptions _htmlHelperOptions; + private readonly CompiledPageActionDescriptor _actionDescriptor; - private CompiledPageActionDescriptor _actionDescriptor; + private Dictionary _arguments; + private HandlerMethodDescriptor _handler; private Page _page; - private object _model; + private object _pageModel; private ViewContext _viewContext; + private PageHandlerSelectedContext _handlerSelectedContext; + private PageHandlerExecutingContext _handlerExecutingContext; + private PageHandlerExecutedContext _handlerExecutedContext; + public PageActionInvoker( IPageHandlerMethodSelector handlerMethodSelector, DiagnosticSource diagnosticSource, ILogger logger, PageContext pageContext, IFilterMetadata[] filterMetadata, - IList valueProviderFactories, PageActionInvokerCacheEntry cacheEntry, ParameterBinder parameterBinder, ITempDataDictionaryFactory tempDataFactory, @@ -48,7 +53,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal logger, pageContext, filterMetadata, - valueProviderFactories) + pageContext.ValueProviderFactories) { _selector = handlerMethodSelector; _pageContext = pageContext; @@ -63,6 +68,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Internal for testing internal PageActionInvokerCacheEntry CacheEntry { get; } + private bool HasPageModel => _actionDescriptor.HandlerTypeInfo != _actionDescriptor.PageTypeInfo; + // Internal for testing internal PageContext PageContext => _pageContext; @@ -81,12 +88,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal await Next(ref next, ref scope, ref state, ref isCompleted); } } - + protected override void ReleaseResources() { - if (_model != null && CacheEntry.ReleaseModel != null) + if (_pageModel != null && CacheEntry.ReleaseModel != null) { - CacheEntry.ReleaseModel(_pageContext, _model); + CacheEntry.ReleaseModel(_pageContext, _pageModel); } if (_page != null && CacheEntry.ReleasePage != null) @@ -95,91 +102,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } } - private Task Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted) + private object CreateInstance() { - var diagnosticSource = _diagnosticSource; - var logger = _logger; - - switch (next) + if (HasPageModel) { - case State.PageBegin: - { - var pageContext = _pageContext; + // Since this is a PageModel, we need to activate it, and then run a handler method on the model. + _pageModel = CacheEntry.ModelFactory(_pageContext); + _pageContext.ViewData.Model = _pageModel; - _cursor.Reset(); - - next = State.PageEnd; - return ExecutePageAsync(); - } - - case State.PageEnd: - { - isCompleted = true; - return TaskCache.CompletedTask; - } - - default: - throw new InvalidOperationException(); - } - } - - private Task ExecutePageAsync() - { - _pageContext.ValueProviderFactories = _valueProviderFactories; - - // There's a fork in the road here between the case where we have a full-fledged PageModel - // vs just a Page. We need to know up front because we want to execute handler methods - // on the PageModel without instantiating the Page or ViewContext. - var hasPageModel = _actionDescriptor.HandlerTypeInfo != _actionDescriptor.PageTypeInfo; - if (hasPageModel) - { - return ExecutePageWithPageModelAsync(); + return _pageModel; } else { - return ExecutePageWithoutPageModelAsync(); - } - } - - private async Task ExecutePageWithPageModelAsync() - { - // Since this is a PageModel, we need to activate it, and then run a handler method on the model. - // - // We also know that the model is the pagemodel at this point. - Debug.Assert(_actionDescriptor.ModelTypeInfo == _actionDescriptor.HandlerTypeInfo); - _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); - } - - // This is a workaround for not yet having proper filter for Pages. - PageSaveTempDataPropertyFilter propertyFilter = null; - for (var i = 0; i < _filters.Length; i++) - { - propertyFilter = _filters[i] as PageSaveTempDataPropertyFilter; - if (propertyFilter != null) - { - break; - } - } - - if (propertyFilter != null) - { - propertyFilter.Subject = _model; - propertyFilter.ApplyTempDataChanges(_pageContext.HttpContext); - } - - _result = await ExecuteHandlerMethod(_model); - if (_result is PageResult pageResult) - { - // If we get here, we are going to render the page, so we need to create it and then initialize - // the context so we can run the result. + // Since this is a Page without a PageModel, we need to create the Page before running a handler method. _viewContext = new ViewContext( _pageContext, NullView.Instance, @@ -190,39 +125,23 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal _page = (Page)CacheEntry.PageFactory(_pageContext, _viewContext); - pageResult.Page = _page; - pageResult.ViewData = pageResult.ViewData ?? _pageContext.ViewData; + if (_actionDescriptor.ModelTypeInfo == _actionDescriptor.PageTypeInfo) + { + _pageContext.ViewData.Model = _page; + } + + return _page; } } - private async Task ExecutePageWithoutPageModelAsync() + private HandlerMethodDescriptor SelectHandler() { - // Since this is a Page without a PageModel, we need to create the Page before running a handler method. - _viewContext = new ViewContext( - _pageContext, - NullView.Instance, - _pageContext.ViewData, - _tempDataFactory.GetTempData(_pageContext.HttpContext), - TextWriter.Null, - _htmlHelperOptions); + return _selector.Select(_pageContext); + } - _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; - _pageContext.ViewData.Model = _model; - } - - if (CacheEntry.PropertyBinder != null) - { - await CacheEntry.PropertyBinder(_pageContext, _model); - } - - // This is a workaround for not yet having proper filter for Pages. + private Task BindArgumentsAsync() + { + // This is a temporary workaround. PageSaveTempDataPropertyFilter propertyFilter = null; for (var i = 0; i < _filters.Length; i++) { @@ -235,27 +154,37 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal if (propertyFilter != null) { - propertyFilter.Subject = _model; + propertyFilter.Subject = _instance; propertyFilter.ApplyTempDataChanges(_pageContext.HttpContext); } - _result = await ExecuteHandlerMethod(_model); - if (_result is PageResult pageResult) + // Perf: Avoid allocating async state machines where possible. We only need the state + // machine if you need to bind properties or arguments. + if (_actionDescriptor.BoundProperties.Count == 0 && (_handler == null || _handler.Parameters.Count == 0)) { - // If we get here we're going to render the page so we need to initialize the context. - pageResult.Page = _page; - pageResult.ViewData = pageResult.ViewData ?? _pageContext.ViewData; + return Task.CompletedTask; } + + return BindArgumentsCoreAsync(); } - private async Task GetArguments(HandlerMethodDescriptor handler) + private async Task BindArgumentsCoreAsync() { - var arguments = new object[handler.Parameters.Count]; + if (CacheEntry.PropertyBinder != null) + { + await CacheEntry.PropertyBinder(_pageContext, _instance); + } + + if (_handler == null) + { + return; + } + var valueProvider = await CompositeValueProvider.CreateAsync(_pageContext, _pageContext.ValueProviderFactories); - for (var i = 0; i < handler.Parameters.Count; i++) + for (var i = 0; i < _handler.Parameters.Count; i++) { - var parameter = handler.Parameters[i]; + var parameter = _handler.Parameters[i]; var result = await _parameterBinder.BindModelAsync( _pageContext, @@ -265,29 +194,50 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal if (result.IsModelSet) { - arguments[i] = result.Model; + _arguments[parameter.Name] = result.Model; + } + } + } + + private static object[] PrepareArguments( + IDictionary argumentsInDictionary, + HandlerMethodDescriptor handler) + { + if (handler.Parameters.Count == 0) + { + return null; + } + + var arguments = new object[handler.Parameters.Count]; + for (var i = 0; i < arguments.Length; i++) + { + var parameter = handler.Parameters[i]; + + if (argumentsInDictionary.TryGetValue(parameter.ParameterInfo.Name, out var value)) + { + // Do nothing, already set the value. } else if (parameter.ParameterInfo.HasDefaultValue) { - arguments[i] = parameter.ParameterInfo.DefaultValue; + value = parameter.ParameterInfo.DefaultValue; } - else if (parameter.ParameterType.GetTypeInfo().IsValueType) + else if (parameter.ParameterInfo.ParameterType.IsValueType) { - arguments[i] = Activator.CreateInstance(parameter.ParameterType); + value = Activator.CreateInstance(parameter.ParameterInfo.ParameterType); } + + arguments[i] = value; } return arguments; } - private async Task ExecuteHandlerMethod(object instance) + private async Task InvokeHandlerMethodAsync() { - IActionResult result = null; - - var handler = _selector.Select(_pageContext); - if (handler != null) + var handler = _handler; + if (_handler != null) { - var arguments = await GetArguments(handler); + var arguments = PrepareArguments(_arguments, handler); Func> executor = null; for (var i = 0; i < _actionDescriptor.HandlerMethods.Count; i++) @@ -299,15 +249,423 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } } - result = await executor(instance, arguments); + _diagnosticSource.BeforeHandlerMethod(_pageContext, handler, _arguments, _instance); + _logger.ExecutingHandlerMethod(_pageContext, handler, arguments); + + try + { + _result = await executor(_instance, arguments); + _logger.ExecutedHandlerMethod(_pageContext, handler, _result); + } + finally + { + _diagnosticSource.AfterHandlerMethod(_pageContext, handler, _arguments, _instance, _result); + } } - if (result == null) + // Pages have an implicit 'return Page()' even without a handler method. + if (_result == null) { - result = new PageResult(); + _result = new PageResult(); } - return result; + // We also have some special initialization we need to do for PageResult. + if (_result is PageResult pageResult) + { + // If we used a PageModel then the Page isn't initialized yet. + if (_viewContext == null) + { + _viewContext = new ViewContext( + _pageContext, + NullView.Instance, + _pageContext.ViewData, + _tempDataFactory.GetTempData(_pageContext.HttpContext), + TextWriter.Null, + _htmlHelperOptions); + } + + if (_page == null) + { + _page = (Page)CacheEntry.PageFactory(_pageContext, _viewContext); + } + + pageResult.Page = _page; + pageResult.ViewData = pageResult.ViewData ?? _pageContext.ViewData; + } + } + + private Task Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted) + { + switch (next) + { + case State.PageBegin: + { + _instance = CreateInstance(); + + goto case State.PageSelectHandlerBegin; + } + + case State.PageSelectHandlerBegin: + { + _cursor.Reset(); + + _handler = SelectHandler(); + + goto case State.PageSelectHandlerNext; + } + + case State.PageSelectHandlerNext: + + var currentSelector = _cursor.GetNextFilter(); + if (currentSelector.FilterAsync != null) + { + if (_handlerSelectedContext == null) + { + _handlerSelectedContext = new PageHandlerSelectedContext(_pageContext, _filters, _instance) + { + HandlerMethod = _handler, + }; + } + + state = currentSelector.FilterAsync; + goto case State.PageSelectHandlerAsyncBegin; + } + else if (currentSelector.Filter != null) + { + if (_handlerSelectedContext == null) + { + _handlerSelectedContext = new PageHandlerSelectedContext(_pageContext, _filters, _instance) + { + HandlerMethod = _handler, + }; + } + + state = currentSelector.Filter; + goto case State.PageSelectHandlerSync; + } + else + { + goto case State.PageSelectHandlerEnd; + } + + case State.PageSelectHandlerAsyncBegin: + { + Debug.Assert(state != null); + Debug.Assert(_handlerSelectedContext != null); + + var filter = (IAsyncPageFilter)state; + var handlerSelectedContext = _handlerSelectedContext; + + _diagnosticSource.BeforeOnPageHandlerSelection(handlerSelectedContext, filter); + + var task = filter.OnPageHandlerSelectionAsync(handlerSelectedContext); + if (task.Status != TaskStatus.RanToCompletion) + { + next = State.PageSelectHandlerAsyncEnd; + return task; + } + + goto case State.PageSelectHandlerAsyncEnd; + } + + case State.PageSelectHandlerAsyncEnd: + { + Debug.Assert(state != null); + Debug.Assert(_handlerSelectedContext != null); + + var filter = (IAsyncPageFilter)state; + + _diagnosticSource.AfterOnPageHandlerSelection(_handlerSelectedContext, filter); + + goto case State.PageSelectHandlerNext; + } + + case State.PageSelectHandlerSync: + { + Debug.Assert(state != null); + Debug.Assert(_handlerSelectedContext != null); + + var filter = (IPageFilter)state; + var handlerSelectedContext = _handlerSelectedContext; + + _diagnosticSource.BeforeOnPageHandlerSelected(handlerSelectedContext, filter); + + filter.OnPageHandlerSelected(handlerSelectedContext); + + _diagnosticSource.AfterOnPageHandlerSelected(handlerSelectedContext, filter); + + goto case State.PageSelectHandlerNext; + } + + case State.PageSelectHandlerEnd: + { + if (_handlerSelectedContext != null) + { + _handler = _handlerSelectedContext.HandlerMethod; + } + + _arguments = new Dictionary(StringComparer.OrdinalIgnoreCase); + + _cursor.Reset(); + + var task = BindArgumentsAsync(); + if (task.Status != TaskStatus.RanToCompletion) + { + next = State.PageNext; + return task; + } + + goto case State.PageNext; + } + + case State.PageNext: + { + var current = _cursor.GetNextFilter(); + if (current.FilterAsync != null) + { + if (_handlerExecutingContext == null) + { + _handlerExecutingContext = new PageHandlerExecutingContext(_pageContext, _filters, _handler, _arguments, _instance); + } + + state = current.FilterAsync; + goto case State.PageAsyncBegin; + } + else if (current.Filter != null) + { + if (_handlerExecutingContext == null) + { + _handlerExecutingContext = new PageHandlerExecutingContext(_pageContext, _filters,_handler, _arguments, _instance); + } + + state = current.Filter; + goto case State.PageSyncBegin; + } + else + { + goto case State.PageInside; + } + } + + case State.PageAsyncBegin: + { + Debug.Assert(state != null); + Debug.Assert(_handlerExecutingContext != null); + + var filter = (IAsyncPageFilter)state; + var handlerExecutingContext = _handlerExecutingContext; + + _diagnosticSource.BeforeOnPageHandlerExecution(handlerExecutingContext, filter); + + var task = filter.OnPageHandlerExecutionAsync(handlerExecutingContext, InvokeNextPageFilterAwaitedAsync); + if (task.Status != TaskStatus.RanToCompletion) + { + next = State.PageAsyncEnd; + return task; + } + + goto case State.PageAsyncEnd; + } + + case State.PageAsyncEnd: + { + Debug.Assert(state != null); + Debug.Assert(_handlerExecutingContext != null); + + var filter = (IAsyncPageFilter)state; + + if (_handlerExecutedContext == null) + { + // If we get here then the filter didn't call 'next' indicating a short circuit. + _logger.PageFilterShortCircuited(filter); + + _handlerExecutedContext = new PageHandlerExecutedContext( + _pageContext, + _filters, + _handler, + _instance) + { + Canceled = true, + Result = _handlerExecutingContext.Result, + }; + } + + _diagnosticSource.AfterOnPageHandlerExecution(_handlerExecutedContext, filter); + + goto case State.PageEnd; + } + + case State.PageSyncBegin: + { + Debug.Assert(state != null); + Debug.Assert(_handlerExecutingContext != null); + + var filter = (IPageFilter)state; + var handlerExecutingContext = _handlerExecutingContext; + + _diagnosticSource.BeforeOnPageHandlerExecuting(handlerExecutingContext, filter); + + filter.OnPageHandlerExecuting(handlerExecutingContext); + + _diagnosticSource.AfterOnPageHandlerExecuting(handlerExecutingContext, filter); + + if (handlerExecutingContext.Result != null) + { + // Short-circuited by setting a result. + _logger.PageFilterShortCircuited(filter); + + _handlerExecutedContext = new PageHandlerExecutedContext( + _pageContext, + _filters, + _handler, + _instance) + { + Canceled = true, + Result = _handlerExecutingContext.Result, + }; + + goto case State.PageEnd; + } + + var task = InvokeNextPageFilterAsync(); + if (task.Status != TaskStatus.RanToCompletion) + { + next = State.PageSyncEnd; + return task; + } + + goto case State.PageSyncEnd; + } + + case State.PageSyncEnd: + { + Debug.Assert(state != null); + Debug.Assert(_handlerExecutingContext != null); + Debug.Assert(_handlerExecutedContext != null); + + var filter = (IPageFilter)state; + var handlerExecutedContext = _handlerExecutedContext; + + _diagnosticSource.BeforeOnPageHandlerExecuted(handlerExecutedContext, filter); + + filter.OnPageHandlerExecuted(handlerExecutedContext); + + _diagnosticSource.AfterOnPageHandlerExecuted(handlerExecutedContext, filter); + + goto case State.PageEnd; + } + + case State.PageInside: + { + var task = InvokeHandlerMethodAsync(); + if (task.Status != TaskStatus.RanToCompletion) + { + next = State.PageEnd; + return task; + } + + goto case State.PageEnd; + } + + case State.PageEnd: + { + if (scope == Scope.Page) + { + if (_handlerExecutedContext == null) + { + _handlerExecutedContext = new PageHandlerExecutedContext(_pageContext, _filters, _handler, _instance) + { + Result = _result, + }; + } + + isCompleted = true; + return Task.CompletedTask; + } + + var handlerExecutedContext = _handlerExecutedContext; + Rethrow(handlerExecutedContext); + + if (handlerExecutedContext != null) + { + _result = handlerExecutedContext.Result; + } + + isCompleted = true; + return Task.CompletedTask; + } + + default: + throw new InvalidOperationException(); + } + } + + private async Task InvokeNextPageFilterAsync() + { + try + { + var next = State.PageNext; + var state = (object)null; + var scope = Scope.Page; + var isCompleted = false; + while (!isCompleted) + { + await Next(ref next, ref scope, ref state, ref isCompleted); + } + } + catch (Exception exception) + { + _handlerExecutedContext = new PageHandlerExecutedContext(_pageContext, _filters, _handler, _instance) + { + ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception), + }; + } + + Debug.Assert(_handlerExecutedContext != null); + } + + private async Task InvokeNextPageFilterAwaitedAsync() + { + Debug.Assert(_handlerExecutingContext != null); + if (_handlerExecutingContext.Result != null) + { + // If we get here, it means that an async filter set a result AND called next(). This is forbidden. + var message = Resources.FormatAsyncPageFilter_InvalidShortCircuit( + typeof(IAsyncPageFilter).Name, + nameof(PageHandlerExecutingContext.Result), + typeof(PageHandlerExecutingContext).Name, + typeof(PageHandlerExecutionDelegate).Name); + + throw new InvalidOperationException(message); + } + + await InvokeNextPageFilterAsync(); + + Debug.Assert(_handlerExecutedContext != null); + return _handlerExecutedContext; + } + + private static void Rethrow(PageHandlerExecutedContext 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 @@ -319,6 +677,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private enum State { PageBegin, + PageSelectHandlerBegin, + PageSelectHandlerNext, + PageSelectHandlerAsyncBegin, + PageSelectHandlerAsyncEnd, + PageSelectHandlerSync, + PageSelectHandlerEnd, + PageNext, + PageAsyncBegin, + PageAsyncEnd, + PageSyncBegin, + PageSyncEnd, + PageInside, PageEnd, } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs index c320aedf1a..ddaf39b835 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs @@ -149,6 +149,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var pageContext = new PageContext(actionContext) { ActionDescriptor = cacheEntry.ActionDescriptor, + ValueProviderFactories = new CopyOnWriteList(_valueProviderFactories), ViewData = cacheEntry.ViewDataFactory(_modelMetadataProvider, actionContext.ModelState), ViewStartFactories = cacheEntry.ViewStartFactories.ToList(), }; @@ -159,7 +160,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal _logger, pageContext, filters, - new CopyOnWriteList(_valueProviderFactories), cacheEntry, _parameterBinder, _tempDataFactory, diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageLoggerExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageLoggerExtensions.cs index 516f86f719..ad213fa507 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageLoggerExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageLoggerExtensions.cs @@ -2,11 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections; -using System.Collections.Generic; using System.Diagnostics; -using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal @@ -14,27 +13,23 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal internal static class PageLoggerExtensions { private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; - private static readonly Action _pageExecuting; - private static readonly Action _pageExecuted; - private static readonly Action _exceptionFilterShortCircuit; + private static readonly Action _handlerMethodExecuting; + private static readonly Action _handlerMethodExecuted; private static readonly Action _pageFilterShortCircuit; static PageLoggerExtensions() { - _pageExecuting = LoggerMessage.Define( - LogLevel.Debug, - 1, - "Executing page {ActionName}"); + // These numbers start at 101 intentionally to avoid conflict with the IDs used by ResourceInvoker. - _pageExecuted = LoggerMessage.Define( + _handlerMethodExecuting = LoggerMessage.Define( LogLevel.Information, - 2, - "Executed page {ActionName} in {ElapsedMilliseconds}ms"); + 101, + "Executing handler method {HandlerName} with arguments ({Arguments}) - ModelState is {ValidationState}"); - _exceptionFilterShortCircuit = LoggerMessage.Define( + _handlerMethodExecuted = LoggerMessage.Define( LogLevel.Debug, - 4, - "Request was short circuited at exception filter '{ExceptionFilter}'."); + 102, + "Executed handler method {HandlerName}, returned result {ActionResult}."); _pageFilterShortCircuit = LoggerMessage.Define( LogLevel.Debug, @@ -42,36 +37,39 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal "Request was short circuited at page filter '{PageFilter}'."); } - public static IDisposable PageScope(this ILogger logger, ActionDescriptor actionDescriptor) + public static void ExecutingHandlerMethod(this ILogger logger, PageContext context, HandlerMethodDescriptor handler, object[] arguments) { - Debug.Assert(logger != null); - Debug.Assert(actionDescriptor != null); - - return logger.BeginScope(new PageLogScope(actionDescriptor)); - } - - public static void ExecutingPage(this ILogger logger, ActionDescriptor action) - { - _pageExecuting(logger, action.DisplayName, null); - } - - public static void ExecutedAction(this ILogger logger, ActionDescriptor action, long startTimestamp) - { - // Don't log if logging wasn't enabled at start of request as time will be wildly wrong. - if (logger.IsEnabled(LogLevel.Information) && startTimestamp != 0) + if (logger.IsEnabled(LogLevel.Information)) { - var currentTimestamp = Stopwatch.GetTimestamp(); - var elapsed = new TimeSpan((long)(TimestampToTicks * (currentTimestamp - startTimestamp))); + var handlerName = handler.MethodInfo.Name; - _pageExecuted(logger, action.DisplayName, elapsed.TotalMilliseconds, null); + string[] convertedArguments; + if (arguments == null) + { + convertedArguments = null; + } + else + { + convertedArguments = new string[arguments.Length]; + for (var i = 0; i < arguments.Length; i++) + { + convertedArguments[i] = Convert.ToString(arguments[i]); + } + } + + var validationState = context.ModelState.ValidationState; + + _handlerMethodExecuting(logger, handlerName, convertedArguments, validationState, null); } } - public static void ExceptionFilterShortCircuited( - this ILogger logger, - IFilterMetadata filter) + public static void ExecutedHandlerMethod(this ILogger logger, PageContext context, HandlerMethodDescriptor handler, IActionResult result) { - _exceptionFilterShortCircuit(logger, filter, null); + if (logger.IsEnabled(LogLevel.Debug)) + { + var handlerName = handler.MethodInfo.Name; + _handlerMethodExecuted(logger, handlerName, Convert.ToString(result), null); + } } public static void PageFilterShortCircuited( @@ -80,50 +78,5 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { _pageFilterShortCircuit(logger, filter, null); } - - private class PageLogScope : IReadOnlyList> - { - private readonly ActionDescriptor _action; - - public PageLogScope(ActionDescriptor action) - { - _action = action; - } - - public KeyValuePair this[int index] - { - get - { - if (index == 0) - { - return new KeyValuePair("ActionId", _action.Id); - } - else if (index == 1) - { - return new KeyValuePair("PageName", _action.DisplayName); - } - throw new IndexOutOfRangeException(nameof(index)); - } - } - - public int Count => 2; - - public IEnumerator> GetEnumerator() - { - for (var i = 0; i < Count; ++i) - { - yield return this[i]; - } - } - - public override string ToString() - { - // We don't include the _action.Id here because it's just an opaque guid, and if - // you have text logging, you can already use the requestId for correlation. - return _action.DisplayName; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj b/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj index 883f50a639..8b5913ca2a 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj @@ -18,4 +18,8 @@ + + + + diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageActionDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageActionDescriptor.cs index 19650a12ad..cfc631dae1 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageActionDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageActionDescriptor.cs @@ -1,6 +1,7 @@ // 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 System.Diagnostics; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -35,5 +36,29 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages /// Gets or sets the path relative to the base path for page discovery. /// public string ViewEnginePath { get; set; } + + /// + public override string DisplayName + { + get + { + if (base.DisplayName == null && ViewEnginePath != null) + { + base.DisplayName = ViewEnginePath; + } + + return base.DisplayName; + } + + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + base.DisplayName = value; + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs index 97366d9018..f3559d62e7 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs @@ -122,6 +122,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages internal static string FormatPathMustBeAnAppRelativePath() => GetString("PathMustBeAnAppRelativePath"); + /// + /// If an {0} provides a result value by setting the {1} property of {2} to a non-null value, then it cannot call the next filter by invoking {3}. + /// + internal static string AsyncPageFilter_InvalidShortCircuit + { + get => GetString("AsyncPageFilter_InvalidShortCircuit"); + } + + /// + /// If an {0} provides a result value by setting the {1} property of {2} to a non-null value, then it cannot call the next filter by invoking {3}. + /// + internal static string FormatAsyncPageFilter_InvalidShortCircuit(object p0, object p1, object p2, object p3) + => string.Format(CultureInfo.CurrentCulture, GetString("AsyncPageFilter_InvalidShortCircuit"), p0, p1, p2, p3); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx index dfcbcac906..f85bb00639 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx @@ -141,4 +141,7 @@ Path must be an application relative path that starts with a forward slash '/'. + + If an {0} provides a result value by setting the {1} property of {2} to a non-null value, then it cannot call the next filter by invoking {3}. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs index c74d590e56..7ca7972864 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs @@ -2,15 +2,14 @@ // 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; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -22,10 +21,11 @@ using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging.Testing; using Moq; using Xunit; @@ -33,6 +33,1058 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public class PageActionInvokerTest : CommonResourceInvokerTest { + #region Diagnostics + + [Fact] + public async Task Invoke_Success_LogsCorrectValues() + { + // Arrange + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var logger = loggerFactory.CreateLogger(); + + var actionDescriptor = CreateDescriptorForSimplePage(); + + var displayName = "/A/B/C"; + actionDescriptor.DisplayName = displayName; + + var invoker = CreateInvoker(filters: null, actionDescriptor: actionDescriptor, logger: logger); + + // Act + await invoker.InvokeAsync(); + + // Assert + Assert.Single(sink.Scopes); + Assert.Equal(displayName, sink.Scopes[0].Scope?.ToString()); + + Assert.Equal(4, sink.Writes.Count); + Assert.Equal($"Executing action {displayName}", sink.Writes[0].State?.ToString()); + Assert.Equal($"Executing handler method OnGetHandler1 with arguments ((null)) - ModelState is Valid", sink.Writes[1].State?.ToString()); + Assert.Equal($"Executed handler method OnGetHandler1, returned result {typeof(PageResult).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()); + } + + [Fact] + public async Task Invoke_WritesDiagnostic_ActionSelected() + { + // Arrange + var actionDescriptor = CreateDescriptorForSimplePage(); + var displayName = actionDescriptor.DisplayName; + + var routeData = new RouteData(); + routeData.Values.Add("tag", "value"); + + var listener = new TestDiagnosticListener(); + + var invoker = CreateInvoker(filters: null, actionDescriptor: actionDescriptor, listener: listener, routeData: routeData); + + // Act + await invoker.InvokeAsync(); + + // Assert + Assert.NotNull(listener.BeforeAction?.ActionDescriptor); + Assert.NotNull(listener.BeforeAction?.HttpContext); + + var routeValues = listener.BeforeAction?.RouteData?.Values; + Assert.NotNull(routeValues); + + Assert.Equal(1, routeValues.Count); + Assert.Contains(routeValues, kvp => kvp.Key == "tag" && string.Equals(kvp.Value, "value")); + } + + [Fact] + public async Task Invoke_WritesDiagnostic_ActionInvoked() + { + // Arrange + var actionDescriptor = CreateDescriptorForSimplePage(); + var displayName = actionDescriptor.DisplayName; + + var routeData = new RouteData(); + routeData.Values.Add("tag", "value"); + + var listener = new TestDiagnosticListener(); + + var invoker = CreateInvoker(filters: null, actionDescriptor: actionDescriptor, listener: listener, routeData: routeData); + + // Act + await invoker.InvokeAsync(); + + // Assert + Assert.NotNull(listener.AfterAction?.ActionDescriptor); + Assert.NotNull(listener.AfterAction?.HttpContext); + } + + #endregion + + #region Page Context + + [Fact] + public async Task AddingValueProviderFactory_AtResourceFilter_IsAvailableInPageContext() + { + // Arrange + var valueProviderFactory2 = Mock.Of(); + var resourceFilter = new Mock(); + resourceFilter + .Setup(f => f.OnResourceExecuting(It.IsAny())) + .Callback((resourceExecutingContext) => + { + resourceExecutingContext.ValueProviderFactories.Add(valueProviderFactory2); + }); + var valueProviderFactory1 = Mock.Of(); + var valueProviderFactories = new List(); + valueProviderFactories.Add(valueProviderFactory1); + + var invoker = CreateInvoker( + new IFilterMetadata[] { resourceFilter.Object }, valueProviderFactories: valueProviderFactories); + + // Act + await invoker.InvokeAsync(); + + // Assert + var pageContext = Assert.IsType(invoker).PageContext; + Assert.NotNull(pageContext); + Assert.Equal(2, pageContext.ValueProviderFactories.Count); + Assert.Same(valueProviderFactory1, pageContext.ValueProviderFactories[0]); + Assert.Same(valueProviderFactory2, pageContext.ValueProviderFactories[1]); + } + + [Fact] + public async Task DeletingValueProviderFactory_AtResourceFilter_IsNotAvailableInPageContext() + { + // Arrange + var resourceFilter = new Mock(); + resourceFilter + .Setup(f => f.OnResourceExecuting(It.IsAny())) + .Callback((resourceExecutingContext) => + { + resourceExecutingContext.ValueProviderFactories.RemoveAt(0); + }); + + var valueProviderFactory1 = Mock.Of(); + var valueProviderFactory2 = Mock.Of(); + var valueProviderFactories = new List(); + valueProviderFactories.Add(valueProviderFactory1); + valueProviderFactories.Add(valueProviderFactory2); + + var invoker = CreateInvoker( + new IFilterMetadata[] { resourceFilter.Object }, valueProviderFactories: valueProviderFactories); + + // Act + await invoker.InvokeAsync(); + + // Assert + var pageContext = Assert.IsType(invoker).PageContext; + Assert.NotNull(pageContext); + Assert.Equal(1, pageContext.ValueProviderFactories.Count); + Assert.Same(valueProviderFactory2, pageContext.ValueProviderFactories[0]); + } + + #endregion + + #region Page vs PageModel + + [Fact] + public async Task InvokeAction_WithSimplePage_FlowsRightValues() + { + // Arrange + object instance = null; + IActionResult result = null; + + var pageFilter = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter); + pageFilter + .Setup(f => f.OnPageHandlerExecuting(It.IsAny())) + .Callback(c => + { + instance = c.HandlerInstance; + }); + pageFilter + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Callback(c => + { + Assert.Same(instance, c.HandlerInstance); + }); + + var resultFilter = new Mock(MockBehavior.Strict); + resultFilter + .Setup(f => f.OnResultExecuting(It.IsAny())) + .Callback(c => + { + Assert.Same(instance, c.Controller); + result = c.Result; + }); + resultFilter + .Setup(f => f.OnResultExecuted(It.IsAny())) + .Callback(c => + { + Assert.Same(instance, c.Controller); + Assert.Same(result, c.Result); + }); + + var filters = new IFilterMetadata[] { pageFilter.Object, resultFilter.Object }; + + var invoker = CreateInvoker(filters, CreateDescriptorForSimplePage()); + + // Act + await invoker.InvokeAsync(); + + // Assert + var page = Assert.IsType(instance); + Assert.IsType>(page.ViewContext.ViewData); + Assert.Same(page, page.ViewContext.ViewData.Model); + + var pageResult = Assert.IsType(result); + Assert.Same(page, pageResult.Page); + Assert.Same(page, pageResult.Model); + Assert.Same(page.ViewContext.ViewData, pageResult.ViewData); + } + + [Fact] + public async Task InvokeAction_WithSimplePageWithPocoModel_FlowsRightValues() + { + // Arrange + object instance = null; + IActionResult result = null; + + var pageFilter = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter); + pageFilter + .Setup(f => f.OnPageHandlerExecuting(It.IsAny())) + .Callback(c => + { + instance = c.HandlerInstance; + }); + pageFilter + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Callback(c => + { + Assert.Same(instance, c.HandlerInstance); + }); + + var resultFilter = new Mock(MockBehavior.Strict); + resultFilter + .Setup(f => f.OnResultExecuting(It.IsAny())) + .Callback(c => + { + Assert.Same(instance, c.Controller); + result = c.Result; + }); + resultFilter + .Setup(f => f.OnResultExecuted(It.IsAny())) + .Callback(c => + { + Assert.Same(instance, c.Controller); + Assert.Same(result, c.Result); + }); + + var filters = new IFilterMetadata[] { pageFilter.Object, resultFilter.Object }; + + var invoker = CreateInvoker(filters, CreateDescriptorForSimplePageWithPocoModel()); + + // Act + await invoker.InvokeAsync(); + + // Assert + var page = Assert.IsType(instance); + Assert.IsType>(page.PageContext.ViewData); + Assert.Null(page.PageContext.ViewData.Model); + + var pageResult = Assert.IsType(result); + Assert.Same(page, pageResult.Page); + Assert.Null(pageResult.Model); + Assert.Same(page.ViewContext.ViewData, pageResult.ViewData); + + } + + [Fact] + public async Task InvokeAction_WithPageModel_FlowsRightValues() + { + // Arrange + object instance = null; + IActionResult result = null; + + var pageFilter = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter); + pageFilter + .Setup(f => f.OnPageHandlerExecuting(It.IsAny())) + .Callback(c => + { + instance = c.HandlerInstance; + }); + pageFilter + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Callback(c => + { + Assert.Same(instance, c.HandlerInstance); + }); + + var resultFilter = new Mock(MockBehavior.Strict); + resultFilter + .Setup(f => f.OnResultExecuting(It.IsAny())) + .Callback(c => + { + Assert.Same(instance, c.Controller); + result = c.Result; + }); + resultFilter + .Setup(f => f.OnResultExecuted(It.IsAny())) + .Callback(c => + { + Assert.Same(instance, c.Controller); + Assert.Same(result, c.Result); + }); + + var filters = new IFilterMetadata[] { pageFilter.Object, resultFilter.Object }; + + var invoker = CreateInvoker( + filters, + CreateDescriptorForPageModelPage(), + modelFactory: context => new TestPageModel() { PageContext = context }); + + // Act + await invoker.InvokeAsync(); + + // Assert + var pageModel = Assert.IsType(instance); + Assert.IsType>(pageModel.PageContext.ViewData); + Assert.Same(pageModel, pageModel.ViewData.Model); + + var pageResult = Assert.IsType(result); + Assert.IsType(pageResult.Page); + Assert.Same(pageModel, pageResult.Model); + Assert.Same(pageModel.PageContext.ViewData, pageResult.ViewData); + } + + #endregion + + #region Handler Selection + + [Fact] + public async Task InvokeAction_InvokesPageFilter_CanModifySelectedHandler() + { + // Arrange + HandlerMethodDescriptor handler = null; + + var filter1 = new Mock(MockBehavior.Strict); + filter1 + .Setup(f => f.OnPageHandlerSelected(It.IsAny())) + .Callback(c => + { + handler = c.HandlerMethod = c.ActionDescriptor.HandlerMethods[1]; + }) + .Verifiable(); + filter1 + .Setup(f => f.OnPageHandlerExecuting(It.IsAny())) + .Callback(c => Assert.Same(handler, c.HandlerMethod)) + .Verifiable(); + filter1 + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Callback(c => Assert.Same(handler, c.HandlerMethod)) + .Verifiable(); + + var filter2 = new Mock(MockBehavior.Strict); + filter2 + .Setup(f => f.OnPageHandlerSelected(It.IsAny())) + .Callback(c => Assert.Same(handler, c.HandlerMethod)) + .Verifiable(); + filter2 + .Setup(f => f.OnPageHandlerExecuting(It.IsAny())) + .Callback(c => Assert.Same(handler, c.HandlerMethod)) + .Verifiable(); + filter2 + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Callback(c => Assert.Same(handler, c.HandlerMethod)) + .Verifiable(); + + var filters = new IFilterMetadata[] { filter1.Object, filter2.Object }; + + var invoker = CreateInvoker(filters, actionDescriptor: CreateDescriptorForSimplePage()); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter1.Verify(f => f.OnPageHandlerSelected(It.IsAny()), Times.Once()); + filter1.Verify(f => f.OnPageHandlerExecuting(It.IsAny()), Times.Once()); + filter1.Verify(f => f.OnPageHandlerExecuted(It.IsAny()), Times.Once()); + + filter2.Verify(f => f.OnPageHandlerSelected(It.IsAny()), Times.Once()); + filter2.Verify(f => f.OnPageHandlerExecuting(It.IsAny()), Times.Once()); + filter2.Verify(f => f.OnPageHandlerExecuted(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncPageFilter_CanModifySelectedHandler() + { + // Arrange + HandlerMethodDescriptor handler = null; + + var filter1 = new Mock(MockBehavior.Strict); + filter1 + .Setup(f => f.OnPageHandlerSelectionAsync(It.IsAny())) + .Callback(c => + { + handler = c.HandlerMethod = c.ActionDescriptor.HandlerMethods[1]; + }) + .Returns(Task.CompletedTask) + .Verifiable(); + filter1 + .Setup(f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async(c, next) => + { + Assert.Same(handler, c.HandlerMethod); + await next(); + }) + .Verifiable(); + + var filter2 = new Mock(MockBehavior.Strict); + filter2 + .Setup(f => f.OnPageHandlerSelectionAsync(It.IsAny())) + .Callback(c => Assert.Same(handler, c.HandlerMethod)) + .Returns(Task.CompletedTask) + .Verifiable(); + filter2 + .Setup(f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (c, next) => + { + Assert.Same(handler, c.HandlerMethod); + await next(); + }) + .Verifiable(); + + var filters = new IFilterMetadata[] { filter1.Object, filter2.Object }; + + var invoker = CreateInvoker(filters, actionDescriptor: CreateDescriptorForSimplePage()); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter1.Verify(f => f.OnPageHandlerSelectionAsync(It.IsAny()), Times.Once()); + filter1.Verify(f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny()), Times.Once()); + + filter2.Verify(f => f.OnPageHandlerSelectionAsync(It.IsAny()), Times.Once()); + filter2.Verify(f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny()), Times.Once()); + } + + #endregion + + #region Page Filters + + [Fact] + public async Task InvokeAction_InvokesPageFilter() + { + // Arrange + IActionResult result = null; + + var filter = new Mock(MockBehavior.Strict); + AllowSelector(filter); + filter.Setup(f => f.OnPageHandlerExecuting(It.IsAny())).Verifiable(); + filter + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Callback(c => result = c.Result) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object, result: Result); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter.Verify(f => f.OnPageHandlerExecuting(It.IsAny()), Times.Once()); + filter.Verify(f => f.OnPageHandlerExecuted(It.IsAny()), Times.Once()); + + Assert.Same(Result, result); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncPageFilter() + { + // Arrange + IActionResult result = null; + + var filter = new Mock(MockBehavior.Strict); + AllowSelector(filter); + filter + .Setup(f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (context, next) => + { + var resultContext = await next(); + result = resultContext.Result; + }) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object, result: Result); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter.Verify( + f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + + Assert.Same(Result, result); + } + + [Fact] + public async Task InvokeAction_InvokesPageFilter_ShortCircuit() + { + // Arrange + var result = new Mock(MockBehavior.Strict); + result + .Setup(r => r.ExecuteResultAsync(It.IsAny())) + .Returns(Task.FromResult(true)) + .Verifiable(); + + PageHandlerExecutedContext context = null; + + var pageFilter1 = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter1); + pageFilter1.Setup(f => f.OnPageHandlerExecuting(It.IsAny())).Verifiable(); + pageFilter1 + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Callback(c => context = c) + .Verifiable(); + + var pageFilter2 = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter2); + pageFilter2 + .Setup(f => f.OnPageHandlerExecuting(It.IsAny())) + .Callback(c => c.Result = result.Object) + .Verifiable(); + + var pageFilter3 = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter3); + + var resultFilter = new Mock(MockBehavior.Strict); + resultFilter.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); + resultFilter.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); + + var invoker = CreateInvoker(new IFilterMetadata[] + { + pageFilter1.Object, + pageFilter2.Object, + pageFilter3.Object, + resultFilter.Object, + }); + + // Act + await invoker.InvokeAsync(); + + // Assert + result.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); + pageFilter1.Verify(f => f.OnPageHandlerExecuting(It.IsAny()), Times.Once()); + pageFilter1.Verify(f => f.OnPageHandlerExecuted(It.IsAny()), Times.Once()); + + pageFilter2.Verify(f => f.OnPageHandlerExecuting(It.IsAny()), Times.Once()); + pageFilter2.Verify(f => f.OnPageHandlerExecuted(It.IsAny()), Times.Never()); + + resultFilter.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + resultFilter.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + + Assert.True(context.Canceled); + Assert.Same(context.Result, result.Object); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncPageFilter_ShortCircuit_WithResult() + { + // Arrange + var result = new Mock(MockBehavior.Strict); + result + .Setup(r => r.ExecuteResultAsync(It.IsAny())) + .Returns(Task.FromResult(true)) + .Verifiable(); + + PageHandlerExecutedContext context = null; + + var pageFilter1 = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter1); + pageFilter1.Setup(f => f.OnPageHandlerExecuting(It.IsAny())).Verifiable(); + pageFilter1 + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Callback(c => context = c) + .Verifiable(); + + var pageFilter2 = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter2); + pageFilter2 + .Setup(f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny())) + .Returns((c, next) => + { + // Notice we're not calling next + c.Result = result.Object; + return Task.FromResult(true); + }) + .Verifiable(); + + var pageFilter3 = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter3); + + var resultFilter1 = new Mock(MockBehavior.Strict); + resultFilter1.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); + resultFilter1.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); + var resultFilter2 = new Mock(MockBehavior.Strict); + resultFilter2.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); + resultFilter2.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); + + var invoker = CreateInvoker(new IFilterMetadata[] + { + pageFilter1.Object, + pageFilter2.Object, + pageFilter3.Object, + resultFilter1.Object, + resultFilter2.Object, + }); + + // Act + await invoker.InvokeAsync(); + + // Assert + result.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); + pageFilter1.Verify(f => f.OnPageHandlerExecuting(It.IsAny()), Times.Once()); + pageFilter1.Verify(f => f.OnPageHandlerExecuted(It.IsAny()), Times.Once()); + + pageFilter2.Verify( + f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + + resultFilter1.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + resultFilter1.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + resultFilter2.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + resultFilter2.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + + Assert.True(context.Canceled); + Assert.Same(context.Result, result.Object); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncPageFilter_ShortCircuit_WithoutResult() + { + // Arrange + PageHandlerExecutedContext context = null; + + var pageFilter1 = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter1); + pageFilter1.Setup(f => f.OnPageHandlerExecuting(It.IsAny())).Verifiable(); + pageFilter1 + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Callback(c => context = c) + .Verifiable(); + + var pageFilter2 = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter2); + pageFilter2 + .Setup(f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny())) + .Returns((c, next) => + { + // Notice we're not calling next + return Task.FromResult(true); + }) + .Verifiable(); + + var pageFilter3 = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter3); + + var resultFilter = new Mock(MockBehavior.Strict); + resultFilter.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); + resultFilter.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); + + var invoker = CreateInvoker(new IFilterMetadata[] + { + pageFilter1.Object, + pageFilter2.Object, + pageFilter3.Object, + resultFilter.Object, + }); + + // Act + await invoker.InvokeAsync(); + + // Assert + pageFilter1.Verify(f => f.OnPageHandlerExecuting(It.IsAny()), Times.Once()); + pageFilter1.Verify(f => f.OnPageHandlerExecuted(It.IsAny()), Times.Once()); + + pageFilter2.Verify( + f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + + resultFilter.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + resultFilter.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + + Assert.True(context.Canceled); + Assert.Null(context.Result); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncPageFilter_ShortCircuit_WithResult_CallNext() + { + // Arrange + var pageFilter = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter); + pageFilter + .Setup(f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (c, next) => + { + c.Result = new EmptyResult(); + await next(); + }) + .Verifiable(); + + var message = + "If an IAsyncPageFilter provides a result value by setting the Result property of " + + "PageHandlerExecutingContext to a non-null value, then it cannot call the next filter by invoking " + + "PageHandlerExecutionDelegate."; + + var invoker = CreateInvoker(pageFilter.Object); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => invoker.InvokeAsync(), + message); + } + + [Fact] + public async Task InvokeAction_InvokesPageFilter_WithExceptionThrownByAction() + { + // Arrange + PageHandlerExecutedContext context = null; + + var filter = new Mock(MockBehavior.Strict); + AllowSelector(filter); + filter.Setup(f => f.OnPageHandlerExecuting(It.IsAny())).Verifiable(); + filter + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Callback(c => + { + context = c; + + // Handle the exception so the test doesn't throw. + Assert.Same(Exception, c.Exception); + Assert.False(c.ExceptionHandled); + c.ExceptionHandled = true; + }) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object, exception: Exception); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter.Verify(f => f.OnPageHandlerExecuting(It.IsAny()), Times.Once()); + filter.Verify(f => f.OnPageHandlerExecuted(It.IsAny()), Times.Once()); + + Assert.Same(Exception, context.Exception); + Assert.Null(context.Result); + } + + [Fact] + public async Task InvokeAction_InvokesPageFilter_WithExceptionThrownByPageFilter() + { + // Arrange + var exception = new DataMisalignedException(); + PageHandlerExecutedContext context = null; + + var filter1 = new Mock(MockBehavior.Strict); + AllowSelector(filter1); + filter1.Setup(f => f.OnPageHandlerExecuting(It.IsAny())).Verifiable(); + filter1 + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Callback(c => + { + context = c; + + // Handle the exception so the test doesn't throw. + Assert.False(c.ExceptionHandled); + c.ExceptionHandled = true; + }) + .Verifiable(); + + var filter2 = new Mock(MockBehavior.Strict); + AllowSelector(filter2); + filter2 + .Setup(f => f.OnPageHandlerExecuting(It.IsAny())) + .Callback(c => { throw exception; }) + .Verifiable(); + + var invoker = CreateInvoker(new[] { filter1.Object, filter2.Object }); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter1.Verify(f => f.OnPageHandlerExecuting(It.IsAny()), Times.Once()); + filter1.Verify(f => f.OnPageHandlerExecuted(It.IsAny()), Times.Once()); + + filter2.Verify(f => f.OnPageHandlerExecuting(It.IsAny()), Times.Once()); + filter2.Verify(f => f.OnPageHandlerExecuted(It.IsAny()), Times.Never()); + + Assert.Same(exception, context.Exception); + Assert.Null(context.Result); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncPageFilter_WithExceptionThrownByPageFilter() + { + // Arrange + var exception = new DataMisalignedException(); + PageHandlerExecutedContext context = null; + + var filter1 = new Mock(MockBehavior.Strict); + AllowSelector(filter1); + filter1 + .Setup(f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (c, next) => + { + context = await next(); + + // Handle the exception so the test doesn't throw. + Assert.False(context.ExceptionHandled); + context.ExceptionHandled = true; + }) + .Verifiable(); + + var filter2 = new Mock(MockBehavior.Strict); + AllowSelector(filter2); + filter2.Setup(f => f.OnPageHandlerExecuting(It.IsAny())).Verifiable(); + filter2 + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Callback(c => { throw exception; }) + .Verifiable(); + + var invoker = CreateInvoker(new IFilterMetadata[] { filter1.Object, filter2.Object }); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter1.Verify( + f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + + filter2.Verify(f => f.OnPageHandlerExecuting(It.IsAny()), Times.Once()); + + Assert.Same(exception, context.Exception); + Assert.Null(context.Result); + } + + [Fact] + public async Task InvokeAction_InvokesPageFilter_HandleException() + { + // Arrange + var result = new Mock(MockBehavior.Strict); + result + .Setup(r => r.ExecuteResultAsync(It.IsAny())) + .Returns((context) => Task.FromResult(true)) + .Verifiable(); + + var pageFilter = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter); + pageFilter.Setup(f => f.OnPageHandlerExecuting(It.IsAny())).Verifiable(); + pageFilter + .Setup(f => f.OnPageHandlerExecuted(It.IsAny())) + .Callback(c => + { + // Handle the exception so the test doesn't throw. + Assert.False(c.ExceptionHandled); + c.ExceptionHandled = true; + + c.Result = result.Object; + }) + .Verifiable(); + + var resultFilter = new Mock(MockBehavior.Strict); + resultFilter.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); + resultFilter.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); + + var invoker = CreateInvoker( + new IFilterMetadata[] { pageFilter.Object, resultFilter.Object }, + exception: Exception); + + // Act + await invoker.InvokeAsync(); + + // Assert + pageFilter.Verify(f => f.OnPageHandlerExecuting(It.IsAny()), Times.Once()); + pageFilter.Verify(f => f.OnPageHandlerExecuted(It.IsAny()), Times.Once()); + + resultFilter.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + resultFilter.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + + result.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncResourceFilter_WithActionResult_FromPageFilter() + { + // Arrange + var expected = Mock.Of(); + + ResourceExecutedContext context = null; + var resourceFilter = new Mock(MockBehavior.Strict); + resourceFilter + .Setup(f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (c, next) => + { + context = await next(); + }) + .Verifiable(); + + var pageFilter = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter); + pageFilter + .Setup(f => f.OnPageHandlerExecuting(It.IsAny())) + .Callback((c) => + { + c.Result = expected; + }); + + var invoker = CreateInvoker(new IFilterMetadata[] { resourceFilter.Object, pageFilter.Object }); + + // Act + await invoker.InvokeAsync(); + + // Assert + Assert.Same(expected, context.Result); + + resourceFilter.Verify( + f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncResourceFilter_HandleException_FromPageFilter() + { + // Arrange + var expected = new DataMisalignedException(); + + ResourceExecutedContext context = null; + var resourceFilter = new Mock(MockBehavior.Strict); + resourceFilter + .Setup(f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (c, next) => + { + context = await next(); + context.ExceptionHandled = true; + }) + .Verifiable(); + + var pageFilter = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter); + pageFilter + .Setup(f => f.OnPageHandlerExecuting(It.IsAny())) + .Callback((c) => + { + throw expected; + }); + + var invoker = CreateInvoker(new IFilterMetadata[] { resourceFilter.Object, pageFilter.Object }); + + // Act + await invoker.InvokeAsync(); + + // Assert + Assert.Same(expected, context.Exception); + Assert.Same(expected, context.ExceptionDispatchInfo.SourceException); + + resourceFilter.Verify( + f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncResourceFilter_HandlesException_FromExceptionFilter() + { + // Arrange + var expected = new DataMisalignedException(); + + ResourceExecutedContext context = null; + var resourceFilter = new Mock(MockBehavior.Strict); + resourceFilter + .Setup(f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (c, next) => + { + context = await next(); + context.ExceptionHandled = true; + }) + .Verifiable(); + + var exceptionFilter = new Mock(MockBehavior.Strict); + exceptionFilter + .Setup(f => f.OnException(It.IsAny())) + .Callback((c) => + { + throw expected; + }); + + var invoker = CreateInvoker(new IFilterMetadata[] { resourceFilter.Object, exceptionFilter.Object }, exception: Exception); + + // Act + await invoker.InvokeAsync(); + + // Assert + Assert.Same(expected, context.Exception); + Assert.Same(expected, context.ExceptionDispatchInfo.SourceException); + + resourceFilter.Verify( + f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task InvokeAction_ExceptionBubbling_AsyncPageFilter_To_ResourceFilter() + { + // Arrange + var resourceFilter = new Mock(MockBehavior.Strict); + resourceFilter + .Setup(f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (c, next) => + { + var context = await next(); + Assert.Same(Exception, context.Exception); + context.ExceptionHandled = true; + }) + .Verifiable(); + + var pageFilter1 = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter1); + pageFilter1 + .Setup(f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (c, next) => + { + await next(); + }); + + var pageFilter2 = new Mock(MockBehavior.Strict); + AllowSelector(pageFilter2); + pageFilter2 + .Setup(f => f.OnPageHandlerExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (c, next) => + { + await next(); + }); + + var invoker = CreateInvoker( + new IFilterMetadata[] + { + resourceFilter.Object, + pageFilter1.Object, + pageFilter2.Object, + }, + // The action won't run + exception: Exception); + + // Act & Assert + await invoker.InvokeAsync(); + + resourceFilter.Verify(f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny()), Times.Once()); + } + + #endregion + protected override ResourceInvoker CreateInvoker( IFilterMetadata[] filters, Exception exception = null, @@ -47,6 +1099,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal HandlerTypeInfo = typeof(TestPage).GetTypeInfo(), ModelTypeInfo = typeof(TestPage).GetTypeInfo(), PageTypeInfo = typeof(TestPage).GetTypeInfo(), + BoundProperties = new List(), }; var handlers = new List>>(); @@ -69,81 +1122,47 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal }); } - var executor = new TestPageResultExecutor(); return CreateInvoker( filters, actionDescriptor, - executor, - handlers: handlers.ToArray()); + handlers: handlers.ToArray(), + valueProviderFactories: valueProviderFactories); } private PageActionInvoker CreateInvoker( IFilterMetadata[] filters, CompiledPageActionDescriptor actionDescriptor, - PageResultExecutor executor = null, - PageActionInvokerCacheEntry cacheEntry = null, + Func modelFactory = null, ITempDataDictionaryFactory tempDataFactory = null, IList valueProviderFactories = null, Func>[] handlers = null, RouteData routeData = null, - ILogger logger = null) + ILogger logger = null, + TestDiagnosticListener listener = null) { - var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore"); + var diagnosticListener = new DiagnosticListener("Microsoft.AspNetCore"); + if (listener != null) + { + diagnosticListener.SubscribeWithAdapter(listener); + } var httpContext = new DefaultHttpContext(); - var serviceCollection = new ServiceCollection(); - if (executor == null) - { - executor = new PageResultExecutor( - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - diagnosticSource, - HtmlEncoder.Default); - } + var services = new ServiceCollection(); + services.AddSingleton(); + httpContext.RequestServices = services.BuildServiceProvider(); - 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(); - - if (routeData == null) - { - routeData = new RouteData(); - } - - var actionContext = new ActionContext( - httpContext: httpContext, - routeData: routeData, - actionDescriptor: actionDescriptor); - var pageContext = new PageContext(actionContext) + var pageContext = new PageContext() { ActionDescriptor = actionDescriptor, + HttpContext = httpContext, + RouteData = routeData ?? new RouteData(), + ValueProviderFactories = valueProviderFactories?.ToList() ?? new List(), + ViewStartFactories = new List>(), }; var viewDataFactory = ViewDataDictionaryFactory.CreateFactory(actionDescriptor.ModelTypeInfo); pageContext.ViewData = viewDataFactory(new EmptyModelMetadataProvider(), pageContext.ModelState); - if (valueProviderFactories == null) - { - valueProviderFactories = new List(); - } - - if (logger == null) - { - logger = NullLogger.Instance; - } - if (tempDataFactory == null) { tempDataFactory = Mock.Of(m => m.GetTempData(It.IsAny()) == Mock.Of()); @@ -153,15 +1172,30 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { var instance = (Page)Activator.CreateInstance(actionDescriptor.PageTypeInfo.AsType()); instance.PageContext = context; + instance.ViewContext = viewContext; return instance; }; - cacheEntry = new PageActionInvokerCacheEntry( + if (handlers == null) + { + handlers = new Func>[actionDescriptor.HandlerMethods.Count]; + for (var i = 0; i < handlers.Length; i++) + { + handlers[i] = (obj, args) => Task.FromResult(new PageResult()); + } + } + + if (modelFactory == null) + { + modelFactory = _ => Activator.CreateInstance(actionDescriptor.ModelTypeInfo.AsType()); + } + + var cacheEntry = new PageActionInvokerCacheEntry( actionDescriptor, viewDataFactory, pageFactory, (c, viewContext, page) => { (page as IDisposable)?.Dispose(); }, - _ => Activator.CreateInstance(actionDescriptor.ModelTypeInfo.AsType()), + modelFactory, (c, model) => { (model as IDisposable)?.Dispose(); }, null, handlers, @@ -173,14 +1207,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal selector .Setup(s => s.Select(It.IsAny())) .Returns(c => c.ActionDescriptor.HandlerMethods.FirstOrDefault()); - + var invoker = new PageActionInvoker( selector.Object, - diagnosticSource, - logger, + diagnosticListener ?? new DiagnosticListener("Microsoft.AspNetCore"), + logger ?? NullLogger.Instance, pageContext, - filters, - valueProviderFactories.ToArray(), + filters ?? Array.Empty(), cacheEntry, GetParameterBinder(), tempDataFactory, @@ -220,6 +1253,100 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal return mockValidator.Object; } + private CompiledPageActionDescriptor CreateDescriptorForSimplePage() + { + return new CompiledPageActionDescriptor() + { + HandlerTypeInfo = typeof(TestPage).GetTypeInfo(), + ModelTypeInfo = typeof(TestPage).GetTypeInfo(), + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + + BoundProperties = new List(), + + HandlerMethods = new HandlerMethodDescriptor[] + { + new HandlerMethodDescriptor() + { + HttpMethod = "GET", + MethodInfo = typeof(TestPage).GetTypeInfo().GetMethod(nameof(TestPage.OnGetHandler1)), + Parameters = new List(), + }, + new HandlerMethodDescriptor() + { + HttpMethod = "GET", + MethodInfo = typeof(TestPage).GetTypeInfo().GetMethod(nameof(TestPage.OnGetHandler2)), + Parameters = new List(), + }, + }, + }; + } + + private CompiledPageActionDescriptor CreateDescriptorForSimplePageWithPocoModel() + { + return new CompiledPageActionDescriptor() + { + HandlerTypeInfo = typeof(TestPage).GetTypeInfo(), + ModelTypeInfo = typeof(PocoModel).GetTypeInfo(), + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + + BoundProperties = new List(), + + HandlerMethods = new HandlerMethodDescriptor[] + { + new HandlerMethodDescriptor() + { + HttpMethod = "GET", + MethodInfo = typeof(TestPage).GetTypeInfo().GetMethod(nameof(TestPage.OnGetHandler1)), + Parameters = new List(), + }, + new HandlerMethodDescriptor() + { + HttpMethod = "GET", + MethodInfo = typeof(TestPage).GetTypeInfo().GetMethod(nameof(TestPage.OnGetHandler2)), + Parameters = new List(), + }, + }, + }; + } + + private CompiledPageActionDescriptor CreateDescriptorForPageModelPage() + { + return new CompiledPageActionDescriptor() + { + HandlerTypeInfo = typeof(TestPageModel).GetTypeInfo(), + ModelTypeInfo = typeof(TestPageModel).GetTypeInfo(), + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + + BoundProperties = new List(), + + HandlerMethods = new HandlerMethodDescriptor[] + { + new HandlerMethodDescriptor() + { + HttpMethod = "GET", + MethodInfo = typeof(PageModel).GetTypeInfo().GetMethod(nameof(TestPageModel.OnGetHandler1)), + Parameters = new List(), + }, + new HandlerMethodDescriptor() + { + HttpMethod = "GET", + MethodInfo = typeof(PageModel).GetTypeInfo().GetMethod(nameof(TestPageModel.OnGetHandler2)), + Parameters = new List(), + }, + }, + }; + } + + private void AllowSelector(Mock filter) + { + filter.Setup(f => f.OnPageHandlerSelected(It.IsAny())); + } + + private void AllowSelector(Mock filter) + { + filter.Setup(f => f.OnPageHandlerSelectionAsync(It.IsAny())).Returns(Task.CompletedTask); + } + private class TestPageResultExecutor : PageResultExecutor { private readonly Func _executeAction; @@ -247,12 +1374,35 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } } + private class PocoModel + { + } + private class TestPage : Page { + public void OnGetHandler1() + { + } + + public void OnGetHandler2() + { + } + public override Task ExecuteAsync() { throw new NotImplementedException(); } } + + private class TestPageModel : PageModel + { + public void OnGetHandler1() + { + } + + public void OnGetHandler2() + { + } + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj index 74c39bd7a5..c8f8dadc97 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj @@ -10,9 +10,11 @@ + +