Объединить несколько похожих SELECT-выражений в одно выражение

Как объединить несколько похожих SELECT-выражений в одно выражение?

   private static Expression<Func<Agency, AgencyDTO>> CombineSelectors(params Expression<Func<Agency, AgencyDTO>>[] selectors)
    {

        // ???

        return null;
    }

    private void Query()
    {
        Expression<Func<Agency, AgencyDTO>> selector1 = x => new AgencyDTO { Name = x.Name };
        Expression<Func<Agency, AgencyDTO>> selector2 = x => new AgencyDTO { Phone = x.PhoneNumber };
        Expression<Func<Agency, AgencyDTO>> selector3 = x => new AgencyDTO { Location = x.Locality.Name };
        Expression<Func<Agency, AgencyDTO>> selector4 = x => new AgencyDTO { EmployeeCount = x.Employees.Count() };

        using (RealtyContext context = Session.CreateContext())
        {
            IQueryable<AgencyDTO> agencies = context.Agencies.Select(CombineSelectors(selector3, selector4));

            foreach (AgencyDTO agencyDTO in agencies)
            {
                // do something..;
            }
        }
    }

person Boris Mitchenko    schedule 30.05.2011    source источник
comment
Показать данные в списке. Это необходимо для того, чтобы избежать загрузки ненужных полей из базы данных.   -  person Boris Mitchenko    schedule 31.05.2011


Ответы (3)


Не просто; вам нужно переписать все выражения - ну, строго говоря, вы можете переработать большинство из них, но проблема в том, что у вас разные x в каждом (даже если они выглядят одинаково), поэтому вам нужно использовать посетителя для замены все параметры с конечным x. К счастью, это не так уж плохо в 4.0:

static void Main() {
    Expression<Func<Agency, AgencyDTO>> selector1 = x => new AgencyDTO { Name = x.Name };
    Expression<Func<Agency, AgencyDTO>> selector2 = x => new AgencyDTO { Phone = x.PhoneNumber };
    Expression<Func<Agency, AgencyDTO>> selector3 = x => new AgencyDTO { Location = x.Locality.Name };
    Expression<Func<Agency, AgencyDTO>> selector4 = x => new AgencyDTO { EmployeeCount = x.Employees.Count() };

    // combine the assignments from the 4 selectors
    var convert = Combine(selector1, selector2, selector3, selector4);

    // sample data
    var orig = new Agency
    {
        Name = "a",
        PhoneNumber = "b",
        Locality = new Location { Name = "c" },
        Employees = new List<Employee> { new Employee(), new Employee() }
    };

    // check it
    var dto = new[] { orig }.AsQueryable().Select(convert).Single();
    Console.WriteLine(dto.Name); // a
    Console.WriteLine(dto.Phone); // b
    Console.WriteLine(dto.Location); // c
    Console.WriteLine(dto.EmployeeCount); // 2
}
static Expression<Func<TSource, TDestination>> Combine<TSource, TDestination>(
    params Expression<Func<TSource, TDestination>>[] selectors)
{
    var zeroth = ((MemberInitExpression)selectors[0].Body);
    var param = selectors[0].Parameters[0];
    List<MemberBinding> bindings = new List<MemberBinding>(zeroth.Bindings.OfType<MemberAssignment>());
    for (int i = 1; i < selectors.Length; i++)
    {
        var memberInit = (MemberInitExpression)selectors[i].Body;
        var replace = new ParameterReplaceVisitor(selectors[i].Parameters[0], param);
        foreach (var binding in memberInit.Bindings.OfType<MemberAssignment>())
        {
            bindings.Add(Expression.Bind(binding.Member,
                replace.VisitAndConvert(binding.Expression, "Combine")));
        }
    }

    return Expression.Lambda<Func<TSource, TDestination>>(
        Expression.MemberInit(zeroth.NewExpression, bindings), param);
}
class ParameterReplaceVisitor : ExpressionVisitor
{
    private readonly ParameterExpression from, to;
    public ParameterReplaceVisitor(ParameterExpression from, ParameterExpression to)
    {
        this.from = from;
        this.to = to;
    }
    protected override Expression VisitParameter(ParameterExpression node)
    {
        return node == from ? to : base.VisitParameter(node);
    }
}

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

Изменить: в комментариях @Slaks отмечает, что больше LINQ может сделать это короче. Он, конечно, прав - немного туповато для легкого чтения, однако:

