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 here Other references I've looked into are the Marten source code and Greg Young's m-r. I want to achieve the same with Expression Trees.
In essence, I want my aggregate implementation to dynamically execute events passed to it if it has a Handle
method which accepts that event as a parameter.
First I have my events
abstract class Event { }
class Event1 : Event { }
class Event2 : Event { }
I have my aggregate implementation which inherits from an 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);
}
}
And finally, AggregateBase
which performs the reflection and registration of handlers in a member dictionary.
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);
}
}
}
With the code above, get error
variable 'r' of type 'ConsoleApp_TestTypeBuilder.Aggregate' referenced from scope '', but it is not defined'.
What I'm trying to achieve are:
- Get methods named
Handle
in the closed class along with the parameter implementing Event
- store the event parameter's type and the method invocation as an Action delegate in dictionary
- Execute the Action delegate corresponding to an event type when events are applied to the Aggregate. Otherwise, do nothing with the 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.
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);
}