Search code examples
c#.netexpression-treesdynamicmethod

How do I turn an Action<T> to a Compiled Expression or a DynamicMethod by generating IL in C#?


I am having to deal with maintaining a library which allows users to register a generic handler (Action<T>) against it and later on once it receives an event it goes through each of the registered handlers and passes them the event. To keep the question short, let us skip the reasons why it has been done this way.

Because of this design we have had to call DynamicInvoke when passing each event; This is proving quite slow and hence the need to either turn the delegates into a CompiledExpression or a DynamicMethod using IL generation. I have seen various examples of implementing this for PropertyGetters (Matt Warren's excellent article) but I cannot get it to work with Action<T> where T can be both a ValueType or a ReferenceType.

Here's a current (slow) working example to play with (simplified for brevity):

void Main()
{
    var producer = new FancyEventProduder();
    var fancy = new FancyHandler(producer);
    
    fancy.Register<Base>(x => Console.WriteLine(x));
    producer.Publish(new Child());  
}

public sealed class FancyHandler
{
    private readonly List<Delegate> _handlers;
    
    public FancyHandler(FancyEventProduder produer)
    {
        _handlers = new List<Delegate>();
        produer.OnMessge += OnMessage;
    }

    public void Register<T>(Action<T> handler) => _handlers.Add(handler);

    private void OnMessage(object sender, object payload)
    {
        Type payloadType = payload.GetType();
        foreach (Delegate handler in _handlers)
        {
            // this could be cached at the time of registration but has negligable impact
            Type delegParamType = handler.Method.GetParameters()[0].ParameterType;
            if(delegParamType.IsAssignableFrom(payloadType))
            {
                handler.DynamicInvoke(payload);
            }           
        }
    }
}

public sealed class FancyEventProduder
{
    public event EventHandler<object> OnMessge;
    
    public void Publish(object payload) => OnMessge?.Invoke(this, payload);
}

public class Base { }
public sealed class Child : Base { }

Solution

  • Not sure if it is a good idea:

    public sealed class FancyHandler
    {
        private readonly List<Tuple<Delegate, Type, Action<object>>> _handlers = new List<Tuple<Delegate, Type, Action<object>>>();
    
        public FancyHandler(FancyEventProduder produer)
        {
            produer.OnMessge += OnMessage;
        }
    
        public void Register<T>(Action<T> handler)
        {
            _handlers.Add(Tuple.Create((Delegate)handler, typeof(T), BuildExpression(handler)));
        }
    
        private static Action<object> BuildExpression<T>(Action<T> handler)
        {
            var par = Expression.Parameter(typeof(object));
            var casted = Expression.Convert(par, typeof(T));
            var call = Expression.Call(Expression.Constant(handler.Target), handler.Method, casted);
            var exp = Expression.Lambda<Action<object>>(call, par);
            return exp.Compile();
        }
    
        private void OnMessage(object sender, object payload)
        {
            Type payloadType = payload.GetType();
    
            foreach (var handlerDelegate in _handlers)
            {
                // this could be cached at the time of registration but has negligable impact
                Type delegParamType = handlerDelegate.Item2;
                    
                if (delegParamType.IsAssignableFrom(payloadType))
                {
                    handlerDelegate.Item3(payload);
                }
            }
        }
    }
    

    Note that some smaller ideas can be reused even without using expression trees: saving the typeof(T) instead of calling many times handler.Method.GetParameters()[0].ParameterType for example.

    Some test cases:

    fancy.Register<Base>(x => Console.WriteLine($"Base: {x}"));
    fancy.Register<Child>(x => Console.WriteLine($"Child: {x}"));
    fancy.Register<object>(x => Console.WriteLine($"object: {x}"));
    fancy.Register<long>(x => Console.WriteLine($"long: {x}"));
    fancy.Register<long?>(x => Console.WriteLine($"long?: {x}"));
    fancy.Register<int>(x => Console.WriteLine($"int: {x}"));
    fancy.Register<int?>(x => Console.WriteLine($"int?: {x}"));
    
    producer.Publish(new Base());
    producer.Publish(new Child());
    producer.Publish(5);
    

    Full expression tree (the type check is moved INSIDE the expression tree, where the as is done instead of IsAssignableFrom)

    public sealed class FancyHandler
    {
        private readonly List<Action<object>> _handlers = new List<Action<object>>();
    
        public FancyHandler(FancyEventProduder produer)
        {
            produer.OnMessge += OnMessage;
        }
    
        public void Register<T>(Action<T> handler)
        {
            _handlers.Add(BuildExpression(handler));
        }
    
        private static Action<object> BuildExpression<T>(Action<T> handler)
        {
            if (typeof(T) == typeof(object))
            {
                return (Action<object>)(Delegate)handler;
            }
    
            var par = Expression.Parameter(typeof(object));
    
            Expression body;
    
            if (typeof(T).IsValueType)
            {
                // We remove the nullable part of value types
                Type type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
    
                var unbox = Expression.Unbox(par, typeof(T));
                body = Expression.IfThen(Expression.TypeEqual(par, type), Expression.Call(Expression.Constant(handler.Target), handler.Method, unbox));
    
                if (type != typeof(T))
                {
                    // Nullable type
                    // null with methods that accept nullable type: call the method
                    body = Expression.IfThenElse(Expression.Equal(par, Expression.Constant(null)), Expression.Call(Expression.Constant(handler.Target), handler.Method, Expression.Constant(null, typeof(T))), body);
                }
            }
            else
            {
                // Imagine the resulting code will be:
    
                // (object par) => 
                // {
                //     if (par == null)
                //     {
                //         handler(null);
                //     }
                //     else
                //     {
                //         T local;
                //         local = par as T;
                //         if (local != null)
                //         {
                //             handler(local);
                //         }
                //     }
                // }
    
                var local = Expression.Variable(typeof(T));
                    
                var typeAs = Expression.Assign(local, Expression.TypeAs(par, typeof(T)));
    
                var block = Expression.Block(new[]
                {
                    local,
                },
                new Expression[]
                {
                    typeAs,
                    Expression.IfThen(Expression.NotEqual(typeAs, Expression.Constant(null)), Expression.Call(Expression.Constant(handler.Target), handler.Method, typeAs))
                });
    
                // Handling case par == null, call the method
                body = Expression.IfThenElse(Expression.Equal(par, Expression.Constant(null)), Expression.Call(Expression.Constant(handler.Target), handler.Method, Expression.Constant(null, typeof(T))), block);
            }
    
            var exp = Expression.Lambda<Action<object>>(body, par);
            return exp.Compile();
        }
    
        private void OnMessage(object sender, object payload)
        {
            foreach (var handlerDelegate in _handlers)
            {
                handlerDelegate(payload);
            }
        }
    }
    

    This version supports even null:

    producer.Publish(null);
    

    A third version, based on the idea of @BorisB, this version eskews the use of Expression and uses directly delegates. It should have a shorter warmup time (no Expression trees to be compiled). There is still has a minor reflection problem, but fortunately this problem is only present during the adding of new handlers (there is a comment that explains it).

    public sealed class FancyHandler
    {
        private readonly List<Action<object>> _handlers = new List<Action<object>>();
    
        public FancyHandler(FancyEventProduder produer)
        {
            produer.OnMessge += OnMessage;
        }
    
        public void Register<T>(Action<T> handler)
        {
            if (typeof(T).IsValueType)
            {
                _handlers.Add(BuildExpressionValueType(handler));
            }
            else
            {
                // Have to use reflection here because the as operator requires a T is class
                // this check is bypassed by using reflection
                _handlers.Add((Action<object>)_buildExpressionReferenceTypeT.MakeGenericMethod(typeof(T)).Invoke(null, new[] { handler }));
            }
        }
    
        private static Action<object> BuildExpressionValueType<T>(Action<T> handler)
        {
            // We remove the nullable part of value types
            Type type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
    
            if (type == typeof(T))
            {
                // Non nullable
                return (object par) =>
                {
                    if (par is T)
                    {
                        handler((T)par);
                    }
                };
            }
    
            // Nullable type
            return (object par) =>
            {
                if (par == null || par is T)
                {
                    handler((T)par);
                }
            };
        }
    
        private static readonly MethodInfo _buildExpressionReferenceTypeT = typeof(FancyHandler).GetMethod(nameof(BuildExpressionReferenceType), BindingFlags.Static | BindingFlags.NonPublic);
    
        private static Action<object> BuildExpressionReferenceType<T>(Action<T> handler) where T : class
        {
            if (typeof(T) == typeof(object))
            {
                return (Action<object>)(Delegate)handler;
            }
    
            return (object par) =>
            {
                if (par == null)
                {
                    handler((T)par);
                }
                else
                {
                    T local = par as T;
    
                    if (local != null)
                    {
                        handler(local);
                    }
                }
            };
        }
    
        private void OnMessage(object sender, object payload)
        {
            foreach (var handlerDelegate in _handlers)
            {
                handlerDelegate(payload);
            }
        }
    }