Dynamically calling methods corresponding a parame

2019-07-23 20:26发布

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:

  1. Get methods named Handle in the closed class along with the parameter implementing Event
  2. store the event parameter's type and the method invocation as an Action delegate in dictionary
  3. Execute the Action delegate corresponding to an event type when events are applied to the Aggregate. Otherwise, do nothing with the event.

2条回答
SAY GOODBYE
2楼-- · 2019-07-23 20:45

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);
}
查看更多
Summer. ? 凉城
3楼-- · 2019-07-23 20:58

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.

查看更多
登录 后发表回答