Combine several similar SELECT expressions into one expression

How to combine several similar SELECT expressions into one 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..; } } } 
+9
source share
3 answers

Not easy; you need to rewrite all the expressions - well, strictly speaking, you can rework most of them, but the problem is that you have different x in each (although it looks the same), so you need to use the visitor to replace all the parameters with the final x . Fortunately, this is not so 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 need sanity - make sure everyone else uses the trivial constructors in their respective NewExpression s. However, I left it to the reader.

Edit: in the comments, @Slaks notes that more LINQ can make it shorter. He's right, of course - a little tight 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); } 
+15
source

If all selectors will only initialize AgencyDTO objects (for example, your example), you can apply expressions to NewExpression instances, and then call Expression.New using Members expressions.

You will also need an ExpressionVisitor to replace the ParameterExpression from the source expressions with one ParameterExpression for the expression you are creating.

0
source

In case someone else stumbles upon this using the same use case as mine (my choice was aimed at different classes, based on the required level of detail):

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 this:

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

Then the extended class Map method becomes:

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

All Articles