Динамический вызов методов, соответствующих типу параметра, используя деревья выражений в c # - PullRequest
0 голосов
/ 02 октября 2018

Я создаю обработчик событий, который будет работать аналогично тому, как агрегаты ведут себя в системах с источниками событий.

То, чего я пытаюсь достичь, можно сделать способами, описанными в документации здесь Другие ссылки, на которые я смотрел, это исходный код Мартена и мистер Грега ЯнгаЯ хочу добиться того же с деревьями выражений.

По сути, я хочу, чтобы моя агрегатная реализация динамически выполняла передаваемые ей события, если у нее есть метод Handle, который принимает это событие в качестве параметра.

Сначала у меня есть события

abstract class Event { }
class Event1 : Event { }
class Event2 : Event { }

У меня есть агрегатная реализация, которая наследуется от AggregateBase класса.

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);
        }
    }
}

Используя приведенный выше код, получите ошибку

переменная 'r' типа 'ConsoleApp_TestTypeBuilder.Aggregate' ссылка из области видимости '', но он не определен '.

Я пытаюсь достичь:

  1. Получить методы с именем Handle в закрытом классе вместе с реализацией параметраEvent
  2. сохранить тип параметра события и вызов метода в качестве делегата действия в словаре
  3. Выполнить делегат действия, соответствующий типу события, когда событияприменяется к агрегату.В противном случае ничего не делать с событием.

Ответы [ 2 ]

0 голосов
/ 02 октября 2018

Сначала используйте выражение блока, чтобы ввести 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());

Еще одно предложение: уберите критерии выбора / исключения из цикла, чтобы не смешивать опасения, и сохраняйте фактыобнаружен в анонимном объекте для последующего использования.

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);
}

EDIT: При ближайшем рассмотрении я понял, что выражение блока не нужно.Создание runnerParameter константного выражения само по себе решает проблему, выходящую за рамки.

0 голосов
/ 02 октября 2018

Вы можете рассматривать лямбда-функции как обычные статические методы.Это означает, что вы должны передать ему дополнительный параметр (Aggregate в вашем случае).Другими словами, вам нужно создать лямбду, этот тип выглядит как 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);
}
...