¿Cómo creo un árbol de expresiones que llame a IEnumerable? .Alguna(...)?

.net c# expression-trees linq

Pregunta

Estoy tratando de crear un árbol de expresiones que represente lo siguiente:

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

Acortado para mayor claridad, tengo lo siguiente:

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

¿Qué estoy haciendo mal? ¿Alguien tiene alguna sugerencia?

Respuesta aceptada

Hay varias cosas erróneas en cómo lo estás haciendo.

  1. Estás mezclando niveles de abstracción. El parámetro T para GetAnyExpression<T> podría ser diferente al parámetro de tipo utilizado para crear una instancia de propertyExp.Type . El parámetro de tipo T está un paso más cerca en la pila de abstracción para compilar el tiempo, a menos que esté llamando a GetAnyExpression<T> través de la reflexión, se determinará en el momento de la compilación, pero el tipo incrustado en la expresión pasada como propertyExp se determina en tiempo de ejecución . Su paso del predicado como una Expression también es una mezcla de abstracción, que es el siguiente punto.

  2. El predicado que le está pasando a GetAnyExpression debe ser un valor delegado, no una Expression de ningún tipo, ya que está intentando llamar a Enumerable.Any<T> . Si estuviera tratando de llamar a una versión del árbol de expresiones de Any , entonces debería pasar una LambdaExpression , que estaría citando, y es uno de los casos raros en los que podría estar justificado en pasar un tipo más específico que Expression. Lo cual me lleva a mi siguiente punto.

  3. En general, debes pasar los valores de Expression . Cuando trabaje con árboles de expresión en general, y esto se aplica a todo tipo de compiladores, no solo a LINQ y sus amigos, debe hacerlo de una manera que sea agnóstica en cuanto a la composición inmediata del árbol de nodos con el que está trabajando. Está asumiendo que está llamando a Any en una MemberExpression , pero en realidad no necesita saber que está tratando con una MemberExpression , solo una Expression de tipo alguna instanciación de IEnumerable<> . Este es un error común para las personas que no están familiarizadas con los conceptos básicos de los AST del compilador. Frans Bouma repetidamente cometió el mismo error cuando comenzó a trabajar con árboles de expresión, pensando en casos especiales. Piensa en general. Te ahorrarás un montón de problemas a medio y largo plazo.

  4. Y aquí viene la esencia de su problema (aunque la segunda y probablemente las primeras cuestiones lo habrían mordido si lo hubiera superado): debe encontrar la sobrecarga genérica apropiada del método Any y luego crear una instancia con el tipo correcto. La reflexión no te proporciona una salida fácil aquí; necesitas iterar a través y encontrar una versión apropiada.

Entonces, desglosándolo: necesitas encontrar un método genérico ( Any ). Aquí hay una función de utilidad que hace eso:

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

Sin embargo, requiere los argumentos de tipo y los tipos de argumento correctos. Obtener eso de su propertyExp Expression no es completamente trivial, ya que la Expression puede ser de tipo List<T> , o de algún otro tipo, pero necesitamos encontrar la IEnumerable<T> y obtener su argumento de tipo. He encapsulado eso en un par de funciones:

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

Entonces, dado cualquier Type , ahora podemos extraer la IEnumerable<T> fuera de ella, y afirmar si no existe (exactamente) una.

Con ese trabajo fuera del camino, resolver el problema real no es demasiado difícil. Cambié el nombre de su método a CallAny y cambié los tipos de parámetros como se sugiere:

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

Aquí hay una rutina Main() que usa todo el código anterior y verifica que funciona para un caso trivial:

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

Respuesta popular

La respuesta de Barry proporciona una solución de trabajo a la pregunta planteada por el póster original. Gracias a los dos individuos por preguntar y responder.

Encontré este hilo mientras intentaba idear una solución para un problema bastante similar: crear mediante programación un árbol de expresiones que incluya una llamada al método Any (). Sin embargo, como una restricción adicional, el objetivo final de mi solución era pasar una expresión creada de forma dinámica a través de Linq-to-SQL para que el trabajo de la evaluación Any () se realice realmente en el propio DB.

Desafortunadamente, la solución que se analiza hasta ahora no es algo que Linq-to-SQL pueda manejar.

Operando bajo el supuesto de que esta podría ser una razón bastante popular para querer construir un árbol de expresión dinámico, decidí aumentar el hilo con mis hallazgos.

Cuando intenté usar el resultado de Barry's CallAny () como una expresión en una cláusula Where de Linq-to-SQL, recibí una InvalidOperationException con las siguientes propiedades:

  • HResult = -2146233079
  • Mensaje = "Error interno del proveedor de datos de .NET Framework 1025"
  • Fuente = System.Data.Entity

Después de comparar un árbol de expresión codificado con el creado dinámicamente utilizando CallAny (), descubrí que el problema principal se debía a la Compilación () de la expresión de predicado y al intento de invocar al delegado resultante en el CallAny (). Sin profundizar en los detalles de la implementación de Linq-to-SQL, me pareció razonable que Linq-to-SQL no sabría qué hacer con esa estructura.

Por lo tanto, después de algunos experimentos, pude lograr mi objetivo deseado al revisar ligeramente la implementación sugerida de CallAny () para tomar una expresión de predicado en lugar de un delegado para la lógica de predicado Cualquiera ().

Mi método revisado es:

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

Ahora demostraré su uso con EF. Para mayor claridad, primero debo mostrar el modelo de dominio de juguete y el contexto EF que estoy usando. Básicamente, mi modelo es un dominio de Blogs y Publicaciones simplista ... donde un blog tiene varias publicaciones y cada publicación tiene una fecha:

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 ese dominio establecido, aquí está mi código para finalmente ejercer la versión revisada de CallAny () y hacer que Linq-to SQL haga el trabajo de evaluar el Any (). Mi ejemplo particular se centrará en devolver todos los Blogs que tengan al menos una Publicación que sea más reciente que una fecha límite especificada.

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

Donde BuildExpressionForBlogsWithRecentPosts () es una función auxiliar que usa CallAny () de la siguiente manera:

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: Encontré otro delta aparentemente sin importancia entre las expresiones de código rígido y las construidas dinámicamente. El de construcción dinámica tiene una llamada de conversión "extra" que la versión codificada no parece tener (¿o necesita?). La conversión se introduce en la implementación de CallAny (). Linq-to-SQL parece estar bien con eso, así que lo dejé en su lugar (aunque no fue necesario). No estaba completamente seguro de si esta conversión podría ser necesaria en algunos usos más robustos que mi muestra de juguete.



Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow