Combine several similar SELECT-expressions into a single expression
Asked Answered
B

3

10

How to combine several similar SELECT-expressions into a single expression?

   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..;
            }
        }
    }
Bills answered 30/5, 2011 at 21:1 Comment(1)
Show the data in the list. This is necessary in order to avoid loading unnecessary fields from the database.Bills
T
20

Not simple; you need to rewrite all the expressions - well, strictly speaking you can recycle most of one of them, but the problem is that you have different x in each (even though it looks the same), hence you need to use a visitor to replace all the parameters with the final x. Fortunately this isn't too bad in 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);
    }
}

This uses the constructor from the first expression found, so you might want to sanity-check that all of the others use trivial constructors in their respective NewExpressions. I've left that for the reader, though.

Edit: In the comments, @Slaks notes that more LINQ could make this shorter. He is of course right - a bit dense for easy reading, though:

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);        
}
Trachyte answered 30/5, 2011 at 21:35 Comment(5)
+1 for writing all that code. It could be made much simpler by replacing the nested loop with LINQ calls and a new ParameterGlynnis
@Slaks maybe, maybe. It was already complex enough though - the reader probably has more chance to grok it in a more procedural layoutTrachyte
@Slaks - adding, just for funTrachyte
I had adapted you code and works fine.. But I would do like to work with expressions like Expression<Func<Agency, object>> selector1 = x => new { Name = x.Name }; instead of Expression<Func<Agency, AgencyDTO>> selector1 = x => new AgencyDTO { Name = x.Name }; How can I get it working with such anonymous types? This expression (MemberInitExpression)selector.Body breaks if anonymous types is usedStenosis
Is it possible to implement something similar if one of the expressions have return type of the derived object (e.g. AgentDtoDerived)? The idea is that I can have some defined projection in a base class but then derived class extend the projection by simple adding new props to projectionVareck
G
1

If all of the selectors will only initialize AgencyDTO objects (like your example), you can cast the expressions to NewExpression instances, then call Expression.New with the Members of the expressions.

You'll also need an ExpressionVisitor to replace the ParameterExpressions from the original expressions with a single ParameterExpression for the expression you're creating.

Glynnis answered 30/5, 2011 at 21:13 Comment(1)
Actually it is MemberInitExpression (the NewExpression is just the ctor); it can be done though (added)Trachyte
I
1

In case anyone else stumbles upon this with a similar use case as mine (my selects targeted different classes based on the level of detail needed):

Simplified scenario:

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

I adapted the solution provided by @Marc Gravell like so:

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

The Map method of the extended class then becomes:

    public static Expression<Func<Data.Blog, BlogViewModel>> Map()
    {
        return BlogSummaryViewModel.Map().Concat(i => new BlogViewModel
        {
            PostCount = i.Posts.Count()
        });
    }
Inflexion answered 10/1, 2019 at 15:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.