I\'m building an event handler which will work similarly to how aggregates behave in event sourced systems.
What I\'m trying to achieve can be done in ways as documented
You can treat lambda functions like a regular static methods. This mean you should pass additional parameter to it (Aggregate
in your case). In other words you need to create lambda, that type looks like Action<AggregateBase, Event>
.
Change declaration of your _handlers
to
private readonly IDictionary<Type, Action<AggregateBase, Event>> _handlers
= new Dictionary<Type, Action<AggregateBase, Event>>();
Now you can write AggregateBase
constructor like this:
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());
}
EDIT: Also you need to change invocation in Apply
method:
public void Apply(Event @event)
{
var type = @event.GetType();
if (_handlers.ContainsKey(type))
_handlers[type](this, @event);
}
First, use a block expression to introduce runnerParameter
to the context. Second, make parameter e
the base type so you don't have to mess with the delegate type, and then convert it to the derived type with a conversion expression. Third (optional), use a generic Expression.Lambda
overload so you get the desired delegate type without casting.
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);
That will work until you go to call the hander and then you'll get NREs because runnerParameter
doesn't have a value. Change it to a constant so that your block closes on this
.
var runnerParameter = Expression.Constant(this, this.GetType());
One other suggestion: Move your selection/exclusion criteria out of the loop so you're not mixing concerns, and keep facts you've discovered in an anonymous object for use later.
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
};
Then when you loop on methods
, you're only doing delegate creation.
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);
}
EDIT: Upon closer examination, I realized that the block expression is unnecessary. Making runnerParameter
a constant expression solves the out-of-scope problem on its own.