Skip to content

Commit f32e04f

Browse files
committed
Translating calls to generic methods on dynamic objects
1 parent 88745d5 commit f32e04f

File tree

7 files changed

+242
-13
lines changed

7 files changed

+242
-13
lines changed

ReadableExpressions.UnitTests/WhenTranslatingDynamicOperations.cs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,5 +157,126 @@ public void ShouldTranslateAParameterisedMethodCall()
157157

158158
Assert.AreEqual("(obj, ci) => obj.ToString(ci)", translated);
159159
}
160+
161+
[TestMethod]
162+
public void ShouldTranslateAGenericParameterisedMethodCall()
163+
{
164+
var valueConverterConvertCallSiteBinder = Binder.InvokeMember(
165+
CSharpBinderFlags.InvokeSimpleName,
166+
"Convert",
167+
new[] { typeof(string), typeof(int) },
168+
typeof(WhenTranslatingDynamicOperations),
169+
new[]
170+
{
171+
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
172+
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType, null)
173+
});
174+
175+
var dynamicParameter = Expression.Parameter(typeof(ValueConverter), "valueConverter");
176+
var valueParameter = Expression.Parameter(typeof(string), "value");
177+
178+
var dynamicConvertCall = Expression.Dynamic(
179+
valueConverterConvertCallSiteBinder,
180+
typeof(int),
181+
dynamicParameter,
182+
valueParameter);
183+
184+
var dynamicConvertLambda = Expression.Lambda<Func<ValueConverter, string, int>>(
185+
dynamicConvertCall,
186+
dynamicParameter,
187+
valueParameter);
188+
189+
dynamicConvertLambda.Compile();
190+
191+
var translated = dynamicConvertLambda.ToReadableString();
192+
193+
Assert.AreEqual("(valueConverter, value) => valueConverter.Convert<string, int>(value)", translated);
194+
}
195+
196+
[TestMethod]
197+
public void ShouldTranslateACallWithTooFewParameters()
198+
{
199+
var valueConverterConvertCallSiteBinder = Binder.InvokeMember(
200+
CSharpBinderFlags.InvokeSimpleName,
201+
"Convert",
202+
new[] { typeof(int), typeof(string) },
203+
typeof(WhenTranslatingDynamicOperations),
204+
new[]
205+
{
206+
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
207+
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType, null)
208+
});
209+
210+
var dynamicParameter = Expression.Parameter(typeof(ValueConverter), "valueConverter");
211+
212+
var dynamicConvertCall = Expression.Dynamic(
213+
valueConverterConvertCallSiteBinder,
214+
typeof(string),
215+
dynamicParameter);
216+
217+
var dynamicConvertLambda = Expression.Lambda<Func<ValueConverter, string>>(
218+
dynamicConvertCall,
219+
dynamicParameter);
220+
221+
dynamicConvertLambda.Compile();
222+
223+
var translated = dynamicConvertLambda.ToReadableString();
224+
225+
// The method type parameter can't be figured out from the arguments and return type, so are missing:
226+
Assert.AreEqual("valueConverter => valueConverter.Convert()", translated);
227+
}
228+
229+
[TestMethod]
230+
public void ShouldTranslateAParameterlessCallWithGenericParameters()
231+
{
232+
var typePrinterPrintCallSiteBinder = Binder.InvokeMember(
233+
CSharpBinderFlags.InvokeSimpleName,
234+
"Print",
235+
new[] { typeof(DateTime) },
236+
typeof(WhenTranslatingDynamicOperations),
237+
new[]
238+
{
239+
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
240+
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType, null)
241+
});
242+
243+
var dynamicParameter = Expression.Parameter(typeof(TypePrinter), "typePrinter");
244+
245+
var dynamicPrintCall = Expression.Dynamic(
246+
typePrinterPrintCallSiteBinder,
247+
typeof(void),
248+
dynamicParameter);
249+
250+
var dynamicPrintLambda = Expression.Lambda<Action<TypePrinter>>(
251+
dynamicPrintCall,
252+
dynamicParameter);
253+
254+
dynamicPrintLambda.Compile();
255+
256+
var translated = dynamicPrintLambda.ToReadableString();
257+
258+
// The method type parameter can't be figured out from the arguments and return type, so are missing:
259+
Assert.AreEqual("typePrinter => typePrinter.Print()", translated);
260+
}
261+
262+
// ReSharper disable once UnusedMember.Local
263+
private class ValueConverter
264+
{
265+
// ReSharper disable once UnusedMember.Local
266+
public TResult Convert<TValue, TResult>(TValue value)
267+
{
268+
return (TResult)(object)value;
269+
}
270+
}
271+
272+
// ReSharper disable once UnusedMember.Local
273+
private class TypePrinter
274+
{
275+
// ReSharper disable once UnusedMember.Local
276+
public void Print<T>()
277+
{
278+
Console.WriteLine(typeof(T));
279+
}
280+
}
160281
}
161282
}

