How to convert this LINQ to its equivalent Expression Tree and make it performant

时间:2015-07-28 22:28:30

标签: c# expression-trees

Update: You can skip the second part of this question as George already helped answer the first part.

Part I: I am trying to convert the LINQ below

childItems.Where(x => x.ParentID == parentItem.ID).ToList().ForEach(y => parentItem.Children.Add(y));

to its equivalent Expression but I can't get Step 6 to produce a result. I also don't think I am using Expression.Constant in Steps 2 - 6 as appropriate but it seems to be working, for now at least.

Any help in fixing this would be much appreciated!


    public class Parent
    {
        public Guid ID { get; set; }
        public string Name { get; set; }
        public ICollection<Child> Children { get; set; }
    }

    public class Child
    {
        public Guid ParentID { get; set; }
        public Guid ID { get; set; }
        public string Name { get; set; }
    }

    public void SO_Question()
    {
        Parent parentItem       = new Parent() { ID = Guid.NewGuid(), Name = "Parent" };
        parentItem.Children     = new List<Child>();
        List<Child> childItems  = new List<Child>() {   new Child() { ParentID = parentItem.ID, ID = Guid.NewGuid(), Name = "Child 1" }, 
                                                        new Child() { ParentID = parentItem.ID, ID = Guid.NewGuid(), Name = "Child 2" },
                                                        new Child() { ParentID = Guid.NewGuid(),ID = Guid.NewGuid(), Name = "Child 3" } 
                                                    };


        // Linq query that I am trying to write using Expressions
        childItems.Where(x => x.ParentID == parentItem.ID).ToList().ForEach(y => parentItem.Children.Add(y));

        System.Diagnostics.Debug.WriteLine("Children count from LINQ: " + parentItem.Children.Count);   
        parentItem.Children.Clear();

        Type parentEntityType   = parentItem.GetType();
        Type childEntityCollType= childItems.GetType();
        Type childEntityType    = childEntityCollType.GetGenericArguments()[0];


        //1. (x => x.ParentID == parentItem.ID)
        ParameterExpression predParam   = Expression.Parameter(childEntityType, "x");
        Expression left                 = Expression.Property(predParam, childEntityType.GetProperty("ParentID"));
        Expression right                = Expression.Property(Expression.Constant(parentItem), "ID");
        Expression equality             = Expression.Equal(left, right);
        LambdaExpression le             = Expression.Lambda(equality, new ParameterExpression[] { predParam });


        //2. childItems.Where(x => x.ParentID == parentItem.ID)
        Expression targetConstant       = Expression.Constant(childItems, childEntityCollType);
        Expression whereBody            = Expression.Call(typeof(Enumerable), "Where", new Type[] { childEntityType }, targetConstant, le);
        Func<IEnumerable> whereLambda   = Expression.Lambda<Func<IEnumerable>>(whereBody).Compile();
        object whereResult              = whereLambda.Invoke();


        //3. childItems.Where(x => x.ParentID == parentItem.ID).ToList()
        Expression toListConstant   = Expression.Constant(whereResult, whereResult.GetType());
        Expression toListBody       = Expression.Call(typeof(Enumerable), "ToList", new Type[] { childEntityType }, toListConstant);
        Func<IEnumerable> listLambda= Expression.Lambda<Func<IEnumerable>>(toListBody).Compile();
        object toListResult         = listLambda.Invoke();


        //5. (y => parentItem.Children.Add(y))
        ParameterExpression feParam = Expression.Parameter(childEntityType, "y");
        Expression addConst         = Expression.Constant(parentItem, parentEntityType);
        Expression childAccessor    = Expression.Property(addConst, parentEntityType.GetProperty("Children"));
        Expression body             = Expression.Call(childAccessor, "Add", null, feParam);
        LambdaExpression exp2       = Expression.Lambda(body, new ParameterExpression[] { feParam });


        //6. childItems.Where(x => x.ParentID == parentItem.ID).ToList().ForEach(y => parentItem.Children.Add(y));
        Expression targetConst2 = Expression.Constant(toListResult, toListResult.GetType());
        Expression whereBody2   = Expression.Call(targetConst2, toListResult.GetType().GetMethod("ForEach"), exp2);
        Delegate whereLambda2   = Expression.Lambda(whereBody2, feParam).Compile();
        whereLambda.Invoke();

        System.Diagnostics.Debug.WriteLine("Children count from Expressions: " + parentItem.Children.Count);    
    }

