Wie erstelle ich einen Ausdruck Baum Aufruf IEnumerable .Any (...)?
-
11-07-2019 - |
Frage
Ich versuche, einen Ausdrucksbaum zu erstellen, die den folgenden darstellt:
myObject.childObjectCollection.Any(i => i.Name == "name");
Verkürzte für Klarheit, ich habe folgende Möglichkeiten:
//'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);
}
Was mache ich falsch? Jemand irgendwelche Vorschläge?
Lösung
Es gibt mehrere Dinge falsch mit, wie Sie es fahren.
-
Sie sind Mischabstraktionsebenen. Der T-Parameter auf
GetAnyExpression<T>
könnte auf den Typ-Parameter unterschiedlich sein verwendetpropertyExp.Type
zu instanziiert. Der T-Typ-Parameter ist ein Schritt näher in dem Abstraktion Stapel Zeit zu kompilieren - es sei denn, SieGetAnyExpression<T>
über Reflexion anrufen, wird es bei der Kompilierung bestimmt werden - aber die Art des alspropertyExp
gab Ausdruck eingebettet wird zur Laufzeit bestimmt. Ihre Weitergabe des Prädikats alsExpression
ist auch eine Abstraktion mixup -., Die der nächste Punkt ist -
Das Prädikat Sie
GetAnyExpression
geben werden, sollten ein Delegierter Wert, keinExpression
jeglicher Art sein, da Sie versuchen,Enumerable.Any<T>
zu nennen. Wenn Sie versuchen, einen Ausdruck Baum Version vonAny
zu nennen, dann sollten Sie stattdessen eineLambdaExpression
passieren, die Sie zitieren würde, und ist einer der seltenen Fälle, in denen man nebenbei eine spezifischere Art als Ausdruck gerechtfertigt sein könnte, das führt mich zu meinem nächsten Punkt. -
Generell Sie um
Expression
Werte übergeben sollte. Wenn mit Ausdruck Bäume im Allgemeinen arbeiten - und das gilt für alle Arten von Compilern, nicht nur LINQ und seine Freunde - sollten Sie dies in einer Weise tun, die in Bezug auf die unmittelbare Zusammensetzung des Knotenbaum mit dem Sie arbeiten Agnostiker ist. Sie sind Vermutungs , dass Sie anrufenAny
auf einemMemberExpression
, aber Sie nicht wirklich wissen müssen, dass Sie mit einemMemberExpression
zu tun hat, nur einenExpression
vom Typ einige InstanziierungIEnumerable<>
. Dies ist ein häufiger Fehler für Menschen nicht vertraut mit den Grundlagen des Compiler ASTs. Frans Bouma hat immer wieder den gleichen Fehler, wenn er zum ersten Mal mit Ausdruck Bäumen zu arbeiten begann - Denken in besonderen Fällen. Denken Sie im Allgemeinen. Sie finden sich eine Menge Ärger im Medium speichern und längere Sicht. -
Und hier kommt das Fleisch des Problems (obwohl der zweite und wahrscheinlich erste Probleme hätte wenig Sie, wenn Sie an ihm vorbei gekommen war) - Sie die entsprechende allgemeine Überlastung der Any-Methode finden müssen, und dann instanziiert es mit dem richtigen Typ. Reflexion ist Ihnen nicht mit einem einfach hier draußen; Sie müssen durch eine entsprechende Version iterieren und finden.
So, bricht es nach unten: Sie benötigen eine generische Methode (Any
) zu finden. Hier ist eine Utility-Funktion, die das tut:
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);
}
Es erfordert jedoch die Typargumente und die richtigen Argumenttypen. dass von Ihrem propertyExp
Expression
bekommen, ist nicht ganz trivial, weil die Expression
eines List<T>
Typ oder einem anderen Typ sein, aber wir müssen die IEnumerable<T>
Instanziierung finden und seine Art Argument zu bekommen. Ich habe, dass in ein paar Funktionen gekapselt:
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];
}
So gegeben, jeder Type
, können wir aus der IEnumerable<T>
Instanziierung ziehen jetzt -. Und geltend machen, wenn es nicht (genau) ist
Mit dieser Arbeit aus dem Weg, ist das eigentliche Problem zu lösen nicht allzu schwierig. Ich habe Ihre Methode CallAny umbenannt und geändert, um die Parametertypen wie vorgeschlagen:
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));
}
Hier ist eine Main()
Routine, die alle oben genannten Code verwendet und überprüft, ob es für einen trivialen Fall funktioniert:
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();
}
Andere Tipps
Barry Antwort eine funktionierende Lösung für die Frage vom Verfasser gestellt. Dank dieser beiden Personen zu fragen und Antworten.
fand ich diesen Thread als ich eine Lösung für ein ganz ähnliches Problem zu entwickeln versuche: programmatisch einen Ausdruck Baum erstellen, die einen Aufruf an den Any () -Methode. Als zusätzliche Einschränkung, aber die Endziel meiner Lösung war eine solche dynamisch erstellten Ausdruck durch Linq-to-SQL zu übergeben, so dass die Arbeit der Any () Auswertung wird in der tatsächlich durchgeführt DB selbst.
Leider, wie die Lösung bisher diskutiert wird, ist nicht etwas, die Linq-to-SQL verarbeiten kann.
unter der Annahme, Betrieb, dass dies ein ziemlich beliebter Grund für den Wunsch sein könnte, einen dynamischen Ausdruck Baum zu bauen, habe ich beschlossen, den Faden mit meinen Erkenntnissen zu erweitern.
Wenn ich das Ergebnis von Barrys CallAny () als Ausdruck in einer Linq-to-SQL Wo () Klausel verwenden wollte ich eine InvalidOperationException mit den folgenden Eigenschaften erhalten:
- HResult = -2146233079
- Message = "Internal .NET Framework Data Provider Fehler 1025"
- Source = System.Data.Entity
einen hartcodierte Ausdrucksbaumes zu dem dynamisch erstellt einer Verwendung CallAny Nach einem Vergleich (), fand ich, dass das Kernproblem zum Compilieren zurückzuführen war () des Prädikatenausdruckes und der Versuch, die resultierend Delegierten im CallAny aufzuzurufen (). Ohne zu graben tief in Linq to SQL-Implementierungsdetails, schien es mir sinnvoll, dass Linq to SQL würde nicht wissen, was mit einer solchen Struktur zu tun.
Deshalb wird nach einigen Experimenten konnte ich mein gewünschtes Ziel erreichen, indem er leicht die vorgeschlagene CallAny () -Implementierung Überarbeitung für die Any () Pr¨adikatenlogik eine predicateExpression anstatt einen Delegierten zu nehmen.
Meine überarbeitete Methode ist:
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);
}
Jetzt werde ich seine Verwendung mit EF demonstrieren. Aus Gründen der Klarheit sollte ich zeigen zunächst das Spielzeug Domänenmodell & EF Kontext ich verwende. Im Grunde genommen mein Modell ist eine einfache Blogs & Posts Domain ... wo ein Blog mehrere Beiträge hat und jeder Beitrag hat ein Datum:
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; }
}
Mit dieser Domäne eingerichtet, hier ist mein Code schließlich die überarbeitete CallAny auszuüben () und macht Linq-to-SQL die Arbeit des ANY () auswertet. Mein besonderes Beispiel konzentrieren alle Blogs auf Rückkehr, die mindestens einen Beitrag haben, die neuer als eine bestimmte Stichtag ist.
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);
}
}
Wo BuildExpressionForBlogsWithRecentPosts () ist eine Hilfsfunktion, die verwendet CallAny () wie folgt:
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);
}
Hinweis: Ich fand ein anderes scheinbar unwichtiges Delta zwischen der hartcodiert und dynamisch aufgebaut Ausdrücke. Die dynamisch aufgebaut hat man einen „extra“ Aufruf konvertieren darin, dass die hartcodierte Version nicht zu haben scheint (oder müssen?). Die Umwandlung wird in der CallAny () Umsetzung eingeführt. Linq-to-SQL scheint in Ordnung zu sein mit ihm ich es so an seinem Platz belassen (obwohl es nicht notwendig). Ich war nicht ganz sicher, ob diese Umwandlung könnte in einigen robustere Verwendungen als mein Spielzeug Probe benötigt werden.