From 6f33ebc1f56c6d254cbfbfde4910d351ca246229 Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Sun, 12 Feb 2017 23:08:55 -0800 Subject: [PATCH] ObjectMethodExecutor --- .../HubEndPoint.cs | 37 +-- .../Internal/ObjectMethodExecutor.cs | 262 ++++++++++++++++++ .../Microsoft.AspNetCore.SignalR.csproj | 3 +- .../Properties/AssemblyInfo.cs | 7 + .../ObjectMethodExecutorTests.cs | 251 +++++++++++++++++ 5 files changed, 543 insertions(+), 17 deletions(-) create mode 100644 src/Microsoft.AspNetCore.SignalR/Internal/ObjectMethodExecutor.cs create mode 100644 src/Microsoft.AspNetCore.SignalR/Properties/AssemblyInfo.cs create mode 100644 test/Microsoft.AspNetCore.SignalR.Tests/ObjectMethodExecutorTests.cs diff --git a/src/Microsoft.AspNetCore.SignalR/HubEndPoint.cs b/src/Microsoft.AspNetCore.SignalR/HubEndPoint.cs index ea9b4393c6..d3faf14bfc 100644 --- a/src/Microsoft.AspNetCore.SignalR/HubEndPoint.cs +++ b/src/Microsoft.AspNetCore.SignalR/HubEndPoint.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.Sockets; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -251,7 +252,7 @@ namespace Microsoft.AspNetCore.SignalR Id = invocationDescriptor.Id }; - var methodInfo = descriptor.MethodInfo; + var methodExecutor = descriptor.MethodExecutor; using (var scope = _serviceScopeFactory.CreateScope()) { @@ -262,21 +263,24 @@ namespace Microsoft.AspNetCore.SignalR { InitializeHub(hub, connection); - var result = methodInfo.Invoke(hub, invocationDescriptor.Arguments); - var resultTask = result as Task; - if (resultTask != null) + object result = null; + if (methodExecutor.IsMethodAsync) { - await resultTask; - if (methodInfo.ReturnType.GetTypeInfo().IsGenericType) + if (methodExecutor.TaskGenericType == null) { - var property = resultTask.GetType().GetProperty("Result"); - invocationResult.Result = property?.GetValue(resultTask); + await (Task)methodExecutor.Execute(hub, invocationDescriptor.Arguments); + } + else + { + result = await methodExecutor.ExecuteAsync(hub, invocationDescriptor.Arguments); } } else { - invocationResult.Result = result; + result = methodExecutor.Execute(hub, invocationDescriptor.Arguments); } + + invocationResult.Result = result; } catch (TargetInvocationException ex) { @@ -306,9 +310,9 @@ namespace Microsoft.AspNetCore.SignalR private void DiscoverHubMethods() { - var type = typeof(THub); + var typeInfo = typeof(THub).GetTypeInfo(); - foreach (var methodInfo in type.GetTypeInfo().DeclaredMethods.Where(m => IsHubMethod(m))) + foreach (var methodInfo in typeInfo.DeclaredMethods.Where(m => IsHubMethod(m))) { var methodName = methodInfo.Name; @@ -317,7 +321,8 @@ namespace Microsoft.AspNetCore.SignalR throw new NotSupportedException($"Duplicate definitions of '{methodInfo.Name}'. Overloading is not supported."); } - _methods[methodName] = new HubMethodDescriptor(methodInfo); + var executor = ObjectMethodExecutor.Create(methodInfo, typeInfo); + _methods[methodName] = new HubMethodDescriptor(executor); if (_logger.IsEnabled(LogLevel.Debug)) { @@ -362,13 +367,13 @@ namespace Microsoft.AspNetCore.SignalR // REVIEW: We can decide to move this out of here if we want pluggable hub discovery private class HubMethodDescriptor { - public HubMethodDescriptor(MethodInfo methodInfo) + public HubMethodDescriptor(ObjectMethodExecutor methodExecutor) { - MethodInfo = methodInfo; - ParameterTypes = methodInfo.GetParameters().Select(p => p.ParameterType).ToArray(); + MethodExecutor = methodExecutor; + ParameterTypes = methodExecutor.ActionParameters.Select(p => p.ParameterType).ToArray(); } - public MethodInfo MethodInfo { get; } + public ObjectMethodExecutor MethodExecutor { get; } public Type[] ParameterTypes { get; } } diff --git a/src/Microsoft.AspNetCore.SignalR/Internal/ObjectMethodExecutor.cs b/src/Microsoft.AspNetCore.SignalR/Internal/ObjectMethodExecutor.cs new file mode 100644 index 0000000000..bb34b4eb9b --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR/Internal/ObjectMethodExecutor.cs @@ -0,0 +1,262 @@ +// 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.SignalR.Internal +{ + internal class ObjectMethodExecutor + { + private object[] _parameterDefaultValues; + private ActionExecutorAsync _executorAsync; + private 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; + } + + 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; } + + private ActionExecutorAsync TaskOfTActionExecutorAsync + { + get + { + if (_executorAsync == null) + { + _executorAsync = GetExecutorAsync(TaskGenericType, MethodInfo, TargetTypeInfo); + } + + return _executorAsync; + } + } + + public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo) + { + var executor = new ObjectMethodExecutor(methodInfo, targetTypeInfo); + executor._executor = GetExecutor(methodInfo, targetTypeInfo); + return executor; + } + + public Task ExecuteAsync(object target, object[] parameters) + { + return TaskOfTActionExecutorAsync(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)); + } + + EnsureParameterDefaultValues(); + + 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 + MethodCallExpression methodCall; + + if (!methodInfo.IsStatic) + { + var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType()); + methodCall = Expression.Call(instanceCast, methodInfo, parameters); + } + else + { + methodCall = Expression.Call(null, 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 void EnsureParameterDefaultValues() + { + if (_parameterDefaultValues == null) + { + var count = ActionParameters.Length; + _parameterDefaultValues = new object[count]; + + for (var i = 0; i < count; i++) + { + var parameterInfo = ActionParameters[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; + } + } + + _parameterDefaultValues[i] = defaultValue; + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.SignalR/Microsoft.AspNetCore.SignalR.csproj b/src/Microsoft.AspNetCore.SignalR/Microsoft.AspNetCore.SignalR.csproj index a6392ca909..f8785b364a 100644 --- a/src/Microsoft.AspNetCore.SignalR/Microsoft.AspNetCore.SignalR.csproj +++ b/src/Microsoft.AspNetCore.SignalR/Microsoft.AspNetCore.SignalR.csproj @@ -13,7 +13,8 @@ - + + diff --git a/src/Microsoft.AspNetCore.SignalR/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.SignalR/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..91975c8961 --- /dev/null +++ b/src/Microsoft.AspNetCore.SignalR/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// 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.Runtime.CompilerServices; + + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.SignalR.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.SignalR.Tests/ObjectMethodExecutorTests.cs b/test/Microsoft.AspNetCore.SignalR.Tests/ObjectMethodExecutorTests.cs new file mode 100644 index 0000000000..94d1d99629 --- /dev/null +++ b/test/Microsoft.AspNetCore.SignalR.Tests/ObjectMethodExecutorTests.cs @@ -0,0 +1,251 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Internal; +using Xunit; + +namespace Microsoft.AspNetCore.SignalR.Tests +{ + 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 ExecuteStaticValueMethodWithReturnType() + { + var executor = GetExecutorForMethod("StaticValueMethodWithReturnType"); + 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.ThrowsAny( + () => 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 })] + [InlineData("StaticEchoWithDefaultVaules", new object[] { "hello", true, 20 })] + 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 static TestObject StaticValueMethodWithReturnType(int i) + { + return new TestObject() { value = "Hello" }; + } + + 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; + } + + public static string StaticEchoWithDefaultVaules(string input1 = "hello", + bool input2 = true, + int input3 = 20) + { + return input1; + } + } + } +}