バリーの答えは、元のポスターによって提起された質問に対する有効な解決策を提供します。質問と回答をしてくれたこれらの個人の両方に感謝します。
Any() メソッドの呼び出しを含む式ツリーをプログラムで作成するという、非常によく似た問題の解決策を考案しようとしていたときに、このスレッドを見つけました。ただし、追加の制約として、私のソリューションの最終的な目標は、Any() 評価の作業が実際に DB 自体で実行されるように、動的に作成された式を Linq-to-SQL を介して渡すことでした。
残念ながら、これまでに説明したソリューションは、Linq-to-SQL で処理できるものではありません。
これが動的な式ツリーを構築したいという非常に一般的な理由である可能性があるという仮定の下で動作し、私は自分の調査結果をスレッドに追加することにしました。
Barry の CallAny() の結果を Linq-to-SQL Where() 句の式として使用しようとすると、次のプロパティを持つ InvalidOperationException を受け取りました。
- HResult=-2146233079
- Message="内部 .NET Framework データ プロバイダー エラー 1025"
- ソース=System.Data.Entity
ハードコードされた式ツリーと、CallAny() を使用して動的に作成された式ツリーを比較した結果、中核的な問題は、述語式の Compile() と、CallAny() で結果のデリゲートを呼び出そうとしたことが原因であることがわかりました。Linq-to-SQL の実装の詳細を掘り下げることなく、Linq-to-SQL がそのような構造をどう処理すればよいか分からないのは理にかなっているように思えました。
したがって、いくつかの実験の後、提案された CallAny() 実装を少し修正して、Any() 述語ロジックのデリゲートではなく predicateExpression を取ることで、目的の目標を達成することができました。
私の修正された方法は次のとおりです。
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() を評価する作業を実行させるコードを次に示します。私の特定の例では、指定された締め切り日よりも新しい投稿が少なくとも 1 つあるすべてのブログを返すことに焦点を当てています。
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);
}
注: ハードコーディングされた式と動的に構築された式の間に、一見重要ではないように見えるデルタがもう 1 つ見つかりました。動的に構築されたものには、ハードコードされたバージョンにはないように見える(または必要としない)「追加の」変換呼び出しがあります。変換は CallAny() 実装で導入されています。Linq-to-SQL はそれで問題ないように思われるので、そのまま残しました (不要ではありましたが)。おもちゃのサンプルよりも堅牢な用途でこの変換が必要になるかどうかは、完全にはわかりませんでした。