static Expression<Func<TSource, TDestination>> Combine<TSource, TDestination>(
    params Expression<Func<TSource, TDestination>>[] selectors)
{
    var param = Expression.Parameter(typeof(TSource), "x");
    return Expression.Lambda<Func<TSource, TDestination>>(
        Expression.MemberInit(
            Expression.New(typeof(TDestination).GetConstructor(Type.EmptyTypes)),
            from selector in selectors
            let replace = new ParameterReplaceVisitor(
                  selector.Parameters[0], param)
            from binding in ((MemberInitExpression)selector.Body).Bindings
                  .OfType<MemberAssignment>()
            select Expression.Bind(binding.Member,
                  replace.VisitAndConvert(binding.Expression, "Combine")))
        , param);        
}
person Marc Gravell    schedule 30.05.2011
comment
+1 за написание всего этого кода. Это можно сделать намного проще, заменив вложенный цикл вызовами LINQ и новым Parameter - person SLaks; 31.05.2011
comment
@Slaks, может быть, может быть. Однако это уже было достаточно сложно — у читателя, вероятно, больше шансов понять это в более процедурном макете. - person Marc Gravell; 31.05.2011
comment
@Slaks - добавление, просто для удовольствия - person Marc Gravell; 31.05.2011
comment
Я адаптировал ваш код и отлично работает. Но я хотел бы работать с такими выражениями, как Expression‹Func‹Agency, object›› selector1 = x =› new { Name = x.Name }; вместо Expression‹Func‹Agency, AgencyDTO›› selector1 = x =› new AgencyDTO { Name = x.Name }; Как я могу заставить его работать с такими анонимными типами? Это выражение (MemberInitExpression) selector.Body прерывается, если используются анонимные типы. - person alhpe; 29.03.2019

Если все селекторы будут только инициализировать AgencyDTO объекты (как в вашем примере), вы можете привести выражения к NewExpression экземплярам, ​​а затем вызвать Expression.New с Members выражениями.

Вам также понадобится ExpressionVisitor, чтобы заменить ParameterExpression в исходных выражениях одним ParameterExpression для выражения, которое вы создаете.

person SLaks    schedule 30.05.2011
comment
На самом деле это MemberInitExpression (NewExpression - это просто ctor); хотя это можно сделать (добавлено) - person Marc Gravell; 31.05.2011

На случай, если кто-то еще наткнется на это с таким же вариантом использования, как у меня (мой выбор нацелен на разные классы в зависимости от необходимого уровня детализации):

Упрощенный сценарий:

public class BlogSummaryViewModel
{
    public string Name { get; set; }

    public static Expression<Func<Data.Blog, BlogSummaryViewModel>> Map()
    {
        return (i => new BlogSummaryViewModel
        {
            Name = i.Name
        });
    }
}

public class BlogViewModel : BlogSummaryViewModel
{
    public int PostCount { get; set; }

    public static Expression<Func<Data.Blog, BlogViewModel>> Map()
    {
        return (i => new BlogViewModel
        {
            Name = i.Name,
            PostCount = i.Posts.Count()
        });
    }
}

Я адаптировал решение, предоставленное @Marc Gravell, следующим образом:

public static class ExpressionMapExtensions
{
    public static Expression<Func<TSource, TTargetB>> Concat<TSource, TTargetA, TTargetB>(
        this Expression<Func<TSource, TTargetA>> mapA, Expression<Func<TSource, TTargetB>> mapB)
        where TTargetB : TTargetA
    {
        var param = Expression.Parameter(typeof(TSource), "i");

        return Expression.Lambda<Func<TSource, TTargetB>>(
            Expression.MemberInit(
                ((MemberInitExpression)mapB.Body).NewExpression,
                (new LambdaExpression[] { mapA, mapB }).SelectMany(e =>
                {
                    var bindings = ((MemberInitExpression)e.Body).Bindings.OfType<MemberAssignment>();
                    return bindings.Select(b =>
                    {
                        var paramReplacedExp = new ParameterReplaceVisitor(e.Parameters[0], param).VisitAndConvert(b.Expression, "Combine");
                        return Expression.Bind(b.Member, paramReplacedExp);
                    });
                })),
            param);
    }

    private class ParameterReplaceVisitor : ExpressionVisitor
    {
        private readonly ParameterExpression original;
        private readonly ParameterExpression updated;

        public ParameterReplaceVisitor(ParameterExpression original, ParameterExpression updated)
        {
            this.original = original;
            this.updated = updated;
        }

        protected override Expression VisitParameter(ParameterExpression node) => node == original ? updated : base.VisitParameter(node);
    }
}

Затем метод Map расширенного класса становится:

    public static Expression<Func<Data.Blog, BlogViewModel>> Map()
    {
        return BlogSummaryViewModel.Map().Concat(i => new BlogViewModel
        {
            PostCount = i.Posts.Count()
        });
    }
person John    schedule 10.01.2019