Skip to content

Commit c63aff3

Browse files
Shane32sungam3r
andauthored
Pass-through exceptions triggered within Constructor/MethodInfo.Invoke (#210)
* Pass through exceptions triggered within MethodInfo.Invoke * Verify unwrapped constructor exceptions * Update src/GraphQL.Conventions/Extensions/Utilities.cs Co-authored-by: Ivan Maximov <sungam3r@yandex.ru> * Enhanced constructor invokeenhanced also * Update note Co-authored-by: Ivan Maximov <sungam3r@yandex.ru>
1 parent 1937c2c commit c63aff3

File tree

9 files changed

+356
-9
lines changed

9 files changed

+356
-9
lines changed

src/GraphQL.Conventions/Adapters/Engine/GraphQLEngine.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using GraphQL.Conventions.Adapters.Engine.Listeners.DataLoader;
1010
using GraphQL.Conventions.Builders;
1111
using GraphQL.Conventions.Execution;
12+
using GraphQL.Conventions.Extensions;
1213
using GraphQL.Conventions.Types.Descriptors;
1314
using GraphQL.Conventions.Types.Resolution;
1415
using GraphQL.Execution;
@@ -366,7 +367,7 @@ private object CreateInstance(System.Type type)
366367
var parameterValues = parameters
367368
.Select(parameter => _constructor.TypeResolutionDelegate(parameter.ParameterType))
368369
.ToArray();
369-
return ctor.Invoke(parameterValues);
370+
return ctor.InvokeEnhanced(parameterValues);
370371
}
371372
}
372373

src/GraphQL.Conventions/Adapters/Resolvers/FieldResolver.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Reflection;
44
using GraphQL.Conventions.Attributes.Execution.Unwrappers;
55
using GraphQL.Conventions.Attributes.Execution.Wrappers;
6+
using GraphQL.Conventions.Extensions;
67
using GraphQL.Conventions.Handlers;
78
using GraphQL.Conventions.Relay;
89
using GraphQL.Conventions.Types.Descriptors;
@@ -67,7 +68,7 @@ private object CallMethod(GraphFieldInfo fieldInfo, IResolutionContext context)
6768
{
6869
arguments = new[] { source }.Concat(arguments);
6970
}
70-
var result = methodInfo?.Invoke(source, arguments.ToArray());
71+
var result = methodInfo?.InvokeEnhanced(source, arguments.ToArray());
7172
return result;
7273
}
7374

src/GraphQL.Conventions/Adapters/Resolvers/WrappedAsyncFieldResolver.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Threading.Tasks;
66
using GraphQL.Conventions.Attributes.Execution.Unwrappers;
77
using GraphQL.Conventions.Attributes.Execution.Wrappers;
8+
using GraphQL.Conventions.Extensions;
89
using GraphQL.Conventions.Handlers;
910
using GraphQL.Conventions.Relay;
1011
using GraphQL.Conventions.Types.Descriptors;
@@ -70,7 +71,7 @@ private object CallMethod(GraphFieldInfo fieldInfo, IResolutionContext context)
7071
return Task.Run(() => AsyncHelpers.RunTask(resolutionTask, fieldInfo.Type.TypeParameter()));
7172
}
7273

73-
return methodInfo?.Invoke(source, arguments.ToArray());
74+
return methodInfo?.InvokeEnhanced(source, arguments.ToArray());
7475
}
7576

7677
private object GetSource(GraphFieldInfo fieldInfo, IResolutionContext context)

src/GraphQL.Conventions/Adapters/Resolvers/WrappedSyncFieldResolver.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Reflection;
55
using GraphQL.Conventions.Attributes.Execution.Unwrappers;
66
using GraphQL.Conventions.Attributes.Execution.Wrappers;
7+
using GraphQL.Conventions.Extensions;
78
using GraphQL.Conventions.Handlers;
89
using GraphQL.Conventions.Relay;
910
using GraphQL.Conventions.Types.Descriptors;
@@ -69,7 +70,7 @@ private object CallMethod(GraphFieldInfo fieldInfo, IResolutionContext context)
6970
return AsyncHelpers.RunTask(resolutionTask, fieldInfo.Type.TypeParameter());
7071
}
7172

72-
return methodInfo?.Invoke(source, arguments.ToArray());
73+
return methodInfo?.InvokeEnhanced(source, arguments.ToArray());
7374
}
7475

