From 9cd99a42a773ebb7cea2eade6d73edf2002dc168 Mon Sep 17 00:00:00 2001 From: harshgMSFT Date: Thu, 6 Mar 2014 18:35:38 -0800 Subject: [PATCH] ActionExecutor implementation WebFx W113 The changes include: 1. Action executor changes required for supporting sync and async operations Taksk and Task 2. Adding test project for MVC core - This contains ActionExecutor Tests. 3. Also adding a resources file for MVC core project --- WebFx.sln | 10 + .../Filters/AgeEnhancerFilterAttribute.cs | 4 +- .../MvcSample.Web/Filters/UserNameProvider.cs | 4 +- .../Filters/ActionFilterContext.cs | 10 +- .../Filters/ActionFilterEndPoint.cs | 32 +- .../Internal/TypeHelper.cs | 30 ++ .../Properties/Resources.Designer.cs | 78 +++++ .../ReflectedActionExecutor.cs | 151 +++++++++ .../ReflectedActionInvoker.cs | 8 +- src/Microsoft.AspNet.Mvc.Core/Resources.resx | 129 +++++++ src/Microsoft.AspNet.Mvc.Core/project.json | 3 + .../ActionExecutorTests.cs | 314 ++++++++++++++++++ .../TestController.cs | 103 ++++++ .../project.json | 19 ++ 14 files changed, 866 insertions(+), 29 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Internal/TypeHelper.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/ReflectedActionExecutor.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Resources.resx create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ActionExecutorTests.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/TestController.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/project.json diff --git a/WebFx.sln b/WebFx.sln index 909e57b3ae..e78bec887e 100644 --- a/WebFx.sln +++ b/WebFx.sln @@ -61,6 +61,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "net45", "net45", "{49EBEEDD EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Mvc.ModelBinding.Test.k10", "test\Microsoft.AspNet.Mvc.ModelBinding.Test\Microsoft.AspNet.Mvc.ModelBinding.Test.k10.csproj", "{5A219830-3C19-475D-901F-E580BA87DFF8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Mvc.Core.Test.net45", "test\Microsoft.AspNet.Mvc.Core.Test\Microsoft.AspNet.Mvc.Core.Test.net45.csproj", "{998C5A2E-D043-465F-BE19-076D27444289}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -151,6 +153,10 @@ Global {5A219830-3C19-475D-901F-E580BA87DFF8}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A219830-3C19-475D-901F-E580BA87DFF8}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A219830-3C19-475D-901F-E580BA87DFF8}.Release|Any CPU.Build.0 = Release|Any CPU + {998C5A2E-D043-465F-BE19-076D27444289}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {998C5A2E-D043-465F-BE19-076D27444289}.Debug|Any CPU.Build.0 = Debug|Any CPU + {998C5A2E-D043-465F-BE19-076D27444289}.Release|Any CPU.ActiveCfg = Release|Any CPU + {998C5A2E-D043-465F-BE19-076D27444289}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -182,5 +188,9 @@ Global {3EB2CFF9-6E67-4C03-9AC4-2DD169024938} = {49EBEEDD-E117-4B91-B4BA-56FB80AF4F3C} {68FC3791-A9E4-4EDE-93A5-C7AC7DC0ED6E} = {49EBEEDD-E117-4B91-B4BA-56FB80AF4F3C} {75A07B53-C5EE-4995-A55B-27562C23BCCD} = {49EBEEDD-E117-4B91-B4BA-56FB80AF4F3C} + {3EB2CFF9-6E67-4C03-9AC4-2DD169024938} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {75A07B53-C5EE-4995-A55B-27562C23BCCD} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {68FC3791-A9E4-4EDE-93A5-C7AC7DC0ED6E} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {998C5A2E-D043-465F-BE19-076D27444289} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} EndGlobalSection EndGlobal diff --git a/samples/MvcSample.Web/Filters/AgeEnhancerFilterAttribute.cs b/samples/MvcSample.Web/Filters/AgeEnhancerFilterAttribute.cs index e4cd7d39fd..4d4345277f 100644 --- a/samples/MvcSample.Web/Filters/AgeEnhancerFilterAttribute.cs +++ b/samples/MvcSample.Web/Filters/AgeEnhancerFilterAttribute.cs @@ -10,7 +10,7 @@ namespace MvcSample.Web.Filters { object age = null; - if (context.ActionParameters.TryGetValue("age", out age)) + if (context.ActionArguments.TryGetValue("age", out age)) { if (age is int) { @@ -25,7 +25,7 @@ namespace MvcSample.Web.Filters intAge = 29; } - context.ActionParameters["age"] = intAge; + context.ActionArguments["age"] = intAge; } } diff --git a/samples/MvcSample.Web/Filters/UserNameProvider.cs b/samples/MvcSample.Web/Filters/UserNameProvider.cs index 32c18b2cee..1536258604 100644 --- a/samples/MvcSample.Web/Filters/UserNameProvider.cs +++ b/samples/MvcSample.Web/Filters/UserNameProvider.cs @@ -13,13 +13,13 @@ namespace MvcSample.Web.Filters { object originalUserName = null; - context.ActionParameters.TryGetValue("userName", out originalUserName); + context.ActionArguments.TryGetValue("userName", out originalUserName); var userName = originalUserName as string; if (string.IsNullOrWhiteSpace(userName)) { - context.ActionParameters["userName"] = _userNames[(_index++)%3]; + context.ActionArguments["userName"] = _userNames[(_index++)%3]; } await next(); diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterContext.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterContext.cs index cd8b6da53e..215b15b30f 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterContext.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterContext.cs @@ -6,20 +6,16 @@ namespace Microsoft.AspNet.Mvc public class ActionFilterContext { public ActionFilterContext(ActionContext actionContext, - IDictionary actionParameters, - Type methodReturnType) + IDictionary actionArguments) { ActionContext = actionContext; - ActionParameters = actionParameters; - MethodReturnType = methodReturnType; + ActionArguments = actionArguments; } - public virtual IDictionary ActionParameters { get; private set; } + public virtual IDictionary ActionArguments { get; private set; } public virtual ActionContext ActionContext { get; private set; } - public virtual Type MethodReturnType { get; private set; } - public virtual IActionResult Result { get; set; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterEndPoint.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterEndPoint.cs index d14b40273c..2ea2792c33 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterEndPoint.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterEndPoint.cs @@ -1,32 +1,40 @@ using System; -using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Core; namespace Microsoft.AspNet.Mvc.Filters { // This one lives in the Filters namespace, and only intended to be consumed by folks that rewrite the action invoker. public class ReflectedActionFilterEndPoint : IActionFilter { - private readonly Func> _coreMethodInvoker; private readonly IActionResultFactory _actionResultFactory; + private readonly object _controllerInstance; - public ReflectedActionFilterEndPoint(Func> coreMethodInvoker, - IActionResultFactory actionResultFactory) + public ReflectedActionFilterEndPoint(IActionResultFactory actionResultFactory, object controllerInstance) { - _coreMethodInvoker = coreMethodInvoker; _actionResultFactory = actionResultFactory; + _controllerInstance = controllerInstance; } public async Task Invoke(ActionFilterContext context, Func next) { - // TODO: match the parameter names here. - var tempArray = context.ActionParameters.Values.ToArray(); // seriously broken for now, need to organize names to match. + var reflectedActionDescriptor = context.ActionContext.ActionDescriptor as ReflectedActionDescriptor; + if (reflectedActionDescriptor == null) + { + throw new ArgumentException(Resources.ReflectedActionFilterEndPoint_UnexpectedActionDescriptor); + } - var actionReturnValue = await _coreMethodInvoker(tempArray); + var actionMethodInfo = reflectedActionDescriptor.MethodInfo; + var actionReturnValue = await ReflectedActionExecutor.ExecuteAsync( + actionMethodInfo, + _controllerInstance, + context.ActionArguments); - context.Result = _actionResultFactory.CreateActionResult(context.MethodReturnType, - actionReturnValue, - context.ActionContext); + var underlyingReturnType = TypeHelper.GetTaskInnerTypeOrNull(actionMethodInfo.ReturnType) ?? actionMethodInfo.ReturnType; + context.Result = _actionResultFactory.CreateActionResult( + underlyingReturnType, + actionReturnValue, + context.ActionContext); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/TypeHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/TypeHelper.cs new file mode 100644 index 0000000000..72782fdb8f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Internal/TypeHelper.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.Contracts; +using System.Reflection; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc +{ + internal static class TypeHelper + { + private static readonly Type TaskGenericType = typeof(Task<>); + + public static Type GetTaskInnerTypeOrNull([NotNull]Type type) + { + if (type.GetTypeInfo().IsGenericType && !type.GetTypeInfo().IsGenericTypeDefinition) + { + var genericTypeDefinition = type.GetGenericTypeDefinition(); + var genericArguments = type.GetGenericArguments(); + if (genericArguments.Length == 1 && TaskGenericType == genericTypeDefinition) + { + // Only Return if there is a single argument. + return genericArguments[0]; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..51ea1e10f7 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -0,0 +1,78 @@ +// +namespace Microsoft.AspNet.Mvc.Core +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Mvc.Core.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The method '{0}' on type '{1}' returned an instance of '{2}'. Make sure to call Unwrap on the returned value to avoid unobserved faulted Task. + /// + internal static string ActionExecutor_WrappedTaskInstance + { + get { return GetString("ActionExecutor_WrappedTaskInstance"); } + } + + /// + /// The method '{0}' on type '{1}' returned an instance of '{2}'. Make sure to call Unwrap on the returned value to avoid unobserved faulted Task. + /// + internal static string FormatActionExecutor_WrappedTaskInstance(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ActionExecutor_WrappedTaskInstance"), p0, p1, p2); + } + + /// + /// The method '{0}' on type '{1}' returned a Task instance even though it is not an asynchronous method. + /// + internal static string ActionExecutor_UnexpectedTaskInstance + { + get { return GetString("ActionExecutor_UnexpectedTaskInstance"); } + } + + /// + /// The method '{0}' on type '{1}' returned a Task instance even though it is not an asynchronous method. + /// + internal static string FormatActionExecutor_UnexpectedTaskInstance(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ActionExecutor_UnexpectedTaskInstance"), p0, p1); + } + + /// + /// The class ReflectedActionFilterEndPoint only supports ReflectedActionDescriptors. + /// + internal static string ReflectedActionFilterEndPoint_UnexpectedActionDescriptor + { + get { return GetString("ReflectedActionFilterEndPoint_UnexpectedActionDescriptor"); } + } + + /// + /// The class ReflectedActionFilterEndPoint only supports ReflectedActionDescriptors. + /// + internal static string FormatReflectedActionFilterEndPoint_UnexpectedActionDescriptor() + { + return GetString("ReflectedActionFilterEndPoint_UnexpectedActionDescriptor"); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionExecutor.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionExecutor.cs new file mode 100644 index 0000000000..dac59c3835 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionExecutor.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc.Core; +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc +{ + public static class ReflectedActionExecutor + { + private static readonly MethodInfo _convertOfTMethod = typeof(ReflectedActionExecutor).GetRuntimeMethods().Single(methodInfo => methodInfo.Name == "Convert"); + + // Method called via reflection. + private static Task Convert(object taskAsObject) + { + var task = (Task)taskAsObject; + return CastToObject(task); + } + + public static async Task ExecuteAsync(MethodInfo actionMethodInfo, object instance, IDictionary actionArguments) + { + var methodArguments = PrepareArguments(actionArguments, actionMethodInfo.GetParameters()); + object invocationResult = null; + try + { + invocationResult = actionMethodInfo.Invoke(instance, methodArguments); + } + catch (TargetInvocationException targetInvocationException) + { + // Capturing the actual exception and the original callstack and rethrow for external exception handlers to observe. + ExceptionDispatchInfo exceptionDispatchInfo = ExceptionDispatchInfo.Capture(targetInvocationException.InnerException); + exceptionDispatchInfo.Throw(); + } + + return await CoerceResultToTaskAsync(invocationResult, actionMethodInfo.ReturnType, actionMethodInfo.Name, actionMethodInfo.DeclaringType); + } + + // We need to CoerceResult as the object value returned from methodInfo.Invoke has to be cast to a Task. + // This is necessary to enable calling await on the returned task. + // i.e we need to write the following var result = await (Task)mInfo.Invoke. + // Returning Task enables us to await on the result. + private static async Task CoerceResultToTaskAsync(object result, Type returnType, string methodName, Type declaringType) + { + // If it is either a Task or Task + // must coerce the return value to Task + var resultAsTask = result as Task; + if (resultAsTask != null) + { + if (returnType == typeof(Task)) + { + ThrowIfWrappedTaskInstance(resultAsTask.GetType(), methodName, declaringType); + return await CastToObject(resultAsTask); + } + + Type taskValueType = TypeHelper.GetTaskInnerTypeOrNull(returnType); + if (taskValueType != null) + { + // for: public Task Action() + // constructs: return (Task)Convert((Task)result) + var genericMethodInfo = _convertOfTMethod.MakeGenericMethod(taskValueType); + var convertedResult = (Task)genericMethodInfo.Invoke(null, new object[] { result }); + return await convertedResult; + } + + // This will be the case for: + // 1. Types which have derived from Task and Task, + // 2. Action methods which use dynamic keyword but return a Task or Task. + throw new InvalidOperationException(Resources.FormatActionExecutor_UnexpectedTaskInstance(methodName, declaringType)); + } + else + { + return result; + } + } + + private static object[] PrepareArguments(IDictionary actionParameters, ParameterInfo[] declaredParameterInfos) + { + int count = declaredParameterInfos.Length; + if (count == 0) + { + return null; + } + + var arguments = new object[count]; + for (int index = 0; index < count; index++) + { + var parameterInfo = declaredParameterInfos[index]; + object value; + + if (!actionParameters.TryGetValue(parameterInfo.Name, out value)) + { + if (parameterInfo.HasDefaultValue) + { + value = parameterInfo.DefaultValue; + } + else + { + value = parameterInfo.ParameterType.IsValueType() + ? Activator.CreateInstance(parameterInfo.ParameterType) + : null; + } + } + + arguments[index] = value; + } + + return arguments; + } + + private static void ThrowIfWrappedTaskInstance(Type actualTypeReturned, string methodName, Type declaringType) + { + // Throw if a method declares a return type of Task and returns an instance of Task or Task> + // This most likely indicates that the developer forgot to call Unwrap() somewhere. + if (actualTypeReturned != typeof(Task)) + { + Type innerTaskType = TypeHelper.GetTaskInnerTypeOrNull(actualTypeReturned); + if (innerTaskType != null && typeof(Task).IsAssignableFrom(innerTaskType)) + { + throw new InvalidOperationException( + Resources.FormatActionExecutor_WrappedTaskInstance( + methodName, + declaringType, + actualTypeReturned.FullName + )); + } + } + } + + /// + /// Cast Task to Task of object + /// + private static async Task CastToObject(Task task) + { + await task; + return null; + } + + /// + /// Cast Task of T to Task of object + /// + private static async Task CastToObject(Task task) + { + return (object)await task; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs index 683a1e35cc..b718bf1fd3 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs @@ -100,15 +100,11 @@ namespace Microsoft.AspNet.Mvc var parameterValues = await GetParameterValues(modelState); var actionFilterContext = new ActionFilterContext(_actionContext, - parameterValues, - method.ReturnType); + parameterValues); - // TODO: This is extremely temporary and is going to get soon replaced with the action executer - var actionEndPoint = new ReflectedActionFilterEndPoint(async (inArray) => method.Invoke(controller, inArray), - _actionResultFactory); + var actionEndPoint = new ReflectedActionFilterEndPoint(_actionResultFactory, controller); _actionFilters.Add(actionEndPoint); - var actionFilterPipeline = new FilterPipelineBuilder(_actionFilters, actionFilterContext); diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx new file mode 100644 index 0000000000..8f3b0948bb --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The method '{0}' on type '{1}' returned an instance of '{2}'. Make sure to call Unwrap on the returned value to avoid unobserved faulted Task. + + + The method '{0}' on type '{1}' returned a Task instance even though it is not an asynchronous method. + + + The class ReflectedActionFilterEndPoint only supports ReflectedActionDescriptors. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index c2c1878dd1..53a52177fa 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -22,7 +22,10 @@ "System.IO": "4.0.0.0", "System.Linq": "4.0.0.0", "System.Reflection": "4.0.10.0", + "System.Reflection.Emit.ILGeneration": "4.0.0.0", + "System.Reflection.Emit.Lightweight": "4.0.0.0", "System.Reflection.Extensions": "4.0.0.0", + "System.Resources.ResourceManager": "4.0.0.0", "System.Runtime": "4.0.20.0", "System.Runtime.Extensions": "4.0.10.0", "System.Runtime.Hosting": "3.9.0.0", diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionExecutorTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionExecutorTests.cs new file mode 100644 index 0000000000..8eb5501458 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionExecutorTests.cs @@ -0,0 +1,314 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + public class ActionExecutorTests + { + private TestController _controller = new TestController(); + + private delegate void MethodWithVoidReturnType(); + + private delegate string SyncMethod(string s); + + private delegate Task MethodWithTaskReturnType(int i, string s); + + private delegate Task MethodWithTaskOfIntReturnType(int i, string s); + + private delegate Task> MethodWithTaskOfTaskOfIntReturnType(int i, string s); + + public delegate TestController.TaskDerivedType MethodWithCustomTaskReturnType(int i, string s); + + private delegate TestController.TaskOfTDerivedType MethodWithCustomTaskOfTReturnType(int i, string s); + + private delegate dynamic ReturnTaskAsDynamicValue(int i, string s); + + [Fact] + public async Task AsyncAction_WithVoidReturnType() + { + var methodWithVoidReturnType = new MethodWithVoidReturnType(TestController.VoidAction); + var result = await ReflectedActionExecutor.ExecuteAsync( + methodWithVoidReturnType.GetMethodInfo(), + null, + null); + Assert.Same(null, result); + } + + [Fact] + public async Task AsyncAction_TaskReturnType() + { + int inputParam1 = 1; + string inputParam2 = "Second Parameter"; + var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; + + var methodWithTaskReturnType = new MethodWithTaskReturnType(_controller.TaskAction); + var result = await ReflectedActionExecutor.ExecuteAsync( + methodWithTaskReturnType.GetMethodInfo(), + _controller, + actionParameters); + Assert.Same(null, result); + } + + [Fact] + public async Task AsyncAction_TaskOfValueReturnType() + { + int inputParam1 = 1; + string inputParam2 = "Second Parameter"; + var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; + + var methodWithTaskOfIntReturnType = new MethodWithTaskOfIntReturnType(_controller.TaskValueTypeAction); + var result = await ReflectedActionExecutor.ExecuteAsync( + methodWithTaskOfIntReturnType.GetMethodInfo(), + _controller, + actionParameters); + Assert.Equal(inputParam1, result); + } + + [Fact] + public async Task AsyncAction_TaskOfTaskOfValueReturnType() + { + int inputParam1 = 1; + string inputParam2 = "Second Parameter"; + var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; + + var methodWithTaskOfTaskOfIntReturnType = new MethodWithTaskOfTaskOfIntReturnType(_controller.TaskOfTaskAction); + var result = await (Task)(await ReflectedActionExecutor.ExecuteAsync( + methodWithTaskOfTaskOfIntReturnType.GetMethodInfo(), + _controller, + actionParameters)); + Assert.Equal(inputParam1, result); + } + + [Fact] + public async Task AsyncAction_WithAsyncKeywordThrows() + { + int inputParam1 = 1; + string inputParam2 = "Second Parameter"; + var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; + + var methodWithTaskOfIntReturnType = new MethodWithTaskOfIntReturnType(_controller.TaskActionWithException); + await AssertThrowsAsync( + async () => + await ReflectedActionExecutor.ExecuteAsync( + methodWithTaskOfIntReturnType.GetMethodInfo(), + _controller, + actionParameters), + "Not Implemented Exception"); + } + + [Fact] + public async Task AsyncAction_WithoutAsyncThrows() + { + int inputParam1 = 1; + string inputParam2 = "Second Parameter"; + var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; + + var methodWithTaskOfIntReturnType = new MethodWithTaskOfIntReturnType(_controller.TaskActionWithExceptionWithoutAsync); + await AssertThrowsAsync( + async () => + await ReflectedActionExecutor.ExecuteAsync( + methodWithTaskOfIntReturnType.GetMethodInfo(), + _controller, + actionParameters), + "Not Implemented Exception"); + } + + [Fact] + public async Task AsyncAction_WithExceptionsAfterAwait() + { + int inputParam1 = 1; + string inputParam2 = "Second Parameter"; + var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; + + var methodWithTaskOfIntReturnType = new MethodWithTaskOfIntReturnType(_controller.TaskActionThrowAfterAwait); + await AssertThrowsAsync( + async () => + await ReflectedActionExecutor.ExecuteAsync( + methodWithTaskOfIntReturnType.GetMethodInfo(), + _controller, + actionParameters), + "Argument Exception"); + } + + [Fact] + public async Task SyncAction() + { + string inputString = "hello"; + var syncMethod = new SyncMethod(_controller.Echo); + var result = await ReflectedActionExecutor.ExecuteAsync( + syncMethod.GetMethodInfo(), + _controller, + new Dictionary() { { "input", inputString } }); + Assert.Equal(inputString, result); + } + + [Fact] + public async Task SyncAction_WithException() + { + string inputString = "hello"; + var syncMethod = new SyncMethod(_controller.EchoWithException); + var expectedException = "The method or operation is not implemented."; + await AssertThrowsAsync( + async () => + await ReflectedActionExecutor.ExecuteAsync( + syncMethod.GetMethodInfo(), + _controller, + new Dictionary() { { "input", inputString } }), + expectedException); + } + + [Fact] + public async Task AsyncAction_WithCustomTaskReturnTypeThrows() + { + int inputParam1 = 1; + string inputParam2 = "Second Parameter"; + var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; + + // If it is an unrecognized derived type we throw an InvalidOperationException. + var methodWithCutomTaskReturnType = new MethodWithCustomTaskReturnType(_controller.TaskActionWithCustomTaskReturnType); + + string expectedException = string.Format( + CultureInfo.CurrentCulture, + "The method 'TaskActionWithCustomTaskReturnType' on type '{0}' returned a Task instance even though it is not an asynchronous method.", + typeof(TestController)); + await AssertThrowsAsync( + async () => + await ReflectedActionExecutor.ExecuteAsync( + methodWithCutomTaskReturnType.GetMethodInfo(), + _controller, + actionParameters), + expectedException); + } + + [Fact] + public async Task AsyncAction_WithCustomTaskOfTReturnTypeThrows() + { + int inputParam1 = 1; + string inputParam2 = "Second Parameter"; + var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; + + var methodWithCutomTaskOfTReturnType = new MethodWithCustomTaskOfTReturnType(_controller.TaskActionWithCustomTaskOfTReturnType); + string expectedException = string.Format( + CultureInfo.CurrentCulture, + "The method 'TaskActionWithCustomTaskOfTReturnType' on type '{0}' returned a Task instance even though it is not an asynchronous method.", + typeof(TestController)); + + await AssertThrowsAsync( + async () => + await ReflectedActionExecutor.ExecuteAsync( + methodWithCutomTaskOfTReturnType.GetMethodInfo(), + _controller, + actionParameters), + expectedException); + } + + [Fact] + public async Task AsyncAction_ReturningUnwrappedTaskThrows() + { + int inputParam1 = 1; + string inputParam2 = "Second Parameter"; + var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; + + var methodWithUnwrappedTask = new MethodWithTaskReturnType(_controller.UnwrappedTask); + await AssertThrowsAsync( + async () => + await ReflectedActionExecutor.ExecuteAsync( + methodWithUnwrappedTask.GetMethodInfo(), + _controller, + actionParameters), + string.Format(CultureInfo.CurrentCulture, + "The method 'UnwrappedTask' on type '{0}' returned an instance of '{1}'. Make sure to call Unwrap on the returned value to avoid unobserved faulted Task.", + typeof(TestController), + typeof(Task).FullName + )); + } + + [Fact] + public async Task AsyncAction_WithDynamicReturnTypeThrows() + { + int inputParam1 = 1; + string inputParam2 = "Second Parameter"; + var actionParameters = new Dictionary { { "i", inputParam1 }, { "s", inputParam2 } }; + + var dynamicTaskMethod = new ReturnTaskAsDynamicValue(_controller.ReturnTaskAsDynamicValue); + string expectedException = string.Format( + CultureInfo.CurrentCulture, + "The method 'ReturnTaskAsDynamicValue' on type '{0}' returned a Task instance even though it is not an asynchronous method.", + typeof(TestController)); + await AssertThrowsAsync( + async () => + await ReflectedActionExecutor.ExecuteAsync( + dynamicTaskMethod.GetMethodInfo(), + _controller, + actionParameters), + expectedException); + } + + [Fact] + public async Task ParametersInRandomOrder() + { + int inputParam1 = 1; + string inputParam2 = "Second Parameter"; + + // Note that the order of parameters is reversed + var actionParameters = new Dictionary { { "s", inputParam2 }, { "i", inputParam1 } }; + var methodWithTaskOfIntReturnType = new MethodWithTaskOfIntReturnType(_controller.TaskValueTypeAction); + + var result = await ReflectedActionExecutor.ExecuteAsync( + methodWithTaskOfIntReturnType.GetMethodInfo(), + _controller, + actionParameters); + Assert.Equal(inputParam1, result); + } + + [Fact] + public async Task InvalidParameterValueThrows() + { + string inputParam2 = "Second Parameter"; + + var actionParameters = new Dictionary { { "i", "Some Invalid Value" }, { "s", inputParam2 } }; + var methodWithTaskOfIntReturnType = new MethodWithTaskOfIntReturnType(_controller.TaskValueTypeAction); + var expectedException = string.Format( + CultureInfo.CurrentCulture, + "Object of type '{0}' cannot be converted to type '{1}'.", + typeof (string), + typeof (int)); + + // If it is an unrecognized derived type we throw an InvalidOperationException. + await AssertThrowsAsync( + async () => + await ReflectedActionExecutor.ExecuteAsync( + methodWithTaskOfIntReturnType.GetMethodInfo(), + _controller, + actionParameters), + expectedException); + } + + // TODO: XUnit Assert.Throw is not async-aware. Check if the latest version supports it. + private static async Task AssertThrowsAsync(Func> func, string expectedExceptionMessage = "") + { + var expected = typeof(TException); + Type actual = null; + string actualExceptionMessage = string.Empty; + try + { + var result = await func(); + } + catch (Exception e) + { + actual = e.GetType(); + actualExceptionMessage = e.Message; + } + + Assert.Equal(expected, actual); + + Assert.Equal(expectedExceptionMessage, actualExceptionMessage); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/TestController.cs b/test/Microsoft.AspNet.Mvc.Core.Test/TestController.cs new file mode 100644 index 0000000000..3adcef39b7 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/TestController.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + public class TestController + { + public static void VoidAction() + { + } + + public async Task TaskAction(int i, string s) + { + return; + } + + public async Task TaskValueTypeAction(int i, string s) + { + Console.WriteLine(s); + return i; + } + + public async Task> TaskOfTaskAction(int i, string s) + { + return TaskValueTypeAction(i, s); + } + + public Task TaskValueTypeActionWithoutAsync(int i, string s) + { + return TaskValueTypeAction(i, s); + } + + public async Task TaskActionWithException(int i, string s) + { + throw new NotImplementedException("Not Implemented Exception"); + } + + 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) + { + Console.WriteLine(s); + return new TaskDerivedType(); + } + + public TaskOfTDerivedType TaskActionWithCustomTaskOfTReturnType(int i, string s) + { + Console.WriteLine(s); + return new TaskOfTDerivedType(1); + } + + /// + /// Returns a Task instead of a Task. This should throw an InvalidOperationException. + /// + /// + public Task UnwrappedTask(int i, string s) + { + return Task.Factory.StartNew(async () => await Task.Delay(50)); + } + + public string Echo(string input) + { + return input; + } + + public string EchoWithException(string input) + { + throw new NotImplementedException(); + } + + public dynamic ReturnTaskAsDynamicValue(int i, string s) + { + return Task.Factory.StartNew(() => i); + } + + public class TaskDerivedType : Task + { + public TaskDerivedType() + : base(() => Console.WriteLine("In The Constructor")) + { + } + } + + public class TaskOfTDerivedType : Task + { + public TaskOfTDerivedType(T input) + : base(() => input) + { + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/project.json b/test/Microsoft.AspNet.Mvc.Core.Test/project.json new file mode 100644 index 0000000000..dc16a39da9 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/project.json @@ -0,0 +1,19 @@ +{ + "version" : "0.1-alpha-*", + "dependencies": { + "Microsoft.AspNet.Mvc.Core" : "", + "Microsoft.AspNet.Mvc" : "", + "Moq": "4.0.10827", + "Xunit.KRunner": "0.1-alpha-*", + "xunit.abstractions": "2.0.0-aspnet-*", + "xunit.assert": "2.0.0-aspnet-*", + "xunit.core": "2.0.0-aspnet-*", + "xunit.execution": "2.0.0-aspnet-*" + }, + "commands": { + "test": "Xunit.KRunner" + }, + "configurations": { + "net45": { } + } +} \ No newline at end of file