580 lines
21 KiB
C#
580 lines
21 KiB
C#
// 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.Reflection;
|
|
using System.Runtime.ExceptionServices;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
|
using Microsoft.AspNetCore.Mvc.Filters;
|
|
using Microsoft.AspNetCore.Mvc.Internal;
|
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
|
using Microsoft.AspNetCore.Mvc.Razor;
|
|
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
|
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
|
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|
{
|
|
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 CompiledPageActionDescriptor _actionDescriptor;
|
|
private Page _page;
|
|
private object _model;
|
|
private ViewContext _viewContext;
|
|
private ExceptionContext _exceptionContext;
|
|
|
|
public PageActionInvoker(
|
|
IPageHandlerMethodSelector handlerMethodSelector,
|
|
DiagnosticSource diagnosticSource,
|
|
ILogger logger,
|
|
PageContext pageContext,
|
|
IFilterMetadata[] filterMetadata,
|
|
IList<IValueProviderFactory> valueProviderFactories,
|
|
PageActionInvokerCacheEntry cacheEntry,
|
|
ParameterBinder parameterBinder,
|
|
ITempDataDictionaryFactory tempDataFactory,
|
|
HtmlHelperOptions htmlHelperOptions)
|
|
: base(
|
|
diagnosticSource,
|
|
logger,
|
|
pageContext,
|
|
filterMetadata,
|
|
valueProviderFactories)
|
|
{
|
|
_selector = handlerMethodSelector;
|
|
_pageContext = pageContext;
|
|
CacheEntry = cacheEntry;
|
|
_parameterBinder = parameterBinder;
|
|
_tempDataFactory = tempDataFactory;
|
|
_htmlHelperOptions = htmlHelperOptions;
|
|
|
|
_actionDescriptor = pageContext.ActionDescriptor;
|
|
}
|
|
|
|
// Internal for testing
|
|
internal PageActionInvokerCacheEntry CacheEntry { get; }
|
|
|
|
// Internal for testing
|
|
internal PageContext PageContext => _pageContext;
|
|
|
|
/// <remarks>
|
|
/// <see cref="ResourceInvoker"/> for details on what the variables in this method represent.
|
|
/// </remarks>
|
|
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, _viewContext, _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<IExceptionFilter, IAsyncExceptionFilter>();
|
|
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 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();
|
|
}
|
|
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;
|
|
|
|
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.
|
|
_viewContext = new ViewContext(
|
|
_pageContext,
|
|
NullView.Instance,
|
|
_pageContext.ViewData,
|
|
_tempDataFactory.GetTempData(_pageContext.HttpContext),
|
|
TextWriter.Null,
|
|
_htmlHelperOptions);
|
|
|
|
_page = (Page)CacheEntry.PageFactory(_pageContext, _viewContext);
|
|
|
|
pageResult.Page = _page;
|
|
pageResult.ViewData = pageResult.ViewData ?? _pageContext.ViewData;
|
|
}
|
|
|
|
await _result.ExecuteResultAsync(_pageContext);
|
|
}
|
|
|
|
private async Task ExecutePageWithoutPageModelAsync()
|
|
{
|
|
// 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);
|
|
|
|
_page = (Page)CacheEntry.PageFactory(_pageContext, _viewContext);
|
|
|
|
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.
|
|
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're going to render the page so we need to initialize the context.
|
|
pageResult.Page = _page;
|
|
pageResult.ViewData = pageResult.ViewData ?? _pageContext.ViewData;
|
|
}
|
|
|
|
await _result.ExecuteResultAsync(_pageContext);
|
|
}
|
|
|
|
private async Task<object[]> GetArguments(HandlerMethodDescriptor handler)
|
|
{
|
|
var arguments = new object[handler.Parameters.Count];
|
|
var valueProvider = await CompositeValueProvider.CreateAsync(_pageContext, _pageContext.ValueProviderFactories);
|
|
|
|
for (var i = 0; i < handler.Parameters.Count; i++)
|
|
{
|
|
var parameter = handler.Parameters[i];
|
|
|
|
var result = await _parameterBinder.BindModelAsync(
|
|
_pageContext,
|
|
valueProvider,
|
|
parameter,
|
|
value: null);
|
|
|
|
if (result.IsModelSet)
|
|
{
|
|
arguments[i] = result.Model;
|
|
}
|
|
else if (parameter.ParameterInfo.HasDefaultValue)
|
|
{
|
|
arguments[i] = parameter.ParameterInfo.DefaultValue;
|
|
}
|
|
else if (parameter.ParameterType.GetTypeInfo().IsValueType)
|
|
{
|
|
arguments[i] = Activator.CreateInstance(parameter.ParameterType);
|
|
}
|
|
}
|
|
|
|
return arguments;
|
|
}
|
|
|
|
private async Task<IActionResult> ExecuteHandlerMethod(object instance)
|
|
{
|
|
IActionResult result = null;
|
|
|
|
var handler = _selector.Select(_pageContext);
|
|
if (handler != null)
|
|
{
|
|
var arguments = await GetArguments(handler);
|
|
|
|
Func<object, object[], Task<IActionResult>> executor = null;
|
|
for (var i = 0; i < _actionDescriptor.HandlerMethods.Count; i++)
|
|
{
|
|
if (object.ReferenceEquals(handler, _actionDescriptor.HandlerMethods[i]))
|
|
{
|
|
executor = CacheEntry.Executors[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
result = await executor(instance, arguments);
|
|
}
|
|
|
|
if (result == null)
|
|
{
|
|
result = new PageResult();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private async Task InvokeNextExceptionFilterAsync()
|
|
{
|
|
try
|
|
{
|
|
var next = State.ExceptionNext;
|
|
var state = (object)null;
|
|
var scope = Scope.Exception;
|
|
var isCompleted = false;
|
|
while (!isCompleted)
|
|
{
|
|
await Next(ref next, ref scope, ref state, ref isCompleted);
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_exceptionContext = new ExceptionContext(_actionContext, _filters)
|
|
{
|
|
ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception),
|
|
};
|
|
}
|
|
}
|
|
|
|
private static void Rethrow(ExceptionContext context)
|
|
{
|
|
if (context == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (context.ExceptionHandled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (context.ExceptionDispatchInfo != null)
|
|
{
|
|
context.ExceptionDispatchInfo.Throw();
|
|
}
|
|
|
|
if (context.Exception != null)
|
|
{
|
|
throw context.Exception;
|
|
}
|
|
}
|
|
|
|
private enum Scope
|
|
{
|
|
Resource,
|
|
Exception,
|
|
Page,
|
|
}
|
|
|
|
private enum State
|
|
{
|
|
ResourceInnerBegin,
|
|
ExceptionBegin,
|
|
ExceptionNext,
|
|
ExceptionAsyncBegin,
|
|
ExceptionAsyncResume,
|
|
ExceptionAsyncEnd,
|
|
ExceptionSyncBegin,
|
|
ExceptionSyncEnd,
|
|
ExceptionInside,
|
|
ExceptionShortCircuit,
|
|
ExceptionEnd,
|
|
PageBegin,
|
|
PageEnd,
|
|
ResourceInnerEnd,
|
|
}
|
|
}
|
|
}
|