Как добавить новый член в инициализированное дерево выражений? - PullRequest
0 голосов
/ 06 марта 2020

Я создаю выражение для вызова .Select, оно выглядит следующим образом

Моя модель:

public class EmpFull
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public DateTime AddedDate { get; set; }
}

Модель представления:

public class Emp
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
}

И выражение:

Expression<Func<EmpFull, Emp>> expression = t => new Emp
{
    Id = t.Id,
    Name = t.Name
};

Я хочу сделать следующее:

if (WithAddress)
{
    //Add the address to the expression tree
}

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

Итак, мой вопрос: как добавить дополнительный параметр в выражение инициализации?

1 Ответ

1 голос
/ 06 марта 2020

Мы начнем с того, с какого выражения мы имеем дело.

Если мы проверим expression.Body.GetType(), мы увидим, что это MemberInitExpression. Он состоит из NewExpression, который является битом new Emp, и числом Bindings, которые являются инициализаторами членов.

Мы ' мы собираемся использовать ExpressionVisitor, чтобы переписать это, поскольку это немного чище, чем деконструкция / реконструкция его сами.

Мы также собираемся следовать документам для Expression.MakeMemberInit, который описывает, как позвонить Expression.Bind.

Соберите все вместе, и мы получим следующее:

public static class Program
{
    public static void Main()
    {
        Expression<Func<EmpFull, Emp>> expression = t => new Emp
        {
            Id = t.Id,
            Name = t.Name
        };

        // Additional bindings to add to the MemberInit
        var additionalBindings = new[]
        {
            Expression.Bind(
                // Property to assign to
                typeof(Emp).GetMember("Address")[0],
                // Value to assign to it. We need to access the "Address" member
                // of 't' in the 'expression' variable above, which we'll
                // get from grabbing 'expression.Parameters[0]'
                Expression.MakeMemberAccess(
                    expression.Parameters[0],
                    typeof(EmpFull).GetMember("Address")[0])),
        };
        var visitor = new AddNewBindingsVisitor(additionalBindings);
        var rewritten = visitor.Visit(expression);
    }
}
public class AddNewBindingsVisitor : ExpressionVisitor
{
    private readonly IEnumerable<MemberBinding> bindings;
    public AddNewBindingsVisitor(IEnumerable<MemberBinding> bindings) =>
        this.bindings = bindings;
    protected override Expression VisitMemberInit(MemberInitExpression node) =>
        node.Update(node.NewExpression, node.Bindings.Concat(bindings));
}

Это довольно уродливо, но мы можем это улучшить. Мы создадим метод, который принимает два лямбда-выражения, которые принимают MemberInitExpressions, и мы вернем что-то, что содержит комбинацию всех этих инициализаторов.

public static class Program
{
    public static void Main()
    {
        Expression<Func<EmpFull, Emp>> expression = t => new Emp
        {
            Id = t.Id,
            Name = t.Name
        };

        Expression<Func<EmpFull, Emp>> additional = t => new Emp
        {
            Address = t.Address
        };

        var result = CombineInitializers(expression, additional);
    }

    private static Expression<Func<TSource, TNew>> CombineInitializers<TSource, TNew>
        (Expression<Func<TSource, TNew>> first, Expression<Func<TSource, TNew>> second)
    {
        if (!(first.Body is MemberInitExpression firstInit))
            throw new ArgumentException("Must contain a MemberInitExpression", nameof(first));
        if (!(second.Body is MemberInitExpression secondInit))
            throw new ArgumentException("Must contain a MemberInitExpression", nameof(second));

        var secondUsingFirstParameter = (MemberInitExpression)new ParameterReplaceVisitor(second.Parameters[0], first.Parameters[0]).Visit(secondInit);
        var combined = new AddNewBindingsVisitor(secondUsingFirstParameter.Bindings).Visit(firstInit);
        return first.Update(combined, first.Parameters);
    }
}

public class ParameterReplaceVisitor : ExpressionVisitor
{
    private readonly ParameterExpression target;
    private readonly ParameterExpression replacement;
    public ParameterReplaceVisitor(ParameterExpression target, ParameterExpression replacement) =>
        (this.target, this.replacement) = (target, replacement);
    protected override Expression VisitParameter(ParameterExpression node) =>
        node == this.target ? this.replacement : base.VisitParameter(node);
}

public class AddNewBindingsVisitor : ExpressionVisitor
{
    private readonly IEnumerable<MemberBinding> bindings;
    public AddNewBindingsVisitor(IEnumerable<MemberBinding> bindings) =>
        this.bindings = bindings;
    protected override Expression VisitMemberInit(MemberInitExpression node) =>
        node.Update(node.NewExpression, node.Bindings.Concat(bindings));
}

Это довольно прямое расширение нашего предыдущее использование AddNewBindingsVisitor. Суть в том, что first и second имеют разные объекты параметров (то есть t), поэтому сначала нам нужно переписать один из них (second), чтобы использовать тот же параметр, что и другой (first) ). Мы делаем это с помощью другого посетителя, который просто заменяет один ParameterExpression другим.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...