Cómo construir un filtro de colección a través de árboles de expresión en c #

c# expression-trees filtering

Pregunta

Estoy creando un sistema de filtrado para UserProfiles basado en propiedades conocidas pero una combinación desconocida (hasta el tiempo de ejecución) de condiciones de filtrado.

En mi pregunta anterior ¿ Cómo creo una expresión genérica que tenga una expresión como parámetro ? He descubierto una manera de tener una definición de filtro para cualquier propiedad de valor accesible desde la entidad de usuario a través de las propiedades de navegación (es decir, (User)u=> u.NavigationProperty.AnotherNavigationProperty.SomeValue ) y tengo un método que puede devolver un predicado como Expression<Func<User,bool>> para una propiedad dada, operación (> <== etc) y un valor.

Ahora ha llegado el momento de filtrarlos en las propiedades de la colección también. Digamos, por ejemplo, que el usuario tiene la colección CheckedOutBooks (que es una ficción total, pero lo hará) y necesito crear una definición de filtro para la propiedad Name de la colección CheckedOutBooks en el objeto Usuario.

Lo que tengo: Una colección de Usuarios.
La clase de usuario tiene una colección de libros.
Ahora me gustaría crear un método.

Expression<Func<User,bool>> GetPredicate(Expression<User,TProperty>, Operations operation, TProperty value) 

Que puedo llamar como GetPredicate(u=>u.Books.Select(b=>b.Name), Operations.Contains, "C# in a nutshell")

y recuperar una expresión similar a

u=>u.Books.Any(b=>b.Name == "C# in a nutshell")

Estoy pensando que tal vez sea más fácil dividir el primer parámetro en dos para lograr esto. ¿Quizás u=>u.Books y b=>b.Name harán mejor?

EDIT: lo que tengo hasta ahora:

  class FilterDefinitionForCollectionPropertyValues<T>:FilterDefinition, IUserFilter
    {


    public Expression<Func<UserProfile, IEnumerable<T>>> CollectionSelector { get; set; }
    public Expression<Func<T, string>> CollectionPropertySelector { get; set; }


    public Expression<Func<Profile.UserProfile, bool>> GetFilterPredicateFor(FilterOperations operation, string value)
    {
        var propertyParameter = CollectionPropertySelector.Parameters[0];
        var collectionParameter = CollectionSelector.Parameters[0];

// building predicate to supply to Enumerable.Any() method
        var left = CollectionPropertySelector.Body;
        var right = Expression.Constant(value);    
        var innerLambda = Expression.Equal(left, right);    
        Expression<Func<T, bool>> innerFunction = Expression.Lambda<Func<T, bool>>(innerLambda, propertyParameter);



        var method = typeof(Enumerable).GetMethods().Where(m => m.Name == "Any" && m.GetParameters().Length == 2).Single().MakeGenericMethod(typeof(T));

        var outerLambda = Expression.Call(method, Expression.Property(collectionParameter, typeof(UserProfile).GetProperty("StaticSegments")), innerFunction);

        throw new NotImplementedException();

    }

    }

Ahora, este funciona de manera impresionante y hace exactamente lo que se necesita, ahora lo único que tengo que averiguar es cómo reemplazar typeof(UserProfile).GetProperty("StaticSegments")) alguna manera para usar CollectionPropertySelector que en el ejemplo actual sería (UserProfile)u=>u.StaticSegments

Respuesta aceptada

Ok lo tengo resuelto por mi mismo. Y lo publiqué en gitHub:
https://github.com/Alexander-Taran/Lambda-Magic-Filters

Dada la clase de definición de filtro (no reafactored para admitir propiedades distintas a la cadena hasta ahora, pero lo hará más adelante):

class FilterDefinitionForCollectionPropertyValues<T>:FilterDefinition, IUserFilter
{

//This guy just points to a collection property
public Expression<Func<UserProfile, IEnumerable<T>>> CollectionSelector { get; set; }
// This one points to a property of a member of that collection.
public Expression<Func<T, string>> CollectionPropertySelector { get; set; }


//This one does the heavy work of building a predicate based on a collection,   
//it's member property, operation type and a valaue
public System.Linq.Expressions.Expression<Func<Profile.UserProfile, bool>> GetFilterPredicateFor(FilterOperations operation, string value)
{
    var getExpressionBody = CollectionPropertySelector.Body as MemberExpression;
    if (getExpressionBody == null)
    {
        throw new Exception("getExpressionBody is not MemberExpression: " + CollectionPropertySelector.Body);
    }

    var propertyParameter = CollectionPropertySelector.Parameters[0];
    var collectionParameter = CollectionSelector.Parameters[0];
    var left = CollectionPropertySelector.Body;
    var right = Expression.Constant(value);

    // this is so far hardcoded, but might be changed later based on operation type  
    // as well as a "method" below
    var innerLambda = Expression.Equal(left, right);

    Expression<Func<T, bool>> innerFunction = Expression.Lambda<Func<T, bool>>(innerLambda, propertyParameter);
    // this is hadrcoded again, but maybe changed later when other type of operation will be needed
    var method = typeof(Enumerable).GetMethods().Where(m => m.Name == "Any" && m.GetParameters().Length == 2).Single().MakeGenericMethod(typeof(T));

    var outerLambda = Expression.Call(method, Expression.Property(collectionParameter, (CollectionSelector.Body as MemberExpression).Member as System.Reflection.PropertyInfo), innerFunction);

    var result = Expression.Lambda<Func<UserProfile, bool>>(outerLambda, collectionParameter);

    return result;

}

}

Respuesta popular

Ya casi terminas. Ahora solo necesita hacer un pequeño truco: envuelva la expresión lambda CollectionPropertySelector en la expresión lambda CollectionSelector.

Expression<Func<TParent,bool>> Wrap<TParent,TElement>(Expression<Func<TParent, IEnumerable<TElement>>> collection, Expression<Func<TElement, bool>> isOne, Expression<Func<IEnumerable<TElement>, Func<TElement, bool>, bool>> isAny)
{
    var parent = Expression.Parameter(typeof(TParent), "parent");

    return 
        (Expression<Func<TParent, bool>>)Expression.Lambda
        (
            Expression.Invoke
            (
                isAny,
                Expression.Invoke
                (
                    collection,
                    parent
                ),
                isOne
            ),
            parent
        );
}

Puede que tenga que cambiar esto un poco para usarlo en su situación particular, pero la idea debería ser clara. Mi prueba se veía básicamente así:

var user = new User { Books = new List<string> { "Book 1", "Book 2" }};

var query = Wrap<User, string>(u => u.Books, b => b.Contains("Bookx"), (collection, condition) => collection.Any(condition));

Así que especifica el selector de colección, el predicado y el operador de predicado, y listo.

Lo he escrito como un método genérico para mayor claridad, pero es dinámico, no está muy tipeado en esencia, por lo que debería ser bastante fácil cambiarlo a no genérico, si lo necesita.



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