Introduce a base type for executing auth and resource filters.

This commit is contained in:
Pranav K 2016-11-28 13:54:11 -08:00
parent 2b8233932a
commit 78492b39d8
7 changed files with 752 additions and 658 deletions

View File

@ -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<string, object> _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<IValueProviderFactory> 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<IValueProviderFactory>(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<IAuthorizationFilter, IAsyncAuthorizationFilter>();
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<IResourceFilter, IAsyncResourceFilter>();
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<ResourceExecutedContext> 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,
}
/// <summary>
/// A one-way cursor for filters.
/// </summary>
/// <remarks>
/// 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
///
/// </remarks>
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<TFilter, TFilterAsync> GetNextFilter<TFilter, TFilterAsync>()
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<TFilter, TFilterAsync>(_index, filter, filterAsync);
}
}
return default(FilterCursorItem<TFilter, TFilterAsync>);
}
}
private struct FilterCursorItem<TFilter, TFilterAsync>
{
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;
}
}
}
}

View File

@ -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<IValueProviderFactory>(_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);
}
}

View File

@ -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
{
/// <summary>
/// A one-way cursor for filters.
/// </summary>
/// <remarks>
/// 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
///
/// </remarks>
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<TFilter, TFilterAsync> GetNextFilter<TFilter, TFilterAsync>()
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<TFilter, TFilterAsync>(filter, filterAsync);
}
}
return default(FilterCursorItem<TFilter, TFilterAsync>);
}
}
}

View File

@ -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<TFilter, TFilterAsync>
{
public FilterCursorItem(TFilter filter, TFilterAsync filterAsync)
{
Filter = filter;
FilterAsync = filterAsync;
}
public TFilter Filter { get; }
public TFilterAsync FilterAsync { get; }
}
}

View File

@ -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<IValueProviderFactory> _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<IValueProviderFactory> 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<IAuthorizationFilter, IAsyncAuthorizationFilter>();
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<IResourceFilter, IAsyncResourceFilter>();
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<ResourceExecutedContext> 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,
}
}
}

View File

@ -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<ControllerActionInvoker>(),
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<IFilterProvider>(MockBehavior.Strict);
filterProvider
.Setup(fp => fp.OnProvidersExecuting(It.IsAny<FilterProviderContext>()))
.Callback<FilterProviderContext>(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<FilterProviderContext>()))
.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<IValueProviderFactory>();
@ -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<IActionDescriptorProvider>(),
Enumerable.Empty<IActionDescriptorChangeProvider>());
return new ControllerActionInvokerCache(descriptorProvider, filterProviders.AsEnumerable() ?? new List<IFilterProvider>());
}
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<IValueProviderFactory> 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<IValueProviderFactory> valueProviderFactories,
int maxAllowedErrorsInModelState)
{
var controllerContext = new ControllerContext(actionContext)
{
ValueProviderFactories = valueProviderFactories.ToList()
};
controllerContext.ModelState.MaxAllowedErrors = maxAllowedErrorsInModelState;
return controllerContext;
}
}
private class MockAuthorizationFilter : IAuthorizationFilter

View File

@ -276,33 +276,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var actionContext = new ActionContext(httpContext, new RouteData(), actionDescriptor);
var filterProvider = new Mock<IFilterProvider>(MockBehavior.Strict);
filterProvider
.Setup(fp => fp.OnProvidersExecuting(It.IsAny<FilterProviderContext>()))
.Callback<FilterProviderContext>(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<FilterProviderContext>()))
.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<ControllerActionInvoker>(),
@ -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<IValueProviderFactory> 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<IValueProviderFactory> valueProviderFactories,
int maxAllowedErrorsInModelState)
{
var controllerContext = new ControllerContext(actionContext)
{
ValueProviderFactories = valueProviderFactories.ToList()
};
controllerContext.ModelState.MaxAllowedErrors = maxAllowedErrorsInModelState;
return controllerContext;
}
}
private class TestControllerArgumentBinder : IControllerArgumentBinder