Come creo un albero di espressioni che chiama IEnumerable .Qualunque(...)?

.net c# expression-trees linq

Domanda

Sto cercando di creare un albero di espressioni che rappresenti quanto segue:

myObject.childObjectCollection.Any(i => i.Name == "name");

Accorciato per chiarezza, ho il seguente:

//'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);
}

Che cosa sto facendo di sbagliato? Qualcuno ha dei suggerimenti?

Risposta accettata

Ci sono molte cose che non vanno nel modo in cui lo stai facendo.

  1. Stai mescolando i livelli di astrazione. Il parametro T per GetAnyExpression<T> potrebbe essere diverso dal parametro type utilizzato per istanziare propertyExp.Type . Il parametro T type è un gradino più vicino nello stack di astrazione per compilare il tempo - a meno che tu non stia chiamando GetAnyExpression<T> tramite reflection, sarà determinato al momento della compilazione - ma il tipo incorporato nell'espressione passato come propertyExp viene determinato in fase di runtime . Il tuo passaggio del predicato come Expression è anche un mix di astrazione - che è il prossimo punto.

  2. Il predicato che stai passando a GetAnyExpression deve essere un valore delegato, non Expression di alcun tipo, dal momento che stai tentando di chiamare Enumerable.Any<T> . Se stavi tentando di chiamare una versione di un'espressione-albero di Any , dovresti invece passare una LambdaExpression , che vorresti citare, ed è uno dei rari casi in cui potresti essere giustificato nel passare un tipo più specifico di Expression, che mi porta al mio prossimo punto.

  3. In generale, è necessario passare attorno ai valori di Expression . Quando si lavora con gli alberi delle espressioni in generale, e ciò si applica a tutti i tipi di compilatori, non solo a LINQ e ai suoi amici, è necessario farlo in modo agnostico rispetto alla composizione immediata dell'albero dei nodi su cui si sta lavorando. Si presume che tu stia chiamando Any su un MemberExpression , ma in realtà non hai bisogno di sapere che hai a che fare con un MemberExpression , solo Expression di tipo qualche istanziazione di IEnumerable<> . Questo è un errore comune per chi non ha familiarità con le basi del compilatore AST. Frans Bouma ha ripetutamente commesso lo stesso errore quando ha iniziato a lavorare con gli alberi di espressione - pensando in casi speciali. Pensa in generale. Ti risparmierai un sacco di problemi a medio e lungo termine.

  4. E qui arriva la carne del tuo problema (anche se il secondo e probabilmente i primi problemi ti avrebbero morso se lo avessi superato) - devi trovare il sovraccarico generico appropriato del metodo Any e quindi istanziarlo con il tipo corretto. La riflessione non ti fornisce una facile uscita qui; è necessario scorrere e trovare una versione appropriata.

Quindi, scomposizione: è necessario trovare un metodo generico ( Any ). Ecco una funzione di utilità che lo fa:

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);
}

Tuttavia, richiede gli argomenti type e i tipi di argomenti corretti. Ottenere ciò dalla tua propertyExp Expression non è del tutto banale, perché l' Expression può essere di tipo List<T> , o qualche altro tipo, ma dobbiamo trovare l'istanza IEnumerable<T> e ottenere il suo argomento type. L'ho incapsulato in un paio di funzioni:

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];
}

Quindi, dato qualsiasi Type , possiamo ora estrarre l'istanza IEnumerable<T> e asserire se non ce n'è (esattamente) uno.

Con quel lavoro fuori strada, risolvere il vero problema non è troppo difficile. Ho rinominato il metodo su CallAny e modificato i tipi di parametri come suggerito:

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));
}

Ecco una routine Main() che utilizza tutto il codice sopra riportato e verifica che funzioni per un caso banale:

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();
}

Risposta popolare

La risposta di Barry fornisce una soluzione operativa alla domanda posta dal poster originale. Grazie a entrambe le persone per chiedere e rispondere.

Ho trovato questo thread mentre stavo cercando di escogitare una soluzione per un problema abbastanza simile: creando a livello di programmazione un albero di espressioni che includa una chiamata al metodo Any (). Come ulteriore vincolo, tuttavia, l' obiettivo finale della mia soluzione era passare un'espressione creata dinamicamente attraverso Linq-to-SQL in modo che il lavoro della valutazione Any () fosse effettivamente eseguito nel DB stesso.

Sfortunatamente, la soluzione come discusso finora non è qualcosa che Linq-to-SQL può gestire.

Operando partendo dal presupposto che questo potrebbe essere un motivo abbastanza popolare per voler costruire un albero di espressioni dinamiche, ho deciso di aumentare il filo con le mie scoperte.

Quando ho tentato di utilizzare il risultato di Barry CallAny () come espressione in una clausola Where () Linq-to-SQL, ho ricevuto un InvalidOperationException con le seguenti proprietà:

  • HResult = -2.146,233079 millions
  • Messaggio = "Errore 1025 del provider di dati di .NET Framework interno"
  • Fonte = System.Data.Entity

Dopo aver confrontato un albero di espressioni hard-coded con quello creato dinamicamente utilizzando CallAny (), ho scoperto che il problema principale era dovuto al Compile () dell'espressione predicato e al tentativo di richiamare il delegato risultante in CallAny (). Senza approfondire i dettagli di implementazione di Linq-to-SQL, mi sembrava ragionevole che Linq-to-SQL non sapesse cosa fare con una tale struttura.

Pertanto, dopo alcuni esperimenti, sono stato in grado di raggiungere l'obiettivo desiderato rivedendo leggermente l'implementazione CallAny () suggerita per prendere un'espressione predicato anziché un delegato per la logica del predicato Any ().

Il mio metodo rivisto è:

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);
}

Ora dimostrerò il suo utilizzo con EF. Per chiarezza dovrei prima mostrare il modello di dominio del giocattolo e il contesto EF che sto usando. Fondamentalmente il mio modello è un semplicistico dominio di blog e post ... dove un blog ha più post e ogni post ha una data:

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; }
}

Con questo dominio stabilito, ecco il mio codice per esercitare infine la versione aggiornata di CallAny () e fare in modo che Linq-to-SQL svolga il lavoro di valutazione di Any (). Il mio particolare esempio si concentrerà sul ritorno di tutti i blog che hanno almeno un post che è più recente di una data di taglio specificata.

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);
    }
}

Dove BuildExpressionForBlogsWithRecentPosts () è una funzione di supporto che utilizza CallAny () come segue:

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);
}

NOTA: ho trovato un altro delta apparentemente insignificante tra le espressioni codificate e dinamiche. Quello dinamicamente costruito ha una chiamata di conversione "extra" in esso che la versione hard-coded non sembra avere (o bisogno?). La conversione viene introdotta nell'implementazione CallAny (). Linq-to-SQL sembra essere ok, così l'ho lasciato sul posto (anche se non era necessario). Non ero del tutto certo che questa conversione potesse essere necessaria in alcuni usi più robusti del mio campione di giocattoli.



Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché
Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché