[Perf] Optimize controller action invoke

Fixes aspnet/MVC#3903
This commit is contained in:
mnltejaswini 2016-03-02 11:22:34 -08:00
parent 4f709bdbfd
commit 007c47d065
16 changed files with 519 additions and 168 deletions

View File

@ -129,7 +129,7 @@ namespace Microsoft.Extensions.DependencyInjection
// These are stateless
services.TryAddSingleton<IControllerActionArgumentBinder, ControllerArgumentBinder>();
services.TryAddSingleton<FilterCache>();
services.TryAddSingleton<ControllerActionInvokerCache>();
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IFilterProvider, DefaultFilterProvider>());

View File

@ -26,36 +26,25 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
public static Task<object> ExecuteAsync(
MethodInfo actionMethodInfo,
ObjectMethodExecutor actionMethodExecutor,
object instance,
IDictionary<string, object> actionArguments)
{
var orderedArguments = PrepareArguments(actionArguments, actionMethodInfo.GetParameters());
return ExecuteAsync(actionMethodInfo, instance, orderedArguments);
var orderedArguments = PrepareArguments(actionArguments, actionMethodExecutor.MethodInfo.GetParameters());
return ExecuteAsync(actionMethodExecutor, instance, orderedArguments);
}
public static Task<object> ExecuteAsync(
MethodInfo actionMethodInfo,
ObjectMethodExecutor actionMethodExecutor,
object instance,
object[] orderedActionArguments)
{
object invocationResult = null;
try
{
invocationResult = actionMethodInfo.Invoke(instance, orderedActionArguments);
}
catch (TargetInvocationException targetInvocationException)
{
// Capturing the exception and the original callstack and rethrow for external exception handlers.
var exceptionDispatchInfo = ExceptionDispatchInfo.Capture(targetInvocationException.InnerException);
exceptionDispatchInfo.Throw();
}
object invocationResult = actionMethodExecutor.Execute(instance, orderedActionArguments);
return CoerceResultToTaskAsync(
invocationResult,
actionMethodInfo.ReturnType,
actionMethodInfo.Name,
actionMethodInfo.DeclaringType);
actionMethodExecutor.MethodInfo.ReturnType,
actionMethodExecutor.MethodInfo.Name,
actionMethodExecutor.MethodInfo.DeclaringType);
}
// We need to CoerceResult as the object value returned from methodInfo.Invoke has to be cast to a Task<T>.

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public ControllerActionInvoker(
ActionContext actionContext,
FilterCache filterCache,
ControllerActionInvokerCache controllerActionInvokerCache,
IControllerFactory controllerFactory,
ControllerActionDescriptor descriptor,
IReadOnlyList<IInputFormatter> inputFormatters,
@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
int maxModelValidationErrors)
: base(
actionContext,
filterCache,
controllerActionInvokerCache,
inputFormatters,
modelBinders,
modelValidatorProviders,
@ -96,6 +96,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
var actionMethodInfo = _descriptor.MethodInfo;
var methodExecutor = GetControllerActionMethodExecutor();
var arguments = ControllerActionExecutor.PrepareArguments(
actionExecutingContext.ActionArguments,
actionMethodInfo.GetParameters());
@ -103,7 +106,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Logger.ActionMethodExecuting(actionExecutingContext, arguments);
var actionReturnValue = await ControllerActionExecutor.ExecuteAsync(
actionMethodInfo,
methodExecutor,
actionExecutingContext.Controller,
arguments);

View File

@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class FilterCache
public class ControllerActionInvokerCache
{
private readonly IFilterMetadata[] EmptyFilterArray = new IFilterMetadata[0];
@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private volatile InnerCache _currentCache;
public FilterCache(
public ControllerActionInvokerCache(
IActionDescriptorCollectionProvider collectionProvider,
IEnumerable<IFilterProvider> filterProviders)
{
@ -45,24 +45,26 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
public IFilterMetadata[] GetFilters(ActionContext actionContext)
public Entry GetCacheEntry(ControllerContext controllerContext)
{
var cache = CurrentCache;
var actionDescriptor = actionContext.ActionDescriptor;
var actionDescriptor = controllerContext.ActionDescriptor;
CacheEntry entry;
Entry entry;
if (cache.Entries.TryGetValue(actionDescriptor, out entry))
{
return GetFiltersFromEntry(entry, actionContext);
return entry;
}
var executor = ObjectMethodExecutor.Create(actionDescriptor.MethodInfo, actionDescriptor.ControllerTypeInfo);
var items = new List<FilterItem>(actionDescriptor.FilterDescriptors.Count);
for (var i = 0; i < actionDescriptor.FilterDescriptors.Count; i++)
{
items.Add(new FilterItem(actionDescriptor.FilterDescriptors[i]));
}
ExecuteProviders(actionContext, items);
ExecuteProviders(controllerContext, items);
var filters = ExtractFilters(items);
@ -79,30 +81,42 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (allFiltersCached)
{
entry = new CacheEntry(filters);
entry = new Entry(filters, null, executor);
}
else
{
entry = new CacheEntry(items);
entry = new Entry(null, items, executor);
}
cache.Entries.TryAdd(actionDescriptor, entry);
return filters;
return entry;
}
private IFilterMetadata[] GetFiltersFromEntry(CacheEntry entry, ActionContext actionContext)
public IFilterMetadata[] GetFilters(ControllerContext controllerContext)
{
Debug.Assert(entry.Filters != null || entry.Items != null);
var entry = GetCacheEntry(controllerContext);
return GetFiltersFromEntry(entry, controllerContext);
}
public ObjectMethodExecutor GetControllerActionMethodExecutor(ControllerContext controllerContext)
{
var entry = GetCacheEntry(controllerContext);
return entry.ActionMethodExecutor;
}
public IFilterMetadata[] GetFiltersFromEntry(Entry entry, ActionContext actionContext)
{
Debug.Assert(entry.Filters != null || entry.FilterItems != null);
if (entry.Filters != null)
{
return entry.Filters;
}
var items = new List<FilterItem>(entry.Items.Count);
for (var i = 0; i < entry.Items.Count; i++)
var items = new List<FilterItem>(entry.FilterItems.Count);
for (var i = 0; i < entry.FilterItems.Count; i++)
{
var item = entry.Items[i];
var item = entry.FilterItems[i];
if (item.IsReusable)
{
items.Add(item);
@ -172,29 +186,25 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Version = version;
}
public ConcurrentDictionary<ActionDescriptor, CacheEntry> Entries { get; } =
new ConcurrentDictionary<ActionDescriptor, CacheEntry>();
public ConcurrentDictionary<ActionDescriptor, Entry> Entries { get; } =
new ConcurrentDictionary<ActionDescriptor, Entry>();
public int Version { get; }
}
private struct CacheEntry
public struct Entry
{
public CacheEntry(IFilterMetadata[] filters)
public Entry(IFilterMetadata[] filters, List<FilterItem> items, ObjectMethodExecutor executor)
{
FilterItems = items;
Filters = filters;
Items = null;
ActionMethodExecutor = executor;
}
public CacheEntry(List<FilterItem> items)
{
Items = items;
Filters = null;
}
public IFilterMetadata[] Filters { get; }
public List<FilterItem> Items { get; }
public List<FilterItem> FilterItems { get; }
public ObjectMethodExecutor ActionMethodExecutor { get; }
}
}
}

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
private readonly IControllerActionArgumentBinder _argumentBinder;
private readonly IControllerFactory _controllerFactory;
private readonly FilterCache _filterCache;
private readonly ControllerActionInvokerCache _controllerActionInvokerCache;
private readonly IReadOnlyList<IInputFormatter> _inputFormatters;
private readonly IReadOnlyList<IModelBinder> _modelBinders;
private readonly IReadOnlyList<IModelValidatorProvider> _modelValidatorProviders;
@ -30,14 +30,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public ControllerActionInvokerProvider(
IControllerFactory controllerFactory,
FilterCache filterCache,
ControllerActionInvokerCache controllerActionInvokerCache,
IControllerActionArgumentBinder argumentBinder,
IOptions<MvcOptions> optionsAccessor,
ILoggerFactory loggerFactory,
DiagnosticSource diagnosticSource)
{
_controllerFactory = controllerFactory;
_filterCache = filterCache;
_controllerActionInvokerCache = controllerActionInvokerCache;
_argumentBinder = argumentBinder;
_inputFormatters = optionsAccessor.Value.InputFormatters.ToArray();
_modelBinders = optionsAccessor.Value.ModelBinders.ToArray();
@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
context.Result = new ControllerActionInvoker(
context.ActionContext,
_filterCache,
_controllerActionInvokerCache,
_controllerFactory,
actionDescriptor,
_inputFormatters,

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
public abstract class FilterActionInvoker : IActionInvoker
{
private readonly FilterCache _filterCache;
private readonly ControllerActionInvokerCache _controllerActionInvokerCache;
private readonly IReadOnlyList<IInputFormatter> _inputFormatters;
private readonly IReadOnlyList<IModelBinder> _modelBinders;
private readonly IReadOnlyList<IModelValidatorProvider> _modelValidatorProviders;
@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private readonly int _maxModelValidationErrors;
private IFilterMetadata[] _filters;
private ObjectMethodExecutor _controllerActionMethodExecutor;
private FilterCursor _cursor;
private AuthorizationFilterContext _authorizationContext;
@ -44,7 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public FilterActionInvoker(
ActionContext actionContext,
FilterCache filterCache,
ControllerActionInvokerCache controllerActionInvokerCache,
IReadOnlyList<IInputFormatter> inputFormatters,
IReadOnlyList<IModelBinder> modelBinders,
IReadOnlyList<IModelValidatorProvider> modelValidatorProviders,
@ -58,9 +59,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(actionContext));
}
if (filterCache == null)
if (controllerActionInvokerCache == null)
{
throw new ArgumentNullException(nameof(filterCache));
throw new ArgumentNullException(nameof(controllerActionInvokerCache));
}
if (inputFormatters == null)
@ -95,7 +96,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Context = new ControllerContext(actionContext);
_filterCache = filterCache;
_controllerActionInvokerCache = controllerActionInvokerCache;
_inputFormatters = inputFormatters;
_modelBinders = modelBinders;
_modelValidatorProviders = modelValidatorProviders;
@ -130,7 +131,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public virtual async Task InvokeAsync()
{
_filters = GetFilters();
var cacheEntry = _controllerActionInvokerCache.GetCacheEntry(Context);
_filters = _controllerActionInvokerCache.GetFiltersFromEntry(cacheEntry, Context);
_controllerActionMethodExecutor = cacheEntry.ActionMethodExecutor;
_cursor = new FilterCursor(_filters);
Context.ModelState.MaxAllowedErrors = _maxModelValidationErrors;
@ -177,9 +180,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
private IFilterMetadata[] GetFilters()
protected ObjectMethodExecutor GetControllerActionMethodExecutor()
{
return _filterCache.GetFilters(Context);
return _controllerActionMethodExecutor;
}
private Task InvokeAllAuthorizationFiltersAsync()

View File

@ -0,0 +1,91 @@
// 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.Linq.Expressions;
using System.Reflection;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ObjectMethodExecutor
{
private ActionExecutor _executor;
private ObjectMethodExecutor(MethodInfo methodInfo)
{
if (methodInfo == null)
{
throw new ArgumentNullException(nameof(methodInfo));
}
MethodInfo = methodInfo;
}
private delegate object ActionExecutor(object target, object[] parameters);
private delegate void VoidActionExecutor(object target, object[] parameters);
public MethodInfo MethodInfo { get; }
public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo)
{
var executor = new ObjectMethodExecutor(methodInfo);
executor._executor = GetExecutor(methodInfo, targetTypeInfo);
return executor;
}
public object Execute(object target, object[] parameters)
{
return _executor(target, parameters);
}
private static ActionExecutor GetExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo)
{
// Parameters to executor
var targetParameter = Expression.Parameter(typeof(object), "target");
var parametersParameter = Expression.Parameter(typeof(object[]), "parameters");
// Build parameter list
var parameters = new List<Expression>();
var paramInfos = methodInfo.GetParameters();
for (int i = 0; i < paramInfos.Length; i++)
{
var paramInfo = paramInfos[i];
var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i));
var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType);
// valueCast is "(Ti) parameters[i]"
parameters.Add(valueCast);
}
// Call method
var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType());
var methodCall = Expression.Call(instanceCast, methodInfo, parameters);
// methodCall is "((Ttarget) target) method((T0) parameters[0], (T1) parameters[1], ...)"
// Create function
if (methodCall.Type == typeof(void))
{
var lambda = Expression.Lambda<VoidActionExecutor>(methodCall, targetParameter, parametersParameter);
var voidExecutor = lambda.Compile();
return WrapVoidAction(voidExecutor);
}
else
{
// must coerce methodCall to match ActionExecutor signature
var castMethodCall = Expression.Convert(methodCall, typeof(object));
var lambda = Expression.Lambda<ActionExecutor>(castMethodCall, targetParameter, parametersParameter);
return lambda.Compile();
}
}
private static ActionExecutor WrapVoidAction(VoidActionExecutor executor)
{
return delegate (object target, object[] parameters)
{
executor(target, parameters);
return null;
};
}
}
}

View File

@ -127,6 +127,7 @@ namespace Microsoft.Extensions.DependencyInjection
IViewComponentDescriptorCollectionProvider,
DefaultViewComponentDescriptorCollectionProvider>();
services.TryAddSingleton<ViewComponentInvokerCache>();
services.TryAddTransient<IViewComponentDescriptorProvider, DefaultViewComponentDescriptorProvider>();
services.TryAddSingleton<IViewComponentInvokerFactory, DefaultViewComponentInvokerFactory>();
services.TryAddTransient<IViewComponentHelper, DefaultViewComponentHelper>();

View File

@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
public class DefaultViewComponentInvoker : IViewComponentInvoker
{
private readonly IViewComponentFactory _viewComponentFactory;
private readonly ViewComponentInvokerCache _viewComponentInvokerCache;
private readonly DiagnosticSource _diagnosticSource;
private readonly ILogger _logger;
@ -27,10 +28,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
/// Initializes a new instance of <see cref="DefaultViewComponentInvoker"/>.
/// </summary>
/// <param name="viewComponentFactory">The <see cref="IViewComponentFactory"/>.</param>
/// <param name="viewComponentInvokerCache">The <see cref="ViewComponentInvokerCache"/>.</param>
/// <param name="diagnosticSource">The <see cref="DiagnosticSource"/>.</param>
/// <param name="logger">The <see cref="ILogger"/>.</param>
public DefaultViewComponentInvoker(
IViewComponentFactory viewComponentFactory,
ViewComponentInvokerCache viewComponentInvokerCache,
DiagnosticSource diagnosticSource,
ILogger logger)
{
@ -39,6 +42,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
throw new ArgumentNullException(nameof(viewComponentFactory));
}
if (viewComponentInvokerCache == null)
{
throw new ArgumentNullException(nameof(viewComponentInvokerCache));
}
if (diagnosticSource == null)
{
throw new ArgumentNullException(nameof(diagnosticSource));
@ -50,6 +58,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
}
_viewComponentFactory = viewComponentFactory;
_viewComponentInvokerCache = viewComponentInvokerCache;
_diagnosticSource = diagnosticSource;
_logger = logger;
}
@ -95,11 +104,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
var method = context.ViewComponentDescriptor.MethodInfo;
var arguments = ControllerActionExecutor.PrepareArguments(context.Arguments, method.GetParameters());
var methodExecutor = _viewComponentInvokerCache.GetViewComponentMethodExecutor(context);
_diagnosticSource.BeforeViewComponent(context, component);
_logger.ViewComponentExecuting(context, arguments);
var startTimestamp = _logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : 0;
var result = await ControllerActionExecutor.ExecuteAsync(method, component, arguments);
var result = await ControllerActionExecutor.ExecuteAsync(methodExecutor, component, arguments);
var viewComponentResult = CoerceToViewComponentResult(result);
_logger.ViewComponentExecuted(context, startTimestamp, viewComponentResult);
@ -121,6 +132,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
var arguments = ControllerActionExecutor.PrepareArguments(
context.Arguments,
method.GetParameters());
var methodExecutor = _viewComponentInvokerCache.GetViewComponentMethodExecutor(context);
_diagnosticSource.BeforeViewComponent(context, component);
_logger.ViewComponentExecuting(context, arguments);
@ -129,16 +141,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
object result;
try
{
result = method.Invoke(component, arguments);
result = methodExecutor.Execute(component, arguments);
}
catch (TargetInvocationException ex)
finally
{
_viewComponentFactory.ReleaseViewComponent(context, component);
// Preserve callstack of any user-thrown exceptions.
var exceptionInfo = ExceptionDispatchInfo.Capture(ex.InnerException);
exceptionInfo.Throw();
return null; // Unreachable
}
var viewComponentResult = CoerceToViewComponentResult(result);

View File

@ -3,7 +3,7 @@
using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.ViewComponents
@ -11,11 +11,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
public class DefaultViewComponentInvokerFactory : IViewComponentInvokerFactory
{
private readonly IViewComponentFactory _viewComponentFactory;
private readonly ViewComponentInvokerCache _viewComponentInvokerCache;
private readonly ILogger _logger;
private readonly DiagnosticSource _diagnosticSource;
public DefaultViewComponentInvokerFactory(
IViewComponentFactory viewComponentFactory,
ViewComponentInvokerCache viewComponentInvokerCache,
DiagnosticSource diagnosticSource,
ILoggerFactory loggerFactory)
{
@ -24,6 +26,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
throw new ArgumentNullException(nameof(viewComponentFactory));
}
if (viewComponentInvokerCache == null)
{
throw new ArgumentNullException(nameof(viewComponentInvokerCache));
}
if (diagnosticSource == null)
{
throw new ArgumentNullException(nameof(diagnosticSource));
@ -36,6 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
_viewComponentFactory = viewComponentFactory;
_diagnosticSource = diagnosticSource;
_viewComponentInvokerCache = viewComponentInvokerCache;
_logger = loggerFactory.CreateLogger<DefaultViewComponentInvoker>();
}
@ -53,6 +61,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
return new DefaultViewComponentInvoker(
_viewComponentFactory,
_viewComponentInvokerCache,
_diagnosticSource,
_logger);
}

View File

@ -0,0 +1,68 @@
// 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.Collections.Concurrent;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ViewComponents;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class ViewComponentInvokerCache
{
private readonly IViewComponentDescriptorCollectionProvider _collectionProvider;
private volatile InnerCache _currentCache;
public ViewComponentInvokerCache(IViewComponentDescriptorCollectionProvider collectionProvider)
{
_collectionProvider = collectionProvider;
}
private InnerCache CurrentCache
{
get
{
var current = _currentCache;
var actionDescriptors = _collectionProvider.ViewComponents;
if (current == null || current.Version != actionDescriptors.Version)
{
current = new InnerCache(actionDescriptors.Version);
_currentCache = current;
}
return current;
}
}
public ObjectMethodExecutor GetViewComponentMethodExecutor(ViewComponentContext viewComponentContext)
{
var cache = CurrentCache;
var viewComponentDescriptor = viewComponentContext.ViewComponentDescriptor;
ObjectMethodExecutor executor;
if (cache.Entries.TryGetValue(viewComponentDescriptor, out executor))
{
return executor;
}
executor = ObjectMethodExecutor.Create(viewComponentDescriptor.MethodInfo, viewComponentDescriptor.TypeInfo);
cache.Entries.TryAdd(viewComponentDescriptor, executor);
return executor;
}
private class InnerCache
{
public InnerCache(int version)
{
Version = version;
}
public ConcurrentDictionary<ViewComponentDescriptor, ObjectMethodExecutor> Entries { get; } =
new ConcurrentDictionary<ViewComponentDescriptor, ObjectMethodExecutor>();
public int Version { get; }
}
}
}

View File

@ -32,22 +32,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private delegate dynamic ReturnTaskAsDynamicValue(int i, string s);
[Fact]
public async Task AsyncAction_WithVoidReturnType()
{
// Arrange
var methodWithVoidReturnType = new MethodWithVoidReturnType(TestController.VoidAction);
// Act
var result = await ControllerActionExecutor.ExecuteAsync(
methodWithVoidReturnType.GetMethodInfo(),
null,
(IDictionary<string, object>)null);
// Assert
Assert.Same(null, result);
}
[Fact]
public async Task AsyncAction_TaskReturnType()
{
@ -57,12 +41,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var actionParameters = new Dictionary<string, object> { { "i", inputParam1 }, { "s", inputParam2 } };
var methodWithTaskReturnType = new MethodWithTaskReturnType(_controller.TaskAction);
// Act
var result = await ControllerActionExecutor.ExecuteAsync(
methodWithTaskReturnType.GetMethodInfo(),
_controller,
actionParameters);
var result = await ExecuteAction(
methodWithTaskReturnType,
_controller,
actionParameters);
// Assert
Assert.Same(null, result);
@ -79,11 +61,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var methodWithTaskOfIntReturnType = new MethodWithTaskOfIntReturnType(_controller.TaskValueTypeAction);
// Act
var result = await ControllerActionExecutor.ExecuteAsync(
methodWithTaskOfIntReturnType.GetMethodInfo(),
_controller,
actionParameters);
var result = await ExecuteAction(
methodWithTaskOfIntReturnType,
_controller,
actionParameters);
// Assert
Assert.Equal(inputParam1, result);
}
@ -99,10 +80,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var methodWithTaskOfTaskOfIntReturnType = new MethodWithTaskOfTaskOfIntReturnType(_controller.TaskOfTaskAction);
// Act
var result = await (Task<int>)(await ControllerActionExecutor.ExecuteAsync(
methodWithTaskOfTaskOfIntReturnType.GetMethodInfo(),
_controller,
actionParameters));
var result = await (Task<int>)( await ExecuteAction(
methodWithTaskOfTaskOfIntReturnType,
_controller,
actionParameters));
// Assert
Assert.Equal(inputParam1, result);
@ -120,9 +101,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Act and Assert
await Assert.ThrowsAsync<NotImplementedException>(
() => ControllerActionExecutor.ExecuteAsync(methodWithTaskOfIntReturnType.GetMethodInfo(),
_controller,
actionParameters));
() => ExecuteAction(
methodWithTaskOfIntReturnType,
_controller,
actionParameters));
}
[Fact]
@ -137,9 +119,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Act & Assert
await Assert.ThrowsAsync<NotImplementedException>(
() => ControllerActionExecutor.ExecuteAsync(methodWithTaskOfIntReturnType.GetMethodInfo(),
_controller,
actionParameters));
() => ExecuteAction(
methodWithTaskOfIntReturnType,
_controller,
actionParameters));
}
[Fact]
@ -155,8 +138,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Act & Assert
var ex = await Assert.ThrowsAsync<ArgumentException>(
() => ControllerActionExecutor.ExecuteAsync(
methodWithTaskOfIntReturnType.GetMethodInfo(),
() => ExecuteAction(
methodWithTaskOfIntReturnType,
_controller,
actionParameters));
Assert.Equal(expectedException, ex.Message);
@ -170,11 +153,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var syncMethod = new SyncMethod(_controller.Echo);
// Act
var result = await ControllerActionExecutor.ExecuteAsync(
syncMethod.GetMethodInfo(),
_controller,
new Dictionary<string, object>() { { "input", inputString } });
var result = await ExecuteAction(
syncMethod,
_controller,
new Dictionary<string, object>() { { "input", inputString } });
// Assert
Assert.Equal(inputString, result);
}
@ -188,10 +170,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Act & Assert
await Assert.ThrowsAsync<NotImplementedException>(
() => ControllerActionExecutor.ExecuteAsync(
syncMethod.GetMethodInfo(),
_controller,
new Dictionary<string, object>() { { "input", inputString } }));
() => ExecuteAction(
syncMethod,
_controller,
new Dictionary<string, object>() { { "input", inputString } }));
}
[Fact]
@ -201,8 +183,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var syncMethod = new SyncMethod(_controller.EchoWithDefaultValue);
// Act
var result = await ControllerActionExecutor.ExecuteAsync(
syncMethod.GetMethodInfo(),
var result = await ExecuteAction(
syncMethod,
_controller,
new Dictionary<string, object>());
@ -217,8 +199,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var syncMethod = new SyncMethod(_controller.EchoWithDefaultValue);
// Act
var result = await ControllerActionExecutor.ExecuteAsync(
syncMethod.GetMethodInfo(),
var result = await ExecuteAction(
syncMethod,
_controller,
new object[] { null, });
@ -233,8 +215,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var syncMethod = new SyncMethod(_controller.EchoWithDefaultValueAndAttribute);
// Act
var result = await ControllerActionExecutor.ExecuteAsync(
syncMethod.GetMethodInfo(),
var result = await ExecuteAction(
syncMethod,
_controller,
new Dictionary<string, object>());
@ -249,8 +231,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var syncMethod = new SyncMethod(_controller.EchoWithDefaultValueAndAttribute);
// Act
var result = await ControllerActionExecutor.ExecuteAsync(
syncMethod.GetMethodInfo(),
var result = await ExecuteAction(
syncMethod,
_controller,
new Dictionary<string, object>() { { "input", null } });
@ -276,8 +258,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => ControllerActionExecutor.ExecuteAsync(
methodWithCutomTaskReturnType.GetMethodInfo(),
() => ExecuteAction(
methodWithCutomTaskReturnType,
_controller,
actionParameters));
Assert.Equal(expectedException, ex.Message);
@ -299,8 +281,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => ControllerActionExecutor.ExecuteAsync(
methodWithCutomTaskOfTReturnType.GetMethodInfo(),
() => ExecuteAction(
methodWithCutomTaskOfTReturnType,
_controller,
actionParameters));
Assert.Equal(expectedException, ex.Message);
@ -325,8 +307,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => ControllerActionExecutor.ExecuteAsync(
methodWithUnwrappedTask.GetMethodInfo(),
() => ExecuteAction(
methodWithUnwrappedTask,
_controller,
actionParameters));
Assert.Equal(expectedException, ex.Message);
@ -348,10 +330,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => ControllerActionExecutor.ExecuteAsync(
dynamicTaskMethod.GetMethodInfo(),
_controller,
actionParameters));
() => ExecuteAction(
dynamicTaskMethod,
_controller,
actionParameters));
Assert.Equal(expectedException, ex.Message);
}
@ -367,10 +349,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var methodWithTaskOfIntReturnType = new MethodWithTaskOfIntReturnType(_controller.TaskValueTypeAction);
// Act
var result = await ControllerActionExecutor.ExecuteAsync(
methodWithTaskOfIntReturnType.GetMethodInfo(),
_controller,
actionParameters);
var result = await ExecuteAction(
methodWithTaskOfIntReturnType,
_controller,
actionParameters);
// Assert
Assert.Equal(inputParam1, result);
@ -383,32 +365,48 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var inputParam2 = "Second Parameter";
var actionParameters = new Dictionary<string, object> { { "i", "Some Invalid Value" }, { "s", inputParam2 } };
var methodWithTaskOfIntReturnType = new MethodWithTaskOfIntReturnType(_controller.TaskValueTypeAction);
var message = TestPlatformHelper.IsMono ? "Object type {0} cannot be converted to target type: {1}" :
"Object of type '{0}' cannot be converted to type '{1}'.";
var expectedException = string.Format(
CultureInfo.CurrentCulture,
message,
typeof(string),
typeof(int));
var methodWithTaskOfIntReturnType = new MethodWithTaskOfIntReturnType(_controller.TaskValueTypeAction);
// Act & Assert
// If it is an unrecognized derived type we throw an InvalidOperationException.
var ex = await Assert.ThrowsAsync<ArgumentException>(
() => ControllerActionExecutor.ExecuteAsync(
methodWithTaskOfIntReturnType.GetMethodInfo(),
_controller,
actionParameters));
var ex = await Assert.ThrowsAsync<InvalidCastException>(
() => ExecuteAction(
methodWithTaskOfIntReturnType,
_controller,
actionParameters));
}
Assert.Equal(expectedException, ex.Message);
private async Task<object> ExecuteAction(
Delegate methodDelegate,
TestController controller,
IDictionary<string, object> actionParameters)
{
var executor = ObjectMethodExecutor.Create(methodDelegate.GetMethodInfo(), _controller.GetType().GetTypeInfo());
var result = await ControllerActionExecutor.ExecuteAsync(
executor,
controller,
actionParameters);
return result;
}
private async Task<object> ExecuteAction(
Delegate methodDelegate,
TestController controller,
object[] actionParameters)
{
var executor = ObjectMethodExecutor.Create(methodDelegate.GetMethodInfo(), _controller.GetType().GetTypeInfo());
var result = await ControllerActionExecutor.ExecuteAsync(
executor,
controller,
actionParameters);
return result;
}
public class TestController
{
public static void VoidAction()
{
}
#pragma warning disable 1998
public async Task TaskAction(int i, string s)
{

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Reflection;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
@ -12,7 +13,7 @@ using Xunit;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class FilterCacheTest
public class ControllerActionInvokerCacheTest
{
[Fact]
public void GetFilters_CachesAllFilters()
@ -28,9 +29,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
new FilterDescriptor(new TestFilter(), FilterScope.Action),
new FilterDescriptor(new TestFilter(), FilterScope.Action),
},
MethodInfo = typeof(ControllerActionInvokerCache).GetMethod(
nameof(ControllerActionInvokerCache.GetControllerActionMethodExecutor)),
ControllerTypeInfo = typeof(ControllerActionInvokerCache).GetTypeInfo()
};
var context = new ActionContext(new DefaultHttpContext(), new RouteData(), action);
var context = new ControllerContext(new ActionContext(
new DefaultHttpContext(),
new RouteData(),
action));
// Act - 1
var filters1 = cache.GetFilters(context);
@ -66,9 +73,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
new FilterDescriptor(new TestFilterFactory() { IsReusable = true }, FilterScope.Action),
new FilterDescriptor(new TestFilter(), FilterScope.Action),
},
MethodInfo = typeof(ControllerActionInvokerCache).GetMethod(
nameof(ControllerActionInvokerCache.GetControllerActionMethodExecutor)),
ControllerTypeInfo = typeof(ControllerActionInvokerCache).GetTypeInfo()
};
var context = new ActionContext(new DefaultHttpContext(), new RouteData(), action);
var context = new ControllerContext(new ActionContext(
new DefaultHttpContext(),
new RouteData(),
action));
// Act - 1
var filters1 = cache.GetFilters(context);
@ -104,9 +117,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
new FilterDescriptor(new TestFilterFactory() { IsReusable = false }, FilterScope.Action),
new FilterDescriptor(new TestFilter(), FilterScope.Action),
},
MethodInfo = typeof(ControllerActionInvokerCache).GetMethod(
nameof(ControllerActionInvokerCache.GetControllerActionMethodExecutor)),
ControllerTypeInfo = typeof(ControllerActionInvokerCache).GetTypeInfo()
};
var context = new ActionContext(new DefaultHttpContext(), new RouteData(), action);
var context = new ControllerContext(new ActionContext(
new DefaultHttpContext(),
new RouteData(),
action));
// Act - 1
var filters1 = cache.GetFilters(context);
@ -128,6 +147,46 @@ namespace Microsoft.AspNetCore.Mvc.Internal
f => Assert.Same(filters1[1], f)); // Cached
}
[Fact]
public void GetControllerActionMethodExecutor_CachesExecutor()
{
// Arrange
var services = CreateServices();
var cache = CreateCache(new DefaultFilterProvider());
var action = new ControllerActionDescriptor()
{
FilterDescriptors = new[]
{
new FilterDescriptor(new TestFilterFactory() { IsReusable = false }, FilterScope.Action),
new FilterDescriptor(new TestFilter(), FilterScope.Action),
},
MethodInfo = typeof(ControllerActionInvokerCache).GetMethod(
nameof(ControllerActionInvokerCache.GetControllerActionMethodExecutor)),
ControllerTypeInfo = typeof(ControllerActionInvokerCache).GetTypeInfo()
};
var context = new ControllerContext(
new ActionContext(new DefaultHttpContext(),
new RouteData(),
action));
// Act - 1
var executor1 = cache.GetControllerActionMethodExecutor(context);
Assert.NotNull(executor1);
var filters1 = cache.GetFilters(context);
Assert.NotNull(filters1);
// Act - 2
var executor2 = cache.GetControllerActionMethodExecutor(context);
Assert.Same(executor1, executor2);
}
private class TestFilter : IFilterMetadata
{
}
@ -147,11 +206,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return new ServiceCollection().BuildServiceProvider();
}
private static FilterCache CreateCache(params IFilterProvider[] providers)
private static ControllerActionInvokerCache CreateCache(params IFilterProvider[] providers)
{
var services = CreateServices();
var descriptorProvider = new ActionDescriptorCollectionProvider(services);
return new FilterCache(descriptorProvider, providers);
return new ControllerActionInvokerCache(descriptorProvider, providers);
}
}
}

