CreateDelegate<T> – An Exercise in Using Expressions

Category:UncategorizedTag: :

In a previous blog post I showed a basic example of how to use the Delegate.CreateDelegate() method as an alternative to the slow MethodInfo.Invoke() for dynamically invoking a method of a class at runtime. The only downside of using CreateDelegate is that its not strongly typed. This is usually not a problem when the signature of the method that must be invoked is known at compile as is the case in the example shown in the blog post mentioned earlier.

var subject = new Subject();
var doSomething = (Func<String, String>)
    Delegate.CreateDelegate(typeof(Func<String, String>), subject, "DoSomething");
Console.WriteLine(doSomething("Hello Freggles"));

Here we?re simply able to cast the result to the requested delegate type. But what if a generic method must be invoked for which the type parameters can vary at runtime? You can still use the CreateDelegate method, but you can?t cast the result to a strongly typed delegate type. This means that in order to invoke the created delegate, the DynamicInvoke method must be called on the returned Delegate object. This has the nasty side effect that when the original method being called throws an exception, the DynamicInvoke method wraps the original exception in a TargetInvocationException.  

So in order to find a better way and also exercise my Expressions-fu, I tried to come up with a CreateDelegate<T> extension method that can be used to a create a strongly typed delegate for a MethodInfo object.

Suppose we have to dynamically invoke a method with the following signature:

private void Map<TDomainEvent, TEvent>(TDomainEvent domainEvent, TEvent @event)
    where TDomainEvent : IDomainEvent
    where TEvent : IEvent
{
    ...
}

Given an instance of IDomainEvent and IEvent, using the CreateDelegate<T> extension method that I?m about to show, we can dynamically invoke this method using the following code:

var action = GetType()
    .GetMethod("Map", methodBindings)
    .MakeGenericMethod(domainEvent.GetType(), @event.GetType())
    .CreateDelegate<Action<IDomainEvent, IEvent>>(this);

action(domainEvent, @event);

Here we determine a specific MethodInfo object for the Map method using the types of the event objects we have. Now here?s the code for the strongly typed CreateDelegate extension method.

public static class MethodInfoExtensions
{
    public static TDelegate CreateDelegate<TDelegate>(this MethodInfo method, 
                                                      Object instance) 
        where TDelegate : class
    {
        return CreateCachedDelegate<TDelegate>(method, 
            (typeArguments, parameterExpressions) =>
            {
                Expression<Func<Object>> instanceExpression = () => instance;
                return Expression.Call(Expression.Convert(instanceExpression.Body, 
                                                          instance.GetType()),
                                       method.Name,
                                       typeArguments,
                                       ProvideStrongArgumentsFor(method, 
                                                                 parameterExpressions));
            });
    }

    public static TDelegate CreateDelegate<TDelegate>(this MethodInfo method) 
        where TDelegate : class
    {
        return CreateCachedDelegate<TDelegate>(method, 
            (typeArguments, parameterExpressions) =>
                Expression.Call(method.DeclaringType, method.Name, typeArguments,
                                ProvideStrongArgumentsFor(method, parameterExpressions)));
    }

    private static TDelegate CreateCachedDelegate<TDelegate>(MethodBase method, 
        Func<Type[], ParameterExpression[], MethodCallExpression> getCallExpression)
        where TDelegate : class
    {
        var @delegate = GetFromCache<TDelegate>();
        if(null == @delegate)
        {
            @delegate = CreateDelegate<TDelegate>(method, getCallExpression);
            StoreInCache(@delegate);
        }

        return @delegate;
    }

    private static TDelegate GetFromCache<TDelegate>()
    {
        Object delegateObj;
        if(_delegateCache.TryGetValue(typeof(TDelegate), out delegateObj))
            return (TDelegate)delegateObj;

        return default(TDelegate);
    }

    private static void StoreInCache<TDelegate>(TDelegate @delegate)
    {
        _delegateCache.TryAdd(typeof(TDelegate), @delegate);
    }

    private static TDelegate CreateDelegate<TDelegate>(MethodBase method, 
        Func<Type[], ParameterExpression[], MethodCallExpression> getCallExpression)
    {
        var parameterExpressions = ExtractParameterExpressionsFrom<TDelegate>();
        CheckParameterCountsAreEqual(parameterExpressions, method.GetParameters());

        var call = getCallExpression(GetTypeArgumentsFor(method), parameterExpressions);

        var lambda = Expression.Lambda<TDelegate>(call, parameterExpressions);
        return lambda.Compile();
    }

    private static ParameterExpression[] ExtractParameterExpressionsFrom<TDelegate>()
    {
        return typeof(TDelegate)
            .GetMethod("Invoke")
            .GetParameters()
            .ToParameterExpressions()
            .ToArray();
    }

    private static void CheckParameterCountsAreEqual(
        IEnumerable<ParameterExpression> delegateParameters,
        IEnumerable<ParameterInfo> methodParameters)
    {
        if(delegateParameters.Count() != methodParameters.Count())
            throw new InvalidOperationException(
                "The number of parameters of the requested delegate does not match " +
                "the number parameters of the specified method.");
    }

    private static Type[] GetTypeArgumentsFor(MethodBase method)
    {
        var typeArguments = method.GetGenericArguments();
        return (typeArguments.Length > 0) ? typeArguments : null;
    }

    private static Expression[] ProvideStrongArgumentsFor(
        MethodInfo method, ParameterExpression[] parameterExpressions)
    {
        return method.GetParameters()
            .Select((parameter, index) => 
                Expression.Convert(parameterExpressions[index], 
                                   parameter.ParameterType))
            .ToArray();
    }

    private static readonly ConcurrentDictionary<Type, Object> _delegateCache =
        new ConcurrentDictionary<Type, Object>(); 
}

I actually provided two CreateDelegate methods here, one for instance methods (the first one) and one for static methods ( the second one). The created delegates are cached in a dictionary because the call of lambda.Compile() seems to be quite expensive.

Now I have to warn you that, based on first preliminary measurements, this implementation probably isn?t going to outperform  Delegate.CreateDelegate(). In fact, if you?re interested have a look at this approach first as it seems much faster. I added some spike code to the Elegant Code repository for those who fancy to take a look.  

7 thoughts on “CreateDelegate<T> – An Exercise in Using Expressions

  1. While stuff like this definitely gets a nod from the “cool” corner, it adds a bucket-load of complexity. It’s unfortunately that for every one case that something like this is a really good idea, there are probably ten or more cases where it’s complete overkill when a much more simple, readable, and maintainable option could be adopted.

  2. @Steve Py
    I agree that the code of MethodInfoExtensions is not everyday stuff. But what about the use of these extension methods do you consider not to be simple, readable and maintanable? This way you can avoid a lot of the C# ceremony that is needed for casting and type checking.

  3. @881780d6ce97ca1a04900589654acb89:disqus – excellent post. this strategy allows me to hook up dozens of request/response patterns without any manual (and tedious) dictionary<type, Func> or IL Emit code. well done!  

    @33c0c983c18fe1422cface0cfbf9d972:disqus – since when does “[visual] complexity” imply unreliability? 

Comments are closed.

Find me

RSS
Facebook
Twitter
LinkedIn

Disclaimer

The opinions and content expressed here are my own and not those of my employer.