Part II: After @George fixed my issue I have hit a performance problem. I need to run the two lambdas thousands of times within a loop and currently it is very slow, probably because the expression tree is generated every time. How can I get around this? I've restructured the code and simplified it a bit from my initial question.
In the code below parentItem is captured in the closure. How can i rewrite this so that I can use the expression from 1, 2 and 3 and only supply parentItem (on 2 and 3) as a variable to the lambda on every run?


public class Parent
{
    public Guid ID { get; set; }
    public string Name { get; set; }
    public ICollection<Child> Children { get; set; }

    public SingleChild SingleChild { get; set; }
}

public class Child
{
    public Guid ParentID { get; set; }
    public Guid ID { get; set; }
    public string Name { get; set; }
}

public class SingleChild
{
    public Guid ParentID { get; set; }
    public Guid ID { get; set; }
    public string Name { get; set; }
}

public static void SO_Question2()
{
    Parent newParentItem1 = new Parent() { ID = Guid.NewGuid(), Name = "Parent1" };
    Parent newParentItem2 = new Parent() { ID = Guid.NewGuid(), Name = "Parent2" };
    Parent newParentItem3 = new Parent() { ID = Guid.NewGuid(), Name = "Parent3" };

    newParentItem1.Children = new List<Child>();
    newParentItem2.Children = new List<Child>();
    newParentItem3.Children = new List<Child>();

    List<Child> childItems = new List<Child>() {   
        new Child() { ParentID = newParentItem1.ID, ID = Guid.NewGuid(), Name = "Child 1" }, 
        new Child() { ParentID = newParentItem1.ID, ID = Guid.NewGuid(), Name = "Child 2" },
        new Child() { ParentID = newParentItem2.ID, ID = Guid.NewGuid(), Name = "Child 3" },
        new Child() { ParentID = newParentItem2.ID, ID = Guid.NewGuid(), Name = "Child 4" },
        new Child() { ParentID = newParentItem2.ID, ID = Guid.NewGuid(), Name = "Child 5" },
        new Child() { ParentID = newParentItem2.ID, ID = Guid.NewGuid(), Name = "Child 6" },
        new Child() { ParentID = newParentItem2.ID, ID = Guid.NewGuid(), Name = "Child 7" } 
    };
    List<Parent> parentCollection = new List<Parent>() { newParentItem1, newParentItem2, newParentItem3 }; // In reality this can be a collection of over 2000 items


    // Linq query that I am trying to write using Expressions
    foreach (Parent parentItem in parentCollection)
    {
        parentItem.Children.Clear();
        childItems.Where(x => x.ParentID == parentItem.ID).ToList().ForEach(y => parentItem.Children.Add(y));
    }

    System.Diagnostics.Debug.WriteLine("Children count from LINQ: " + newParentItem1.Children.Count);
    newParentItem1.Children.Clear();
    newParentItem2.Children.Clear();
    newParentItem3.Children.Clear();


    Type parentEntityType = parentCollection.GetType().GetGenericArguments()[0];
    Type childEntityCollType = childItems.GetType();
    Type childEntityType = childEntityCollType.GetGenericArguments()[0];

    Parent parentVariable = parentCollection.First();

    // 1. parentItem.Children.Clear()
    var childCollection = Expression.Property(Expression.Constant(parentVariable), "Children");
    Expression clearBody = Expression.Call(childCollection, typeof(ICollection<Child>).GetMethod("Clear"));
    Expression<System.Action> bodyLambda = Expression.Lambda<System.Action>(clearBody);
    System.Action compiledClear = bodyLambda.Compile();

    // How can I change 1 and 2 so that they are not recreated with every iteration?
    // My problem is that parentItem changes but is captured in the closure

    //2. (x => x.ParentID == parentItem.ID)
    ParameterExpression predParam = Expression.Parameter(childEntityType, "x");
    Expression left = Expression.Property(predParam, childEntityType.GetProperty("ParentID"));
    Expression right = Expression.Property(Expression.Constant(parentVariable), "ID");
    Expression equality = Expression.Equal(left, right);
    Expression<Func<Child, bool>> le = Expression.Lambda<Func<Child, bool>>(equality, new ParameterExpression[] { predParam });
    Func<Child, bool> compileLambda = le.Compile();


    //3. (y => parentItem.Children.Add(y))
    ParameterExpression feParam = Expression.Parameter(childEntityType, "y");
    Expression addConst = Expression.Constant(parentVariable, parentEntityType);
    Expression childAccessor = Expression.Property(addConst, parentEntityType.GetProperty("Children"));
    Expression body = Expression.Call(childAccessor, "Add", null, feParam);
    Expression<Action<Child>> exp2 = Expression.Lambda<Action<Child>>(body, new ParameterExpression[] { feParam });
    Action<Child> compileExp2 = exp2.Compile();

    foreach (Parent parentItem in parentCollection)
    {
        parentVariable = parentItem;

        compiledClear();
        childItems.Where(compileLambda).ToList().ForEach(compileExp2);
    }

    System.Diagnostics.Debug.WriteLine("Children count from Expressions: " + parentCollection.First().Children.Count);
}

