// 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.Buffers; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.Internal { public class ControllerActionInvokerTest : CommonResourceInvokerTest { #region Diagnostics [Fact] public async Task Invoke_Success_LogsCorrectValues() { // Arrange var sink = new TestSink(); var loggerFactory = new TestLoggerFactory(sink, enabled: true); var logger = loggerFactory.CreateLogger(); var displayName = "A.B.C"; var actionDescriptor = Mock.Of(a => a.DisplayName == displayName); actionDescriptor.MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ActionMethod)); actionDescriptor.ControllerTypeInfo = typeof(TestController).GetTypeInfo(); actionDescriptor.FilterDescriptors = new List(); actionDescriptor.Parameters = new List(); actionDescriptor.BoundProperties = new List(); var filter = Mock.Of(); var invoker = CreateInvoker( new[] { filter }, actionDescriptor, controller: new TestController(), logger: logger); // Act await invoker.InvokeAsync(); // Assert Assert.Single(sink.Scopes); Assert.Equal(displayName, sink.Scopes[0].Scope?.ToString()); Assert.Equal(4, sink.Writes.Count); Assert.Equal($"Executing action {displayName}", sink.Writes[0].State?.ToString()); Assert.Equal($"Executing action method {displayName} with arguments ((null)) - ModelState is Valid", sink.Writes[1].State?.ToString()); Assert.Equal($"Executed action method {displayName}, returned result {Result.GetType().FullName}.", sink.Writes[2].State?.ToString()); // This message has the execution time embedded, which we don't want to verify. Assert.StartsWith($"Executed action {displayName} ", sink.Writes[3].State?.ToString()); } [Fact] public async Task Invoke_WritesDiagnostic_ActionSelected() { // Arrange var actionDescriptor = new ControllerActionDescriptor() { FilterDescriptors = new List(), Parameters = new List(), BoundProperties = new List(), }; actionDescriptor.MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ActionMethod)); actionDescriptor.ControllerTypeInfo = typeof(TestController).GetTypeInfo(); var listener = new TestDiagnosticListener(); var routeData = new RouteData(); routeData.Values.Add("tag", "value"); var filter = Mock.Of(); var invoker = CreateInvoker( new[] { filter }, actionDescriptor, controller: new TestController(), diagnosticListener: listener, routeData: routeData); // Act await invoker.InvokeAsync(); // Assert Assert.NotNull(listener.BeforeAction?.ActionDescriptor); Assert.NotNull(listener.BeforeAction?.HttpContext); var routeValues = listener.BeforeAction?.RouteData?.Values; Assert.NotNull(routeValues); Assert.Equal(1, routeValues.Count); Assert.Contains(routeValues, kvp => kvp.Key == "tag" && string.Equals(kvp.Value, "value")); } [Fact] public async Task Invoke_WritesDiagnostic_ActionInvoked() { // Arrange var actionDescriptor = new ControllerActionDescriptor() { FilterDescriptors = new List(), Parameters = new List(), BoundProperties = new List(), }; actionDescriptor.MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ActionMethod)); actionDescriptor.ControllerTypeInfo = typeof(TestController).GetTypeInfo(); var listener = new TestDiagnosticListener(); var filter = Mock.Of(); var invoker = CreateInvoker( new[] { filter }, actionDescriptor, controller: new TestController(), diagnosticListener: listener); // Act await invoker.InvokeAsync(); // Assert Assert.NotNull(listener.AfterAction?.ActionDescriptor); Assert.NotNull(listener.AfterAction?.HttpContext); } #endregion #region Controller Context [Fact] public async Task AddingValueProviderFactory_AtResourceFilter_IsAvailableInControllerContext() { // Arrange var valueProviderFactory2 = Mock.Of(); var resourceFilter = new Mock(); resourceFilter .Setup(f => f.OnResourceExecuting(It.IsAny())) .Callback((resourceExecutingContext) => { resourceExecutingContext.ValueProviderFactories.Add(valueProviderFactory2); }); var valueProviderFactory1 = Mock.Of(); var valueProviderFactories = new List(); valueProviderFactories.Add(valueProviderFactory1); var invoker = CreateInvoker( new IFilterMetadata[] { resourceFilter.Object }, valueProviderFactories: valueProviderFactories); // Act await invoker.InvokeAsync(); // Assert var controllerContext = Assert.IsType(invoker).ControllerContext; Assert.NotNull(controllerContext); Assert.Equal(2, controllerContext.ValueProviderFactories.Count); Assert.Same(valueProviderFactory1, controllerContext.ValueProviderFactories[0]); Assert.Same(valueProviderFactory2, controllerContext.ValueProviderFactories[1]); } [Fact] public async Task DeletingValueProviderFactory_AtResourceFilter_IsNotAvailableInControllerContext() { // Arrange var resourceFilter = new Mock(); resourceFilter .Setup(f => f.OnResourceExecuting(It.IsAny())) .Callback((resourceExecutingContext) => { resourceExecutingContext.ValueProviderFactories.RemoveAt(0); }); var valueProviderFactory1 = Mock.Of(); var valueProviderFactory2 = Mock.Of(); var valueProviderFactories = new List(); valueProviderFactories.Add(valueProviderFactory1); valueProviderFactories.Add(valueProviderFactory2); var invoker = CreateInvoker( new IFilterMetadata[] { resourceFilter.Object }, valueProviderFactories: valueProviderFactories); // Act await invoker.InvokeAsync(); // Assert var controllerContext = Assert.IsType(invoker).ControllerContext; Assert.NotNull(controllerContext); Assert.Equal(1, controllerContext.ValueProviderFactories.Count); Assert.Same(valueProviderFactory2, controllerContext.ValueProviderFactories[0]); } #endregion #region Action Filters [Fact] public async Task InvokeAction_InvokesActionFilter() { // Arrange IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker(filter.Object, result: Result); // Act await invoker.InvokeAsync(); // Assert filter.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); filter.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); Assert.Same(Result, result); } [Fact] public async Task InvokeAction_InvokesAsyncActionFilter() { // Arrange IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter .Setup(f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny())) .Returns(async (context, next) => { var resultContext = await next(); result = resultContext.Result; }) .Verifiable(); var invoker = CreateInvoker(filter.Object, result: Result); // Act await invoker.InvokeAsync(); // Assert filter.Verify( f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny()), Times.Once()); Assert.Same(Result, result); } [Fact] public async Task InvokeAction_InvokesActionFilter_ShortCircuit() { // Arrange var result = new Mock(MockBehavior.Strict); result .Setup(r => r.ExecuteResultAsync(It.IsAny())) .Returns(Task.FromResult(true)) .Verifiable(); ActionExecutedContext context = null; var actionFilter1 = new Mock(MockBehavior.Strict); actionFilter1.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); actionFilter1 .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => context = c) .Verifiable(); var actionFilter2 = new Mock(MockBehavior.Strict); actionFilter2 .Setup(f => f.OnActionExecuting(It.IsAny())) .Callback(c => c.Result = result.Object) .Verifiable(); var actionFilter3 = new Mock(MockBehavior.Strict); var resultFilter = new Mock(MockBehavior.Strict); resultFilter.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); resultFilter.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); var invoker = CreateInvoker(new IFilterMetadata[] { actionFilter1.Object, actionFilter2.Object, actionFilter3.Object, resultFilter.Object, }); // Act await invoker.InvokeAsync(); // Assert result.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); actionFilter1.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); actionFilter1.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); actionFilter2.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); actionFilter2.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Never()); resultFilter.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); resultFilter.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); Assert.True(context.Canceled); Assert.Same(context.Result, result.Object); } [Fact] public async Task InvokeAction_InvokesAsyncActionFilter_ShortCircuit_WithResult() { // Arrange var result = new Mock(MockBehavior.Strict); result .Setup(r => r.ExecuteResultAsync(It.IsAny())) .Returns(Task.FromResult(true)) .Verifiable(); ActionExecutedContext context = null; var actionFilter1 = new Mock(MockBehavior.Strict); actionFilter1.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); actionFilter1 .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => context = c) .Verifiable(); var actionFilter2 = new Mock(MockBehavior.Strict); actionFilter2 .Setup(f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny())) .Returns((c, next) => { // Notice we're not calling next c.Result = result.Object; return Task.FromResult(true); }) .Verifiable(); var actionFilter3 = new Mock(MockBehavior.Strict); var resultFilter1 = new Mock(MockBehavior.Strict); resultFilter1.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); resultFilter1.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); var resultFilter2 = new Mock(MockBehavior.Strict); resultFilter2.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); resultFilter2.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); var invoker = CreateInvoker(new IFilterMetadata[] { actionFilter1.Object, actionFilter2.Object, actionFilter3.Object, resultFilter1.Object, resultFilter2.Object, }); // Act await invoker.InvokeAsync(); // Assert result.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); actionFilter1.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); actionFilter1.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); actionFilter2.Verify( f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny()), Times.Once()); resultFilter1.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); resultFilter1.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); resultFilter2.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); resultFilter2.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); Assert.True(context.Canceled); Assert.Same(context.Result, result.Object); } [Fact] public async Task InvokeAction_InvokesAsyncActionFilter_ShortCircuit_WithoutResult() { // Arrange ActionExecutedContext context = null; var actionFilter1 = new Mock(MockBehavior.Strict); actionFilter1.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); actionFilter1 .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => context = c) .Verifiable(); var actionFilter2 = new Mock(MockBehavior.Strict); actionFilter2 .Setup(f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny())) .Returns((c, next) => { // Notice we're not calling next return Task.FromResult(true); }) .Verifiable(); var actionFilter3 = new Mock(MockBehavior.Strict); var resultFilter = new Mock(MockBehavior.Strict); resultFilter.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); resultFilter.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); var invoker = CreateInvoker(new IFilterMetadata[] { actionFilter1.Object, actionFilter2.Object, actionFilter3.Object, resultFilter.Object, }); // Act await invoker.InvokeAsync(); // Assert actionFilter1.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); actionFilter1.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); actionFilter2.Verify( f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny()), Times.Once()); resultFilter.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); resultFilter.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); Assert.True(context.Canceled); Assert.Null(context.Result); } [Fact] public async Task InvokeAction_InvokesAsyncActionFilter_ShortCircuit_WithResult_CallNext() { // Arrange var actionFilter = new Mock(MockBehavior.Strict); actionFilter .Setup(f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny())) .Returns(async (c, next) => { c.Result = new EmptyResult(); await next(); }) .Verifiable(); var message = "If an IAsyncActionFilter provides a result value by setting the Result property of " + "ActionExecutingContext to a non-null value, then it cannot call the next filter by invoking " + "ActionExecutionDelegate."; var invoker = CreateInvoker(actionFilter.Object); // Act & Assert await ExceptionAssert.ThrowsAsync( async () => await invoker.InvokeAsync(), message); } [Fact] public async Task InvokeAction_InvokesActionFilter_WithExceptionThrownByAction() { // Arrange ActionExecutedContext context = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => { context = c; // Handle the exception so the test doesn't throw. Assert.False(c.ExceptionHandled); c.ExceptionHandled = true; }) .Verifiable(); var invoker = CreateInvoker(filter.Object, exception: Exception); // Act await invoker.InvokeAsync(); // Assert filter.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); filter.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); Assert.Same(Exception, context.Exception); Assert.Null(context.Result); } [Fact] public async Task InvokeAction_InvokesActionFilter_WithExceptionThrownByActionFilter() { // Arrange var exception = new DataMisalignedException(); ActionExecutedContext context = null; var filter1 = new Mock(MockBehavior.Strict); filter1.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter1 .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => { context = c; // Handle the exception so the test doesn't throw. Assert.False(c.ExceptionHandled); c.ExceptionHandled = true; }) .Verifiable(); var filter2 = new Mock(MockBehavior.Strict); filter2 .Setup(f => f.OnActionExecuting(It.IsAny())) .Callback(c => { throw exception; }) .Verifiable(); var invoker = CreateInvoker(new[] { filter1.Object, filter2.Object }); // Act await invoker.InvokeAsync(); // Assert filter1.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); filter1.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); filter2.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); filter2.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Never()); Assert.Same(exception, context.Exception); Assert.Null(context.Result); } [Fact] public async Task InvokeAction_InvokesAsyncActionFilter_WithExceptionThrownByActionFilter() { // Arrange var exception = new DataMisalignedException(); ActionExecutedContext context = null; var filter1 = new Mock(MockBehavior.Strict); filter1 .Setup(f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny())) .Returns(async (c, next) => { context = await next(); // Handle the exception so the test doesn't throw. Assert.False(context.ExceptionHandled); context.ExceptionHandled = true; }) .Verifiable(); var filter2 = new Mock(MockBehavior.Strict); filter2.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter2 .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => { throw exception; }) .Verifiable(); var invoker = CreateInvoker(new IFilterMetadata[] { filter1.Object, filter2.Object }); // Act await invoker.InvokeAsync(); // Assert filter1.Verify( f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny()), Times.Once()); filter2.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); Assert.Same(exception, context.Exception); Assert.Null(context.Result); } [Fact] public async Task InvokeAction_InvokesActionFilter_HandleException() { // Arrange var result = new Mock(MockBehavior.Strict); result .Setup(r => r.ExecuteResultAsync(It.IsAny())) .Returns((context) => Task.FromResult(true)) .Verifiable(); var actionFilter = new Mock(MockBehavior.Strict); actionFilter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); actionFilter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => { // Handle the exception so the test doesn't throw. Assert.False(c.ExceptionHandled); c.ExceptionHandled = true; c.Result = result.Object; }) .Verifiable(); var resultFilter = new Mock(MockBehavior.Strict); resultFilter.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); resultFilter.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); var invoker = CreateInvoker( new IFilterMetadata[] { actionFilter.Object, resultFilter.Object }, exception: Exception); // Act await invoker.InvokeAsync(); // Assert actionFilter.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); actionFilter.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); resultFilter.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); resultFilter.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); result.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); } [Fact] public async Task InvokeAction_InvokesAsyncResourceFilter_WithActionResult_FromActionFilter() { // Arrange var expected = Mock.Of(); ResourceExecutedContext context = null; var resourceFilter = new Mock(MockBehavior.Strict); resourceFilter .Setup(f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny())) .Returns(async (c, next) => { context = await next(); }) .Verifiable(); var actionFilter = new Mock(MockBehavior.Strict); actionFilter .Setup(f => f.OnActionExecuting(It.IsAny())) .Callback((c) => { c.Result = expected; }); var invoker = CreateInvoker(new IFilterMetadata[] { resourceFilter.Object, actionFilter.Object }); // Act await invoker.InvokeAsync(); // Assert Assert.Same(expected, context.Result); resourceFilter.Verify( f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny()), Times.Once()); } [Fact] public async Task InvokeAction_InvokesAsyncResourceFilter_HandleException_FromActionFilter() { // Arrange var expected = new DataMisalignedException(); ResourceExecutedContext context = null; var resourceFilter = new Mock(MockBehavior.Strict); resourceFilter .Setup(f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny())) .Returns(async (c, next) => { context = await next(); context.ExceptionHandled = true; }) .Verifiable(); var actionFilter = new Mock(MockBehavior.Strict); actionFilter .Setup(f => f.OnActionExecuting(It.IsAny())) .Callback((c) => { throw expected; }); var invoker = CreateInvoker(new IFilterMetadata[] { resourceFilter.Object, actionFilter.Object }); // Act await invoker.InvokeAsync(); // Assert Assert.Same(expected, context.Exception); Assert.Same(expected, context.ExceptionDispatchInfo.SourceException); resourceFilter.Verify( f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny()), Times.Once()); } [Fact] public async Task InvokeAction_InvokesAsyncResourceFilter_HandlesException_FromExceptionFilter() { // Arrange var expected = new DataMisalignedException(); ResourceExecutedContext context = null; var resourceFilter = new Mock(MockBehavior.Strict); resourceFilter .Setup(f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny())) .Returns(async (c, next) => { context = await next(); context.ExceptionHandled = true; }) .Verifiable(); var exceptionFilter = new Mock(MockBehavior.Strict); exceptionFilter .Setup(f => f.OnException(It.IsAny())) .Callback((c) => { throw expected; }); var invoker = CreateInvoker(new IFilterMetadata[] { resourceFilter.Object, exceptionFilter.Object }, exception: Exception); // Act await invoker.InvokeAsync(); // Assert Assert.Same(expected, context.Exception); Assert.Same(expected, context.ExceptionDispatchInfo.SourceException); resourceFilter.Verify( f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny()), Times.Once()); } [Fact] public async Task InvokeAction_ExceptionBubbling_AsyncActionFilter_To_ResourceFilter() { // Arrange var resourceFilter = new Mock(MockBehavior.Strict); resourceFilter .Setup(f => f.OnResourceExecutionAsync(It.IsAny(), It.IsAny())) .Returns(async (c, next) => { var context = await next(); Assert.Same(Exception, context.Exception); context.ExceptionHandled = true; }); var actionFilter1 = new Mock(MockBehavior.Strict); actionFilter1 .Setup(f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny())) .Returns(async (c, next) => { await next(); }); var actionFilter2 = new Mock(MockBehavior.Strict); actionFilter2 .Setup(f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny())) .Returns(async (c, next) => { await next(); }); var invoker = CreateInvoker( new IFilterMetadata[] { resourceFilter.Object, actionFilter1.Object, actionFilter2.Object, }, // The action won't run exception: Exception); // Act & Assert await invoker.InvokeAsync(); } #endregion #region Action Method Signatures [Fact] public async Task InvokeAction_AsyncAction_TaskReturnType() { // Arrange var inputParam1 = 1; var inputParam2 = "Second Parameter"; var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.TaskAction), actionParameters); // Act await invoker.InvokeAsync(); // Assert Assert.IsType(result); } [Fact] public async Task InvokeAction_AsyncAction_TaskOfValueReturnType() { // Arrange var inputParam1 = 1; var inputParam2 = "Second Parameter"; var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.TaskValueTypeAction), actionParameters); // Act await invoker.InvokeAsync(); // Assert var contentResult = Assert.IsType(result); Assert.Equal(inputParam1, contentResult.Value); } [Fact] public async Task InvokeAction_AsyncAction_WithAsyncKeywordThrows() { // Arrange var inputParam1 = 1; var inputParam2 = "Second Parameter"; var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.TaskActionWithException), actionParameters); // Act and Assert await Assert.ThrowsAsync( () => invoker.InvokeAsync()); } [Fact] public async Task InvokeAction_AsyncAction_WithoutAsyncThrows() { // Arrange var inputParam1 = 1; var inputParam2 = "Second Parameter"; var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.TaskActionWithExceptionWithoutAsync), actionParameters); // Act and Assert await Assert.ThrowsAsync( () => invoker.InvokeAsync()); } [Fact] public async Task InvokeAction_AsyncAction_WithExceptionsAfterAwait() { // Arrange var inputParam1 = 1; var inputParam2 = "Second Parameter"; var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.TaskActionThrowAfterAwait), actionParameters); var expectedException = "Argument Exception"; // Act and Assert var ex = await Assert.ThrowsAsync( () => invoker.InvokeAsync()); Assert.Equal(expectedException, ex.Message); } [Fact] public async Task InvokeAction_SyncAction() { // Arrange var inputString = "hello"; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.Echo), new Dictionary() { { "input", inputString } }); // Act await invoker.InvokeAsync(); // Assert var contentResult = Assert.IsType(result); Assert.Equal(inputString, contentResult.Value); } [Fact] public async Task InvokeAction_SyncAction_WithException() { // Arrange var inputString = "hello"; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker( new[] { filter.Object }, nameof(TestController.EchoWithException), new Dictionary() { { "input", inputString } }); // Act & Assert await Assert.ThrowsAsync( () => invoker.InvokeAsync()); } [Fact] public async Task InvokeAction_SyncMethod_WithArgumentDictionary_DefaultValueAttributeUsed() { // Arrange IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker( new[] { filter.Object }, nameof(TestController.EchoWithDefaultValue), new Dictionary()); // Act await invoker.InvokeAsync(); // Assert var contentResult = Assert.IsType(result); Assert.Equal("hello", contentResult.Value); } [Fact] public async Task InvokeAction_SyncMethod_WithArgumentArray_DefaultValueAttributeIgnored() { // Arrange var inputString = "test"; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker( new[] { filter.Object }, nameof(TestController.EchoWithDefaultValue), new Dictionary() { { "input", inputString } }); // Act await invoker.InvokeAsync(); // Assert var contentResult = Assert.IsType(result); Assert.Equal(inputString, contentResult.Value); } [Fact] public async Task InvokeAction_SyncMethod_WithArgumentDictionary_DefaultParameterValueUsed() { // Arrange IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker( new[] { filter.Object }, nameof(TestController.EchoWithDefaultValueAndAttribute), new Dictionary()); // Act await invoker.InvokeAsync(); // Assert var contentResult = Assert.IsType(result); Assert.Equal("world", contentResult.Value); } [Fact] public async Task InvokeAction_SyncMethod_WithArgumentDictionary_AnyValue_HasPrecedenceOverDefaults() { // Arrange var inputString = "test"; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker( new[] { filter.Object }, nameof(TestController.EchoWithDefaultValueAndAttribute), new Dictionary() { { "input", inputString } }); // Act await invoker.InvokeAsync(); // Assert var contentResult = Assert.IsType(result); Assert.Equal(inputString, contentResult.Value); } [Fact] public async Task InvokeAction_AsyncAction_WithCustomTaskReturnType() { // Arrange var inputParam1 = 1; var inputParam2 = "Second Parameter"; var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker( new[] { filter.Object }, nameof(TestController.TaskActionWithCustomTaskReturnType), actionParameters); // Act await invoker.InvokeAsync(); // Assert Assert.IsType(typeof(EmptyResult), result); } [Fact] public async Task InvokeAction_AsyncAction_WithCustomTaskOfTReturnType() { // Arrange var inputParam1 = 1; var inputParam2 = "Second Parameter"; var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker( new[] { filter.Object }, nameof(TestController.TaskActionWithCustomTaskOfTReturnType), actionParameters); // Act await invoker.InvokeAsync(); // Assert Assert.IsType(typeof(ObjectResult), result); Assert.IsType(typeof(int), ((ObjectResult)result).Value); Assert.Equal(1, ((ObjectResult)result).Value); } [Fact] public async Task InvokeAction_AsyncAction_ReturningUnwrappedTask() { // Arrange var inputParam1 = 1; var inputParam2 = "Second Parameter"; var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.UnwrappedTask), actionParameters); // Act await invoker.InvokeAsync(); // Assert Assert.IsType(result); } [Fact] public async Task InvokeAction_AsyncActionWithTaskOfObjectReturnType_AndReturningTaskOfActionResult() { // Arrange var actionParameters = new Dictionary { ["value"] = 3 }; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result); var invoker = CreateInvoker( new[] { filter.Object }, nameof(TestController.AsyncActionMethodReturningActionResultWithTaskOfObjectAsReturnType), actionParameters); // Act await invoker.InvokeAsync(); // Assert var testResult = Assert.IsType(result); Assert.Equal(3, testResult.Value); } [Fact] public async Task InvokeAction_ActionWithObjectReturnType_AndReturningActionResult() { // Arrange var actionParameters = new Dictionary { ["value"] = 3 }; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result); var invoker = CreateInvoker( new[] { filter.Object }, nameof(TestController.ActionMethodReturningActionResultWithObjectAsReturnType), actionParameters); // Act await invoker.InvokeAsync(); // Assert var testResult = Assert.IsType(result); Assert.Equal(3, testResult.Value); } [Fact] public async Task InvokeAction_AsyncMethod_ParametersInRandomOrder() { //Arrange var inputParam1 = 1; var inputParam2 = "Second Parameter"; // Note that the order of parameters is reversed var actionParameters = new Dictionary { { "s", inputParam2 }, { "i", inputParam1 } }; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker( new[] { filter.Object }, nameof(TestController.TaskValueTypeAction), actionParameters); // Act await invoker.InvokeAsync(); // Assert var contentResult = Assert.IsType(result); Assert.Equal(inputParam1, contentResult.Value); } [Theory] [InlineData(nameof(TestController.AsynActionMethodWithTestActionResult))] [InlineData(nameof(TestController.ActionMethodWithTestActionResult))] public async Task InvokeAction_ReturnTypeAsIActionResult_ReturnsExpected(string methodName) { //Arrange var inputParam = 1; var actionParameters = new Dictionary { { "value", inputParam } }; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker( new[] { filter.Object }, methodName, actionParameters); // Act await invoker.InvokeAsync(); // Assert var contentResult = Assert.IsType(result); Assert.Equal(inputParam, contentResult.Value); } [Fact] public async Task InvokeAction_AsyncMethod_InvalidParameterValueThrows() { //Arrange var inputParam2 = "Second Parameter"; var actionParameters = new Dictionary { { "i", "Some Invalid Value" }, { "s", inputParam2 } }; IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker( new[] { filter.Object }, nameof(TestController.TaskValueTypeAction), actionParameters); // Act & Assert await Assert.ThrowsAsync( () => invoker.InvokeAsync()); } [Theory] [InlineData(nameof(TestController.ActionMethodWithNullActionResult), typeof(IActionResult))] [InlineData(nameof(TestController.TestActionMethodWithNullActionResult), typeof(TestActionResult))] [InlineData(nameof(TestController.AsyncActionMethodWithNullActionResult), typeof(IActionResult))] [InlineData(nameof(TestController.AsyncActionMethodWithNullTestActionResult), typeof(TestActionResult))] [ReplaceCulture] public async Task InvokeAction_WithNullActionResultThrows(string methodName, Type resultType) { // Arrange IActionResult result = null; var filter = new Mock(MockBehavior.Strict); filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); filter .Setup(f => f.OnActionExecuted(It.IsAny())) .Callback(c => result = c.Result) .Verifiable(); var invoker = CreateInvoker( new[] { filter.Object }, methodName, new Dictionary()); // Act & Assert await ExceptionAssert.ThrowsAsync( () => invoker.InvokeAsync(), $"Cannot return null from an action method with a return type of '{resultType}'."); } [Fact] public async Task Invoke_UsesDefaultValuesIfNotBound() { // Arrange var actionDescriptor = new ControllerActionDescriptor { ControllerTypeInfo = typeof(TestController).GetTypeInfo(), BoundProperties = new List(), MethodInfo = typeof(TestController).GetTypeInfo() .DeclaredMethods .First(m => m.Name.Equals("ActionMethodWithDefaultValues", StringComparison.Ordinal)), Parameters = new List { new ParameterDescriptor { Name = "value", ParameterType = typeof(int), BindingInfo = new BindingInfo(), } }, FilterDescriptors = new List() }; var context = new Mock(); context.SetupGet(c => c.Items) .Returns(new Dictionary()); context.Setup(c => c.RequestServices.GetService(typeof(ILoggerFactory))) .Returns(new NullLoggerFactory()); var actionContext = new ActionContext(context.Object, new RouteData(), actionDescriptor); var controllerContext = new ControllerContext(actionContext) { ValueProviderFactories = new IValueProviderFactory[0] }; controllerContext.ModelState.MaxAllowedErrors = 200; var objectMethodExecutor = ObjectMethodExecutor.Create( actionDescriptor.MethodInfo, actionDescriptor.ControllerTypeInfo, ParameterDefaultValues.GetParameterDefaultValues(actionDescriptor.MethodInfo)); var cacheEntry = new ControllerActionInvokerCacheEntry( new FilterItem[0], _ => new TestController(), (_, __) => { }, (_, __, ___) => Task.CompletedTask, actionMethodExecutor: objectMethodExecutor); var invoker = new ControllerActionInvoker( new NullLoggerFactory().CreateLogger(), new DiagnosticListener("Microsoft.AspNetCore"), controllerContext, cacheEntry, new IFilterMetadata[0]); // Act await invoker.InvokeAsync(); // Assert Assert.Equal(5, context.Object.Items["Result"]); } #endregion protected override ResourceInvoker CreateInvoker( IFilterMetadata[] filters, Exception exception = null, IActionResult result = null, IList valueProviderFactories = null) { var actionDescriptor = new ControllerActionDescriptor() { ControllerTypeInfo = typeof(TestController).GetTypeInfo(), FilterDescriptors = new List(), Parameters = new List(), BoundProperties = new List(), }; if (result == Result) { actionDescriptor.MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ActionMethod)); } else if (result != null) { throw new InvalidOperationException($"Unexpected action result {result}."); } else if (exception == Exception) { actionDescriptor.MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ThrowingActionMethod)); } else if (exception != null) { throw new InvalidOperationException($"Unexpected exception {exception}."); } else { actionDescriptor.MethodInfo = typeof(TestController).GetMethod(nameof(TestController.ActionMethod)); } return CreateInvoker( filters, actionDescriptor, new TestController(), valueProviderFactories: valueProviderFactories); } // Used by tests which directly test different types of signatures for controller methods. private ControllerActionInvoker CreateInvoker( IFilterMetadata[] filters, string methodName, IDictionary arguments) { var actionDescriptor = new ControllerActionDescriptor() { ControllerTypeInfo = typeof(TestController).GetTypeInfo(), FilterDescriptors = new List(), Parameters = new List(), BoundProperties = new List(), }; var method = typeof(TestController).GetTypeInfo().GetMethod(methodName); Assert.NotNull(method); actionDescriptor.MethodInfo = method; foreach (var kvp in arguments) { actionDescriptor.Parameters.Add(new ControllerParameterDescriptor() { Name = kvp.Key, ParameterInfo = method.GetParameters().Where(p => p.Name == kvp.Key).Single(), }); } return CreateInvoker(filters, actionDescriptor, new TestController(), arguments); } private ControllerActionInvoker CreateInvoker( IFilterMetadata[] filters, ControllerActionDescriptor actionDescriptor, object controller, IDictionary arguments = null, IList valueProviderFactories = null, RouteData routeData = null, ILogger logger = null, object diagnosticListener = null) { Assert.NotNull(actionDescriptor.MethodInfo); if (arguments == null) { arguments = new Dictionary(); } if (valueProviderFactories == null) { valueProviderFactories = new List(); } if (routeData == null) { routeData = new RouteData(); } if (logger == null) { logger = new NullLoggerFactory().CreateLogger(); } var httpContext = new DefaultHttpContext(); var options = new MvcOptions(); var mvcOptionsAccessor = new TestOptionsManager(options); var services = new ServiceCollection(); services.AddSingleton(NullLoggerFactory.Instance); services.AddSingleton>(mvcOptionsAccessor); services.AddSingleton(new ObjectResultExecutor( mvcOptionsAccessor, new TestHttpResponseStreamWriterFactory(), NullLoggerFactory.Instance)); httpContext.Response.Body = new MemoryStream(); httpContext.RequestServices = services.BuildServiceProvider(); var formatter = new Mock(); formatter .Setup(f => f.CanWriteResult(It.IsAny())) .Returns(true); formatter .Setup(f => f.WriteAsync(It.IsAny())) .Returns(async c => { await c.HttpContext.Response.WriteAsync(c.Object.ToString()); }); options.OutputFormatters.Add(formatter.Object); var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore"); if (diagnosticListener != null) { diagnosticSource.SubscribeWithAdapter(diagnosticListener); } var objectMethodExecutor = ObjectMethodExecutor.Create( actionDescriptor.MethodInfo, actionDescriptor.ControllerTypeInfo, ParameterDefaultValues.GetParameterDefaultValues(actionDescriptor.MethodInfo)); var cacheEntry = new ControllerActionInvokerCacheEntry( new FilterItem[0], (c) => controller, null, (_, __, args) => { foreach (var item in arguments) { args[item.Key] = item.Value; } return Task.CompletedTask; }, objectMethodExecutor); var actionContext = new ActionContext(httpContext, routeData, actionDescriptor); var controllerContext = new ControllerContext(actionContext) { ValueProviderFactories = valueProviderFactories, }; var invoker = new ControllerActionInvoker( logger, diagnosticSource, controllerContext, cacheEntry, filters); return invoker; } public sealed class TestController { public IActionResult ActionMethod() { return Result; } public ObjectResult ThrowingActionMethod() { throw Exception; } public IActionResult ActionMethodWithDefaultValues(int value = 5) { return new TestActionResult { Value = value }; } public TestActionResult ActionMethodWithTestActionResult(int value) { return new TestActionResult { Value = value }; } public async Task AsynActionMethodWithTestActionResult(int value) { return await Task.FromResult(new TestActionResult { Value = value }); } public IActionResult ActionMethodWithNullActionResult() { return null; } public object ActionMethodReturningActionResultWithObjectAsReturnType(int value = 5) { return new TestActionResult { Value = value }; } public async Task AsyncActionMethodReturningActionResultWithTaskOfObjectAsReturnType(int value = 5) { return await Task.FromResult(new TestActionResult { Value = value }); } public TestActionResult TestActionMethodWithNullActionResult() { return null; } public async Task AsyncActionMethodWithNullActionResult() { return await Task.FromResult(null); } public async Task AsyncActionMethodWithNullTestActionResult() { return await Task.FromResult(null); } #pragma warning disable 1998 public async Task TaskAction(int i, string s) { return; } #pragma warning restore 1998 #pragma warning disable 1998 public async Task TaskValueTypeAction(int i, string s) { return i; } #pragma warning restore 1998 #pragma warning disable 1998 public async Task> TaskOfTaskAction(int i, string s) { return TaskValueTypeAction(i, s); } #pragma warning restore 1998 public Task TaskValueTypeActionWithoutAsync(int i, string s) { return TaskValueTypeAction(i, s); } #pragma warning disable 1998 public async Task TaskActionWithException(int i, string s) { throw new NotImplementedException("Not Implemented Exception"); } #pragma warning restore 1998 public Task TaskActionWithExceptionWithoutAsync(int i, string s) { throw new NotImplementedException("Not Implemented Exception"); } public async Task TaskActionThrowAfterAwait(int i, string s) { await Task.Delay(500); throw new ArgumentException("Argument Exception"); } public TaskDerivedType TaskActionWithCustomTaskReturnType(int i, string s) { var task = new TaskDerivedType(); task.Start(); return task; } public TaskOfTDerivedType TaskActionWithCustomTaskOfTReturnType(int i, string s) { var task = new TaskOfTDerivedType(1); task.Start(); return task; } /// /// Returns a instead of a . /// public Task UnwrappedTask(int i, string s) { return Task.Factory.StartNew(async () => await Task.Factory.StartNew(() => i)); } public string Echo(string input) { return input; } public string EchoWithException(string input) { throw new NotImplementedException(); } public string EchoWithDefaultValue([DefaultValue("hello")] string input) { return input; } public string EchoWithDefaultValueAndAttribute([DefaultValue("hello")] string input = "world") { return input; } public class TaskDerivedType : Task { public TaskDerivedType() : base(() => Console.WriteLine("In The Constructor")) { } } public class TaskOfTDerivedType : Task { public TaskOfTDerivedType(T input) : base(() => input) { } } } public sealed class TestActionResult : IActionResult { public int Value { get; set; } public Task ExecuteResultAsync(ActionContext context) { context.HttpContext.Items["Result"] = Value; return Task.FromResult(0); } } private static ObjectMethodExecutor CreateExecutor(ControllerActionDescriptor actionDescriptor) { return ObjectMethodExecutor.Create( actionDescriptor.MethodInfo, actionDescriptor.ControllerTypeInfo, ParameterDefaultValues.GetParameterDefaultValues(actionDescriptor.MethodInfo)); } } }