View File

@ -1977,6 +1977,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
actionDescriptor.MethodInfo = typeof(ControllerActionInvokerTest).GetMethod("ActionMethod");
}
actionDescriptor.ControllerTypeInfo = typeof(ControllerActionInvokerTest).GetTypeInfo();
var httpContext = new Mock<HttpContext>(MockBehavior.Loose);
@ -2224,11 +2225,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
private static FilterCache CreateFilterCache(IFilterProvider[] filterProviders = null)
private static ControllerActionInvokerCache CreateFilterCache(IFilterProvider[] filterProviders = null)
{
var services = new ServiceCollection().BuildServiceProvider();
var descriptorProvider = new ActionDescriptorCollectionProvider(services);
return new FilterCache(descriptorProvider, filterProviders.AsEnumerable() ?? new List<IFilterProvider>());
return new ControllerActionInvokerCache(descriptorProvider, filterProviders.AsEnumerable() ?? new List<IFilterProvider>());
}
private class TestControllerActionInvoker : ControllerActionInvoker

View File

@ -0,0 +1,111 @@
// 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.ComponentModel;
using System.Globalization;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ObjectMethodExecutorTest
{
private TestObject _targetObject = new TestObject();
private TypeInfo targetTypeInfo = typeof(TestObject).GetTypeInfo();
[Fact]
public void ExecuteValueMethod()
{
var executor = GetExecutorForMethod("ValueMethod");
var result = executor.Execute(
_targetObject,
new object[] { 10 , 20 });
Assert.Equal(30, (int)result);
}
[Fact]
public void ExecuteVoidValueMethod()
{
var executor = GetExecutorForMethod("VoidValueMethod");
var result = executor.Execute(
_targetObject,
new object[] { 10 });
Assert.Same(null, result);
}
[Fact]
public void ExecuteValueMethodWithReturnType()
{
var executor = GetExecutorForMethod("ValueMethodWithReturnType");
var result = executor.Execute(
_targetObject,
new object[] { 10 });
var resultObject = Assert.IsType<TestObject>(result);
Assert.Equal("Hello", resultObject.value);
}
[Fact]
public void ExecuteValueMethodUpdateValue()
{
var executor = GetExecutorForMethod("ValueMethodUpdateValue");
var parameter = new TestObject();
var result = executor.Execute(
_targetObject,
new object[] { parameter });
var resultObject = Assert.IsType<TestObject>(result);
Assert.Equal("HelloWorld", resultObject.value);
}
[Fact]
public void ExecuteValueMethodWithReturnTypeThrowsException()
{
var executor = GetExecutorForMethod("ValueMethodWithReturnTypeThrowsException");
var parameter = new TestObject();
Assert.Throws<NotImplementedException>(
() => executor.Execute(
_targetObject,
new object[] { parameter }));
}
private ObjectMethodExecutor GetExecutorForMethod(string methodName)
{
var method = typeof(TestObject).GetMethod(methodName);
var executor = ObjectMethodExecutor.Create(method, targetTypeInfo);
return executor;
}
public class TestObject
{
public string value;
public int ValueMethod(int i, int j)
{
return i+j;
}
public void VoidValueMethod(int i)
{
}
public TestObject ValueMethodWithReturnType(int i)
{
return new TestObject() { value = "Hello" }; ;
}
public TestObject ValueMethodWithReturnTypeThrowsException(TestObject i)
{
throw new NotImplementedException("Not Implemented Exception");
}
public TestObject ValueMethodUpdateValue(TestObject parameter)
{
parameter.value = "HelloWorld";
return parameter;
}
}
}
}

View File

@ -518,6 +518,7 @@ namespace Microsoft.AspNetCore.Mvc
var services = new ServiceCollection();
services.AddSingleton<DiagnosticSource>(diagnosticSource);
services.AddSingleton<ViewComponentInvokerCache>();
services.AddSingleton<IOptions<MvcViewOptions>, TestOptionsManager<MvcViewOptions>>();
services.AddTransient<IViewComponentHelper, DefaultViewComponentHelper>();
services.AddSingleton<IViewComponentSelector, DefaultViewComponentSelector>();