diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionExecutor.cs index 9f7b4b5808..fba1fa2365 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionExecutor.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Reflection; using System.Threading.Tasks; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Mvc.Internal { @@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal IDictionary actionParameters, ObjectMethodExecutor actionMethodExecutor) { - var declaredParameterInfos = actionMethodExecutor.ActionParameters; + var declaredParameterInfos = actionMethodExecutor.MethodParameters; var count = declaredParameterInfos.Length; if (count == 0) { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs index 6b0fe4dd13..ea9e76bb98 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvoker.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Reflection; using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -12,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Core.Internal; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc.Internal @@ -776,16 +778,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal var returnType = executor.MethodReturnType; if (returnType == typeof(void)) { + // 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 (executor.TaskGenericType == typeof(IActionResult)) + 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) { @@ -793,45 +800,49 @@ namespace Microsoft.AspNetCore.Mvc.Internal Resources.FormatActionResult_ActionReturnValueCannotBeNull(typeof(IActionResult))); } } - else if (executor.IsTypeAssignableFromIActionResult) + 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.TaskGenericType ?? returnType)); + Resources.FormatActionResult_ActionReturnValueCannotBeNull(_executor.AsyncResultType ?? returnType)); } } else if (!executor.IsMethodAsync) { + // Sync method returning arbitrary object var resultAsObject = executor.Execute(controller, orderedArguments); result = resultAsObject as IActionResult ?? new ObjectResult(resultAsObject) { DeclaredType = returnType, }; } - else if (executor.TaskGenericType != null) + else if (executor.AsyncResultType == typeof(void)) { - var resultAsObject = await executor.ExecuteAsync(controller, orderedArguments); - result = resultAsObject as IActionResult ?? new ObjectResult(resultAsObject) - { - DeclaredType = executor.TaskGenericType, - }; + // Async method returning awaitable-of-void + await executor.ExecuteAsync(controller, orderedArguments); + result = new EmptyResult(); } else { - // This will be the case for types which have derived from Task and Task or non Task types. - throw new InvalidOperationException(Resources.FormatActionExecutor_UnexpectedTaskInstance( - executor.MethodInfo.Name, - executor.MethodInfo.DeclaringType)); + // Async method returning awaitable-of-nonvoid + var resultAsObject = await executor.ExecuteAsync(controller, orderedArguments); + result = resultAsObject as IActionResult ?? new ObjectResult(resultAsObject) + { + DeclaredType = executor.AsyncResultType, + }; } _result = result; @@ -847,6 +858,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal } } + private static bool IsResultIActionResult(ObjectMethodExecutor executor) + { + var resultType = executor.AsyncResultType ?? executor.MethodReturnType; + return typeof(IActionResult).IsAssignableFrom(resultType); + } + private async Task InvokeNextResultFilterAsync() { try diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs index b910d38e00..defa5d223e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs @@ -7,6 +7,7 @@ using System.Linq; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Mvc.Internal { @@ -54,9 +55,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal var filterFactoryResult = FilterFactory.GetAllFilters(_filterProviders, controllerContext); filters = filterFactoryResult.Filters; + var parameterDefaultValues = ParameterDefaultValues + .GetParameterDefaultValues(actionDescriptor.MethodInfo); + var executor = ObjectMethodExecutor.Create( actionDescriptor.MethodInfo, - actionDescriptor.ControllerTypeInfo); + actionDescriptor.ControllerTypeInfo, + parameterDefaultValues); cacheEntry = new Entry(filterFactoryResult.CacheableFilters, executor); cacheEntry = cache.Entries.GetOrAdd(actionDescriptor, cacheEntry); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectMethodExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectMethodExecutor.cs deleted file mode 100644 index 3205709c68..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectMethodExecutor.cs +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.Extensions.Internal; - -namespace Microsoft.AspNetCore.Mvc.Internal -{ - public class ObjectMethodExecutor - { - private readonly object[] _parameterDefaultValues; - private readonly ActionExecutorAsync _executorAsync; - private readonly ActionExecutor _executor; - - private static readonly MethodInfo _convertOfTMethod = - typeof(ObjectMethodExecutor).GetRuntimeMethods().Single(methodInfo => methodInfo.Name == nameof(ObjectMethodExecutor.Convert)); - - private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo) - { - if (methodInfo == null) - { - throw new ArgumentNullException(nameof(methodInfo)); - } - - MethodInfo = methodInfo; - TargetTypeInfo = targetTypeInfo; - ActionParameters = methodInfo.GetParameters(); - MethodReturnType = methodInfo.ReturnType; - IsMethodAsync = typeof(Task).IsAssignableFrom(MethodReturnType); - TaskGenericType = IsMethodAsync ? GetTaskInnerTypeOrNull(MethodReturnType) : null; - IsTypeAssignableFromIActionResult = typeof(IActionResult).IsAssignableFrom(TaskGenericType ?? MethodReturnType); - - if (IsMethodAsync && TaskGenericType != null) - { - // For backwards compatibility we're creating a sync-executor for an async method. This was - // supported in the past even though MVC wouldn't have called it. - _executor = GetExecutor(methodInfo, targetTypeInfo); - _executorAsync = GetExecutorAsync(TaskGenericType, methodInfo, targetTypeInfo); - } - else - { - _executor = GetExecutor(methodInfo, targetTypeInfo); - } - - _parameterDefaultValues = GetParameterDefaultValues(ActionParameters); - } - - private delegate Task ActionExecutorAsync(object target, object[] parameters); - - private delegate object ActionExecutor(object target, object[] parameters); - - private delegate void VoidActionExecutor(object target, object[] parameters); - - public MethodInfo MethodInfo { get; } - - public ParameterInfo[] ActionParameters { get; } - - public TypeInfo TargetTypeInfo { get; } - - public Type TaskGenericType { get; } - - // This field is made internal set because it is set in unit tests. - public Type MethodReturnType { get; internal set; } - - public bool IsMethodAsync { get; } - - public bool IsTypeAssignableFromIActionResult { get; } - - public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo) - { - var executor = new ObjectMethodExecutor(methodInfo, targetTypeInfo); - return executor; - } - - public Task ExecuteAsync(object target, object[] parameters) - { - return _executorAsync(target, parameters); - } - - public object Execute(object target, object[] parameters) - { - return _executor(target, parameters); - } - - public object GetDefaultValueForParameter(int index) - { - if (index < 0 || index > ActionParameters.Length - 1) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - return _parameterDefaultValues[index]; - } - - private static ActionExecutor GetExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo) - { - // Parameters to executor - var targetParameter = Expression.Parameter(typeof(object), "target"); - var parametersParameter = Expression.Parameter(typeof(object[]), "parameters"); - - // Build parameter list - var parameters = new List(); - var paramInfos = methodInfo.GetParameters(); - for (int i = 0; i < paramInfos.Length; i++) - { - var paramInfo = paramInfos[i]; - var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i)); - var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType); - - // valueCast is "(Ti) parameters[i]" - parameters.Add(valueCast); - } - - // Call method - var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType()); - var methodCall = Expression.Call(instanceCast, methodInfo, parameters); - - // methodCall is "((Ttarget) target) method((T0) parameters[0], (T1) parameters[1], ...)" - // Create function - if (methodCall.Type == typeof(void)) - { - var lambda = Expression.Lambda(methodCall, targetParameter, parametersParameter); - var voidExecutor = lambda.Compile(); - return WrapVoidAction(voidExecutor); - } - else - { - // must coerce methodCall to match ActionExecutor signature - var castMethodCall = Expression.Convert(methodCall, typeof(object)); - var lambda = Expression.Lambda(castMethodCall, targetParameter, parametersParameter); - return lambda.Compile(); - } - } - - private static ActionExecutor WrapVoidAction(VoidActionExecutor executor) - { - return delegate (object target, object[] parameters) - { - executor(target, parameters); - return null; - }; - } - - private static ActionExecutorAsync GetExecutorAsync(Type taskInnerType, MethodInfo methodInfo, TypeInfo targetTypeInfo) - { - // Parameters to executor - var targetParameter = Expression.Parameter(typeof(object), "target"); - var parametersParameter = Expression.Parameter(typeof(object[]), "parameters"); - - // Build parameter list - var parameters = new List(); - var paramInfos = methodInfo.GetParameters(); - for (int i = 0; i < paramInfos.Length; i++) - { - var paramInfo = paramInfos[i]; - var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i)); - var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType); - - // valueCast is "(Ti) parameters[i]" - parameters.Add(valueCast); - } - - // Call method - var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType()); - var methodCall = Expression.Call(instanceCast, methodInfo, parameters); - - var coerceMethodCall = GetCoerceMethodCallExpression(taskInnerType, methodCall, methodInfo); - var lambda = Expression.Lambda(coerceMethodCall, targetParameter, parametersParameter); - return lambda.Compile(); - } - - // 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 Expression GetCoerceMethodCallExpression( - Type taskValueType, - MethodCallExpression methodCall, - MethodInfo methodInfo) - { - var castMethodCall = Expression.Convert(methodCall, typeof(object)); - // for: public Task Action() - // constructs: return (Task)Convert((Task)result) - var genericMethodInfo = _convertOfTMethod.MakeGenericMethod(taskValueType); - var genericMethodCall = Expression.Call(null, genericMethodInfo, castMethodCall); - var convertedResult = Expression.Convert(genericMethodCall, typeof(Task)); - return convertedResult; - } - - /// - /// Cast Task of T to Task of object - /// - private static async Task CastToObject(Task task) - { - return (object)await task; - } - - private static Type GetTaskInnerTypeOrNull(Type type) - { - var genericType = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(Task<>)); - - return genericType?.GenericTypeArguments[0]; - } - - private static Task Convert(object taskAsObject) - { - var task = (Task)taskAsObject; - return CastToObject(task); - } - - private static object[] GetParameterDefaultValues(ParameterInfo[] parameters) - { - var values = new object[parameters.Length]; - - for (var i = 0; i < parameters.Length; i++) - { - var parameterInfo = parameters[i]; - object defaultValue; - - if (parameterInfo.HasDefaultValue) - { - defaultValue = parameterInfo.DefaultValue; - } - else - { - var defaultValueAttribute = parameterInfo - .GetCustomAttribute(inherit: false); - - if (defaultValueAttribute?.Value == null) - { - defaultValue = parameterInfo.ParameterType.GetTypeInfo().IsValueType - ? Activator.CreateInstance(parameterInfo.ParameterType) - : null; - } - else - { - defaultValue = defaultValueAttribute.Value; - } - } - - values[i] = defaultValue; - } - - return values; - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ParameterDefaultValues.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ParameterDefaultValues.cs new file mode 100644 index 0000000000..948638f247 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ParameterDefaultValues.cs @@ -0,0 +1,54 @@ +// 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.ComponentModel; +using System.Reflection; + +namespace Microsoft.AspNetCore.Mvc.Internal +{ + public static class ParameterDefaultValues + { + public static object[] GetParameterDefaultValues(MethodInfo methodInfo) + { + if (methodInfo == null) + { + throw new ArgumentNullException(nameof(methodInfo)); + } + + var parameters = methodInfo.GetParameters(); + var values = new object[parameters.Length]; + + for (var i = 0; i < parameters.Length; i++) + { + var parameterInfo = parameters[i]; + object defaultValue; + + if (parameterInfo.HasDefaultValue) + { + defaultValue = parameterInfo.DefaultValue; + } + else + { + var defaultValueAttribute = parameterInfo + .GetCustomAttribute(inherit: false); + + if (defaultValueAttribute?.Value == null) + { + defaultValue = parameterInfo.ParameterType.GetTypeInfo().IsValueType + ? Activator.CreateInstance(parameterInfo.ParameterType) + : null; + } + else + { + defaultValue = defaultValueAttribute.Value; + } + } + + values[i] = defaultValue; + } + + return values; + } + } +} 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 945a2baf05..a0768f6465 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj @@ -33,6 +33,8 @@ Microsoft.AspNetCore.Mvc.RouteAttribute + + diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvoker.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvoker.cs index e086c3a633..d8a0d866ec 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentInvoker.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc.ViewComponents @@ -109,17 +110,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents var startTimestamp = _logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : 0; object resultAsObject = null; - var taskGenericType = executor.TaskGenericType; + var returnType = executor.MethodReturnType; - if (taskGenericType == typeof(IViewComponentResult)) + if (returnType == typeof(Task)) { resultAsObject = await (Task)executor.Execute(component, arguments); } - else if (taskGenericType == typeof(string)) + else if (returnType == typeof(Task)) { resultAsObject = await (Task)executor.Execute(component, arguments); } - else if (taskGenericType == typeof(IHtmlContent)) + else if (returnType == typeof(Task)) { resultAsObject = await (Task)executor.Execute(component, arguments); } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentInvokerCache.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentInvokerCache.cs index c3d57f846d..701dfc8a0c 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentInvokerCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentInvokerCache.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Concurrent; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { @@ -55,7 +56,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal nameof(ViewComponentDescriptor))); } - executor = ObjectMethodExecutor.Create(viewComponentDescriptor.MethodInfo, viewComponentDescriptor.TypeInfo); + var parameterDefaultValues = ParameterDefaultValues + .GetParameterDefaultValues(methodInfo); + + executor = ObjectMethodExecutor.Create( + viewComponentDescriptor.MethodInfo, + viewComponentDescriptor.TypeInfo, + parameterDefaultValues); cache.Entries.TryAdd(viewComponentDescriptor, executor); return executor; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs index c574ffeb76..92a5013954 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs @@ -21,6 +21,7 @@ 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; @@ -2626,7 +2627,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal } [Fact] - public async Task InvokeAction_AsyncAction_WithCustomTaskReturnTypeThrows() + public async Task InvokeAction_AsyncAction_WithCustomTaskReturnType() { // Arrange var inputParam1 = 1; @@ -2646,19 +2647,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal nameof(TestController.TaskActionWithCustomTaskReturnType), actionParameters); - var 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)); + // Act + await invoker.InvokeAsync(); - // Act & Assert - var ex = await Assert.ThrowsAsync( - () => invoker.InvokeAsync()); - Assert.Equal(expectedException, ex.Message); + // Assert + Assert.IsType(typeof(EmptyResult), result); } [Fact] - public async Task InvokeAction_AsyncAction_WithCustomTaskOfTReturnTypeThrows() + public async Task InvokeAction_AsyncAction_WithCustomTaskOfTReturnType() { // Arrange var inputParam1 = 1; @@ -2678,15 +2675,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal nameof(TestController.TaskActionWithCustomTaskOfTReturnType), actionParameters); - var 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)); + // Act + await invoker.InvokeAsync(); - // Act & Assert - var ex = await Assert.ThrowsAsync( - () => invoker.InvokeAsync()); - Assert.Equal(expectedException, ex.Message); + // Assert + Assert.IsType(typeof(ObjectResult), result); + Assert.IsType(typeof(int), ((ObjectResult)result).Value); + Assert.Equal(1, ((ObjectResult)result).Value); } [Fact] @@ -2941,7 +2936,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal new IFilterMetadata[0], ObjectMethodExecutor.Create( actionDescriptor.MethodInfo, - actionDescriptor.ControllerTypeInfo)); + actionDescriptor.ControllerTypeInfo, + ParameterDefaultValues.GetParameterDefaultValues(actionDescriptor.MethodInfo))); // Act await invoker.InvokeAsync(); @@ -3378,12 +3374,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal public TaskDerivedType TaskActionWithCustomTaskReturnType(int i, string s) { - return new TaskDerivedType(); + var task = new TaskDerivedType(); + task.Start(); + return task; } public TaskOfTDerivedType TaskActionWithCustomTaskOfTReturnType(int i, string s) { - return new TaskOfTDerivedType(1); + var task = new TaskOfTDerivedType(1); + task.Start(); + return task; } /// @@ -3517,7 +3517,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal private static ObjectMethodExecutor CreateExecutor(ControllerActionDescriptor actionDescriptor) { - return ObjectMethodExecutor.Create(actionDescriptor.MethodInfo, actionDescriptor.ControllerTypeInfo); + return ObjectMethodExecutor.Create( + actionDescriptor.MethodInfo, + actionDescriptor.ControllerTypeInfo, + ParameterDefaultValues.GetParameterDefaultValues(actionDescriptor.MethodInfo)); } private static ControllerContext CreatControllerContext( diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs index a1526adfb2..77d0cc47d0 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MiddlewareFilterTest.cs @@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ObjectMethodExecutorTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ObjectMethodExecutorTest.cs deleted file mode 100644 index 9c186ddf69..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ObjectMethodExecutorTest.cs +++ /dev/null @@ -1,227 +0,0 @@ -// 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.ComponentModel; -using System.Globalization; -using System.Reflection; -using System.Threading.Tasks; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.Internal -{ - public class ObjectMethodExecutorTest - { - private TestObject _targetObject = new TestObject(); - private TypeInfo targetTypeInfo = typeof(TestObject).GetTypeInfo(); - - [Fact] - public void ExecuteValueMethod() - { - var executor = GetExecutorForMethod("ValueMethod"); - var result = executor.Execute( - _targetObject, - new object[] { 10, 20 }); - Assert.Equal(30, (int)result); - } - - [Fact] - public void ExecuteVoidValueMethod() - { - var executor = GetExecutorForMethod("VoidValueMethod"); - var result = executor.Execute( - _targetObject, - new object[] { 10 }); - Assert.Same(null, result); - } - - [Fact] - public void ExecuteValueMethodWithReturnType() - { - var executor = GetExecutorForMethod("ValueMethodWithReturnType"); - var result = executor.Execute( - _targetObject, - new object[] { 10 }); - var resultObject = Assert.IsType(result); - Assert.Equal("Hello", resultObject.value); - } - - [Fact] - public void ExecuteValueMethodUpdateValue() - { - var executor = GetExecutorForMethod("ValueMethodUpdateValue"); - var parameter = new TestObject(); - var result = executor.Execute( - _targetObject, - new object[] { parameter }); - var resultObject = Assert.IsType(result); - Assert.Equal("HelloWorld", resultObject.value); - } - - [Fact] - public void ExecuteValueMethodWithReturnTypeThrowsException() - { - var executor = GetExecutorForMethod("ValueMethodWithReturnTypeThrowsException"); - var parameter = new TestObject(); - Assert.Throws( - () => executor.Execute( - _targetObject, - new object[] { parameter })); - } - - [Fact] - public async Task ExecuteValueMethodAsync() - { - var executor = GetExecutorForMethod("ValueMethodAsync"); - var result = await executor.ExecuteAsync( - _targetObject, - new object[] { 10, 20 }); - Assert.Equal(30, (int)result); - } - - [Fact] - public async Task ExecuteValueMethodWithReturnTypeAsync() - { - var executor = GetExecutorForMethod("ValueMethodWithReturnTypeAsync"); - var result = await executor.ExecuteAsync( - _targetObject, - new object[] { 10 }); - var resultObject = Assert.IsType(result); - Assert.Equal("Hello", resultObject.value); - } - - [Fact] - public async Task ExecuteValueMethodUpdateValueAsync() - { - var executor = GetExecutorForMethod("ValueMethodUpdateValueAsync"); - var parameter = new TestObject(); - var result = await executor.ExecuteAsync( - _targetObject, - new object[] { parameter }); - var resultObject = Assert.IsType(result); - Assert.Equal("HelloWorld", resultObject.value); - } - - [Fact] - public async Task ExecuteValueMethodWithReturnTypeThrowsExceptionAsync() - { - var executor = GetExecutorForMethod("ValueMethodWithReturnTypeThrowsExceptionAsync"); - var parameter = new TestObject(); - await Assert.ThrowsAsync( - () => executor.ExecuteAsync( - _targetObject, - new object[] { parameter })); - } - - [Theory] - [InlineData("EchoWithDefaultAttributes", new object[] { "hello", true, 10 })] - [InlineData("EchoWithDefaultValues", new object[] { "hello", true, 20 })] - [InlineData("EchoWithDefaultValuesAndAttributes", new object[] { "hello", 20 })] - [InlineData("EchoWithNoDefaultAttributesAndValues", new object[] { null, 0, false, null })] - public void GetDefaultValueForParameters_ReturnsExpectedValues(string methodName, object[] expectedValues) - { - var executor = GetExecutorForMethod(methodName); - var defaultValues = new object[expectedValues.Length]; - - for (var index = 0; index < expectedValues.Length; index++) - { - defaultValues[index] = executor.GetDefaultValueForParameter(index); - } - - Assert.Equal(expectedValues, defaultValues); - } - - private ObjectMethodExecutor GetExecutorForMethod(string methodName) - { - var method = typeof(TestObject).GetMethod(methodName); - var executor = ObjectMethodExecutor.Create(method, targetTypeInfo); - return executor; - } - - public class TestObject - { - public string value; - public int ValueMethod(int i, int j) - { - return i + j; - } - - public void VoidValueMethod(int i) - { - - } - public TestObject ValueMethodWithReturnType(int i) - { - return new TestObject() { value = "Hello" }; ; - } - - public TestObject ValueMethodWithReturnTypeThrowsException(TestObject i) - { - throw new NotImplementedException("Not Implemented Exception"); - } - - public TestObject ValueMethodUpdateValue(TestObject parameter) - { - parameter.value = "HelloWorld"; - return parameter; - } - - public Task ValueMethodAsync(int i, int j) - { - return Task.FromResult(i + j); - } - - public async Task VoidValueMethodAsync(int i) - { - await ValueMethodAsync(3, 4); - } - public Task ValueMethodWithReturnTypeAsync(int i) - { - return Task.FromResult(new TestObject() { value = "Hello" }); - } - - public Task ValueMethodWithReturnTypeThrowsExceptionAsync(TestObject i) - { - throw new NotImplementedException("Not Implemented Exception"); - } - - public Task ValueMethodUpdateValueAsync(TestObject parameter) - { - parameter.value = "HelloWorld"; - return Task.FromResult(parameter); - } - - public string EchoWithDefaultAttributes( - [DefaultValue("hello")] string input1, - [DefaultValue(true)] bool input2, - [DefaultValue(10)] int input3) - { - return input1; - } - - public string EchoWithDefaultValues( - string input1 = "hello", - bool input2 = true, - int input3 = 20) - { - return input1; - } - - public string EchoWithDefaultValuesAndAttributes( - [DefaultValue("Hi")] string input1 = "hello", - [DefaultValue(10)] int input3 = 20) - { - return input1; - } - - public string EchoWithNoDefaultAttributesAndValues( - string input1, - int input2, - bool input3, - TestObject input4) - { - return input1; - } - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ParameterDefaultValuesTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ParameterDefaultValuesTest.cs new file mode 100644 index 0000000000..79f72a53c7 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ParameterDefaultValuesTest.cs @@ -0,0 +1,59 @@ +// 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.ComponentModel; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Internal +{ + public class ParameterDefaultValuesTest + { + [Theory] + [InlineData("DefaultAttributes", new object[] { "hello", true, 10 })] + [InlineData("DefaultValues", new object[] { "hello", true, 20 })] + [InlineData("DefaultValuesAndAttributes", new object[] { "hello", 20 })] + [InlineData("NoDefaultAttributesAndValues", new object[] { null, 0, false, null })] + public void GetParameterDefaultValues_ReturnsExpectedValues(string methodName, object[] expectedValues) + { + // Arrange + var methodInfo = typeof(TestObject).GetMethod(methodName); + + // Act + var actualValues = ParameterDefaultValues.GetParameterDefaultValues(methodInfo); + + // Assert + Assert.Equal(expectedValues, actualValues); + } + + private class TestObject + { + public void DefaultAttributes( + [DefaultValue("hello")] string input1, + [DefaultValue(true)] bool input2, + [DefaultValue(10)] int input3) + { + } + + public void DefaultValues( + string input1 = "hello", + bool input2 = true, + int input3 = 20) + { + } + + public void DefaultValuesAndAttributes( + [DefaultValue("Hi")] string input1 = "hello", + [DefaultValue(10)] int input3 = 20) + { + } + + public void NoDefaultAttributesAndValues( + string input1, + int input2, + bool input3, + TestObject input4) + { + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AsyncActionsTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AsyncActionsTests.cs new file mode 100644 index 0000000000..222d029176 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/AsyncActionsTests.cs @@ -0,0 +1,282 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class AsyncActionsTests : IClassFixture> + { + public AsyncActionsTests(MvcTestFixture fixture) + { + Client = fixture.Client; + } + + public HttpClient Client { get; } + + [Fact] + public async Task AsyncVoidAction_ReturnsOK() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/AsyncVoidAction"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(0, responseBody.Length); + } + + [Fact] + public async Task TaskAction_ReturnsOK() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/TaskAction"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(0, responseBody.Length); + } + + [Fact] + public async Task TaskExceptionAction_ReturnsCorrectError() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/TaskExceptionAction"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal("Action exception message: This is a custom exception.", responseBody); + } + + [Fact] + public async Task TaskOfObjectAction_ReturnsJsonFormattedObject() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/TaskOfObjectAction?message=Alpha"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("{\"text\":\"Alpha\"}", responseBody); + } + + [Fact] + public async Task TaskOfObjectExceptionAction_ReturnsCorrectError() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/TaskOfObjectExceptionAction?message=Alpha"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal("Action exception message: This is a custom exception.", responseBody); + } + + [Fact] + public async Task TaskOfIActionResultAction_ReturnsString() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/TaskOfIActionResultAction?message=Beta"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Beta", responseBody); + } + + [Fact] + public async Task TaskOfIActionResultExceptionAction_ReturnsCorrectError() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/TaskOfIActionResultExceptionAction?message=Beta"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal("Action exception message: This is a custom exception.", responseBody); + } + + [Fact] + public async Task TaskOfContentResultAction_ReturnsString() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/TaskOfContentResultAction?message=Gamma"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Gamma", responseBody); + } + + [Fact] + public async Task TaskOfContentResultExceptionAction_ReturnsCorrectError() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/TaskOfContentResultExceptionAction?message=Gamma"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal("Action exception message: This is a custom exception.", responseBody); + } + + [Fact] + public async Task PreCompletedValueTaskOfObjectAction_ReturnsJsonFormattedObject() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/PreCompletedValueTaskOfObjectAction?message=Delta"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("{\"text\":\"Delta\"}", responseBody); + } + + [Fact] + public async Task PreCompletedValueTaskOfObjectExceptionAction_ReturnsCorrectError() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/PreCompletedValueTaskOfObjectExceptionAction?message=Delta"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal("Action exception message: This is a custom exception.", responseBody); + } + + [Fact] + public async Task PreCompletedValueTaskOfIActionResultAction_ReturnsString() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/PreCompletedValueTaskOfIActionResultAction?message=Epsilon"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Epsilon", responseBody); + } + + [Fact] + public async Task PreCompletedValueTaskOfIActionResultExceptionAction_ReturnsCorrectError() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/PreCompletedValueTaskOfIActionResultExceptionAction?message=Epsilon"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal("Action exception message: This is a custom exception.", responseBody); + } + + [Fact] + public async Task PreCompletedValueTaskOfContentResultAction_ReturnsString() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/PreCompletedValueTaskOfContentResultAction?message=Zeta"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Zeta", responseBody); + } + + [Fact] + public async Task PreCompletedValueTaskOfContentResultExceptionAction_ReturnsCorrectError() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/PreCompletedValueTaskOfContentResultExceptionAction?message=Zeta"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal("Action exception message: This is a custom exception.", responseBody); + } + + [Fact] + public async Task CustomAwaitableVoidAction_ReturnsOK() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/CustomAwaitableVoidAction"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(0, responseBody.Length); + } + + [Fact] + public async Task CustomAwaitableVoidExceptionAction_ReturnsCorrectError() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/CustomAwaitableVoidExceptionAction"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal("Action exception message: This is a custom exception.", responseBody); + } + + [Fact] + public async Task CustomAwaitableOfObjectAction_ReturnsJsonFormattedObject() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/CustomAwaitableOfObjectAction?message=Eta"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("{\"text\":\"Eta\"}", responseBody); + } + + [Fact] + public async Task CustomAwaitableOfObjectExceptionAction_ReturnsCorrectError() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/CustomAwaitableOfObjectExceptionAction?message=Eta"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal("Action exception message: This is a custom exception.", responseBody); + } + + [Fact] + public async Task CustomAwaitableOfIActionResultAction_ReturnsString() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/CustomAwaitableOfIActionResultAction?message=Theta"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Theta", responseBody); + } + + [Fact] + public async Task CustomAwaitableOfIActionResultExceptionAction_ReturnsCorrectError() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/CustomAwaitableOfIActionResultExceptionAction?message=Theta"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal("Action exception message: This is a custom exception.", responseBody); + } + + [Fact] + public async Task CustomAwaitableOfContentResultAction_ReturnsString() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/CustomAwaitableOfContentResultAction?message=Iota"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Iota", responseBody); + } + + [Fact] + public async Task CustomAwaitableOfContentResultExceptionAction_ReturnsCorrectError() + { + // Act + var response = await Client.GetAsync("http://localhost/AsyncActions/CustomAwaitableOfContentResultExceptionAction?message=Iota"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal("Action exception message: This is a custom exception.", responseBody); + } + } +} diff --git a/test/WebSites/BasicWebSite/Controllers/AsyncActionsController.cs b/test/WebSites/BasicWebSite/Controllers/AsyncActionsController.cs new file mode 100644 index 0000000000..446c387009 --- /dev/null +++ b/test/WebSites/BasicWebSite/Controllers/AsyncActionsController.cs @@ -0,0 +1,254 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace BasicWebSite.Controllers +{ + public class AsyncActionsController : Controller + { + const int SimulateDelayMilliseconds = 20; + + public override void OnActionExecuted(ActionExecutedContext context) + { + // So that tests can observe we're following the proper flow after an exception, surface a + // message saying what the exception was. + if (context.Exception != null) + { + context.Result = Content($"Action exception message: {context.Exception.Message}"); + context.ExceptionHandled = true; + } + } + + public async void AsyncVoidAction() + { + await Task.Delay(SimulateDelayMilliseconds); + } + + public Task TaskAction() + { + return Task.Delay(SimulateDelayMilliseconds); + } + + public async Task TaskExceptionAction() + { + await Task.Delay(SimulateDelayMilliseconds); + throw new CustomException(); + } + + public async Task TaskOfObjectAction(string message) + { + await Task.Delay(SimulateDelayMilliseconds); + return new Message { Text = message }; + } + + public async Task TaskOfObjectExceptionAction(string message) + { + await Task.Delay(SimulateDelayMilliseconds); + throw new CustomException(); + } + + public async Task TaskOfIActionResultAction(string message) + { + await Task.Delay(SimulateDelayMilliseconds); + return Content(message); + } + + public async Task TaskOfIActionResultExceptionAction(string message) + { + await Task.Delay(SimulateDelayMilliseconds); + throw new CustomException(); + } + + public async Task TaskOfContentResultAction(string message) + { + await Task.Delay(SimulateDelayMilliseconds); + return Content(message); + } + + public async Task TaskOfContentResultExceptionAction(string message) + { + await Task.Delay(SimulateDelayMilliseconds); + throw new CustomException(); + } + + public ValueTask PreCompletedValueTaskOfObjectAction(string message) + { + return new ValueTask(new Message { Text = message }); + } + + public ValueTask PreCompletedValueTaskOfObjectExceptionAction(string message) + { + throw new CustomException(); + } + + public ValueTask PreCompletedValueTaskOfIActionResultAction(string message) + { + return new ValueTask(Content(message)); + } + + public ValueTask PreCompletedValueTaskOfIActionResultExceptionAction(string message) + { + throw new CustomException(); + } + + public ValueTask PreCompletedValueTaskOfContentResultAction(string message) + { + return new ValueTask(Content(message)); + } + + public ValueTask PreCompletedValueTaskOfContentResultExceptionAction(string message) + { + throw new CustomException(); + } + + public CustomAwaitable CustomAwaitableVoidAction() + { + return new CustomAwaitable(SimulateDelayMilliseconds); + } + + public CustomAwaitable CustomAwaitableVoidExceptionAction() + { + throw new CustomException(); + } + + public CustomAwaitable CustomAwaitableOfObjectAction(string message) + { + return new CustomAwaitable( + SimulateDelayMilliseconds, + new Message { Text = message }); + } + + public CustomAwaitable CustomAwaitableOfObjectExceptionAction(string message) + { + throw new CustomException(); + } + + public CustomAwaitable CustomAwaitableOfIActionResultAction(string message) + { + return new CustomAwaitable(SimulateDelayMilliseconds, Content(message)); + } + + public CustomAwaitable CustomAwaitableOfIActionResultExceptionAction(string message) + { + throw new CustomException(); + } + + public CustomAwaitable CustomAwaitableOfContentResultAction(string message) + { + return new CustomAwaitable(SimulateDelayMilliseconds, Content(message)); + } + + public CustomAwaitable CustomAwaitableOfContentResultExceptionAction(string message) + { + throw new CustomException(); + } + + public class Message + { + public string Text { get; set; } + } + + public class CustomAwaitable + { + protected readonly int _simulateDelayMilliseconds; + + public CustomAwaitable(int simulateDelayMilliseconds) + { + _simulateDelayMilliseconds = simulateDelayMilliseconds; + } + + public CustomAwaiter GetAwaiter() + { + return new CustomAwaiter(_simulateDelayMilliseconds); + } + } + + public class CustomAwaitable : CustomAwaitable + { + private readonly T _result; + + public CustomAwaitable(int simulateDelayMilliseconds, T result) + : base(simulateDelayMilliseconds) + { + _result = result; + } + + public new CustomAwaiter GetAwaiter() + { + return new CustomAwaiter(_simulateDelayMilliseconds, _result); + } + } + + public class CustomAwaiter : INotifyCompletion + { + private IList _continuations = new List(); + + public CustomAwaiter(int simulateDelayMilliseconds) + { + Task.Factory.StartNew(() => + { + Thread.Sleep(simulateDelayMilliseconds); + lock(_continuations) + { + IsCompleted = true; + + foreach (var continuation in _continuations) + { + continuation(); + } + + _continuations.Clear(); + } + }); + } + + public bool IsCompleted { get; private set; } + + public void OnCompleted(Action continuation) + { + lock (_continuations) + { + if (IsCompleted) + { + continuation(); + } + else + { + _continuations.Add(continuation); + } + } + } + + public void GetResult() + { + } + } + + public class CustomAwaiter : CustomAwaiter + { + private readonly T _result; + + public CustomAwaiter(int simulateDelayMilliseconds, T result) + : base(simulateDelayMilliseconds) + { + _result = result; + } + + public new T GetResult() => _result; + } + + public class CustomException : Exception + { + public CustomException() : base("This is a custom exception.") + { + } + } + } +}