From 007c47d065a79c3145eb760c6af555411e8b5130 Mon Sep 17 00:00:00 2001 From: mnltejaswini Date: Wed, 2 Mar 2016 11:22:34 -0800 Subject: [PATCH] [Perf] Optimize controller action invoke Fixes aspnet/MVC#3903 --- .../MvcCoreServiceCollectionExtensions.cs | 2 +- .../Internal/ControllerActionExecutor.cs | 27 +-- .../Internal/ControllerActionInvoker.cs | 9 +- ...che.cs => ControllerActionInvokerCache.cs} | 66 ++++--- .../ControllerActionInvokerProvider.cs | 8 +- .../Internal/FilterActionInvoker.cs | 19 +- .../Internal/ObjectMethodExecutor.cs | 91 +++++++++ ...MvcViewFeaturesMvcCoreBuilderExtensions.cs | 1 + .../DefaultViewComponentInvoker.cs | 23 ++- .../DefaultViewComponentInvokerFactory.cs | 11 +- .../ViewComponentInvokerCache.cs | 68 +++++++ .../Internal/ControllerActionExecutorTests.cs | 174 +++++++++--------- ...cs => ControllerActionInvokerCacheTest.cs} | 71 ++++++- .../Internal/ControllerActionInvokerTest.cs | 5 +- .../Internal/ObjectMethodExecutorTest.cs | 111 +++++++++++ .../ViewComponentResultTest.cs | 1 + 16 files changed, 519 insertions(+), 168 deletions(-) rename src/Microsoft.AspNetCore.Mvc.Core/Internal/{FilterCache.cs => ControllerActionInvokerCache.cs} (71%) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectMethodExecutor.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentInvokerCache.cs rename test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/{FilterCacheTest.cs => ControllerActionInvokerCacheTest.cs} (62%) create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ObjectMethodExecutorTest.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index b0454b4410..e226f08472 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -129,7 +129,7 @@ namespace Microsoft.Extensions.DependencyInjection // These are stateless services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddEnumerable( ServiceDescriptor.Singleton()); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionExecutor.cs index 3a680d29cf..e3fc64041f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionExecutor.cs @@ -26,36 +26,25 @@ namespace Microsoft.AspNetCore.Mvc.Internal } public static Task ExecuteAsync( - MethodInfo actionMethodInfo, + ObjectMethodExecutor actionMethodExecutor, object instance, IDictionary 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 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. diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs index 38c2da8966..c81b3b7996 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal public ControllerActionInvoker( ActionContext actionContext, - FilterCache filterCache, + ControllerActionInvokerCache controllerActionInvokerCache, IControllerFactory controllerFactory, ControllerActionDescriptor descriptor, IReadOnlyList 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); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCache.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs similarity index 71% rename from src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCache.cs rename to src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs index 21dbd321a0..7ac32a063f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs @@ -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 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(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(entry.Items.Count); - for (var i = 0; i < entry.Items.Count; i++) + var items = new List(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 Entries { get; } = - new ConcurrentDictionary(); + public ConcurrentDictionary Entries { get; } = + new ConcurrentDictionary(); public int Version { get; } } - private struct CacheEntry + public struct Entry { - public CacheEntry(IFilterMetadata[] filters) + public Entry(IFilterMetadata[] filters, List items, ObjectMethodExecutor executor) { + FilterItems = items; Filters = filters; - Items = null; + ActionMethodExecutor = executor; } - - public CacheEntry(List items) - { - Items = items; - Filters = null; - } - public IFilterMetadata[] Filters { get; } - public List Items { get; } + public List FilterItems { get; } + + public ObjectMethodExecutor ActionMethodExecutor { get; } } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerProvider.cs index 2ac06e5a04..f5ce2d1810 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerProvider.cs @@ -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 _inputFormatters; private readonly IReadOnlyList _modelBinders; private readonly IReadOnlyList _modelValidatorProviders; @@ -30,14 +30,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal public ControllerActionInvokerProvider( IControllerFactory controllerFactory, - FilterCache filterCache, + ControllerActionInvokerCache controllerActionInvokerCache, IControllerActionArgumentBinder argumentBinder, IOptions 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, diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterActionInvoker.cs index 6bca1a41cb..3a9ab93d62 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterActionInvoker.cs @@ -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 _inputFormatters; private readonly IReadOnlyList _modelBinders; private readonly IReadOnlyList _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 inputFormatters, IReadOnlyList modelBinders, IReadOnlyList 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() diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectMethodExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectMethodExecutor.cs new file mode 100644 index 0000000000..b2e98fb717 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectMethodExecutor.cs @@ -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(); + 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(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(castMethodCall, targetParameter, parametersParameter); + return lambda.Compile(); + } + } + + private static ActionExecutor WrapVoidAction(VoidActionExecutor executor) + { + return delegate (object target, object[] parameters) + { + executor(target, parameters); + return null; + }; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs index cc9a3b30e4..b6f1ae818e 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs @@ -127,6 +127,7 @@ namespace Microsoft.Extensions.DependencyInjection IViewComponentDescriptorCollectionProvider, DefaultViewComponentDescriptorCollectionProvider>(); + services.TryAddSingleton(); services.TryAddTransient(); services.TryAddSingleton(); services.TryAddTransient(); diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvoker.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvoker.cs index ff9dcfdbbc..4b5d1da067 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvoker.cs @@ -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 . /// /// The . + /// The . /// The . /// The . 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); diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvokerFactory.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvokerFactory.cs index 24ea02c26d..1dd5d172e6 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvokerFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvokerFactory.cs @@ -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(); } @@ -53,6 +61,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents return new DefaultViewComponentInvoker( _viewComponentFactory, + _viewComponentInvokerCache, _diagnosticSource, _logger); } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentInvokerCache.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentInvokerCache.cs new file mode 100644 index 0000000000..285312c5bc --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentInvokerCache.cs @@ -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 Entries { get; } = + new ConcurrentDictionary(); + + public int Version { get; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionExecutorTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionExecutorTests.cs index b830be2867..bf1d49155a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionExecutorTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionExecutorTests.cs @@ -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)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 { { "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)(await ControllerActionExecutor.ExecuteAsync( - methodWithTaskOfTaskOfIntReturnType.GetMethodInfo(), - _controller, - actionParameters)); + var result = await (Task)( 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( - () => 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( - () => 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( - () => 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() { { "input", inputString } }); - + var result = await ExecuteAction( + syncMethod, + _controller, + new Dictionary() { { "input", inputString } }); // Assert Assert.Equal(inputString, result); } @@ -188,10 +170,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Act & Assert await Assert.ThrowsAsync( - () => ControllerActionExecutor.ExecuteAsync( - syncMethod.GetMethodInfo(), - _controller, - new Dictionary() { { "input", inputString } })); + () => ExecuteAction( + syncMethod, + _controller, + new Dictionary() { { "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()); @@ -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()); @@ -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() { { "input", null } }); @@ -276,8 +258,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Act & Assert var ex = await Assert.ThrowsAsync( - () => 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( - () => 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( - () => 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( - () => 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 { { "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( - () => ControllerActionExecutor.ExecuteAsync( - methodWithTaskOfIntReturnType.GetMethodInfo(), - _controller, - actionParameters)); + var ex = await Assert.ThrowsAsync( + () => ExecuteAction( + methodWithTaskOfIntReturnType, + _controller, + actionParameters)); + } - Assert.Equal(expectedException, ex.Message); + private async Task ExecuteAction( + Delegate methodDelegate, + TestController controller, + IDictionary actionParameters) + { + var executor = ObjectMethodExecutor.Create(methodDelegate.GetMethodInfo(), _controller.GetType().GetTypeInfo()); + + var result = await ControllerActionExecutor.ExecuteAsync( + executor, + controller, + actionParameters); + + return result; + } + + private async Task 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) { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/FilterCacheTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerCacheTest.cs similarity index 62% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/FilterCacheTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerCacheTest.cs index f247d40110..a21c0285c4 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/FilterCacheTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerCacheTest.cs @@ -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); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs index 4defb687e0..512bad203b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs @@ -1977,6 +1977,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal { actionDescriptor.MethodInfo = typeof(ControllerActionInvokerTest).GetMethod("ActionMethod"); } + actionDescriptor.ControllerTypeInfo = typeof(ControllerActionInvokerTest).GetTypeInfo(); var httpContext = new Mock(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()); + return new ControllerActionInvokerCache(descriptorProvider, filterProviders.AsEnumerable() ?? new List()); } private class TestControllerActionInvoker : ControllerActionInvoker diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ObjectMethodExecutorTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ObjectMethodExecutorTest.cs new file mode 100644 index 0000000000..9c2f5142e0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ObjectMethodExecutorTest.cs @@ -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(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(result); + Assert.Equal("HelloWorld", resultObject.value); + } + + [Fact] + public void ExecuteValueMethodWithReturnTypeThrowsException() + { + var executor = GetExecutorForMethod("ValueMethodWithReturnTypeThrowsException"); + var parameter = new TestObject(); + Assert.Throws( + () => 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; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs index 3b2bd85a75..c7edc18b2e 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs @@ -518,6 +518,7 @@ namespace Microsoft.AspNetCore.Mvc var services = new ServiceCollection(); services.AddSingleton(diagnosticSource); + services.AddSingleton(); services.AddSingleton, TestOptionsManager>(); services.AddTransient(); services.AddSingleton();