巴里的回答为原始海报提出的问题提供了一个可行的解决方案。感谢这两个人的提问和回答。
当我试图为一个非常相似的问题设计一个解决方案时,我发现了这个线程:以编程方式创建一个包含对 Any() 方法的调用的表达式树。然而,作为一个额外的限制,我的解决方案的最终目标是通过 Linq-to-SQL 传递这样一个动态创建的表达式,以便 Any() 评估的工作实际上在 DB 本身中执行。
不幸的是,到目前为止所讨论的解决方案并不是 Linq-to-SQL 可以处理的。
假设这可能是想要构建动态表达式树的一个非常流行的原因,我决定用我的发现来扩充线程。
当我尝试使用 Barry 的 CallAny() 的结果作为 Linq-to-SQL Where() 子句中的表达式时,我收到了具有以下属性的 InvalidOperationException:
- H结果=-2146233079
- Message="内部 .NET Framework 数据提供程序错误 1025"
- 源=系统.数据.实体
在将硬编码的表达式树与使用 CallAny() 动态创建的树进行比较后,我发现核心问题是由于谓词表达式的 Compile() 以及尝试在 CallAny() 中调用生成的委托所致。在不深入研究 Linq-to-SQL 实现细节的情况下,我认为 Linq-to-SQL 不知道如何处理这种结构似乎是合理的。
因此,经过一些实验,我能够通过稍微修改建议的 CallAny() 实现以采用 predicateExpression 而不是 Any() 谓词逻辑的委托来实现我想要的目标。
我修改后的方法是:
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);
}
现在我将演示它与 EF 的用法。为了清楚起见,我应该首先展示我正在使用的玩具域模型和 EF 上下文。基本上我的模型是一个简单的博客和帖子域......其中一个博客有多个帖子,每个帖子都有一个日期:
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; }
}
建立该域后,这是我最终执行修改后的 CallAny() 并使 Linq-to-SQL 执行评估 Any() 的工作的代码。我的特定示例将着重于返回至少有一篇比指定截止日期更新的帖子的所有博客。
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);
}
}
其中 BuildExpressionForBlogsWithRecentPosts() 是一个使用 CallAny() 的辅助函数,如下所示:
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);
}
注意:我在硬编码和动态构建的表达式之间发现了另一个看似不重要的差异。动态构建的有一个“额外的”转换调用,硬编码版本似乎没有(或不需要?)。在 CallAny() 实现中引入了转换。Linq-to-SQL 似乎没问题,所以我把它留在原地(尽管它是不必要的)。我不完全确定在比我的玩具样本更强大的用途中是否需要这种转换。