Combine several similar SELECT-expressions into a single expression

后端 未结 3 1417
予麋鹿
予麋鹿 2020-12-09 12:23

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

   private static Expression> CombineSelector         


        
3条回答
  •  [愿得一人]
    2020-12-09 13:01

    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> selector1 = x => new AgencyDTO { Name = x.Name };
        Expression> selector2 = x => new AgencyDTO { Phone = x.PhoneNumber };
        Expression> selector3 = x => new AgencyDTO { Location = x.Locality.Name };
        Expression> 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 { 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> Combine(
        params Expression>[] selectors)
    {
        var zeroth = ((MemberInitExpression)selectors[0].Body);
        var param = selectors[0].Parameters[0];
        List bindings = new List(zeroth.Bindings.OfType());
        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())
            {
                bindings.Add(Expression.Bind(binding.Member,
                    replace.VisitAndConvert(binding.Expression, "Combine")));
            }
        }
    
        return Expression.Lambda>(
            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> Combine(
        params Expression>[] selectors)
    {
        var param = Expression.Parameter(typeof(TSource), "x");
        return Expression.Lambda>(
            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()
                select Expression.Bind(binding.Member,
                      replace.VisitAndConvert(binding.Expression, "Combine")))
            , param);        
    }
    

提交回复
热议问题