Árboles de expresiones: Recuento filtrado en la propiedad de navegación

asp.net-mvc c# expression-trees linq

Pregunta

Estoy creando un generador de informes dinámico que permite al usuario seleccionar campos de clases predefinidas (que se asignan a tablas de base de datos a través de Entity Framework) como filtros para sus datos. Para crear mi consulta de LINQ to Entities, estoy usando los árboles de expresiones debido a la naturaleza dinámica de las consultas. Lo tengo funcionando para casi todos los escenarios no personalizados, pero realmente estoy luchando para que funcione en unos pocos escenarios personalizados.

Una versión abreviada de mis modelos para una de mis consultas personalizadas se ve así:

public class Attendee {
    public int ID { get; set; }
    public DateTime? CancelledOn { get; set; }

    [ForeignKey("Event")]
    public int Event_ID { get; set; }
    public virtual Event Event { get; set; }        
}

public class Event {
    public int ID { get; set; }
    public virtual ICollection<Attendee> Attendees { get; set; }        
}

Una consulta de ejemplo que un usuario desea ejecutar es filtrar para mostrar solo los Eventos que tienen más de 10 Asistentes que no han cancelado. Si escribiera esto en una consulta normal de IQueryable, escribiría esto como

db.Event.Where(s => s.Attendees.Count(a => a.CancelledOn == null) > 10);

Con el marco de Expression Tree que tengo configurado, ya puedo manejar la parte "> 10", pero no puedo averiguar cómo generar dinámicamente la parte "s.Attendees.Count (a => a.CancelledOn == null)". He leído SO publicaciones sobre cómo hacer el conteo o una suma en una propiedad de nivel superior, pero no he podido hackear ninguna de esas soluciones para que funcione para una propiedad de navegación filtrada. Ejemplo de publicación: Dynamic LINQ, función Select, funciona en Enumerable, pero no en Queryable

La captura de pantalla a continuación es un ejemplo de un filtro diferente construido con árboles de expresiones para que pueda ver un ejemplo de lo que estoy trabajando. "pe" es la ParameterExpression para el tipo que se pasa, "Evento". "expresión" es lo que estoy tratando de crear y evaluar. http://grab.by/RoYm

La consulta LINQ que se está ejecutando arriba es

db.Event.Where(s=> s.StartDate >= '1/1/2016 12:00 am')

Cualquier ayuda o dirección sobre esto sería muy apreciada y, por favor, avíseme si necesito incluir otros fragmentos de código.

Respuesta aceptada

No está seguro de cuáles son los parámetros de entrada para el método que está buscando, pero lo siguiente debe darle un punto de partida. La parte esencial es crear una llamada de método al método Enumerable.Count(predicate) .

static Expression<Func<TSource, bool>> MakeCountPredicate<TSource>(string collectionName, string itemName, ExpressionType itemComparison, string itemValue, ExpressionType countComparison, int countValue)
{
    var source = Expression.Parameter(typeof(TSource), "s");
    var collection = Expression.Property(source, collectionName);
    var itemType = collection.Type.GetInterfaces()
        .Single(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
        .GetGenericArguments()[0];
    var item = Expression.Parameter(itemType, "e");
    var itemProperty = Expression.Property(item, itemName);
    var itemPredicate = Expression.Lambda(
        Expression.MakeBinary(itemComparison, itemProperty, Expression.Constant(
            string.IsNullOrEmpty(itemValue) || itemValue.Equals("null", StringComparison.OrdinalIgnoreCase) ? null :
            Convert.ChangeType(itemValue, itemProperty.Type))),
        item);
    var itemCount = Expression.Call(
        typeof(Enumerable), "Count", new[] { itemType },
        collection, itemPredicate);
    var predicate = Expression.Lambda<Func<TSource, bool>>(
        Expression.MakeBinary(countComparison, itemCount, Expression.Constant(countValue)),
        source);
    return predicate;
}

por lo que la expresión predicada muestra

Expression<Func<Event, bool>> predicate =
    s => s.Attendees.Count(a => a.CancelledOn == null) > 10

Se puede construir dinámicamente así

var predicate = MakeCountPredicate<Event>("Attendees", 
    "CancelledOn", ExpressionType.Equal, "null", ExpressionType.GreaterThan, 10);

Respuesta popular

Para generar el árbol de Expression para el Count , debe generar una Call al Method correspondiente.

Aquí está un borrador inicial del código ...

Expression callExpr = Expression.Call(
    Expression.Constant(s.Attendees), 
    typeof(ICollection<Attendee>).GetMethod("get_Count")); // + 2 Arguments

Por supuesto, debe elaborar más y combinar esto en su programa principal.

Un ejemplo de uso básico sería

// Print out the expression.
Debug.WriteLine(callExpr.ToString());

// The following statement first creates an expression tree,
// then compiles it, and then executes it.  
Debug.WriteLine(Expression.Lambda<Func<int>>(callExpr).Compile()());

Por último, el Contador de Expression (NodeType Call ) tendrá que contener 2 Argumentos (como tercer parámetro, que no se muestra más arriba):

  • la colección
  • Lambda para a => a.CancelledOn == null


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