diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs index 973167c80d..b330c07016 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs @@ -4,42 +4,27 @@ using System; using System.Collections.Generic; using System.Diagnostics; -#if NETSTANDARD1_6 -using System.Reflection; -#endif using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.Mvc.Core.Internal; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc.Internal { - public class ControllerActionInvoker : IActionInvoker + public class ControllerActionInvoker : ResourceInvoker, IActionInvoker { private readonly IControllerFactory _controllerFactory; private readonly IControllerArgumentBinder _controllerArgumentBinder; - private readonly DiagnosticSource _diagnosticSource; - private readonly ILogger _logger; private readonly ControllerContext _controllerContext; - private readonly IFilterMetadata[] _filters; private readonly ObjectMethodExecutor _executor; - // Do not make this readonly, it's mutable. We don't want to make a copy. - // https://blogs.msdn.microsoft.com/ericlippert/2008/05/14/mutating-readonly-structs/ - private FilterCursor _cursor; private object _controller; private Dictionary _arguments; - private IActionResult _result; - - private AuthorizationFilterContext _authorizationContext; - - private ResourceExecutingContext _resourceExecutingContext; - private ResourceExecutedContext _resourceExecutedContext; private ExceptionContext _exceptionContext; @@ -50,19 +35,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal private ResultExecutedContext _resultExecutedContext; public ControllerActionInvoker( - ControllerActionInvokerCache cache, IControllerFactory controllerFactory, IControllerArgumentBinder controllerArgumentBinder, ILogger logger, DiagnosticSource diagnosticSource, - ActionContext actionContext, - IReadOnlyList valueProviderFactories, - int maxModelValidationErrors) + ControllerContext controllerContext, + IFilterMetadata[] filters, + ObjectMethodExecutor objectMethodExecutor) + : base(diagnosticSource, logger, controllerContext, filters, controllerContext.ValueProviderFactories) { - if (cache == null) - { - throw new ArgumentNullException(nameof(cache)); - } if (controllerFactory == null) { @@ -74,41 +55,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal throw new ArgumentNullException(nameof(controllerArgumentBinder)); } - if (logger == null) + if (objectMethodExecutor == null) { - throw new ArgumentNullException(nameof(logger)); - } - - if (diagnosticSource == null) - { - throw new ArgumentNullException(nameof(diagnosticSource)); - } - - if (actionContext == null) - { - throw new ArgumentNullException(nameof(actionContext)); - } - - if (valueProviderFactories == null) - { - throw new ArgumentNullException(nameof(valueProviderFactories)); + throw new ArgumentNullException(nameof(objectMethodExecutor)); } _controllerFactory = controllerFactory; _controllerArgumentBinder = controllerArgumentBinder; - _logger = logger; - _diagnosticSource = diagnosticSource; - - _controllerContext = new ControllerContext(actionContext); - _controllerContext.ModelState.MaxAllowedErrors = maxModelValidationErrors; - - // PERF: These are rarely going to be changed, so let's go copy-on-write. - _controllerContext.ValueProviderFactories = new CopyOnWriteList(valueProviderFactories); - - var cacheEntry = cache.GetState(_controllerContext); - _filters = cacheEntry.Filters; - _executor = cacheEntry.ActionMethodExecutor; - _cursor = new FilterCursor(_filters); + _controllerContext = controllerContext; + _executor = objectMethodExecutor; } public virtual async Task InvokeAsync() @@ -126,43 +81,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal var startTimestamp = _logger.IsEnabled(LogLevel.Information) ? Stopwatch.GetTimestamp() : 0; - // The invoker is implemented using a 'Taskerator' or perhaps an 'Asyncerator' (both terms are correct - // and in common usage). This method is the main 'driver' loop and will call into the `Next` method - // (`await`ing the result) until a terminal state is reached. - // - // The `Next` method walks through the state transitions of the invoker and returns a `Task` when there's - // actual async work that we need to await. As an optimization that Next method won't return a `Task` - // that completes synchronously. - // - // Additionally the `Next` funtion will be called recursively when we're 'inside' a filter invocation. - // Executing 'inside' a filter requires an async method call within a `try`/`catch` for error handling, so - // we have to recurse. Each 'frame' calls into `Next` with a value of `Scope` that communicates what kind - // of 'frame' is executing. This has an effect on the state machine transitions as well as what kinds of - // contexts need to be constructed to communicate the result of execution of the 'frame'. - - // When returning, the `Next` method will set `next` to the state to goto on the subsequent invocation. - // This is similar to `Task.ContinueWith`, but since we have a fixed number of states we can avoid - // the overhead of actually using `Task.ContinueWith`. - var next = State.InvokeBegin; - - // The `scope` tells the `Next` method who the caller is, and what kind of state to initialize to - // communicate a result. The outermost scope is `Scope.Invoker` and doesn't require any type - // of context or result other than throwing. - var scope = Scope.Invoker; - - // The `state` is used for internal state handling during transitions between states. In practice this - // means storing a filter instance in `state` and then retrieving it in the next state. - var state = (object)null; - - // `isCompleted` will be set to true when we've reached a terminal state. - var isCompleted = false; - try { - while (!isCompleted) - { - await Next(ref next, ref scope, ref state, ref isCompleted); - } + await InvokeFilterPipelineAsync(); + } finally { @@ -192,309 +114,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal switch (next) { case State.InvokeBegin: - { - goto case State.AuthorizationBegin; - } - - case State.AuthorizationBegin: - { - _cursor.Reset(); - goto case State.AuthorizationNext; - } - - case State.AuthorizationNext: - { - var current = _cursor.GetNextFilter(); - if (current.FilterAsync != null) - { - if (_authorizationContext == null) - { - _authorizationContext = new AuthorizationFilterContext(_controllerContext, _filters); - } - - state = current.FilterAsync; - goto case State.AuthorizationAsyncBegin; - } - else if (current.Filter != null) - { - if (_authorizationContext == null) - { - _authorizationContext = new AuthorizationFilterContext(_controllerContext, _filters); - } - - state = current.Filter; - goto case State.AuthorizationSync; - } - else - { - goto case State.AuthorizationEnd; - } - } - - case State.AuthorizationAsyncBegin: - { - Debug.Assert(state != null); - Debug.Assert(_authorizationContext != null); - - var filter = (IAsyncAuthorizationFilter)state; - var authorizationContext = _authorizationContext; - - _diagnosticSource.BeforeOnAuthorizationAsync(authorizationContext, filter); - - var task = filter.OnAuthorizationAsync(authorizationContext); - if (task.Status != TaskStatus.RanToCompletion) - { - next = State.AuthorizationAsyncEnd; - return task; - } - - goto case State.AuthorizationAsyncEnd; - } - - case State.AuthorizationAsyncEnd: - { - Debug.Assert(state != null); - Debug.Assert(_authorizationContext != null); - - var filter = (IAsyncAuthorizationFilter)state; - var authorizationContext = _authorizationContext; - - _diagnosticSource.AfterOnAuthorizationAsync(authorizationContext, filter); - - if (authorizationContext.Result != null) - { - goto case State.AuthorizationShortCircuit; - } - - goto case State.AuthorizationNext; - } - - case State.AuthorizationSync: - { - Debug.Assert(state != null); - Debug.Assert(_authorizationContext != null); - - var filter = (IAuthorizationFilter)state; - var authorizationContext = _authorizationContext; - - _diagnosticSource.BeforeOnAuthorization(authorizationContext, filter); - - filter.OnAuthorization(authorizationContext); - - _diagnosticSource.AfterOnAuthorization(authorizationContext, filter); - - if (authorizationContext.Result != null) - { - goto case State.AuthorizationShortCircuit; - } - - goto case State.AuthorizationNext; - } - - case State.AuthorizationShortCircuit: - { - Debug.Assert(state != null); - Debug.Assert(_authorizationContext != null); - - _logger.AuthorizationFailure((IFilterMetadata)state); - - // If an authorization filter short circuits, the result is the last thing we execute - // so just return that task instead of calling back into the state machine. - isCompleted = true; - return InvokeResultAsync(_authorizationContext.Result); - } - - case State.AuthorizationEnd: - { - goto case State.ResourceBegin; - } - - case State.ResourceBegin: - { - _cursor.Reset(); - goto case State.ResourceNext; - } - - case State.ResourceNext: - { - var current = _cursor.GetNextFilter(); - if (current.FilterAsync != null) - { - if (_resourceExecutingContext == null) - { - _resourceExecutingContext = new ResourceExecutingContext( - _controllerContext, - _filters, - _controllerContext.ValueProviderFactories); - } - - state = current.FilterAsync; - goto case State.ResourceAsyncBegin; - } - else if (current.Filter != null) - { - if (_resourceExecutingContext == null) - { - _resourceExecutingContext = new ResourceExecutingContext( - _controllerContext, - _filters, - _controllerContext.ValueProviderFactories); - } - - state = current.Filter; - goto case State.ResourceSyncBegin; - } - else if (scope == Scope.Resource) - { - // All resource filters are currently on the stack - now execute the 'inside'. - Debug.Assert(_resourceExecutingContext != null); - goto case State.ResourceInside; - } - else - { - // There are no resource filters - so jump right to 'inside'. - Debug.Assert(scope == Scope.Invoker); - goto case State.ExceptionBegin; - } - } - - case State.ResourceAsyncBegin: - { - Debug.Assert(state != null); - Debug.Assert(_resourceExecutingContext != null); - - var filter = (IAsyncResourceFilter)state; - var resourceExecutingContext = _resourceExecutingContext; - - _diagnosticSource.BeforeOnResourceExecution(resourceExecutingContext, filter); - - var task = filter.OnResourceExecutionAsync(resourceExecutingContext, InvokeNextResourceFilterAwaitedAsync); - if (task.Status != TaskStatus.RanToCompletion) - { - next = State.ResourceAsyncEnd; - return task; - } - - goto case State.ResourceAsyncEnd; - } - - case State.ResourceAsyncEnd: - { - Debug.Assert(state != null); - Debug.Assert(_resourceExecutingContext != null); - - var filter = (IAsyncResourceFilter)state; - if (_resourceExecutedContext == null) - { - // If we get here then the filter didn't call 'next' indicating a short circuit. - _resourceExecutedContext = new ResourceExecutedContext(_resourceExecutingContext, _filters) - { - Canceled = true, - Result = _resourceExecutingContext.Result, - }; - - _diagnosticSource.AfterOnResourceExecution(_resourceExecutedContext, filter); - - // A filter could complete a Task without setting a result - if (_resourceExecutingContext.Result != null) - { - goto case State.ResourceShortCircuit; - } - } - - goto case State.ResourceEnd; - } - - case State.ResourceSyncBegin: - { - Debug.Assert(state != null); - Debug.Assert(_resourceExecutingContext != null); - - var filter = (IResourceFilter)state; - var resourceExecutingContext = _resourceExecutingContext; - - _diagnosticSource.BeforeOnResourceExecuting(resourceExecutingContext, filter); - - filter.OnResourceExecuting(resourceExecutingContext); - - _diagnosticSource.AfterOnResourceExecuting(resourceExecutingContext, filter); - - if (resourceExecutingContext.Result != null) - { - _resourceExecutedContext = new ResourceExecutedContext(resourceExecutingContext, _filters) - { - Canceled = true, - Result = _resourceExecutingContext.Result, - }; - - goto case State.ResourceShortCircuit; - } - - var task = InvokeNextResourceFilter(); - if (task.Status != TaskStatus.RanToCompletion) - { - next = State.ResourceSyncEnd; - return task; - } - - goto case State.ResourceSyncEnd; - } - - case State.ResourceSyncEnd: - { - Debug.Assert(state != null); - Debug.Assert(_resourceExecutingContext != null); - Debug.Assert(_resourceExecutedContext != null); - - var filter = (IResourceFilter)state; - var resourceExecutedContext = _resourceExecutedContext; - - _diagnosticSource.BeforeOnResourceExecuted(resourceExecutedContext, filter); - - filter.OnResourceExecuted(resourceExecutedContext); - - _diagnosticSource.AfterOnResourceExecuted(resourceExecutedContext, filter); - - goto case State.ResourceEnd; - } - - case State.ResourceShortCircuit: - { - Debug.Assert(state != null); - Debug.Assert(_resourceExecutingContext != null); - Debug.Assert(_resourceExecutedContext != null); - - _logger.ResourceFilterShortCircuited((IFilterMetadata)state); - - var task = InvokeResultAsync(_resourceExecutingContext.Result); - if (task.Status != TaskStatus.RanToCompletion) - { - next = State.ResourceEnd; - return task; - } - - goto case State.ResourceEnd; - } - - case State.ResourceInside: { goto case State.ExceptionBegin; } - case State.ResourceEnd: - { - if (scope == Scope.Resource) - { - isCompleted = true; - return TaskCache.CompletedTask; - } - - Debug.Assert(scope == Scope.Invoker); - Rethrow(_resourceExecutedContext); - - goto case State.InvokeEnd; - } - case State.ExceptionBegin: { _cursor.Reset(); @@ -522,7 +145,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal else { // There are no exception filters - so jump right to 'inside'. - Debug.Assert(scope == Scope.Invoker || scope == Scope.Resource); + Debug.Assert(scope == Scope.Invoker); goto case State.ActionBegin; } } @@ -631,33 +254,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal Debug.Assert(state != null); Debug.Assert(_exceptionContext != null); - Task task; - if (scope == Scope.Resource) + if (scope == Scope.Invoker) { Debug.Assert(_exceptionContext.Result != null); - _resourceExecutedContext = new ResourceExecutedContext(_controllerContext, _filters) - { - Result = _exceptionContext.Result, - }; - - task = InvokeResultAsync(_exceptionContext.Result); - if (task.Status != TaskStatus.RanToCompletion) - { - next = State.ResourceEnd; - return task; - } - - goto case State.ResourceEnd; + _result = _exceptionContext.Result; } - task = InvokeResultAsync(_exceptionContext.Result); + var task = InvokeResultAsync(_exceptionContext.Result); if (task.Status != TaskStatus.RanToCompletion) { next = State.InvokeEnd; return task; } - goto case State.ResourceEnd; + goto case State.InvokeEnd; } case State.ExceptionEnd: @@ -881,7 +491,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal return TaskCache.CompletedTask; } - Debug.Assert(scope == Scope.Invoker || scope == Scope.Resource); + Debug.Assert(scope == Scope.Invoker); goto case State.ResultBegin; } @@ -1068,16 +678,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal Rethrow(_resultExecutedContext); - if (scope == Scope.Resource) - { - _resourceExecutedContext = new ResourceExecutedContext(_controllerContext, _filters) - { - Result = result, - }; - - goto case State.ResourceEnd; - } - goto case State.InvokeEnd; } @@ -1092,51 +692,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal } } - private async Task InvokeNextResourceFilter() - { - try - { - var next = State.ResourceNext; - var state = (object)null; - var scope = Scope.Resource; - var isCompleted = false; - while (!isCompleted) - { - await Next(ref next, ref scope, ref state, ref isCompleted); - } - } - catch (Exception exception) - { - _resourceExecutedContext = new ResourceExecutedContext(_resourceExecutingContext, _filters) - { - ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception), - }; - } - - Debug.Assert(_resourceExecutedContext != null); - } - - private async Task InvokeNextResourceFilterAwaitedAsync() - { - Debug.Assert(_resourceExecutingContext != null); - - if (_resourceExecutingContext.Result != null) - { - // If we get here, it means that an async filter set a result AND called next(). This is forbidden. - var message = Resources.FormatAsyncResourceFilter_InvalidShortCircuit( - typeof(IAsyncResourceFilter).Name, - nameof(ResourceExecutingContext.Result), - typeof(ResourceExecutingContext).Name, - typeof(ResourceExecutionDelegate).Name); - throw new InvalidOperationException(message); - } - - await InvokeNextResourceFilter(); - - Debug.Assert(_resourceExecutedContext != null); - return _resourceExecutedContext; - } - private async Task InvokeNextExceptionFilterAsync() { try @@ -1344,42 +899,42 @@ namespace Microsoft.AspNetCore.Mvc.Internal return _resultExecutedContext; } - private async Task InvokeResultAsync(IActionResult result) + protected override async Task InvokeInnerFilterAsync() { - var controllerContext = _controllerContext; + // The invoker is implemented using a 'Taskerator' or perhaps an 'Asyncerator' (both terms are correct + // and in common usage). This method is the main 'driver' loop and will call into the `Next` method + // (`await`ing the result) until a terminal state is reached. + // + // The `Next` method walks through the state transitions of the invoker and returns a `Task` when there's + // actual async work that we need to await. As an optimization that Next method won't return a `Task` + // that completes synchronously. + // + // Additionally the `Next` funtion will be called recursively when we're 'inside' a filter invocation. + // Executing 'inside' a filter requires an async method call within a `try`/`catch` for error handling, so + // we have to recurse. Each 'frame' calls into `Next` with a value of `Scope` that communicates what kind + // of 'frame' is executing. This has an effect on the state machine transitions as well as what kinds of + // contexts need to be constructed to communicate the result of execution of the 'frame'. - _diagnosticSource.BeforeActionResult(controllerContext, result); + // When returning, the `Next` method will set `next` to the state to goto on the subsequent invocation. + // This is similar to `Task.ContinueWith`, but since we have a fixed number of states we can avoid + // the overhead of actually using `Task.ContinueWith`. + var next = State.InvokeBegin; - try - { - await result.ExecuteResultAsync(controllerContext); - } - finally - { - _diagnosticSource.AfterActionResult(controllerContext, result); - } - } + // The `scope` tells the `Next` method who the caller is, and what kind of state to initialize to + // communicate a result. The outermost scope is `Scope.Invoker` and doesn't require any type + // of context or result other than throwing. + var scope = Scope.Invoker; - private static void Rethrow(ResourceExecutedContext context) - { - if (context == null) - { - return; - } + // The `state` is used for internal state handling during transitions between states. In practice this + // means storing a filter instance in `state` and then retrieving it in the next state. + var state = (object)null; - if (context.ExceptionHandled) - { - return; - } + // `isCompleted` will be set to true when we've reached a terminal state. + var isCompleted = false; - if (context.ExceptionDispatchInfo != null) + while (!isCompleted) { - context.ExceptionDispatchInfo.Throw(); - } - - if (context.Exception != null) - { - throw context.Exception; + await Next(ref next, ref scope, ref state, ref isCompleted); } } @@ -1455,7 +1010,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal private enum Scope { Invoker, - Resource, Exception, Action, Result, @@ -1464,22 +1018,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal private enum State { InvokeBegin, - AuthorizationBegin, - AuthorizationNext, - AuthorizationAsyncBegin, - AuthorizationAsyncEnd, - AuthorizationSync, - AuthorizationShortCircuit, - AuthorizationEnd, - ResourceBegin, - ResourceNext, - ResourceAsyncBegin, - ResourceAsyncEnd, - ResourceSyncBegin, - ResourceSyncEnd, - ResourceShortCircuit, - ResourceInside, - ResourceEnd, + InvokeBeginOutside, + InvokeBeginInside, ExceptionBegin, ExceptionNext, ExceptionAsyncBegin, @@ -1508,82 +1048,5 @@ namespace Microsoft.AspNetCore.Mvc.Internal ResultEnd, InvokeEnd, } - - /// - /// A one-way cursor for filters. - /// - /// - /// This will iterate the filter collection once per-stage, and skip any filters that don't have - /// the one of interfaces that applies to the current stage. - /// - /// Filters are always executed in the following order, but short circuiting plays a role. - /// - /// Indentation reflects nesting. - /// - /// 1. Exception Filters - /// 2. Authorization Filters - /// 3. Action Filters - /// Action - /// - /// 4. Result Filters - /// Result - /// - /// - private struct FilterCursor - { - private int _index; - private readonly IFilterMetadata[] _filters; - - public FilterCursor(int index, IFilterMetadata[] filters) - { - _index = index; - _filters = filters; - } - - public FilterCursor(IFilterMetadata[] filters) - { - _index = 0; - _filters = filters; - } - - public void Reset() - { - _index = 0; - } - - public FilterCursorItem GetNextFilter() - where TFilter : class - where TFilterAsync : class - { - while (_index < _filters.Length) - { - var filter = _filters[_index] as TFilter; - var filterAsync = _filters[_index] as TFilterAsync; - - _index += 1; - - if (filter != null || filterAsync != null) - { - return new FilterCursorItem(_index, filter, filterAsync); - } - } - - return default(FilterCursorItem); - } - } - - private struct FilterCursorItem - { - public readonly int Index; - public readonly TFilter Filter; - public readonly TFilterAsync FilterAsync; - - public FilterCursorItem(int index, TFilter filter, TFilterAsync filterAsync) - { - Index = index; - Filter = filter; - FilterAsync = filterAsync; - } - } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerProvider.cs index 8114594a1a..7bb877a245 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerProvider.cs @@ -57,15 +57,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal if (actionDescriptor != null) { + var controllerContext = new ControllerContext(context.ActionContext); + // PERF: These are rarely going to be changed, so let's go copy-on-write. + controllerContext.ValueProviderFactories = new CopyOnWriteList(_valueProviderFactories); + controllerContext.ModelState.MaxAllowedErrors = _maxModelValidationErrors; + + var cacheState = _controllerActionInvokerCache.GetState(controllerContext); + context.Result = new ControllerActionInvoker( - _controllerActionInvokerCache, _controllerFactory, _argumentBinder, _logger, _diagnosticSource, - context.ActionContext, - _valueProviderFactories, - _maxModelValidationErrors); + controllerContext, + cacheState.Filters, + cacheState.ActionMethodExecutor); } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCursor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCursor.cs new file mode 100644 index 0000000000..b297712bd0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCursor.cs @@ -0,0 +1,64 @@ +// 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 Microsoft.AspNetCore.Mvc.Filters; + +namespace Microsoft.AspNetCore.Mvc.Internal +{ + /// + /// A one-way cursor for filters. + /// + /// + /// This will iterate the filter collection once per-stage, and skip any filters that don't have + /// the one of interfaces that applies to the current stage. + /// + /// Filters are always executed in the following order, but short circuiting plays a role. + /// + /// Indentation reflects nesting. + /// + /// 1. Exception Filters + /// 2. Authorization Filters + /// 3. Action Filters + /// Action + /// + /// 4. Result Filters + /// Result + /// + /// + public struct FilterCursor + { + private readonly IFilterMetadata[] _filters; + private int _index; + + public FilterCursor(IFilterMetadata[] filters) + { + _filters = filters; + _index = 0; + } + + public void Reset() + { + _index = 0; + } + + public FilterCursorItem GetNextFilter() + where TFilter : class + where TFilterAsync : class + { + while (_index < _filters.Length) + { + var filter = _filters[_index] as TFilter; + var filterAsync = _filters[_index] as TFilterAsync; + + _index += 1; + + if (filter != null || filterAsync != null) + { + return new FilterCursorItem(filter, filterAsync); + } + } + + return default(FilterCursorItem); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCursorItem.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCursorItem.cs new file mode 100644 index 0000000000..ccb42dc3f7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCursorItem.cs @@ -0,0 +1,18 @@ +// 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.Internal +{ + public struct FilterCursorItem + { + public FilterCursorItem(TFilter filter, TFilterAsync filterAsync) + { + Filter = filter; + FilterAsync = filterAsync; + } + + public TFilter Filter { get; } + + public TFilterAsync FilterAsync { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ResourceInvoker.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ResourceInvoker.cs new file mode 100644 index 0000000000..83b9882b67 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ResourceInvoker.cs @@ -0,0 +1,552 @@ +// 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.Filters; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Mvc.Core.Internal +{ + public abstract class ResourceInvoker + { + protected readonly DiagnosticSource _diagnosticSource; + protected readonly ILogger _logger; + protected readonly ActionContext _actionContext; + protected readonly IFilterMetadata[] _filters; + protected readonly IList _valueProviderFactories; + + private AuthorizationFilterContext _authorizationContext; + private ResourceExecutingContext _resourceExecutingContext; + private ResourceExecutedContext _resourceExecutedContext; + + // Do not make this readonly, it's mutable. We don't want to make a copy. + // https://blogs.msdn.microsoft.com/ericlippert/2008/05/14/mutating-readonly-structs/ + protected FilterCursor _cursor; + protected IActionResult _result; + + public ResourceInvoker( + DiagnosticSource diagnosticSource, + ILogger logger, + ActionContext actionContext, + IFilterMetadata[] filters, + IList valueProviderFactories) + { + + if (diagnosticSource == null) + { + throw new ArgumentNullException(nameof(diagnosticSource)); + } + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + if (actionContext == null) + { + throw new ArgumentNullException(nameof(actionContext)); + } + + if (filters == null) + { + throw new ArgumentNullException(nameof(filters)); + } + + if (valueProviderFactories == null) + { + throw new ArgumentNullException(nameof(valueProviderFactories)); + } + + _diagnosticSource = diagnosticSource; + _logger = logger; + _actionContext = actionContext; + + _filters = filters; + _valueProviderFactories = valueProviderFactories; + _cursor = new FilterCursor(filters); + } + + protected async Task InvokeFilterPipelineAsync() + { + var next = State.InvokeBegin; + + // The `scope` tells the `Next` method who the caller is, and what kind of state to initialize to + // communicate a result. The outermost scope is `Scope.Invoker` and doesn't require any type + // of context or result other than throwing. + var scope = Scope.Invoker; + + // The `state` is used for internal state handling during transitions between states. In practice this + // means storing a filter instance in `state` and then retrieving it in the next state. + var state = (object)null; + + // `isCompleted` will be set to true when we've reached a terminal state. + var isCompleted = false; + + while (!isCompleted) + { + await Next(ref next, ref scope, ref state, ref isCompleted); + } + } + + protected abstract Task InvokeInnerFilterAsync(); + + protected async Task InvokeResultAsync(IActionResult result) + { + var actionContext = _actionContext; + + _diagnosticSource.BeforeActionResult(actionContext, result); + + try + { + await result.ExecuteResultAsync(actionContext); + } + finally + { + _diagnosticSource.AfterActionResult(actionContext, result); + } + } + + 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.InvokeBegin: + { + goto case State.AuthorizationBegin; + } + + case State.AuthorizationBegin: + { + _cursor.Reset(); + goto case State.AuthorizationNext; + } + + case State.AuthorizationNext: + { + var current = _cursor.GetNextFilter(); + if (current.FilterAsync != null) + { + if (_authorizationContext == null) + { + _authorizationContext = new AuthorizationFilterContext(_actionContext, _filters); + } + + state = current.FilterAsync; + goto case State.AuthorizationAsyncBegin; + } + else if (current.Filter != null) + { + if (_authorizationContext == null) + { + _authorizationContext = new AuthorizationFilterContext(_actionContext, _filters); + } + + state = current.Filter; + goto case State.AuthorizationSync; + } + else + { + goto case State.AuthorizationEnd; + } + } + + case State.AuthorizationAsyncBegin: + { + Debug.Assert(state != null); + Debug.Assert(_authorizationContext != null); + + var filter = (IAsyncAuthorizationFilter)state; + var authorizationContext = _authorizationContext; + + _diagnosticSource.BeforeOnAuthorizationAsync(authorizationContext, filter); + + var task = filter.OnAuthorizationAsync(authorizationContext); + if (task.Status != TaskStatus.RanToCompletion) + { + next = State.AuthorizationAsyncEnd; + return task; + } + + goto case State.AuthorizationAsyncEnd; + } + + case State.AuthorizationAsyncEnd: + { + Debug.Assert(state != null); + Debug.Assert(_authorizationContext != null); + + var filter = (IAsyncAuthorizationFilter)state; + var authorizationContext = _authorizationContext; + + _diagnosticSource.AfterOnAuthorizationAsync(authorizationContext, filter); + + if (authorizationContext.Result != null) + { + goto case State.AuthorizationShortCircuit; + } + + goto case State.AuthorizationNext; + } + + case State.AuthorizationSync: + { + Debug.Assert(state != null); + Debug.Assert(_authorizationContext != null); + + var filter = (IAuthorizationFilter)state; + var authorizationContext = _authorizationContext; + + _diagnosticSource.BeforeOnAuthorization(authorizationContext, filter); + + filter.OnAuthorization(authorizationContext); + + _diagnosticSource.AfterOnAuthorization(authorizationContext, filter); + + if (authorizationContext.Result != null) + { + goto case State.AuthorizationShortCircuit; + } + + goto case State.AuthorizationNext; + } + + case State.AuthorizationShortCircuit: + { + Debug.Assert(state != null); + Debug.Assert(_authorizationContext != null); + + _logger.AuthorizationFailure((IFilterMetadata)state); + + // If an authorization filter short circuits, the result is the last thing we execute + // so just return that task instead of calling back into the state machine. + isCompleted = true; + return InvokeResultAsync(_authorizationContext.Result); + } + + case State.AuthorizationEnd: + { + goto case State.ResourceBegin; + } + + case State.ResourceBegin: + { + _cursor.Reset(); + goto case State.ResourceNext; + } + + case State.ResourceNext: + { + var current = _cursor.GetNextFilter(); + if (current.FilterAsync != null) + { + if (_resourceExecutingContext == null) + { + _resourceExecutingContext = new ResourceExecutingContext( + _actionContext, + _filters, + _valueProviderFactories); + } + + state = current.FilterAsync; + goto case State.ResourceAsyncBegin; + } + else if (current.Filter != null) + { + if (_resourceExecutingContext == null) + { + _resourceExecutingContext = new ResourceExecutingContext( + _actionContext, + _filters, + _valueProviderFactories); + } + + state = current.Filter; + goto case State.ResourceSyncBegin; + } + else if (scope == Scope.Resource) + { + // All resource filters are currently on the stack - now execute the 'inside'. + Debug.Assert(_resourceExecutingContext != null); + goto case State.ResourceInside; + } + else + { + // There are no resource filters - so jump right to 'inside'. + Debug.Assert(scope == Scope.Invoker); + next = State.InvokeEnd; + return InvokeInnerFilterAsync(); + } + } + + case State.ResourceAsyncBegin: + { + Debug.Assert(state != null); + Debug.Assert(_resourceExecutingContext != null); + + var filter = (IAsyncResourceFilter)state; + var resourceExecutingContext = _resourceExecutingContext; + + _diagnosticSource.BeforeOnResourceExecution(resourceExecutingContext, filter); + + var task = filter.OnResourceExecutionAsync(resourceExecutingContext, InvokeNextResourceFilterAwaitedAsync); + if (task.Status != TaskStatus.RanToCompletion) + { + next = State.ResourceAsyncEnd; + return task; + } + + goto case State.ResourceAsyncEnd; + } + + case State.ResourceAsyncEnd: + { + Debug.Assert(state != null); + Debug.Assert(_resourceExecutingContext != null); + + var filter = (IAsyncResourceFilter)state; + if (_resourceExecutedContext == null) + { + // If we get here then the filter didn't call 'next' indicating a short circuit. + _resourceExecutedContext = new ResourceExecutedContext(_resourceExecutingContext, _filters) + { + Canceled = true, + Result = _resourceExecutingContext.Result, + }; + + _diagnosticSource.AfterOnResourceExecution(_resourceExecutedContext, filter); + + // A filter could complete a Task without setting a result + if (_resourceExecutingContext.Result != null) + { + goto case State.ResourceShortCircuit; + } + } + + goto case State.ResourceEnd; + } + + case State.ResourceSyncBegin: + { + Debug.Assert(state != null); + Debug.Assert(_resourceExecutingContext != null); + + var filter = (IResourceFilter)state; + var resourceExecutingContext = _resourceExecutingContext; + + _diagnosticSource.BeforeOnResourceExecuting(resourceExecutingContext, filter); + + filter.OnResourceExecuting(resourceExecutingContext); + + _diagnosticSource.AfterOnResourceExecuting(resourceExecutingContext, filter); + + if (resourceExecutingContext.Result != null) + { + _resourceExecutedContext = new ResourceExecutedContext(resourceExecutingContext, _filters) + { + Canceled = true, + Result = _resourceExecutingContext.Result, + }; + + goto case State.ResourceShortCircuit; + } + + var task = InvokeNextResourceFilter(); + if (task.Status != TaskStatus.RanToCompletion) + { + next = State.ResourceSyncEnd; + return task; + } + + goto case State.ResourceSyncEnd; + } + + case State.ResourceSyncEnd: + { + Debug.Assert(state != null); + Debug.Assert(_resourceExecutingContext != null); + Debug.Assert(_resourceExecutedContext != null); + + var filter = (IResourceFilter)state; + var resourceExecutedContext = _resourceExecutedContext; + + _diagnosticSource.BeforeOnResourceExecuted(resourceExecutedContext, filter); + + filter.OnResourceExecuted(resourceExecutedContext); + + _diagnosticSource.AfterOnResourceExecuted(resourceExecutedContext, filter); + + goto case State.ResourceEnd; + } + + case State.ResourceShortCircuit: + { + Debug.Assert(state != null); + Debug.Assert(_resourceExecutingContext != null); + Debug.Assert(_resourceExecutedContext != null); + + _logger.ResourceFilterShortCircuited((IFilterMetadata)state); + + var task = InvokeResultAsync(_resourceExecutingContext.Result); + if (task.Status != TaskStatus.RanToCompletion) + { + next = State.ResourceEnd; + return task; + } + + goto case State.ResourceEnd; + } + + case State.ResourceInside: + { + next = State.ResourceOutside; + return InvokeInnerFilterAsync(); + } + case State.ResourceOutside: + { + var result = _result; + + if (scope == Scope.Resource) + { + _resourceExecutedContext = new ResourceExecutedContext(_actionContext, _filters) + { + Result = _result, + }; + + goto case State.ResourceEnd; + } + + goto case State.InvokeEnd; + } + + case State.ResourceEnd: + { + if (scope == Scope.Resource) + { + isCompleted = true; + return TaskCache.CompletedTask; + } + + Debug.Assert(scope == Scope.Invoker); + Rethrow(_resourceExecutedContext); + + goto case State.InvokeEnd; + } + + case State.InvokeEnd: + { + isCompleted = true; + return TaskCache.CompletedTask; + } + + default: + throw new InvalidOperationException(); + } + } + + private async Task InvokeNextResourceFilterAwaitedAsync() + { + Debug.Assert(_resourceExecutingContext != null); + + if (_resourceExecutingContext.Result != null) + { + // If we get here, it means that an async filter set a result AND called next(). This is forbidden. + var message = Resources.FormatAsyncResourceFilter_InvalidShortCircuit( + typeof(IAsyncResourceFilter).Name, + nameof(ResourceExecutingContext.Result), + typeof(ResourceExecutingContext).Name, + typeof(ResourceExecutionDelegate).Name); + throw new InvalidOperationException(message); + } + + await InvokeNextResourceFilter(); + + Debug.Assert(_resourceExecutedContext != null); + return _resourceExecutedContext; + } + + private async Task InvokeNextResourceFilter() + { + try + { + var scope = Scope.Resource; + var next = State.ResourceNext; + var state = (object)null; + var isCompleted = false; + while (!isCompleted) + { + await Next(ref next, ref scope, ref state, ref isCompleted); + } + } + catch (Exception exception) + { + _resourceExecutedContext = new ResourceExecutedContext(_resourceExecutingContext, _filters) + { + ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception), + }; + } + + Debug.Assert(_resourceExecutedContext != null); + } + + private static void Rethrow(ResourceExecutedContext 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, + Resource, + } + + private enum State + { + InvokeBegin, + AuthorizationBegin, + AuthorizationNext, + AuthorizationAsyncBegin, + AuthorizationAsyncEnd, + AuthorizationSync, + AuthorizationShortCircuit, + AuthorizationEnd, + ResourceBegin, + ResourceNext, + ResourceAsyncBegin, + ResourceAsyncEnd, + ResourceSyncBegin, + ResourceSyncEnd, + ResourceShortCircuit, + ResourceInside, + ResourceOutside, + ResourceEnd, + InvokeEnd, + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs index 7f3de8ceaa..1293802b4d 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs @@ -2686,15 +2686,22 @@ namespace Microsoft.AspNetCore.Mvc.Internal TestModelBinderFactory.CreateDefault(metadataProvider), new DefaultObjectValidator(metadataProvider, new IModelValidatorProvider[0])); + var controllerContext = new ControllerContext(actionContext) + { + ValueProviderFactories = new IValueProviderFactory[0] + }; + controllerContext.ModelState.MaxAllowedErrors = 200; + var invoker = new ControllerActionInvoker( - CreateFilterCache(), controllerFactory.Object, argumentBinder, new NullLoggerFactory().CreateLogger(), new DiagnosticListener("Microsoft.AspNetCore"), - actionContext, - new IValueProviderFactory[0], - 200); + controllerContext, + new IFilterMetadata[0], + ObjectMethodExecutor.Create( + actionDescriptor.MethodInfo, + actionDescriptor.ControllerTypeInfo)); // Act await invoker.InvokeAsync(); @@ -2996,24 +3003,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal routeData: routeData, actionDescriptor: actionDescriptor); - var filterProvider = new Mock(MockBehavior.Strict); - filterProvider - .Setup(fp => fp.OnProvidersExecuting(It.IsAny())) - .Callback(context => - { - foreach (var filterMetadata in filters) - { - context.Results.Add(new FilterItem(new FilterDescriptor(filterMetadata, FilterScope.Action)) - { - Filter = filterMetadata, - }); - } - }); - - filterProvider - .Setup(fp => fp.OnProvidersExecuted(It.IsAny())) - .Verifiable(); - IControllerArgumentBinder argumentBinder = null; if (controllerArgumentBinder == null) @@ -3032,10 +3021,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal argumentBinder = controllerArgumentBinder; } - filterProvider - .SetupGet(fp => fp.Order) - .Returns(-1000); - if (valueProviderFactories == null) { valueProviderFactories = new List(); @@ -3053,7 +3038,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal } var invoker = new TestControllerActionInvoker( - new[] { filterProvider.Object }, + filters, new MockControllerFactory(controller ?? this), argumentBinder, logger, @@ -3299,18 +3284,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal } } - private static ControllerActionInvokerCache CreateFilterCache(IFilterProvider[] filterProviders = null) - { - var descriptorProvider = new ActionDescriptorCollectionProvider( - Enumerable.Empty(), - Enumerable.Empty()); - return new ControllerActionInvokerCache(descriptorProvider, filterProviders.AsEnumerable() ?? new List()); - } - private class TestControllerActionInvoker : ControllerActionInvoker { public TestControllerActionInvoker( - IFilterProvider[] filterProviders, + IFilterMetadata[] filters, MockControllerFactory controllerFactory, IControllerArgumentBinder argumentBinder, ILogger logger, @@ -3319,14 +3296,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal IReadOnlyList valueProviderFactories, int maxAllowedErrorsInModelState) : base( - CreateFilterCache(filterProviders), controllerFactory, argumentBinder, logger, diagnosticSource, - actionContext, - valueProviderFactories, - maxAllowedErrorsInModelState) + CreatControllerContext(actionContext, valueProviderFactories, maxAllowedErrorsInModelState), + filters, + CreateExecutor((ControllerActionDescriptor)actionContext.ActionDescriptor)) { ControllerFactory = controllerFactory; } @@ -3340,6 +3316,25 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Make sure that the controller was disposed in every test that creates ones. ControllerFactory.Verify(); } + + private static ObjectMethodExecutor CreateExecutor(ControllerActionDescriptor actionDescriptor) + { + return ObjectMethodExecutor.Create(actionDescriptor.MethodInfo, actionDescriptor.ControllerTypeInfo); + } + + private static ControllerContext CreatControllerContext( + ActionContext actionContext, + IReadOnlyList valueProviderFactories, + int maxAllowedErrorsInModelState) + { + var controllerContext = new ControllerContext(actionContext) + { + ValueProviderFactories = valueProviderFactories.ToList() + }; + controllerContext.ModelState.MaxAllowedErrors = maxAllowedErrorsInModelState; + + return controllerContext; + } } private class MockAuthorizationFilter : IAuthorizationFilter diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs index 57f68a122a..aa39c23941 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs @@ -276,33 +276,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionContext = new ActionContext(httpContext, new RouteData(), actionDescriptor); - var filterProvider = new Mock(MockBehavior.Strict); - filterProvider - .Setup(fp => fp.OnProvidersExecuting(It.IsAny())) - .Callback(context => - { - foreach (var filterMetadata in filters) - { - context.Results.Add(new FilterItem(new FilterDescriptor(filterMetadata, FilterScope.Action)) - { - Filter = filterMetadata, - }); - } - }); - - filterProvider - .Setup(fp => fp.OnProvidersExecuted(It.IsAny())) - .Verifiable(); - - filterProvider - .SetupGet(fp => fp.Order) - .Returns(-1000); - var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore"); diagnosticSource.SubscribeWithAdapter(new TestDiagnosticListener()); var invoker = new TestControllerActionInvoker( - new[] { filterProvider.Object }, + filters, new MockControllerFactory(controller ?? this), new TestControllerArgumentBinder(actionParameters: null), new NullLoggerFactory().CreateLogger(), @@ -412,7 +390,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal private class TestControllerActionInvoker : ControllerActionInvoker { public TestControllerActionInvoker( - IFilterProvider[] filterProviders, + IFilterMetadata[] filters, MockControllerFactory controllerFactory, IControllerArgumentBinder argumentBinder, ILogger logger, @@ -421,14 +399,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal IReadOnlyList valueProviderFactories, int maxAllowedErrorsInModelState) : base( - CreateFilterCache(filterProviders), controllerFactory, argumentBinder, logger, diagnosticSource, - actionContext, - valueProviderFactories, - maxAllowedErrorsInModelState) + CreatControllerContext(actionContext, valueProviderFactories, maxAllowedErrorsInModelState), + filters, + CreateExecutor((ControllerActionDescriptor)actionContext.ActionDescriptor)) { ControllerFactory = controllerFactory; } @@ -442,6 +419,25 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Make sure that the controller was disposed in every test that creates ones. ControllerFactory.Verify(); } + + private static ObjectMethodExecutor CreateExecutor(ControllerActionDescriptor actionDescriptor) + { + return ObjectMethodExecutor.Create(actionDescriptor.MethodInfo, actionDescriptor.ControllerTypeInfo); + } + + private static ControllerContext CreatControllerContext( + ActionContext actionContext, + IReadOnlyList valueProviderFactories, + int maxAllowedErrorsInModelState) + { + var controllerContext = new ControllerContext(actionContext) + { + ValueProviderFactories = valueProviderFactories.ToList() + }; + controllerContext.ModelState.MaxAllowedErrors = maxAllowedErrorsInModelState; + + return controllerContext; + } } private class TestControllerArgumentBinder : IControllerArgumentBinder