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 implementingEvent
- 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.
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 likeAction<AggregateBase, Event>
.Change declaration of your
_handlers
toNow you can write
AggregateBase
constructor like this:EDIT: Also you need to change invocation in
Apply
method:First, use a block expression to introduce
runnerParameter
to the context. Second, make parametere
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 genericExpression.Lambda
overload so you get the desired delegate type without casting.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 onthis
.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.
Then when you loop on
methods
, you're only doing delegate creation.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.