// 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.Diagnostics; using System.IO; using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; 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.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public class PageActionInvoker : ResourceInvoker, IActionInvoker { private readonly IPageHandlerMethodSelector _selector; private readonly PageContext _pageContext; private readonly ParameterBinder _parameterBinder; private readonly ITempDataDictionaryFactory _tempDataFactory; private readonly HtmlHelperOptions _htmlHelperOptions; private readonly CompiledPageActionDescriptor _actionDescriptor; private Dictionary _arguments; private HandlerMethodDescriptor _handler; private PageBase _page; private object _pageModel; private ViewContext _viewContext; private PageHandlerSelectedContext _handlerSelectedContext; private PageHandlerExecutingContext _handlerExecutingContext; private PageHandlerExecutedContext _handlerExecutedContext; public PageActionInvoker( IPageHandlerMethodSelector handlerMethodSelector, DiagnosticSource diagnosticSource, ILogger logger, IActionResultTypeMapper mapper, PageContext pageContext, IFilterMetadata[] filterMetadata, PageActionInvokerCacheEntry cacheEntry, ParameterBinder parameterBinder, ITempDataDictionaryFactory tempDataFactory, HtmlHelperOptions htmlHelperOptions) : base( diagnosticSource, logger, mapper, pageContext, filterMetadata, pageContext.ValueProviderFactories) { _selector = handlerMethodSelector; _pageContext = pageContext; CacheEntry = cacheEntry; _parameterBinder = parameterBinder; _tempDataFactory = tempDataFactory; _htmlHelperOptions = htmlHelperOptions; _actionDescriptor = pageContext.ActionDescriptor; } // Internal for testing internal PageActionInvokerCacheEntry CacheEntry { get; } private bool HasPageModel => _actionDescriptor.HandlerTypeInfo != _actionDescriptor.PageTypeInfo; // Internal for testing internal PageContext PageContext => _pageContext; /// /// for details on what the variables in this method represent. /// protected override async Task InvokeInnerFilterAsync() { var next = State.PageBegin; var scope = Scope.Invoker; var state = (object)null; var isCompleted = false; while (!isCompleted) { await Next(ref next, ref scope, ref state, ref isCompleted); } } protected override void ReleaseResources() { if (_pageModel != null && CacheEntry.ReleaseModel != null) { CacheEntry.ReleaseModel(_pageContext, _pageModel); } if (_page != null && CacheEntry.ReleasePage != null) { CacheEntry.ReleasePage(_pageContext, _viewContext, _page); } } private object CreateInstance() { if (HasPageModel) { // 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; return _pageModel; } else { // 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); _viewContext.ExecutingFilePath = _pageContext.ActionDescriptor.RelativePath; _page = (PageBase)CacheEntry.PageFactory(_pageContext, _viewContext); if (_actionDescriptor.ModelTypeInfo == _actionDescriptor.PageTypeInfo) { _pageContext.ViewData.Model = _page; } return _page; } } private HandlerMethodDescriptor SelectHandler() { return _selector.Select(_pageContext); } private Task BindArgumentsAsync() { // 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)) { return Task.CompletedTask; } return BindArgumentsCoreAsync(); } private async Task BindArgumentsCoreAsync() { await CacheEntry.PropertyBinder(_pageContext, _instance); if (_handler == null) { return; } // We do two separate cache lookups, once for the binder and once for the executor. // Reduding it to a single lookup requires a lot of code change with little value. PageHandlerBinderDelegate handlerBinder = null; for (var i = 0; i < _actionDescriptor.HandlerMethods.Count; i++) { if (object.ReferenceEquals(_handler, _actionDescriptor.HandlerMethods[i])) { handlerBinder = CacheEntry.HandlerBinders[i]; break; } } await handlerBinder(_pageContext, _arguments); } 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 (!ParameterDefaultValue.TryGetDefaultValue(parameter.ParameterInfo, out value) && parameter.ParameterInfo.ParameterType.IsValueType) { value = Activator.CreateInstance(parameter.ParameterInfo.ParameterType); } arguments[i] = value; } return arguments; } private async Task InvokeHandlerMethodAsync() { var handler = _handler; if (_handler != null) { var arguments = PrepareArguments(_arguments, handler); PageHandlerExecutorDelegate executor = null; for (var i = 0; i < _actionDescriptor.HandlerMethods.Count; i++) { if (object.ReferenceEquals(handler, _actionDescriptor.HandlerMethods[i])) { executor = CacheEntry.HandlerExecutors[i]; break; } } Debug.Assert(executor != null, "We should always find a executor for a handler"); _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); } } // Pages have an implicit 'return Page()' even without a handler method. if (_result == null) { _result = new PageResult(); } // 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); _viewContext.ExecutingFilePath = _pageContext.ActionDescriptor.RelativePath; } if (_page == null) { _page = (PageBase)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); _logger.BeforeExecutingMethodOnFilter( PageLoggerExtensions.PageFilter, nameof(IAsyncPageFilter.OnPageHandlerSelectionAsync), 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); _logger.AfterExecutingMethodOnFilter( PageLoggerExtensions.PageFilter, nameof(IAsyncPageFilter.OnPageHandlerSelectionAsync), 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); _logger.BeforeExecutingMethodOnFilter( PageLoggerExtensions.PageFilter, nameof(IPageFilter.OnPageHandlerSelected), 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); _logger.BeforeExecutingMethodOnFilter( PageLoggerExtensions.PageFilter, nameof(IAsyncPageFilter.OnPageHandlerExecutionAsync), 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); _logger.AfterExecutingMethodOnFilter( PageLoggerExtensions.PageFilter, nameof(IAsyncPageFilter.OnPageHandlerExecutionAsync), 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); _logger.BeforeExecutingMethodOnFilter( PageLoggerExtensions.PageFilter, nameof(IPageFilter.OnPageHandlerExecuting), filter); filter.OnPageHandlerExecuting(handlerExecutingContext); _diagnosticSource.AfterOnPageHandlerExecuting(handlerExecutingContext, filter); _logger.AfterExecutingMethodOnFilter( PageLoggerExtensions.PageFilter, nameof(IPageFilter.OnPageHandlerExecuting), 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); _logger.BeforeExecutingMethodOnFilter( PageLoggerExtensions.PageFilter, nameof(IPageFilter.OnPageHandlerExecuted), filter); filter.OnPageHandlerExecuted(handlerExecutedContext); _diagnosticSource.AfterOnPageHandlerExecuted(handlerExecutedContext, filter); _logger.AfterExecutingMethodOnFilter( PageLoggerExtensions.PageFilter, nameof(IPageFilter.OnPageHandlerExecuted), 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 { Invoker, Page, } private enum State { PageBegin, PageSelectHandlerBegin, PageSelectHandlerNext, PageSelectHandlerAsyncBegin, PageSelectHandlerAsyncEnd, PageSelectHandlerSync, PageSelectHandlerEnd, PageNext, PageAsyncBegin, PageAsyncEnd, PageSyncBegin, PageSyncEnd, PageInside, PageEnd, } } }