// 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.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Core.Internal; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.ViewFeatures.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 Page _page; private object _model; private ExceptionContext _exceptionContext; public PageActionInvoker( IPageHandlerMethodSelector handlerMethodSelector, DiagnosticSource diagnosticSource, ILogger logger, PageContext pageContext, IFilterMetadata[] filterMetadata, IList valueProviderFactories, PageActionInvokerCacheEntry cacheEntry) : base( diagnosticSource, logger, pageContext, filterMetadata, valueProviderFactories) { _selector = handlerMethodSelector; _pageContext = pageContext; CacheEntry = cacheEntry; } public PageActionInvokerCacheEntry CacheEntry { get; } /// /// for details on what the variables in this method represent. /// protected override async Task InvokeInnerFilterAsync() { var next = State.ResourceInnerBegin; var scope = Scope.Resource; var state = (object)null; var isCompleted = false; while (!isCompleted) { await Next(ref next, ref scope, ref state, ref isCompleted); } } protected override void ReleaseResources() { if (_model != null && CacheEntry.ReleaseModel != null) { CacheEntry.ReleaseModel(_pageContext, _model); } if (_page != null && CacheEntry.ReleasePage != null) { CacheEntry.ReleasePage(_pageContext, _page); } } private Task Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted) { var diagnosticSource = _diagnosticSource; var logger = _logger; switch (next) { case State.ResourceInnerBegin: { goto case State.ExceptionBegin; } case State.ExceptionBegin: { _cursor.Reset(); goto case State.ExceptionNext; } case State.ExceptionNext: { var current = _cursor.GetNextFilter(); if (current.FilterAsync != null) { state = current.FilterAsync; goto case State.ExceptionAsyncBegin; } else if (current.Filter != null) { state = current.Filter; goto case State.ExceptionSyncBegin; } else if (scope == Scope.Exception) { // All exception filters are on the stack already - so execute the 'inside'. goto case State.ExceptionInside; } else { // There are no exception filters - so jump right to 'inside'. Debug.Assert(scope == Scope.Resource); goto case State.PageBegin; } } case State.ExceptionAsyncBegin: { var task = InvokeNextExceptionFilterAsync(); if (task.Status != TaskStatus.RanToCompletion) { next = State.ExceptionAsyncResume; return task; } goto case State.ExceptionAsyncResume; } case State.ExceptionAsyncResume: { Debug.Assert(state != null); var filter = (IAsyncExceptionFilter)state; var exceptionContext = _exceptionContext; // When we get here we're 'unwinding' the stack of exception filters. If we have an unhandled exception, // we'll call the filter. Otherwise there's nothing to do. if (exceptionContext?.Exception != null && !exceptionContext.ExceptionHandled) { _diagnosticSource.BeforeOnExceptionAsync(exceptionContext, filter); var task = filter.OnExceptionAsync(exceptionContext); if (task.Status != TaskStatus.RanToCompletion) { next = State.ExceptionAsyncEnd; return task; } goto case State.ExceptionAsyncEnd; } goto case State.ExceptionEnd; } case State.ExceptionAsyncEnd: { Debug.Assert(state != null); Debug.Assert(_exceptionContext != null); var filter = (IAsyncExceptionFilter)state; var exceptionContext = _exceptionContext; _diagnosticSource.AfterOnExceptionAsync(exceptionContext, filter); if (exceptionContext.Exception == null || exceptionContext.ExceptionHandled) { _logger.ExceptionFilterShortCircuited(filter); } goto case State.ExceptionEnd; } case State.ExceptionSyncBegin: { var task = InvokeNextExceptionFilterAsync(); if (task.Status != TaskStatus.RanToCompletion) { next = State.ExceptionSyncEnd; return task; } goto case State.ExceptionSyncEnd; } case State.ExceptionSyncEnd: { Debug.Assert(state != null); var filter = (IExceptionFilter)state; var exceptionContext = _exceptionContext; // When we get here we're 'unwinding' the stack of exception filters. If we have an unhandled exception, // we'll call the filter. Otherwise there's nothing to do. if (exceptionContext?.Exception != null && !exceptionContext.ExceptionHandled) { _diagnosticSource.BeforeOnException(exceptionContext, filter); filter.OnException(exceptionContext); _diagnosticSource.AfterOnException(exceptionContext, filter); if (exceptionContext.Exception == null || exceptionContext.ExceptionHandled) { _logger.ExceptionFilterShortCircuited(filter); } } goto case State.ExceptionEnd; } case State.ExceptionInside: { goto case State.PageBegin; } case State.ExceptionShortCircuit: { Debug.Assert(state != null); Debug.Assert(_exceptionContext != null); if (scope == Scope.Resource) { Debug.Assert(_exceptionContext.Result != null); _result = _exceptionContext.Result; } var task = InvokeResultAsync(_exceptionContext.Result); if (task.Status != TaskStatus.RanToCompletion) { next = State.ResourceInnerEnd; return task; } goto case State.ResourceInnerEnd; } case State.ExceptionEnd: { var exceptionContext = _exceptionContext; if (scope == Scope.Exception) { isCompleted = true; return TaskCache.CompletedTask; } if (exceptionContext != null) { if (exceptionContext.Result != null && !exceptionContext.ExceptionHandled) { goto case State.ExceptionShortCircuit; } Rethrow(exceptionContext); } goto case State.ResourceInnerEnd; } case State.PageBegin: { var pageContext = _pageContext; _cursor.Reset(); next = State.PageEnd; return ExecutePageAsync(); } case State.PageEnd: { if (scope == Scope.Exception) { // If we're inside an exception filter, let's allow those filters to 'unwind' before // the result. isCompleted = true; return TaskCache.CompletedTask; } Debug.Assert(scope == Scope.Resource); goto case State.ResourceInnerEnd; } case State.ResourceInnerEnd: { isCompleted = true; return TaskCache.CompletedTask; } default: throw new InvalidOperationException(); } } private async Task ExecutePageAsync() { var actionDescriptor = _pageContext.ActionDescriptor; _page = (Page)CacheEntry.PageFactory(_pageContext); _pageContext.Page = _page; _pageContext.ValueProviderFactories = _valueProviderFactories; IRazorPage[] pageStarts; if (CacheEntry.PageStartFactories == null || CacheEntry.PageStartFactories.Count == 0) { pageStarts = Array.Empty(); } else { pageStarts = new IRazorPage[CacheEntry.PageStartFactories.Count]; for (var i = 0; i < pageStarts.Length; i++) { var pageFactory = CacheEntry.PageStartFactories[i]; pageStarts[i] = pageFactory(); } } _pageContext.PageStarts = pageStarts; if (actionDescriptor.ModelTypeInfo == null) { _model = _page; } else { _model = CacheEntry.ModelFactory(_pageContext); } if (_model != null) { _pageContext.ViewData.Model = _model; } if (CacheEntry.PropertyBinder != null && !string.Equals(_pageContext.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) { // Don't bind properties on GET requests await CacheEntry.PropertyBinder(_page, _model); } // This is a workaround for not yet having proper filter for Pages. SaveTempDataPropertyFilter propertyFilter = null; for (var i = 0; i < _filters.Length; i++) { propertyFilter = _filters[i] as SaveTempDataPropertyFilter; if (propertyFilter != null) { break; } } if (propertyFilter != null) { propertyFilter.Subject = _page; propertyFilter.ApplyTempDataChanges(_pageContext.HttpContext); } IActionResult result = null; var handler = _selector.Select(_pageContext); if (handler != null) { var executor = handler.Executor; result = await executor(_page, _model); } if (result == null) { result = new PageViewResult(_page); } await result.ExecuteResultAsync(_pageContext); } private async Task InvokeNextExceptionFilterAsync() { try { var next = State.ExceptionNext; var state = (object)null; var scope = Scope.Exception; var isCompleted = false; while (!isCompleted) { await Next(ref next, ref scope, ref state, ref isCompleted); } } catch (Exception exception) { _exceptionContext = new ExceptionContext(_actionContext, _filters) { ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception), }; } } private static void Rethrow(ExceptionContext context) { if (context == null) { return; } if (context.ExceptionHandled) { return; } if (context.ExceptionDispatchInfo != null) { context.ExceptionDispatchInfo.Throw(); } if (context.Exception != null) { throw context.Exception; } } private enum Scope { Resource, Exception, Page, } private enum State { ResourceInnerBegin, ExceptionBegin, ExceptionNext, ExceptionAsyncBegin, ExceptionAsyncResume, ExceptionAsyncEnd, ExceptionSyncBegin, ExceptionSyncEnd, ExceptionInside, ExceptionShortCircuit, ExceptionEnd, PageBegin, PageEnd, ResourceInnerEnd, } } }