How do I create an expression tree calling IEnumerable.Any(…)?

前端 未结 2 1403
半阙折子戏
半阙折子戏 2020-11-30 21:56

I am trying to create an expression tree that represents the following:

myObject.childObjectCollection.Any(i => i.Name == \"name\");

Sho

2条回答
  •  不思量自难忘°
    2020-11-30 22:40

    Barry's answer provides a working solution to the question posed by the original poster. Thanks to both of those individuals for asking and answering.

    I found this thread as I was trying to devise a solution to a quite similar problem: programmatically creating an expression tree that includes a call to the Any() method. As an additional constraint, however, the ultimate goal of my solution was to pass such a dynamically-created expression through Linq-to-SQL so that the work of the Any() evaluation is actually performed in the DB itself.

    Unfortunately, the solution as discussed so far is not something that Linq-to-SQL can handle.

    Operating under the assumption that this might be a pretty popular reason for wanting to build a dynamic expression tree, I decided to augment the thread with my findings.

    When I attempted to use the result of Barry's CallAny() as an expression in a Linq-to-SQL Where() clause, I received an InvalidOperationException with the following properties:

    • HResult=-2146233079
    • Message="Internal .NET Framework Data Provider error 1025"
    • Source=System.Data.Entity

    After comparing a hard-coded expression tree to the dynamically-created one using CallAny(), I found that the core problem was due to the Compile() of the predicate expression and the attempt to invoke the resulting delegate in the CallAny(). Without digging deep into Linq-to-SQL implementation details, it seemed reasonable to me that Linq-to-SQL wouldn't know what to do with such a structure.

    Therefore, after some experimentation, I was able to achieve my desired goal by slightly revising the suggested CallAny() implementation to take a predicateExpression rather than a delegate for the Any() predicate logic.

    My revised method is:

    static Expression CallAny(Expression collection, Expression predicateExpression)
    {
        Type cType = GetIEnumerableImpl(collection.Type);
        collection = Expression.Convert(collection, cType); // (see "NOTE" below)
    
        Type elemType = cType.GetGenericArguments()[0];
        Type predType = typeof(Func<,>).MakeGenericType(elemType, typeof(bool));
    
        // Enumerable.Any(IEnumerable, Func)
        MethodInfo anyMethod = (MethodInfo)
            GetGenericMethod(typeof(Enumerable), "Any", new[] { elemType }, 
                new[] { cType, predType }, BindingFlags.Static);
    
        return Expression.Call(
            anyMethod,
            collection,
            predicateExpression);
    }
    

    Now I will demonstrate its usage with EF. For clarity I should first show the toy domain model & EF context I am using. Basically my model is a simplistic Blogs & Posts domain ... where a blog has multiple posts and each post has a date:

    public class Blog
    {
        public int BlogId { get; set; }
        public string Name { get; set; }
    
        public virtual List Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public DateTime Date { get; set; }
    
        public int BlogId { get; set; }
        public virtual Blog Blog { get; set; }
    }
    
    public class BloggingContext : DbContext
    {
        public DbSet Blogs { get; set; }
        public DbSet Posts { get; set; }
    }
    

    With that domain established, here is my code to ultimately exercise the revised CallAny() and make Linq-to-SQL do the work of evaluating the Any(). My particular example will focus on returning all Blogs which have at least one Post that is newer than a specified cutoff date.

    static void Main()
    {
        Database.SetInitializer(
            new DropCreateDatabaseAlways());
    
        using (var ctx = new BloggingContext())
        {
            // insert some data
            var blog  = new Blog(){Name = "blog"};
            blog.Posts = new List() 
                { new Post() { Title = "p1", Date = DateTime.Parse("01/01/2001") } };
            blog.Posts = new List()
                { new Post() { Title = "p2", Date = DateTime.Parse("01/01/2002") } };
            blog.Posts = new List() 
                { new Post() { Title = "p3", Date = DateTime.Parse("01/01/2003") } };
            ctx.Blogs.Add(blog);
    
            blog = new Blog() { Name = "blog 2" };
            blog.Posts = new List()
                { new Post() { Title = "p1", Date = DateTime.Parse("01/01/2001") } };
            ctx.Blogs.Add(blog);
            ctx.SaveChanges();
    
    
            // first, do a hard-coded Where() with Any(), to demonstrate that
            // Linq-to-SQL can handle it
            var cutoffDateTime = DateTime.Parse("12/31/2001");
            var hardCodedResult = 
                ctx.Blogs.Where((b) => b.Posts.Any((p) => p.Date > cutoffDateTime));
            var hardCodedResultCount = hardCodedResult.ToList().Count;
            Debug.Assert(hardCodedResultCount > 0);
    
    
            // now do a logically equivalent Where() with Any(), but programmatically
            // build the expression tree
            var blogsWithRecentPostsExpression = 
                BuildExpressionForBlogsWithRecentPosts(cutoffDateTime);
            var dynamicExpressionResult = 
                ctx.Blogs.Where(blogsWithRecentPostsExpression);
            var dynamicExpressionResultCount = dynamicExpressionResult.ToList().Count;
            Debug.Assert(dynamicExpressionResultCount > 0);
            Debug.Assert(dynamicExpressionResultCount == hardCodedResultCount);
        }
    }
    

    Where BuildExpressionForBlogsWithRecentPosts() is a helper function that uses CallAny() as follows:

    private Expression> BuildExpressionForBlogsWithRecentPosts(
        DateTime cutoffDateTime)
    {
        var blogParam = Expression.Parameter(typeof(Blog), "b");
        var postParam = Expression.Parameter(typeof(Post), "p");
    
        // (p) => p.Date > cutoffDateTime
        var left = Expression.Property(postParam, "Date");
        var right = Expression.Constant(cutoffDateTime);
        var dateGreaterThanCutoffExpression = Expression.GreaterThan(left, right);
        var lambdaForTheAnyCallPredicate = 
            Expression.Lambda>(dateGreaterThanCutoffExpression, 
                postParam);
    
        // (b) => b.Posts.Any((p) => p.Date > cutoffDateTime))
        var collectionProperty = Expression.Property(blogParam, "Posts");
        var resultExpression = CallAny(collectionProperty, lambdaForTheAnyCallPredicate);
        return Expression.Lambda>(resultExpression, blogParam);
    }
    

    NOTE: I found one other seemingly unimportant delta between the hard-coded and dynamically-built expressions. The dynamically-built one has an "extra" convert call in it that the hard-coded version doesn't seem to have (or need?). The conversion is introduced in the CallAny() implementation. Linq-to-SQL seems to be ok with it so I left it in place (although it was unnecessary). I was not entirely certain if this conversion might be needed in some more robust usages than my toy sample.

提交回复
热议问题