Warm tip: This article is reproduced from serverfault.com, please click

Create executable expression calling generic method

发布于 2021-10-14 12:04:16

Ok so let's assume I have this kind of class hierarchy:

/// In 3rd party library 
public class WidgetBase
{
    protected void Register<THandler>(Action<THandler> handler) { /* do something */ }
}

public record Message1();
public record Message2();

public sealed class MyWidget : Base
{
    public MyClass()
    {      
        RegisterHandlers(this);
    }

    [Handler]
    private void Handle(Message1 msg) {}
    
    [Handler]
    private void Handle(Message2 msg) {}
}

public static class Ext
{
    // Would prefer extension or normal static method
    // and not impose inheritance by putting this
    // in an intermediatery base class.
    public static void RegisterHandlers<T>(this T t)
    {
        // Discovers methods with 'Handler' attribute and calls t.Register()
    }
}

So the objective is to implement RegisterHandlers which would introspect over the object's methods and then produce an executable Expression which calls the base classes register method. Think on the lines of Asp.Net Core Controller handlers.

I just can't figure out how to do this. The point of the expression would be to be more performant though even a pure reflection based solution would be ok.

I can discover the methods and even produce an expression like t => this.Handle(t) but can't understand how the call to the generic base class method is done without the type.

There are a lot of similar questions in SO but couldn't find exact solution.

[Edit] Made the example more clear.

Questioner
MarkkuL
Viewed
0
canton7 2021-10-14 21:19:38

You can do something like:

public static void RegisterHandlers<T>(T t) where T : Base
{
    var methods = typeof(T).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(x => x.GetCustomAttribute<HandlerAttribute>() != null);
    var registerMethod = typeof(Base).GetMethod("Register", BindingFlags.NonPublic | BindingFlags.Instance);
    
    var blockItems = new List<Expression>();
    foreach (var method in methods)
    {
        if (method.IsGenericMethod || method.GetParameters().Length != 1 || method.ReturnType != typeof(void))
            throw new Exception($"Invalid method signature for method {method}");
        
        // The type of the Handle method's parameter (e.g. SomeType1 or SomeType2)
        var parameterType = method.GetParameters()[0].ParameterType;
        
        // MethodInfo for e.g. the Register<SomeType1> method
        var typedRegisterMethod = registerMethod.MakeGenericMethod(parameterType);
        
        // The type of delegate we'll pass to Register, e.g. Action<SomeType1>
        var delegateType = typeof(Action<>).MakeGenericType(parameterType);
        
        // Construct the x => Handle(x) delegate
        var delegateParameter = Expression.Parameter(parameterType);
        var delegateConstruction = Expression.Lambda(delegateType, Expression.Call(Expression.Constant(t), method, delegateParameter), delegateParameter);
        
        // Construct the Register(delegate) call
        var methodCall = Expression.Call(Expression.Constant(t), typedRegisterMethod, new[] { delegateConstruction });
        
        // Add this to the list of expressions we'll put in our block
        blockItems.Add(methodCall);
    }
    
    var compiled = Expression.Lambda<Action>(Expression.Block(blockItems)).Compile();
    compiled();
}

See it on dotnetfiddle.net.

Note that there's no particular advantage to doing this, over just using reflection. You're not caching the generated compiled or blockItems, which is where the savings in using compiled expressions for this sort of thing come from.


You could extend it slightly however, and add in such a cache:

private static class Cache<T> where T : Base
{
    public static readonly Action<T> Instance = CreateInstance();
    
    private static Action<T> CreateInstance()
    {
        var methods = typeof(T).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(x => x.GetCustomAttribute<HandlerAttribute>() != null);
        var registerMethod = typeof(Base).GetMethod("Register", BindingFlags.NonPublic | BindingFlags.Instance);

        var instanceParameter = Expression.Parameter(typeof(T));

        var blockItems = new List<Expression>();
        foreach (var method in methods)
        {
            if (method.IsGenericMethod || method.GetParameters().Length != 1 || method.ReturnType != typeof(void))
                throw new Exception($"Invalid method signature for method {method}");

            // The type of the Handle method's parameter (e.g. SomeType1 or SomeType2)
            var parameterType = method.GetParameters()[0].ParameterType;

            // MethodInfo for e.g. the Register<SomeType1> method
            var typedRegisterMethod = registerMethod.MakeGenericMethod(parameterType);

            // The type of delegate we'll pass to Register, e.g. Action<SomeType1>
            var delegateType = typeof(Action<>).MakeGenericType(parameterType);

            // Construct the x => Handle(x) delegate
            var delegateParameter = Expression.Parameter(parameterType);
            var delegateConstruction = Expression.Lambda(delegateType, Expression.Call(instanceParameter, method, delegateParameter), delegateParameter);

            // Construct the Register(delegate) call
            var methodCall = Expression.Call(instanceParameter, typedRegisterMethod, new[] { delegateConstruction });

            // Add this to the list of expressions we'll put in our block
            blockItems.Add(methodCall);
        }

        var compiled = Expression.Lambda<Action<T>>(Expression.Block(blockItems), instanceParameter).Compile();
        return compiled;
    }
}

public static void RegisterHandlers<T>(T t) where T : Base
{
    Cache<T>.Instance(t);
}

See it on dotnetfiddle.net.

Note how we're now taking the T instance as a parameter, and this lets us cache the generated Action<T>.