在 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'.
我要实现的目标是:
- 在关闭的 class 中获取名为
Handle
的方法以及实现 Event
的参数
- 将事件参数的类型和方法调用存储为字典中的 Action 委托
- 当事件应用于聚合时,执行与事件类型对应的动作委托。否则,不对事件执行任何操作。
您可以像对待常规静态方法一样对待 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
成为常量表达式可以自行解决超出范围的问题。
我正在构建一个事件处理程序,其工作方式类似于聚合在事件源系统中的行为方式。
我试图实现的目标可以按照记录的方式完成 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'.
我要实现的目标是:
- 在关闭的 class 中获取名为
Handle
的方法以及实现Event
的参数
- 将事件参数的类型和方法调用存储为字典中的 Action 委托
- 当事件应用于聚合时,执行与事件类型对应的动作委托。否则,不对事件执行任何操作。
您可以像对待常规静态方法一样对待 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
成为常量表达式可以自行解决超出范围的问题。