ReadableExpressions/Extensions/TypeExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ internal static class TypeExtensions
3131

3232
public static string GetFriendlyName(this Type type)
3333
{
34+
if (type.FullName == null)
35+
{
36+
// An open generic parameter Type:
37+
return null;
38+
}
39+
3440
if (type.IsArray)
3541
{
3642
return type.GetElementType().GetFriendlyName() + "[]";

ReadableExpressions/Translators/BclMethodInfoWrapper.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ namespace AgileObjects.ReadableExpressions.Translators
22
{
33
using System;
44
using System.Collections.Generic;
5+
using System.Linq;
56
using System.Reflection;
67
using Extensions;
78

89
internal class BclMethodInfoWrapper : IMethodInfo
910
{
1011
private readonly MethodInfo _method;
12+
private IEnumerable<Type> _genericArguments;
1113

12-
public BclMethodInfoWrapper(MethodInfo method)
14+
public BclMethodInfoWrapper(MethodInfo method, IEnumerable<Type> genericArguments = null)
1315
{
1416
_method = method;
17+
_genericArguments = genericArguments;
1518
IsExtensionMethod = method.IsExtensionMethod();
1619
}
1720

@@ -23,8 +26,16 @@ public BclMethodInfoWrapper(MethodInfo method)
2326

2427
public MethodInfo GetGenericMethodDefinition() => _method.GetGenericMethodDefinition();
2528

26-
public IEnumerable<Type> GetGenericArguments() => _method.GetGenericArguments();
29+
public IEnumerable<Type> GetGenericArguments() =>
30+
(_genericArguments ?? (_genericArguments = _method.GetGenericArguments()));
2731

2832
public IEnumerable<ParameterInfo> GetParameters() => _method.GetParameters();
33+
34+
public Type GetGenericArgumentFor(Type parameterType)
35+
{
36+
var parameterIndex = Array.IndexOf(_method.GetGenericArguments(), parameterType, 0);
37+
38+
return _genericArguments.ElementAt(parameterIndex);
39+
}
2940
}
3041
}

ReadableExpressions/Translators/DynamicExpressionTranslator.cs

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,21 +167,91 @@ protected override bool DoTranslate(
167167
var subject = context.Translate(subjectObject);
168168
var methodName = match.Groups["MethodName"].Value;
169169
var method = subjectObject.Type.GetMethod(methodName);
170+
var methodArguments = dynamicExpression.Arguments.Skip(1).ToArray();
170171

171-
var methodInfo = (method != null)
172-
? new BclMethodInfoWrapper(method)
173-
: (IMethodInfo)new MissingMethodInfo(methodName);
172+
var methodInfo = GetMethodInfo(
173+
methodName,
174+
method,
175+
methodArguments,
176+
dynamicExpression.Type);
174177

175178
translated = _methodCallTranslator.GetMethodCall(
176179
subject,
177180
methodInfo,
178-
dynamicExpression.Arguments.Skip(1).ToArray(),
181+
methodArguments,
179182
dynamicExpression,
180183
context);
181184

182185
return true;
183186
}
184187

188+
private static IMethodInfo GetMethodInfo(
189+
string methodName,
190+
MethodInfo method,
191+
Expression[] methodArguments,
192+
Type methodReturnType)
193+
{
194+
if (method == null)
195+
{
196+
return new MissingMethodInfo(methodName);
197+
}
198+
199+
return new BclMethodInfoWrapper(
200+
method,
201+
GetGenericArgumentsOrNull(method, methodArguments, methodReturnType));
202+
}
203+
204+
private static IEnumerable<Type> GetGenericArgumentsOrNull(
205+
MethodInfo method,
206+
Expression[] methodArguments,
207+
Type methodReturnType)
208+
{
209+
if (!method.IsGenericMethod)
210+
{
211+
return null;
212+
}
213+
214+
var genericParameterTypes = method.GetGenericArguments();
215+
216+
var methodParameters = method
217+
.GetParameters()
218+
.Select((p, i) => new { Index = i, Parameter = p })
219+
.ToArray();
220+
221+
var genericArguments = new Type[genericParameterTypes.Length];
222+
223+
for (var i = 0; i < genericParameterTypes.Length; i++)
224+
{
225+
var genericParameterType = genericParameterTypes[i];
226+
227+
if (genericParameterType == method.ReturnType)
228+
{
229+
genericArguments[i] = methodReturnType;
230+
continue;
231+
}
232+
233+
var matchingMethodParameter = methodParameters
234+
.FirstOrDefault(p => p.Parameter.ParameterType == genericParameterType);
235+
236+
if (matchingMethodParameter == null)
237+
{
238+
return null;
239+
}
240+
241+
var matchingMethodArgument = methodArguments
242+
.ElementAtOrDefault(matchingMethodParameter.Index);
243+
244+
if (matchingMethodArgument == null)
245+
{
246+
return null;
247+
}
248+
249+
genericArguments[i] = matchingMethodArgument.Type;
250+
}
251+
252+
return genericArguments;
253+
}
254+
185255
private class MissingMethodInfo : IMethodInfo
186256
{
187257
public MissingMethodInfo(string name)
@@ -200,6 +270,8 @@ public MissingMethodInfo(string name)
200270
public IEnumerable<Type> GetGenericArguments() => Enumerable.Empty<Type>();
201271

202272
public IEnumerable<ParameterInfo> GetParameters() => Enumerable.Empty<ParameterInfo>();
273+
274+
public Type GetGenericArgumentFor(Type parameterType) => null;
203275
}
204276
}
205277

ReadableExpressions/Translators/Formatting/ParameterSet.cs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ private IEnumerable<Func<Expression, string>> GetArgumentTranslators(
9393
{
9494
var parameter = parameters.ElementAtOrDefault(i);
9595

96-
if (IsNotFuncType(parameter))
96+
if (IsNotFuncType(parameter, method))
9797
{
9898
return defaultArgumentTranslator;
9999
}
@@ -105,7 +105,7 @@ private IEnumerable<Func<Expression, string>> GetArgumentTranslators(
105105
.ToArray();
106106
}
107107

108-
private static bool IsNotFuncType(ParameterInfo parameter)
108+
private static bool IsNotFuncType(ParameterInfo parameter, IMethodInfo method)
109109
{
110110
if (parameter == null)
111111
{
@@ -117,11 +117,21 @@ private static bool IsNotFuncType(ParameterInfo parameter)
117117
return false;
118118
}
119119

120-
var parameterTypeName = parameter.ParameterType.FullName;
120+
var parameterType = parameter.ParameterType;
121121

122-
return !(parameterTypeName.StartsWith("System.Action", StringComparison.Ordinal) ||
123-
parameterTypeName.StartsWith("System.Func", StringComparison.Ordinal)) ||
124-
(parameter.ParameterType.Assembly != typeof(Action).Assembly);
122+
if (parameterType.FullName == null)
123+
{
124+
if (!method.IsGenericMethod)
125+
{
126+
return true;
127+
}
128+
129+
parameterType = method.GetGenericArgumentFor(parameter.ParameterType);
130+
}
131+
132+
return !(parameterType.FullName.StartsWith("System.Action", StringComparison.Ordinal) ||
133+
parameterType.FullName.StartsWith("System.Func", StringComparison.Ordinal)) ||
134+
(parameter.ParameterType.Assembly != typeof(Action).Assembly);
125135
}
126136

127137
private static bool CanBeConvertedToMethodGroup(Expression argument)

ReadableExpressions/Translators/IMethodInfo.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,7 @@ internal interface IMethodInfo
1717
IEnumerable<Type> GetGenericArguments();
1818

1919
IEnumerable<ParameterInfo> GetParameters();
20+
21+
Type GetGenericArgumentFor(Type parameterType);
2022
}
2123
}

ReadableExpressions/Translators/MethodCallExpressionTranslator.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,14 @@ private static string GetGenericArgumentsIfNecessary(IMethodInfo method)
145145

146146
var argumentNames = method
147147
.GetGenericArguments()
148-
.Select(a => a.GetFriendlyName());
148+
.Select(a => a.GetFriendlyName())
149+
.Where(name => name != null)
150+
.ToArray();
151+
152+
if (!argumentNames.Any())
153+
{
154+
return null;
155+
}
149156

150157
return $"<{string.Join(", ", argumentNames)}>";
151158
}

0 commit comments

Comments
 (0)