2 个答案:

答案 0 :(得分:1)

public static void SO_Question()
    {
        Parent parentItem = new Parent() { ID = Guid.NewGuid(), Name = "Parent" };
        parentItem.Children = new List<Child>();
        List<Child> childItems = new List<Child>() {   
            new Child() { ParentID = parentItem.ID, ID = Guid.NewGuid(), Name = "Child 1" }, 
            new Child() { ParentID = parentItem.ID, ID = Guid.NewGuid(), Name = "Child 2" },
            new Child() { ParentID = Guid.NewGuid(),ID = Guid.NewGuid(), Name = "Child 3" } 
        };


        // Linq query that I am trying to write using Expressions
        childItems.Where(x => x.ParentID == parentItem.ID).ToList().ForEach(y => parentItem.Children.Add(y));

        System.Diagnostics.Debug.WriteLine("Children count from LINQ: " + parentItem.Children.Count);
        parentItem.Children.Clear();

        Type parentEntityType = parentItem.GetType();
        Type childEntityCollType = childItems.GetType();
        Type childEntityType = childEntityCollType.GetGenericArguments()[0];


        //1. (x => x.ParentID == parentItem.ID)
        ParameterExpression predParam = Expression.Parameter(childEntityType, "x");
        Expression left = Expression.Property(predParam, childEntityType.GetProperty("ParentID"));
        Expression right = Expression.Property(Expression.Constant(parentItem), "ID");
        Expression equality = Expression.Equal(left, right);
        LambdaExpression le = Expression.Lambda(equality, new ParameterExpression[] { predParam });


        //2. childItems.Where(x => x.ParentID == parentItem.ID)
        Expression targetConstant = Expression.Constant(childItems, childEntityCollType);
        Expression whereBody = Expression.Call(typeof(Enumerable), "Where", new Type[] { childEntityType }, targetConstant, le);
        Func<IEnumerable> whereLambda = Expression.Lambda<Func<IEnumerable>>(whereBody).Compile();
        object whereResult = whereLambda.Invoke();


        //3. childItems.Where(x => x.ParentID == parentItem.ID).ToList()
        Expression toListConstant = Expression.Constant(whereResult, whereResult.GetType());
        Expression toListBody = Expression.Call(typeof(Enumerable), "ToList", new Type[] { childEntityType }, toListConstant);
        Func<IEnumerable> listLambda = Expression.Lambda<Func<IEnumerable>>(toListBody).Compile();
        object toListResult = listLambda.Invoke();


        //5. (y => parentItem.Children.Add(y))
        ParameterExpression feParam = Expression.Parameter(childEntityType, "y");
        Expression addConst = Expression.Constant(parentItem, parentEntityType);
        Expression childAccessor = Expression.Property(addConst, parentEntityType.GetProperty("Children"));
        Expression body = Expression.Call(childAccessor, "Add", null, feParam);
        LambdaExpression exp2 = Expression.Lambda(body, new ParameterExpression[] { feParam });


        //6. childItems.Where(x => x.ParentID == parentItem.ID).ToList().ForEach(y => parentItem.Children.Add(y));
        Expression targetConst2 = Expression.Constant(toListResult, toListResult.GetType());
        Expression whereBody2 = Expression.Call(targetConst2, toListResult.GetType().GetMethod("ForEach"), exp2);

        Delegate d = Expression.Lambda(whereBody2).Compile();
        d.DynamicInvoke();

        System.Diagnostics.Debug.WriteLine("Children count from Expressions: " + parentItem.Children.Count);
    }