7576
private object GetSource(GraphFieldInfo fieldInfo, IResolutionContext context)

src/GraphQL.Conventions/Extensions/Utilities.cs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
using System.Reflection;
6+
using System.Runtime.ExceptionServices;
7+
using System.Security.Cryptography.X509Certificates;
8+
19
namespace GraphQL.Conventions.Extensions
210
{
311
public static class Utilities
@@ -19,5 +27,115 @@ public static bool IsIdentifierForType<T>(this string id) =>
1927

2028
public static bool IsIdentifierForType<T>(this NonNull<string> id) =>
2129
id.Value.IsIdentifierForType<T>();
30+
31+
/// <summary>
32+
/// Invokes a method represented by a specified <see cref="MethodInfo"/>, using
33+
/// the specified parameters.
34+
/// </summary>
35+
/// <param name="methodInfo">
36+
/// The method representation.
37+
/// </param>
38+
/// <param name="instance">
39+
/// The object on which to invoke the method. If the method is static, this
40+
/// value is ignored.
41+
/// </param>
42+
/// <param name="arguments">
43+
/// An argument list for the invoked method. This is an array of objects
44+
/// with the same number, order, and type as the parameters of the method
45+
/// to be invoked. If there are no parameters, parameters should be null.
46+
/// Ref/out parameters are not supported.
47+
/// </param>
48+
/// <returns>
49+
/// The return value from the method, or null for methods that return void.
50+
/// </returns>
51+
public static object InvokeEnhanced(this MethodInfo methodInfo, object instance, object[] arguments)
52+
{
53+
//just good practice
54+
if (methodInfo == null)
55+
throw new ArgumentNullException(nameof(methodInfo));
56+
//the following check is more descriptive than the NullReferenceException that would otherwise occur
57+
if (!methodInfo.IsStatic && instance == null)
58+
throw new ArgumentNullException(nameof(instance), "Instance is required for non static methods");
59+
//the lambda ignores extra arguments so the following check is necessary
60+
if (methodInfo.GetParameters().Length != (arguments?.Length ?? 0))
61+
throw new ArgumentException("Invalid number of arguments for this method", "arguments");
62+
//type errors will be thrown as InvalidCastException inside the lambda
63+
64+
var lambda = _methodDictionary.GetOrAdd(methodInfo, InvokeEnhancedUncachedMethod);
65+
66+
return lambda(instance, arguments);
67+
}
68+
private static ConcurrentDictionary<MethodInfo, Func<object, object[], object>> _methodDictionary = new ConcurrentDictionary<MethodInfo, Func<object, object[], object>>();
69+
private static Func<object, object[], object> InvokeEnhancedUncachedMethod(MethodInfo methodInfo)
70+
{
71+
var instanceParameter = Expression.Parameter(typeof(object));
72+
var argumentsParameter = Expression.Parameter(typeof(object[]));
73+
var instanceExpression = methodInfo.IsStatic ? null : Expression.Convert(instanceParameter, methodInfo.DeclaringType);
74+
var methodParameters = methodInfo.GetParameters();
75+
var parameters = methodParameters.Select((param, index) => {
76+
return Expression.Convert(Expression.ArrayAccess(argumentsParameter, Expression.Constant(index)), param.ParameterType);
77+
});
78+
var call = Expression.Call(instanceExpression, methodInfo, parameters);
79+
Expression body;
80+
if (methodInfo.ReturnType == typeof(void))
81+
{
82+
body = Expression.Block(new Expression[] {
83+
call,
84+
Expression.Constant(null, typeof(object))
85+
});
86+
}
87+
else
88+
{
89+
body = Expression.Convert(call, typeof(object));
90+
}
91+
var lambda = Expression.Lambda<Func<object, object[], object>>(body, instanceParameter, argumentsParameter);
92+
return lambda.Compile();
93+
}
94+
95+
/// <summary>
96+
/// Invokes a constructor represented by a specified <see cref="ConstructorInfo"/>, using
97+
/// the specified parameters.
98+
/// </summary>
99+
/// <param name="constructorInfo">
100+
/// The constructor representation. Must not be a static constructor.
101+
/// </param>
102+
/// <param name="arguments">
103+
/// An argument list for the invoked constructor. This is an array of objects
104+
/// with the same number, order, and type as the parameters of the constructor
105+
/// to be invoked. If there are no parameters, parameters should be null.
106+
/// Ref/out parameters are not supported.
107+
/// </param>
108+
/// <returns>
109+
/// The constructed object.
110+
/// </returns>
111+
public static object InvokeEnhanced(this ConstructorInfo constructorInfo, object[] arguments)
112+
{
113+
//just good practice
114+
if (constructorInfo == null)
115+
throw new ArgumentNullException(nameof(constructorInfo));
116+
//the lambda ignores extra arguments so the following check is necessary
117+
if (constructorInfo.GetParameters().Length != (arguments?.Length ?? 0))
118+
throw new ArgumentException("Invalid number of arguments for this constructor", "arguments");
119+
//type errors will be thrown as InvalidCastException inside the lambda
120+
121+
var lambda = _constructorDictionary.GetOrAdd(constructorInfo, InvokeEnhancedUncachedConstructor);
122+
123+
return lambda(arguments);
124+
}
125+
private static ConcurrentDictionary<ConstructorInfo, Func<object[], object>> _constructorDictionary = new ConcurrentDictionary<ConstructorInfo, Func<object[], object>>();
126+
private static Func<object[], object> InvokeEnhancedUncachedConstructor(ConstructorInfo constructorInfo)
127+
{
128+
if (constructorInfo.IsStatic)
129+
throw new ArgumentException("Constructor must not be static", nameof(constructorInfo));
130+
var argumentsParameter = Expression.Parameter(typeof(object[]));
131+
var constructorParameters = constructorInfo.GetParameters();
132+
var parameters = constructorParameters.Select((param, index) => {
133+
return Expression.Convert(Expression.ArrayAccess(argumentsParameter, Expression.Constant(index)), param.ParameterType);
134+
});
135+
var call = Expression.New(constructorInfo, parameters);
136+
var body = Expression.Convert(call, typeof(object));
137+
var lambda = Expression.Lambda<Func<object[], object>>(body, argumentsParameter);
138+
return lambda.Compile();
139+
}
22140
}
23141
}

