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<T>(IEnumerable<T>, Func<T,bool>)
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<Post> 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<Blog> Blogs { get; set; }
public DbSet<Post> 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<BloggingContext>(
new DropCreateDatabaseAlways<BloggingContext>());
using (var ctx = new BloggingContext())
{
// insert some data
var blog = new Blog(){Name = "blog"};
blog.Posts = new List<Post>()
{ new Post() { Title = "p1", Date = DateTime.Parse("01/01/2001") } };
blog.Posts = new List<Post>()
{ new Post() { Title = "p2", Date = DateTime.Parse("01/01/2002") } };
blog.Posts = new List<Post>()
{ new Post() { Title = "p3", Date = DateTime.Parse("01/01/2003") } };
ctx.Blogs.Add(blog);
blog = new Blog() { Name = "blog 2" };
blog.Posts = new List<Post>()
{ 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<Func<Blog, Boolean>> 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<Func<Post, Boolean>>(dateGreaterThanCutoffExpression,
postParam);
// (b) => b.Posts.Any((p) => p.Date > cutoffDateTime))
var collectionProperty = Expression.Property(blogParam, "Posts");
var resultExpression = CallAny(collectionProperty, lambdaForTheAnyCallPredicate);
return Expression.Lambda<Func<Blog, Boolean>>(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.