A fellow Fluent NHibernate contributor Andrew Stewart called out for ideas on how some of his code could be refactored and if you've programmed with me you will know that I jumped at the chance. Here is the rather unfortunate code:
var findAutoMapMethod = typeof(AutoMapper).GetMethod("MergeMap"); var genericfindAutoMapMethod = findAutoMapMethod.MakeGenericMethod(type); genericfindAutoMapMethod.Invoke(autoMap, new[] { mapping });
Here is what Andrew wants to do (sort of):
Type type = GetSomeType();
autoMap.MergeMap<type>(mapping);
This is illegal because you can't pass a type instance as a type parameter to a generic method. For example, you can't declare a list of strings like this:
List<typeof(string)> stringList;
Andrew is calling a generic method but he doesn't know the type argument at compile time. The only way around this is reflection, and we all know that reflection is fragile - if someone changes the MergeMap method to be called MergeMapping, the code will compile and fail at runtime (hopefully when your unit tests are executed!). Fortunately, there is a way to make this block of code significantly less fragile. To do this, I'm going to rely on the handy ReflectionHelper that is part of the Fluent NHibernate code base. Here's the method I'm going to use
public static MethodInfo GetMethod(Expression<Func<object>> expression) { MethodCallExpression methodCall = (MethodCallExpression)expression.Body; return methodCall.Method; }
So if I pass this baby a lambda expression containing a method invocation, it gives me the corresponding MethodInfo. Sweet! Lets use it
var templateMethod = ReflectionHelper.GetMethod((AutoMapper a) => a.MergeMap<object>(null));
Notice the type argument of 'object' ? Its a placeholder. The parameter 'null' is also a placeholder. What you need to realise is that we are not going to -execute- the code in that lambda. The reflection helper is going to inspect it and give us a MethodInfo to work with. Before we can invoke the MethodInfo, we need to replace the first placeholder, which is why I have called this one templateMethodInfo. Lets replace the placeholder:
var realMethodInfo = templateMethod.GetGenericMethodDefinition()
.MakeGenericMethod(type);
The GetGenericMethodDefinition call lets you obtain a MethodInfo for an unbound version of MergeMap. Once the method is unbound, we supply the particular type argument we want, being 'type' in this case. MakeGenericMethod takes an array of types so this process would also work fine for a generic method that has more than one type argument. To further illustrate this process, here is another example:
public static void DoSomething<T, U>() { }
...
static void Main(string[] args) { Action doSomethingAction = DoSomething<object, object>; MethodInfo info = doSomethingAction.Method; Console.WriteLine(info); info = info.GetGenericMethodDefinition(); Console.WriteLine(info); info = info.MakeGenericMethod(typeof(string), typeof(int)); Console.WriteLine(info); Console.ReadLine(); }
OUTPUT: Void DoSomething[Object,Object]() Void DoSomething[T,U]() Void DoSomething[String,Int32]()
Getting back to the task at hand, we can now invoke realMethodInfo, but we must be sure to pass the original intended argument 'mapping':
realMethodInfo.Invoke(autoMap, new[] { mapping });
And thats it! Now if MergeMap is renamed, the lambda body will be updated accordingly by the refactoring tools. Now this code is still fragile because if another type argument is added to MergeMap, this code won't fail until runtime. Lets specifically check for this case and throw an appropriate exception. Lets also wrap it all up in one nice convienent method:
public static class InvocationHelper { public static object InvokeGenericMethodWithDynamicTypeArguments<T>(T target, Expression<Func<T, object>> expression, object[] methodArguments, params Type[] typeArguments) { var methodInfo = ReflectionHelper.GetMethod(expression); if (methodInfo.GetGenericArguments().Length != typeArguments.Length) throw new ArgumentException( string.Format("The method '{0}' has {1} type argument(s) but {2} type argument(s) were passed. The amounts must be equal.", methodInfo.Name, methodInfo.GetGenericArguments().Length, typeArguments.Length)); return methodInfo .GetGenericMethodDefinition() .MakeGenericMethod(typeArguments) .Invoke(target, methodArguments); } }
And call it:
InvocationHelper.InvokeGenericMethodWithDynamicTypeArguments( autoMap, a => a.MergeMap<object>(null), new[] { mapping }, type);
Ok so its certainly not as clear or robust as:
autoMap.MergeMap<type>(mapping);
But the former has the advantage of actually COMPILING, while the latter does not.