创建调用泛型方法的可执行表达式

Create executable expression calling generic method

好吧,假设我有这种 class 层次结构:

/// 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()
    }
}

所以 objective 是实现 RegisterHandlers,它会自省对象的方法,然后生成一个可执行文件 Expression,它调用基础 classes 注册方法。考虑 Asp.Net 核心控制器处理程序。

我只是不知道该怎么做。表达式的要点是提高性能,尽管即使是基于纯反射的解决方案也可以。

我可以发现这些方法,甚至可以生成像 t => this.Handle(t) 这样的表达式,但无法理解如何在没有类型的情况下调用通用基础 class 方法。

SO里有很多类似的问题,但找不到确切的解决方案。

[编辑] 使示例更加清晰。

可以做如下事情:

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.

请注意,与仅使用反射相比,这样做并没有特别的优势。您没有缓存生成的 compiledblockItems,这是为此类事情使用编译表达式节省的地方。


不过您可以稍微扩展它,并添加这样的缓存:

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.

请注意我们现在如何将 T 实例作为参数,这让我们可以缓存生成的 Action<T>.

到目前为止,您的代码存在一些问题,主要是关于未正确定义扩展方法。此外,当您编写使用多个不同泛型的代码时,这些泛型并非都接受相同的类型,这有助于为类型参数提供比 T.

更具描述性的名称

另一点是您的扩展可能应该作为受保护的方法进入基 class。您的实际用例可能与上述不同,足以保证扩展形式,但请考虑一下您是否真的需要这种方式。扩展是否与 Base class 紧密耦合?如果是这样,除非基数 class 不在您的控制范围内,否则它不是扩展的好选择。即便如此,代理基地 class 可能是更好的选择。

总之,进入直接解决方案:反射。

基本流程是这样的:

  • 获取 Register<> 方法的通用方法模板。
  • 对于派生的 class 中具有 Handler 属性的每个方法:
    • 使用正确的参数类型专门化 Register<> 方法。
    • 创建正确类型的委托以调用当前实例上的方法。
    • 以委托作为参数调用专用方法。
  • 利润。

(好的,我只是假设最后一点。)

最有趣的部分是从方法中获取“正确类型的委托”。虽然 MethodInfo 包含 CreateDelegate 方法,但您必须传入正确的专用 Action<T> 类型。幸运的是,您的专用 Register<> 方法的参数类型正是我们在这里想要的。

让我们尝试一个简单的实现:

static class Ext
{
    public static void RegisterHandlers<T>(this T instance)
    {
        // Get the generic method template for `Register<T>`
        var registerTemplate = typeof(T).GetMethod("Register", BindingFlags.Instance | BindingFlags.NonPublic);
        
        // Locate all handler methods
        var handlerQuery = 
            from m in typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
            where m.ReturnType == typeof(void) && m.GetCustomAttribute<HandlerAttribute>() != null
            let parms = m.GetParameters()
            where parms.Length == 1
            select (m, parms[0].ParameterType);
        
        foreach (var (handler, parmType) in handlerQuery)
        {
            // Specialize the Register<> method
            var registerMethod = registerTemplate.MakeGenericMethod(parmType);
            
            // Create Action<T> delegate for method
            var actionType = registerMethod.GetParameters()[0].ParameterType;
            object action = handler.CreateDelegate(actionType, instance);
            
            // Call the specialized Register<> method
            registerMethod.Invoke(instance, new[] { action });
        }
    }
}

您需要添加适当的错误处理等,但这是基本思想。

虽然它在这个特定案例中有效,但我使用了一个非常简单的 GetMethod 调用来获取通用方法模板。验证您是否拥有正确的方法 - 它是一个通用方法模板(提示:IsGenericMethodDefinition)并且参数是正确的类型 - 有点困难。常见问题可以这样解决:

    var registerTemplate = 
    (
        from m in typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
        where m.Name == "Register" && m.IsGenericMethodDefinition
        let args = m.GetGenericArguments()
        where args.Length == 1
        let actionTemplate = typeof(Action<>).MakeGenericType(args[0])
        let parms = m.GetParameters()
        where parms.Length == 1 && parms[0].ParameterType == actionTemplate
        select m
    ).FirstOrDefault();
    if (registerTemplate is null)
        return;

如果定义了不兼容的 Register 方法,或者匹配不明确,或者...等等,这至少可以避免令人尴尬的崩溃。


虽然这很有趣,但我会认真考虑如何找到一种非反射方法。反射可能有点混乱,而且速度很慢。如果你不能消除反射,至少尽量减少它。您可以让 Register 方法采用 DelegateType 而不是需要 Action<T>.

的通用方法