Search code examples
c#reflectionlambdadelegatessystem.reflection

How to create lambdas and add them to actions using reflection


Suppose in C# I have class that has an arbitrary number of Actions, which can have any number of generic arguments:

public class Container
{
    public Action a;
    public Action<float> b;
    public Action<int, float> c;
    // etc...
}

And I am registering some debug lambdas on an instance of this class which just print out the name of the action's field:

public static void Main()
{
    Container container = new Container();

    container.a += () => Console.WriteLine("a was called");
    container.b += (temp1) => Console.WriteLine("b was called");
    container.c += (temp1, temp2) => Console.WriteLine("c was called");

    container.a();
    container.b(1.5f);
    container.c(1, 1.5f);
}

I would like to automate the creation of these debug lambdas using reflection, as follows:

public static void Main()
{
    Container container = new Container();

    GenerateDebug(container);

    if(container.a != null) container.a();
    if(container.b != null) container.b(1.5f);
    if(container.c != null) container.c(1, 1.5f);
}

public static void GenerateDebug(Container c)
{
    Type t = c.GetType();
    FieldInfo[] fields = t.GetFields(BindingFlags.Instance | BindingFlags.Public);
    foreach(FieldInfo field in fields)
    {
        Action callback = () => Console.WriteLine(field.Name + " was called");

        Type[] actionArgTypes = field.FieldType.GetGenericArguments();
        if(actionArgTypes.Length == 0)
        {
            Action action = field.GetValue(c) as System.Action;
            action += callback;
            field.SetValue(c, action);
        }
        else
        {
            // 1. Create an Action<T1, T2, ...> object that takes the types in 'actionArgTypes' which wraps the 'callback' action
            // 2. Add this new lambda to the current Action<T1, T2, ...> field 
        }   
    }
}

I'm able to get the desired result for Actions with no arguments - the above code does indeed print out "a was called" - but I don't know how to do it for generics.

I believe I know what I need to do, just not how:

  1. Use reflection to create an Action<T1, T2, ...> object using the types in actionArgTypes, which wraps a call to the callback action.
  2. Add this newly created object to the Generic Action specified by the field.

How would I go about doing this, or similar that achieves the desired effect of adding such a debug callback?


Solution

  • Here is a rather simple implementation using Expressions, one could resort to use a ILGenerator directly, but that is not worth the hussle in this case.

    public static void GenerateDebug(Container c)
    {
        Type t = c.GetType();
        FieldInfo[] fields = t.GetFields(BindingFlags.Instance | BindingFlags.Public);
        foreach(FieldInfo field in fields)
        {
            var fieldName = field.Name;
            Type[] actionArgTypes = field.FieldType.GetGenericArguments();
            // Create paramter expression for each argument
            var parameters = actionArgTypes.Select(Expression.Parameter).ToArray();
            // Create method call expression with a constant argument
            var writeLineCall = Expression.Call(typeof(Console).GetMethod("WriteLine", new [] {typeof(string)}), Expression.Constant(fieldName + " was called"));
            // Create and compile lambda using the fields type
            var lambda = Expression.Lambda(field.FieldType, writeLineCall, parameters);
            var @delegate = lambda.Compile();
            var action = field.GetValue(c) as Delegate;
            // Combine and set delegates
            action = Delegate.Combine(action, @delegate);
            field.SetValue(c, action);
        }
    }
    

    Here is the same function using ILGenerator, that should work with .net framework 2.0+ aswell as .net core. In a real life application there should be checks, caching and probably a whole assemblybuilder:

    public static void GenerateDebug(Container c)
    {
        Type t = c.GetType();
        FieldInfo[] fields = t.GetFields(BindingFlags.Instance | BindingFlags.Public);
        foreach(FieldInfo field in fields)
        {
            var fieldName = field.Name;
            Type[] actionArgTypes = field.FieldType.GetGenericArguments();
    
            var dm = new DynamicMethod(fieldName, typeof(void), actionArgTypes);
            var il = dm.GetILGenerator();
            il.Emit(OpCodes.Ldstr, fieldName + " was called using ilgen");
            il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new [] {typeof(string)}));
            il.Emit(OpCodes.Ret);
    
            var @delegate = dm.CreateDelegate(field.FieldType);
            var action = field.GetValue(c) as Delegate;
            // Combine and set delegates
            action = Delegate.Combine(action, @delegate);
            field.SetValue(c, action);
        }
    }