diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionMethodExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionMethodExecutor.cs new file mode 100644 index 0000000000..d0e3991d3c --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionMethodExecutor.cs @@ -0,0 +1,211 @@ +// 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.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Mvc.Internal +{ + internal abstract class ActionMethodExecutor + { + private static readonly ActionMethodExecutor[] Executors = new ActionMethodExecutor[] + { + // Executors for sync methods + new VoidResultExecutor(), + new SyncActionResultExecutor(), + new SyncObjectResultExecutor(), + + // Executors for async methods + new AwaitableResultExecutor(), + new TaskResultExecutor(), + new TaskOfIActionResultExecutor(), + new TaskOfActionResultExecutor(), + new AwaitableObjectResultExecutor(), + }; + + public abstract ValueTask Execute(ObjectMethodExecutor executor, object controller, object[] arguments); + + protected abstract bool CanExecute(ObjectMethodExecutor executor); + + public static ActionMethodExecutor GetExecutor(ObjectMethodExecutor executor) + { + for (var i = 0; i < Executors.Length; i++) + { + if (Executors[i].CanExecute(executor)) + { + return Executors[i]; + } + } + + Debug.Fail("Should not get here"); + throw new Exception(); + } + + // void LogMessage(..) + private class VoidResultExecutor : ActionMethodExecutor + { + public override ValueTask Execute(ObjectMethodExecutor executor, object controller, object[] arguments) + { + executor.Execute(controller, arguments); + return new ValueTask(new EmptyResult()); + } + + protected override bool CanExecute(ObjectMethodExecutor executor) + => !executor.IsMethodAsync && executor.MethodReturnType == typeof(void); + } + + // IActionResult Post(..) + // CreatedAtResult Put(..) + private class SyncActionResultExecutor : ActionMethodExecutor + { + public override ValueTask Execute(ObjectMethodExecutor executor, object controller, object[] arguments) + { + var actionResult = (IActionResult)executor.Execute(controller, arguments); + EnsureActionResultNotNull(executor, actionResult); + + return new ValueTask(actionResult); + } + + protected override bool CanExecute(ObjectMethodExecutor executor) + => !executor.IsMethodAsync && typeof(IActionResult).IsAssignableFrom(executor.MethodReturnType); + } + + // Person GetPerson(..) + // object Index(..) + private class SyncObjectResultExecutor : ActionMethodExecutor + { + public override ValueTask Execute(ObjectMethodExecutor executor, object controller, object[] arguments) + { + // Sync method returning arbitrary object + var returnValue = executor.Execute(controller, arguments); + var actionResult = ConvertToActionResult(returnValue, executor.MethodReturnType); + return new ValueTask(actionResult); + } + + // Catch-all for sync methods + protected override bool CanExecute(ObjectMethodExecutor executor) => !executor.IsMethodAsync; + } + + // Task SaveState(..) + private class TaskResultExecutor : ActionMethodExecutor + { + public override async ValueTask Execute(ObjectMethodExecutor executor, object controller, object[] arguments) + { + await (Task)executor.Execute(controller, arguments); + return new EmptyResult(); + } + + protected override bool CanExecute(ObjectMethodExecutor executor) => executor.MethodReturnType == typeof(Task); + } + + // CustomAsync PerformActionAsync(..) + // Custom task-like type with no return value. + private class AwaitableResultExecutor : ActionMethodExecutor + { + public override async ValueTask Execute(ObjectMethodExecutor executor, object controller, object[] arguments) + { + await executor.ExecuteAsync(controller, arguments); + return new EmptyResult(); + } + + protected override bool CanExecute(ObjectMethodExecutor executor) + { + // Async method returning void + return executor.IsMethodAsync && executor.AsyncResultType == typeof(void); + } + } + + // Task Post(..) + private class TaskOfIActionResultExecutor : ActionMethodExecutor + { + public override async ValueTask Execute(ObjectMethodExecutor executor, object controller, object[] arguments) + { + // Async method returning Task + // Avoid extra allocations by calling Execute rather than ExecuteAsync and casting to Task. + var returnValue = executor.Execute(controller, arguments); + var actionResult = await (Task)returnValue; + EnsureActionResultNotNull(executor, actionResult); + + return actionResult; + } + + protected override bool CanExecute(ObjectMethodExecutor executor) + => typeof(Task).IsAssignableFrom(executor.MethodReturnType); + } + + // Task DownloadFile(..) + // ValueTask GetViewsAsync(..) + private class TaskOfActionResultExecutor : ActionMethodExecutor + { + public override async ValueTask Execute(ObjectMethodExecutor executor, object controller, object[] arguments) + { + // Async method returning awaitable-of-IActionResult (e.g., Task) + // We have to use ExecuteAsync because we don't know the awaitable's type at compile time. + var actionResult = (IActionResult)await executor.ExecuteAsync(controller, arguments); + EnsureActionResultNotNull(executor, actionResult); + return actionResult; + } + + protected override bool CanExecute(ObjectMethodExecutor executor) + { + // Async method returning awaitable-of - IActionResult(e.g., Task) + return executor.IsMethodAsync && typeof(IActionResult).IsAssignableFrom(executor.AsyncResultType); + } + } + + // Task GetPerson(..) + // Task GetCustomerAsync(..) + private class AwaitableObjectResultExecutor : ActionMethodExecutor + { + public override async ValueTask Execute(ObjectMethodExecutor executor, object controller, object[] arguments) + { + // Async method returning awaitable-of-nonvoid + var returnValue = await executor.ExecuteAsync(controller, arguments); + var actionResult = ConvertToActionResult(returnValue, executor.MethodReturnType); + return actionResult; + } + + protected override bool CanExecute(ObjectMethodExecutor executor) => true; + } + + private static void EnsureActionResultNotNull(ObjectMethodExecutor executor, IActionResult actionResult) + { + if (actionResult == null) + { + throw new InvalidOperationException( + Resources.FormatActionResult_ActionReturnValueCannotBeNull(executor.AsyncResultType ?? executor.MethodReturnType)); + } + } + + private static IActionResult ConvertToActionResult(object returnValue, Type declaredType) + { + IActionResult result; + switch (returnValue) + { + case IActionResult actionResult: + result = actionResult; + break; + case IConvertToActionResult convertToActionResult: + result = convertToActionResult.Convert(); + break; + default: + result = new ObjectResult(returnValue) + { + DeclaredType = declaredType, + }; + break; + } + + if (result == null) + { + throw new InvalidOperationException(Resources.FormatActionResult_ActionReturnValueCannotBeNull(declaredType)); + } + + return result; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs index 14fdfc29b5..bb01965a79 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal { private readonly ControllerActionInvokerCacheEntry _cacheEntry; private readonly ControllerContext _controllerContext; - + private Dictionary _arguments; private ActionExecutingContext _actionExecutingContext; @@ -305,14 +305,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal private async Task InvokeActionMethodAsync() { var controllerContext = _controllerContext; - var executor = _cacheEntry.ActionMethodExecutor; + var objectMethodExecutor = _cacheEntry.ObjectMethodExecutor; var controller = _instance; var arguments = _arguments; - var orderedArguments = PrepareArguments(arguments, executor); + var actionMethodExecutor = _cacheEntry.ActionMethodExecutor; + var orderedArguments = PrepareArguments(arguments, objectMethodExecutor); var diagnosticSource = _diagnosticSource; var logger = _logger; - var returnType = executor.MethodReturnType; IActionResult result = null; try @@ -323,92 +323,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal controller); logger.ActionMethodExecuting(controllerContext, orderedArguments); - if (returnType == typeof(void)) + var actionResultValueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, orderedArguments); + if (actionResultValueTask.IsCompletedSuccessfully) { - // Sync method returning void - executor.Execute(controller, orderedArguments); - result = new EmptyResult(); - } - else if (returnType == typeof(Task)) - { - // Async method returning Task - // Avoid extra allocations by calling Execute rather than ExecuteAsync and casting to Task. - await (Task)executor.Execute(controller, orderedArguments); - result = new EmptyResult(); - } - else if (returnType == typeof(Task)) - { - // Async method returning Task - // Avoid extra allocations by calling Execute rather than ExecuteAsync and casting to Task. - result = await (Task)executor.Execute(controller, orderedArguments); - if (result == null) - { - throw new InvalidOperationException( - Resources.FormatActionResult_ActionReturnValueCannotBeNull(typeof(IActionResult))); - } - } - else if (IsConvertibleToActionResult(executor)) - { - IConvertToActionResult convertToActionResult; - if (executor.IsMethodAsync) - { - // Async method returning awaitable-of-ActionResult (e.g., Task>) - // We have to use ExecuteAsync because we don't know the awaitable's type at compile time. - convertToActionResult = (IConvertToActionResult)await executor.ExecuteAsync(controller, orderedArguments); - } - else - { - // Sync method returning ActionResult - convertToActionResult = (IConvertToActionResult)executor.Execute(controller, orderedArguments); - } - - result = convertToActionResult.Convert(); - - if (result == null) - { - throw new InvalidOperationException( - Resources.FormatActionResult_ActionReturnValueCannotBeNull(typeof(IConvertToActionResult))); - } - - } - else if (IsResultIActionResult(executor)) - { - if (executor.IsMethodAsync) - { - // Async method returning awaitable-of-IActionResult (e.g., Task) - // We have to use ExecuteAsync because we don't know the awaitable's type at compile time. - result = (IActionResult)await executor.ExecuteAsync(controller, orderedArguments); - } - else - { - // Sync method returning IActionResult (e.g., ViewResult) - result = (IActionResult)executor.Execute(controller, orderedArguments); - } - - if (result == null) - { - throw new InvalidOperationException( - Resources.FormatActionResult_ActionReturnValueCannotBeNull(executor.AsyncResultType ?? returnType)); - } - } - else if (!executor.IsMethodAsync) - { - // Sync method returning arbitrary object - var resultAsObject = executor.Execute(controller, orderedArguments); - ConvertToActionResult(resultAsObject); - - } - else if (executor.AsyncResultType == typeof(void)) - { - // Async method returning awaitable-of-void - await executor.ExecuteAsync(controller, orderedArguments); - result = new EmptyResult(); + result = actionResultValueTask.Result; } else { - // Async method returning awaitable-of-nonvoid - var resultAsObject = await executor.ExecuteAsync(controller, orderedArguments); - ConvertToActionResult(resultAsObject); + result = await actionResultValueTask; } _result = result; @@ -422,25 +344,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal controllerContext, result); } - - void ConvertToActionResult(object resultAsObject) - { - if (resultAsObject is IActionResult actionResult) - { - result = actionResult; - } - else if (resultAsObject is IConvertToActionResult convertToActionResult) - { - result = convertToActionResult.Convert(); - } - else - { - result = new ObjectResult(resultAsObject) - { - DeclaredType = returnType, - }; - } - } } private static bool IsResultIActionResult(ObjectMethodExecutor executor) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs index 073f4b6897..8f012d0d10 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs @@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal var parameterDefaultValues = ParameterDefaultValues .GetParameterDefaultValues(actionDescriptor.MethodInfo); - var executor = ObjectMethodExecutor.Create( + var objectMethodExecutor = ObjectMethodExecutor.Create( actionDescriptor.MethodInfo, actionDescriptor.ControllerTypeInfo, parameterDefaultValues); @@ -84,12 +84,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal _modelMetadataProvider, actionDescriptor); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + cacheEntry = new ControllerActionInvokerCacheEntry( filterFactoryResult.CacheableFilters, controllerFactory, controllerReleaser, propertyBinderFactory, - executor); + objectMethodExecutor, + actionMethodExecutor); cacheEntry = cache.Entries.GetOrAdd(actionDescriptor, cacheEntry); } else diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCacheEntry.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCacheEntry.cs index cd8404518a..6b06f0e02b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCacheEntry.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCacheEntry.cs @@ -14,12 +14,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal Func controllerFactory, Action controllerReleaser, ControllerBinderDelegate controllerBinderDelegate, - ObjectMethodExecutor actionMethodExecutor) + ObjectMethodExecutor objectMethodExecutor, + ActionMethodExecutor actionMethodExecutor) { ControllerFactory = controllerFactory; ControllerReleaser = controllerReleaser; ControllerBinderDelegate = controllerBinderDelegate; CachedFilters = cachedFilters; + ObjectMethodExecutor = objectMethodExecutor; ActionMethodExecutor = actionMethodExecutor; } @@ -31,6 +33,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal public ControllerBinderDelegate ControllerBinderDelegate { get; } - internal ObjectMethodExecutor ActionMethodExecutor { get; } + internal ObjectMethodExecutor ObjectMethodExecutor { get; } + + internal ActionMethodExecutor ActionMethodExecutor { get; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj index 252f0052cd..2741982ffb 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj @@ -37,6 +37,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute + diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionMethodExecutorTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionMethodExecutorTest.cs new file mode 100644 index 0000000000..e29e74dc10 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionMethodExecutorTest.cs @@ -0,0 +1,313 @@ +// 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.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.Extensions.Internal; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Core.Internal +{ + public class ActionMethodExecutorTest + { + [Fact] + public void ActionMethodExecutor_ExecutesVoidActions() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.VoidAction)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act + var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty()); + + // Assert + Assert.True(controller.Executed); + Assert.IsType(valueTask.Result); + } + + [Fact] + public void ActionMethodExecutor_ExecutesActionsReturningIActionResult() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnIActionResult)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act + var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty()); + + // Assert + Assert.True(valueTask.IsCompleted); + Assert.IsType(valueTask.Result); + } + + [Fact] + public void ActionMethodExecutor_ExecutesActionsReturningSubTypeOfActionResult() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsIActionResultSubType)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act + var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty()); + + // Assert + Assert.IsType(valueTask.Result); + } + + [Fact] + public void ActionMethodExecutor_ExecutesActionsReturningActionResultOfT() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsActionResultOfT)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act + var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty()); + + // Assert + var result = Assert.IsType(valueTask.Result); + Assert.NotNull(result.Value); + Assert.IsType(result.Value); + } + + [Fact] + public void ActionMethodExecutor_ExecutesActionsReturningModelAsModel() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsModelAsModel)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act + var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty()); + + // Assert + var result = Assert.IsType(valueTask.Result); + Assert.NotNull(result.Value); + Assert.IsType(result.Value); + } + + [Fact] + public void ActionMethodExecutor_ExecutesActionsReturningModelAsObject() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnModelAsObject)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act + var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty()); + + // Assert + var result = Assert.IsType(valueTask.Result); + Assert.NotNull(result.Value); + Assert.IsType(result.Value); + } + + [Fact] + public void ActionMethodExecutor_ExecutesActionsReturningActionResultAsObject() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsIActionResultSubType)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act + var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty()); + + // Assert + Assert.IsType(valueTask.Result); + } + + [Fact] + public void ActionMethodExecutor_ExecutesActionsReturnTask() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsTask)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act + var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty()); + + // Assert + Assert.True(controller.Executed); + Assert.IsType(valueTask.Result); + } + + [Fact] + public void ActionMethodExecutorExecutesActionsAsynchronouslyReturningIActionResult() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnIActionResultAsync)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act + var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty()); + + // Assert + Assert.IsType(valueTask.Result); + } + + [Fact] + public async Task ActionMethodExecutor_ExecutesActionsAsynchronouslyReturningActionResultSubType() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnIActionResultAsync)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act + var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty()); + + // Assert + await valueTask; + Assert.IsType(valueTask.Result); + } + + [Fact] + public void ActionMethodExecutor_ExecutesActionsAsynchronouslyReturningModel() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsModelAsModelAsync)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act + var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty()); + + // Assert + var result = Assert.IsType(valueTask.Result); + Assert.NotNull(result.Value); + Assert.IsType(result.Value); + } + + [Fact] + public void ActionMethodExecutor_ExecutesActionsAsynchronouslyReturningModelAsObject() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsModelAsObjectAsync)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act + var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty()); + + // Assert + var result = Assert.IsType(valueTask.Result); + Assert.NotNull(result.Value); + Assert.IsType(result.Value); + } + + [Fact] + public void ActionMethodExecutor_ExecutesActionsAsynchronouslyReturningIActionResultAsObject() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnIActionResultAsObjectAsync)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act + var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty()); + + // Assert + Assert.IsType(valueTask.Result); + } + + [Fact] + public void ActionMethodExecutor_ExecutesActionsAsynchronouslyReturningActionResultOfT() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnActionResultOFTAsync)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act + var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty()); + + // Assert + var result = Assert.IsType(valueTask.Result); + Assert.NotNull(result.Value); + Assert.IsType(result.Value); + } + + [Fact] + public void ActionMethodExecutor_ThrowsIfIConvertFromIActionResult_ReturnsNull() + { + // Arrange + var controller = new TestController(); + var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsCustomConvertibleFromIActionResult)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + + // Act & Assert + var ex = Assert.Throws( + () => actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty())); + + Assert.Equal($"Cannot return null from an action method with a return type of '{typeof(CustomConvertibleFromAction)}'.", ex.Message); + } + + private static ObjectMethodExecutor GetExecutor(string methodName) + { + var type = typeof(TestController); + var methodInfo = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance); + Assert.NotNull(methodInfo); + return ObjectMethodExecutor.Create(methodInfo, type.GetTypeInfo()); + } + + private class TestController + { + public bool Executed { get; set; } + + public void VoidAction() => Executed = true; + + public IActionResult ReturnIActionResult() => new ContentResult(); + + public PartialViewResult ReturnsIActionResultSubType() => new PartialViewResult(); + + public ActionResult ReturnsActionResultOfT() => new ActionResult(new TestModel()); + + public CustomConvertibleFromAction ReturnsCustomConvertibleFromIActionResult() => new CustomConvertibleFromAction(); + + public TestModel ReturnsModelAsModel() => new TestModel(); + + public object ReturnModelAsObject() => new TestModel(); + + public object ReturnIActionResultAsObject() => new RedirectResult("/foo"); + + public Task ReturnsTask() + { + Executed = true; + return Task.CompletedTask; + } + + public Task ReturnIActionResultAsync() => Task.FromResult((IActionResult)new ViewResult()); + + public Task ReturnsIActionResultSubTypeAsync() => Task.FromResult(new ViewResult()); + + public Task ReturnsModelAsModelAsync() => Task.FromResult(new TestModel()); + + public Task ReturnsModelAsObjectAsync() => Task.FromResult((object)new TestModel()); + + public Task ReturnIActionResultAsObjectAsync() => Task.FromResult((object)new OkResult()); + + public Task> ReturnActionResultOFTAsync() => Task.FromResult(new ActionResult(new TestModel())); + } + + private class TestModel + { + } + + private class CustomConvertibleFromAction : IConvertToActionResult + { + public IActionResult Convert() => null; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs index 96294a767c..1aae7a099d 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs @@ -1370,12 +1370,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal actionDescriptor.ControllerTypeInfo, ParameterDefaultValues.GetParameterDefaultValues(actionDescriptor.MethodInfo)); + var controllerMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + var cacheEntry = new ControllerActionInvokerCacheEntry( new FilterItem[0], _ => new TestController(), (_, __) => { }, (_, __, ___) => Task.CompletedTask, - actionMethodExecutor: objectMethodExecutor); + objectMethodExecutor, + controllerMethodExecutor); var invoker = new ControllerActionInvoker( new NullLoggerFactory().CreateLogger(), @@ -1632,6 +1635,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal actionDescriptor.ControllerTypeInfo, ParameterDefaultValues.GetParameterDefaultValues(actionDescriptor.MethodInfo)); + var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor); + var cacheEntry = new ControllerActionInvokerCacheEntry( new FilterItem[0], (c) => controller, @@ -1645,7 +1650,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal return Task.CompletedTask; }, - objectMethodExecutor); + objectMethodExecutor, + actionMethodExecutor); var actionContext = new ActionContext(httpContext, routeData, actionDescriptor); var controllerContext = new ControllerContext(actionContext) diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs index 8524010df9..9270ec6b64 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs @@ -430,12 +430,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal ControllerActionDescriptor actionDescriptor, MockControllerFactory controllerFactory) { + var objectMethodExecutor = CreateExecutor(actionDescriptor); return new ControllerActionInvokerCacheEntry( new FilterItem[0], controllerFactory.CreateController, controllerFactory.ReleaseController, null, - CreateExecutor(actionDescriptor)); + objectMethodExecutor, + ActionMethodExecutor.GetExecutor(objectMethodExecutor)); } }