IEnumerable< TSource> .Any(…)を呼び出す式ツリーを作成するにはどうすればよいですか?
-
11-07-2019 - |
質問
次を表す式ツリーを作成しようとしています:
myObject.childObjectCollection.Any(i => i.Name == "name");
わかりやすくするために簡略化して、次のものがあります:
//'myObject.childObjectCollection' is represented here by 'propertyExp'
//'i => i.Name == "name"' is represented here by 'predicateExp'
//but I am struggling with the Any() method reference - if I make the parent method
//non-generic Expression.Call() fails but, as per below, if i use <T> the
//MethodInfo object is always null - I can't get a reference to it
private static MethodCallExpression GetAnyExpression<T>(MemberExpression propertyExp, Expression predicateExp)
{
MethodInfo method = typeof(Enumerable).GetMethod("Any", new[]{ typeof(Func<IEnumerable<T>, Boolean>)});
return Expression.Call(propertyExp, method, predicateExp);
}
間違っているのは何ですか?誰でも提案がありますか?
解決
あなたのやり方にはいくつかの間違いがあります。
-
あなたは抽象化レベルを混合しています。
GetAnyExpression&lt; T&gt;
のTパラメーターは、propertyExp.Type
のインスタンス化に使用されるtypeパラメーターとは異なる場合があります。 T型パラメーターは、抽象スタック内でコンパイル時間に1ステップ近い-リフレクションを介してGetAnyExpression&lt; T&gt;
を呼び出す場合を除き、コンパイル時に決定されますが、式に埋め込まれた型propertyExp
として渡されるものは、実行時に決定されます。述語をExpression
として渡すことも抽象化の混乱です。これが次のポイントです。 -
GetAnyExpression
に渡す述語は、Expression
ではなくデリゲート値である必要があります。これは、Enumerable.Any&lt; T&gt;
。Any
の式ツリーバージョンを呼び出そうとしている場合は、代わりにLambdaExpression
を渡す必要があります。 Expressionよりも具体的な型を渡すことで正当化される可能性があるため、次のポイントに進みます。 -
一般に、
Expression
値を渡す必要があります。一般的に式ツリーを使用する場合(これはLINQとその友人だけでなく、あらゆる種類のコンパイラに適用されます)、作業しているノードツリーの直接の構成に関して不可知な方法で行う必要があります。MemberExpression
でAny
を呼び出していることを推定していますが、実際には知る必要はありませんMemberExpression
、IEnumerable&lt;&gt;
のインスタンス化タイプのExpression
だけを扱っていること。これは、コンパイラASTの基本に精通していない人にとってよくある間違いです。 Frans Bouma 式ツリーを初めて使い始めたとき、同じ間違いを繰り返しました-特別な場合を考えて。一般的に考えてください。中長期的には手間を大幅に節約できます。 -
そして、ここにあなたの問題の要点があります(2つ目とおそらく最初の問題は、それを過ぎてしまった場合はあなたに噛み付くでしょう)-Anyメソッドの適切な一般的なオーバーロードを見つけて、インスタンス化する必要があります正しいタイプを使用してください。 Reflectionは、ここで簡単に使えるものではありません。繰り返して適切なバージョンを見つける必要があります。
それで、分解:ジェネリックメソッド( Any
)を見つける必要があります。これを行うユーティリティ関数を次に示します。
static MethodBase GetGenericMethod(Type type, string name, Type[] typeArgs,
Type[] argTypes, BindingFlags flags)
{
int typeArity = typeArgs.Length;
var methods = type.GetMethods()
.Where(m => m.Name == name)
.Where(m => m.GetGenericArguments().Length == typeArity)
.Select(m => m.MakeGenericMethod(typeArgs));
return Type.DefaultBinder.SelectMethod(flags, methods.ToArray(), argTypes, null);
}
ただし、型引数と正しい引数型が必要です。 propertyExp
Expression
から取得するのは、 Expression
が List&lt; T&gt;
型、またはその他の型ですが、 IEnumerable&lt; T&gt;
のインスタンス化を見つけて、その型引数を取得する必要があります。これをいくつかの関数にカプセル化しました:
static bool IsIEnumerable(Type type)
{
return type.IsGenericType
&& type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
}
static Type GetIEnumerableImpl(Type type)
{
// Get IEnumerable implementation. Either type is IEnumerable<T> for some T,
// or it implements IEnumerable<T> for some T. We need to find the interface.
if (IsIEnumerable(type))
return type;
Type[] t = type.FindInterfaces((m, o) => IsIEnumerable(m), null);
Debug.Assert(t.Length == 1);
return t[0];
}
つまり、任意の Type
が与えられた場合、 IEnumerable&lt; T&gt;
のインスタンス化を引き出し、それが(正確に)存在しない場合にアサートできます。
この作業が邪魔にならない限り、実際の問題を解決するのはそれほど難しくありません。メソッドの名前をCallAnyに変更し、提案されているようにパラメータータイプを変更しました。
static Expression CallAny(Expression collection, Delegate predicate)
{
Type cType = GetIEnumerableImpl(collection.Type);
collection = Expression.Convert(collection, cType);
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,
Expression.Constant(predicate));
}
上記のすべてのコードを使用して、些細なケースで機能することを検証する Main()
ルーチンを次に示します。
static void Main()
{
// sample
List<string> strings = new List<string> { "foo", "bar", "baz" };
// Trivial predicate: x => x.StartsWith("b")
ParameterExpression p = Expression.Parameter(typeof(string), "item");
Delegate predicate = Expression.Lambda(
Expression.Call(
p,
typeof(string).GetMethod("StartsWith", new[] { typeof(string) }),
Expression.Constant("b")),
p).Compile();
Expression anyCall = CallAny(
Expression.Constant(strings),
predicate);
// now test it.
Func<bool> a = (Func<bool>) Expression.Lambda(anyCall).Compile();
Console.WriteLine("Found? {0}", a());
Console.ReadLine();
}
他のヒント
Barryの回答は、元のポスターによって提起された質問に対する実用的なソリューションを提供します。質問と回答をしてくれた彼らの両方に感謝します。
このスレッドは、Any()メソッドの呼び出しを含む式ツリーをプログラムで作成するという非常に類似した問題の解決策を考案しようとしていたときに見つかりました。ただし、追加の制約として、私のソリューションの最終的な目標は、動的に作成されたこのような式をLinq-to-SQLに渡して、Any()評価の作業が実際にDB自体。
残念ながら、これまでに説明したソリューションは、Linq-to-SQLで処理できるものではありません。
これが動的な式ツリーを構築したいという非常に一般的な理由であるかもしれないという仮定の下で動作して、私は私の発見でスレッドを増強することに決めました。
BarryのCallAny()の結果をLinq-to-SQLのWhere()句の式として使用しようとすると、次のプロパティを持つInvalidOperationExceptionを受け取りました。
- HResult = -2146233079
- Message =&quot; Internal .NET Framework Data Provider error 1025&quot;
- Source = 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での使用方法を示します。明確にするために、最初におもちゃのドメインモデル&amp;を示します。私が使用しているEFコンテキスト。基本的に、私のモデルは単純なブログ&amp;です。投稿ドメイン...ブログには複数の投稿があり、各投稿には日付があります:
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はそれで問題ないように思えるので、そのままにしておきました(不要ですが)。おもちゃのサンプルよりも堅牢な使用法でこの変換が必要かどうかは完全にはわかりませんでした。