Thursday 21 August 2008

A less fragile way to invoke a generic method with a dynamic type argument

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.

10 comments:

  1. Hi Paul,

    I found this post quite informative and coincidentally extremely useful with what I'm doing right now.

    The answer to my question might seem obvious but I've had quite a hard time struggling with it.

    Basically I want to use reflection to instantiate a generic class. I've got something that looks like this:

    public class InternalClass"T" Where T: IEntity, class
    {
    }

    Please excuse the "". The parser doesn't like angled brackets.

    I already know what T is and I can use reflection to create an object of that type but I cannot for the life of me find out how to upcast it to the right type.

    Your post gave me a few ideas I might try later but I'm keen to see your response for this.

    Cheers
    Alvin

    ReplyDelete
  2. Hey there Alvin,

    Instantiation is a slightly different story. What constructors are defined on your generic class?

    I'm not sure I follow what you mean by "upcast it to the right type". Can you expand on this a bit?

    ReplyDelete
  3. My generic class just has a default constructor (no parameters). It literally looks something like:

    public class Foo : BaseClass"T"

    In my factory class I have some code which does:

    object theClass = Activator.CreateInstance(theType, null, null);

    If I can discover the type of T, is there a way to cast it from an object to BaseClass"T"?

    I apologise for my mistake in saying "upcast". In this instance, it's more of downcasting it to the correct type.

    ReplyDelete
  4. I'm having trouble making sense of what you are after, because it sounds like you are trying to CAST to a dynamic type. As in:

    Type theType = GetSomeTypeAtRuntime();
    object newInstance = Activator.CreateInstance(theType, null, null);

    ??? entity = (Entity<theType>)newInstance;

    The last line doesn't compile for the same reason that my example with the list of strings doesn't. But there is another problem: what the hell would you replace those questions marks with? As far as I can tell, there is nothing that you CAN put there. C# is an early bound language.

    Now if your factory method is generic, then you are obviously fine. Why isn't your factory method generic?

    ReplyDelete
  5. Err.. whoops. I mean C# is statically typed. Early/late binding is something altogether different.

    ReplyDelete
  6. The factory started out as generic but the problem was with the method calls in LINQ. Based on the type, the SQL expression changes. i.e. DataContext.GetTable"User"().Where(u => u.UserID == x);

    Thus, my interest in finding a way to downcast if I knew the type.

    This is a bit of a sticking point right now and I feel like I'm trying to fit a square peg in a round hole. I might send Malcolm an email and ask if he's got any insight on how to tackle this problem or if I'm just trying to complicate things.

    This thread has made me realise that I'm banging my head on a wall. Cheers Paul.

    ReplyDelete
  7. So what's the situation where this is actually needed/desirable?

    e.g. If you want to create a List<type-not-known-at-compile-time> don't you just want a List<object> anyway? Like one of those damn ArrayList things?

    ReplyDelete
  8. Well say for example you have this class

    public class Artist
    {
    private IList<Album> _albums;
    }

    And you want an ORM to instantiate one for you. This is illegal:

    _albums = new List<object>;

    Because contravariant generic type arguments are not supported. So what should it do?

    ReplyDelete
  9. I'm not getting this at all. If you know the type parameter at runtime why can't you just use Type.MakeGenericType?

    ReplyDelete
  10. Hey Mal,

    Which part don't you get? My post or the comments?

    The mechanism I described still uses MakeGenericMethod. The point is that it doesn't use a string to identify the method (it uses an expression tree) so it will survive a refactoring.

    ReplyDelete