答案 1 :(得分:0)

仅针对问题2:
您可以将父变量设为lambda的变量。但是,这不会反映您的LINQ代码。

另一种方法是创建一个虚拟变量,根据虚拟变量构建表达式,然后循环将设置虚拟变量。我评论了第一个lambda,因为它没有编译,似乎与这个问题无关:

public static void SO_Question2()
{
    Parent newParentItem = new Parent() { ID = Guid.NewGuid(), Name = "Parent" };
    newParentItem.Children = new List<Child>();
    List<Child> childItems = new List<Child>() {   
        new Child() { ParentID = newParentItem.ID, ID = Guid.NewGuid(), Name = "Child 1" }, 
        new Child() { ParentID = newParentItem.ID, ID = Guid.NewGuid(), Name = "Child 2" },
        new Child() { ParentID = Guid.NewGuid(),ID = Guid.NewGuid(), Name = "Child 3" } 
    };
    List<Parent> parentCollection = new List<Parent>() { newParentItem }; // In reality this can be a collection of over 2000 items

    // Linq query that I am trying to write using Expressions
    foreach (Parent parentItem in parentCollection)
    {
        childItems.Where(x => x.ParentID == parentItem.ID).ToList().ForEach(y => parentItem.Children.Add(y));
    }

    System.Diagnostics.Debug.WriteLine("Children count from LINQ: " + newParentItem.Children.Count);
    newParentItem.Children.Clear();


    Type parentEntityType = parentCollection.GetType().GetGenericArguments()[0];
    Type childEntityCollType = childItems.GetType();
    Type childEntityType = childEntityCollType.GetGenericArguments()[0];

    Parent parentVariable = parentCollection.First();
    // 1. parentItem.Children.Clear()
    //var childCollection = Expression.Property(Expression.Constant(parentVariable), "Children");
    //Expression clearBody = Expression.Call(childCollection, typeof(List<Child>).GetMethod("Clear"));
    //Delegate bodyLambda = Expression.Lambda(clearBody).Compile();
    //bodyLambda.DynamicInvoke();


    // How can I change 2 and 3 so that they are not recreated with every iteration?
    // My problem is that parentItem changes but is captured in the closure

    //2. (x => x.ParentID == parentItem.ID)
    ParameterExpression predParam = Expression.Parameter(childEntityType, "x");
    Expression left = Expression.Property(predParam, childEntityType.GetProperty("ParentID"));
    Expression right = Expression.Property(Expression.Constant(parentVariable), "ID");
    Expression equality = Expression.Equal(left, right);
    Expression<Func<Child, bool>> le = Expression.Lambda<Func<Child, bool>>(equality, new ParameterExpression[] { predParam });
    Func<Child, bool> compileLambda = le.Compile();


    //3. (y => parentItem.Children.Add(y))
    ParameterExpression feParam = Expression.Parameter(childEntityType, "y");
    Expression addConst = Expression.Constant(parentVariable, parentEntityType);
    Expression childAccessor = Expression.Property(addConst, parentEntityType.GetProperty("Children"));
    Expression body = Expression.Call(childAccessor, "Add", null, feParam);
    Expression<Action<Child>> exp2 = Expression.Lambda<Action<Child>>(body, new ParameterExpression[] { feParam });
    Action<Child> compileExp2 = exp2.Compile();

    foreach (Parent parentItem in parentCollection)
    {
        parentVariable = parentItem;
        childItems.Where(compileLambda).ToList().ForEach(compileExp2);
    }

    System.Diagnostics.Debug.WriteLine("Children count from Expressions: " + parentCollection.First().Children.Count);
}

请注意,编译(昂贵的部分)只运行一次。执行多次运行。这是有效的,因为Expression.Constant(parentVariable)每次检查该参考位置中的任何内容。更改引用,您更改变量。另请注意,parentVariable可以是静态的,公共的,等等。它不一定是本地人。