src/GraphQL.Conventions/Types/Resolution/Extensions/ReflectionExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Reflection;
66
using System.Runtime.CompilerServices;
77
using System.Threading.Tasks;
8+
using GraphQL.Conventions.Extensions;
89
using GraphQL.Conventions.Types.Descriptors;
910

1011
namespace GraphQL.Conventions.Types.Resolution.Extensions
@@ -115,7 +116,7 @@ public static object ConvertToArrayRuntime(this IList list, Type elementType)
115116
.GetTypeInfo()
116117
.GetMethod("ConvertToArray", BindingFlags.Static | BindingFlags.Public);
117118
var genericMethod = convertMethod.MakeGenericMethod(elementType);
118-
return genericMethod.Invoke(null, new object[] { list });
119+
return genericMethod.InvokeEnhanced(null, new object[] { list });
119120
}
120121

121122
public static bool IsExtensionMethod(this MethodInfo methodInfo)

src/GraphQL.Conventions/Utilities/AsyncHelpers.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System;
1+
using GraphQL.Conventions.Extensions;
2+
using System;
23
using System.Collections.Generic;
34
using System.Reflection;
45
using System.Threading;
@@ -49,7 +50,7 @@ public static object RunTask(Delegate task, TypeInfo typeInfo)
4950
var asyncMethod = typeof(AsyncHelpers)
5051
.GetMethod(nameof(RunSync))
5152
.MakeGenericMethod(typeInfo.AsType());
52-
return asyncMethod.Invoke(null, new object[] { task });
53+
return asyncMethod.InvokeEnhanced(null, new object[] { task });
5354
}
5455
catch (Exception ex)
5556
{

test/GraphQL.Conventions.Tests/Adapters/Engine/CustomErrorTransformationTests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ public async Task Will_Use_Custom_Error_Transformation_When_Provided()
4242
result.Errors.Count.ShouldEqual(1);
4343
var error = result.Errors.First();
4444
error.ShouldBeOfType<ExecutionError>();
45-
error.InnerException.ShouldBeOfType<TargetInvocationException>();
46-
error.InnerException.InnerException.ShouldBeOfType<CustomException>();
45+
error.InnerException.ShouldBeOfType<CustomException>();
4746
}
4847

4948
class Query

0 commit comments

Comments
 (0)