在 C# 中使用表达式树动态调用对应于参数类型的方法

Dynamically calling methods corresponding a parameter's type using expression trees in c#

我正在构建一个事件处理程序,其工作方式类似于聚合在事件源系统中的行为方式。

我试图实现的目标可以按照记录的方式完成 here 我研究过的其他参考资料是 Marten 源代码和 Greg Young 的 m-r。我想用表达式树达到同样的效果。

本质上,我希望我的聚合实现动态执行传递给它的事件,如果它有一个接受该事件作为参数的 Handle 方法。

首先我有我的活动

abstract class Event { }
class Event1 : Event { }
class Event2 : Event { }

我的聚合实现继承自 AggregateBase class。

class Aggregate : AggregateBase
{
    public int Counter { get; set; } = 10;
    public void Handle(Event1 @event)
    {
        Counter++;
        Console.WriteLine(Counter);
    }

    public void Handle(Event2 @event)
    {
        Counter = 100;
        Console.WriteLine(Counter);
    }
}

最后,AggregateBase 在成员字典中执行处理程序的反射和注册。

abstract class AggregateBase
{
    // We're only interested in methods named Handle
    const string HandleMethodName = "Handle";
    private readonly IDictionary<Type, Action<Event>> _handlers = new Dictionary<Type, Action<Event>>();

    public AggregateBase()
    {
        var methods = this.GetType().GetMethods()
            .Where(p => p.Name == HandleMethodName
                && p.GetParameters().Length == 1);

        var runnerParameter = Expression.Parameter(this.GetType(), "r");

        foreach(var method in methods)
        {
            var eventType = method.GetParameters().Single<ParameterInfo>().ParameterType;

            // if parameter is not assignable from one event, then skip
            if (!eventType.IsClass || eventType.IsAbstract || !typeof(Event).IsAssignableFrom(eventType)) continue;

            var eventParameter = Expression.Parameter(eventType, "e");
            var body = Expression.Call(runnerParameter, method, eventParameter);
            var lambda = Expression.Lambda(body, eventParameter);
            var compiled = lambda.Compile();
            _handlers.Add(eventType, (Action<Event>)compiled);
        }
    }

    public void Apply(Event @event)
    {
        var type = @event.GetType();
        if(_handlers.ContainsKey(type))
        {
            _handlers[type](@event);
        }
    }
}

使用上面的代码,报错

variable 'r' of type 'ConsoleApp_TestTypeBuilder.Aggregate' referenced from scope '', but it is not defined'.

我要实现的目标是:

  1. 在关闭的 class 中获取名为 Handle 的方法以及实现 Event
  2. 的参数
  3. 将事件参数的类型和方法调用存储为字典中的 Action 委托
  4. 当事件应用于聚合时,执行与事件类型对应的动作委托。否则,不对事件执行任何操作。

您可以像对待常规静态方法一样对待 lambda 函数。这意味着您应该向它传递额外的参数(在您的情况下为 Aggregate)。换句话说,您需要创建 lambda,该类型看起来像 Action<AggregateBase, Event>.

将您的 _handlers 声明更改为

private readonly IDictionary<Type, Action<AggregateBase, Event>> _handlers
  = new Dictionary<Type, Action<AggregateBase, Event>>();

现在你可以像这样编写 AggregateBase 构造函数:

var methods = this.GetType().GetMethods()
    .Where(p => p.Name == handleMethodName
                && p.GetParameters().Length == 1);

var runnerParameter = Expression.Parameter(typeof(AggregateBase), "r");
var commonEventParameter = Expression.Parameter(typeof(Event), "e");

foreach (var method in methods)
{
    var eventType = method.GetParameters().Single().ParameterType;

    var body = Expression.Call(
        Expression.Convert(runnerParameter, GetType()),
        method,
        Expression.Convert(commonEventParameter, eventType)
      );

    var lambda = Expression.Lambda<Action<AggregateBase, Event>>(
      body, runnerParameter, commonEventParameter);

    _handlers.Add(eventType, lambda.Compile());
}

编辑:您还需要更改 Apply 方法中的调用:

public void Apply(Event @event)
{
    var type = @event.GetType();
    if (_handlers.ContainsKey(type))
        _handlers[type](this, @event);
}

首先,使用块表达式将runnerParameter引入上下文。其次,使参数 e 成为基类型,这样您就不必弄乱委托类型,然后使用转换表达式将其转换为派生类型。第三(可选),使用泛型 Expression.Lambda 重载,这样您无需转换即可获得所需的委托类型。

var eventParameter = Expression.Parameter(typeof(Event), "e");
var body = Expression.Call(runnerParameter, method, Expression.Convert(eventParameter, eventType));
var block = Expression.Block(runnerParameter, body);
var lambda = Expression.Lambda<Action<Event>>(block, eventParameter);
var compiled = lambda.Compile();
_handlers.Add(eventType, compiled);

这将一直有效,直到您调用处理程序,然后您将获得 NRE,因为 runnerParameter 没有值。将其更改为常量,以便您的块在 this.

上关闭
var runnerParameter = Expression.Constant(this, this.GetType());

另一项建议:将您的 selection/exclusion 标准移出循环,这样您就不会混淆问题,并将您发现的事实保存在匿名对象中以供日后使用。

var methods = from m in this.GetType().GetMethods()
              where m.Name == HandleMethodName
              let parameters = m.GetParameters()
              where parameters.Length == 1
              let p = parameters[0]
              let pt = p.ParameterType
              where pt.IsClass
              where !pt.IsAbstract
              where typeof(Event).IsAssignableFrom(pt)
              select new
              {
                  MethodInfo = m,
                  ParameterType = pt
              };

然后当您循环 methods 时,您只是在创建委托。

foreach (var method in methods)
{
    var eventType = method.ParameterType;
    var eventParameter = Expression.Parameter(typeof(Event), "e");
    var body = Expression.Call(runnerParameter, method.MethodInfo, Expression.Convert(eventParameter, eventType));
    var block = Expression.Block(runnerParameter, body);
    var lambda = Expression.Lambda<Action<Event>>(block, eventParameter);
    var compiled = lambda.Compile();
    _handlers.Add(eventType, compiled);
}

编辑: 仔细检查后,我意识到块表达式是不必要的。使 runnerParameter 成为常量表达式可以自行解决超出范围的问题。