Come costruire un filtro di raccolta tramite alberi di espressione in c #

c# expression-trees filtering

Domanda

Sto costruendo un sistema di filtraggio per UserProfiles basato su proprietà conosciute ma combinazione sconosciuta (fino al runtime) di condizioni di filtraggio.

Nella mia domanda precedente Come faccio a creare un'espressione generica che ha un'espressione come parametro , ho trovato un modo per avere una definizione FilterDefinition per qualsiasi proprietà value raggiungibile dall'entità User tramite le proprietà di navigazione (ad esempio (User)u=> u.NavigationProperty.AnotherNavigationProperty.SomeValue ) e ho un metodo che può restituire un predicato come Expression<Func<User,bool>> per una determinata proprietà, operazione (> <== etc) e un valore.

Ora è giunto il momento di filtrarli anche sulle proprietà della collezione. Supponiamo, ad esempio, che l'utente abbia la raccolta CheckedOutBooks (che è una finzione totale, ma lo farà) e ho bisogno di creare una definizione di filtro per la proprietà Name della raccolta CheckedOutBooks sull'oggetto User.

Cosa ho: una collezione di utenti
La classe utente ha una raccolta di libri
ora mi piacerebbe creare un metodo

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

Che posso chiamare come GetPredicate(u=>u.Books.Select(b=>b.Name), Operations.Contains, "C# in a nutshell")

e ottenere un'espressione simile a

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

Sto pensando che forse sarà più facile dividere il primo parametro in due per raggiungere questo obiettivo. Forse u=>u.Books e b=>b.Name andrà meglio?

EDIT: quello che ho ottenuto finora:

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

    }

    }

Ora questo funziona incredibilmente e fa esattamente ciò che è necessario, ora l'unica cosa che devo capire è come sostituire typeof(UserProfile).GetProperty("StaticSegments")) qualche modo usare CollectionPropertySelector che è nell'esempio corrente sarebbe (UserProfile)u=>u.StaticSegments

Risposta accettata

Ok ho risolto per me stesso. E l'ho pubblicato su gitHub:
https://github.com/Alexander-Taran/Lambda-Magic-Filters

Data la classe di definizione del filtro (non recuperata per supportare proprietà diverse dalla stringa finora, ma lo farà in seguito):

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;

}

}

Risposta popolare

Hai quasi finito. Ora devi solo eseguire un piccolo trucco: avvolgere l'espressione lambda CollectionPropertySelector nell'espressione 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
        );
}

Potrebbe essere necessario cambiarlo un po 'per essere utilizzato per il tuo particolare scenario, ma l'idea dovrebbe essere chiara. Il mio test sembrava sostanzialmente questo:

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

Quindi si specifica il selettore di raccolta, l'operatore di predicati e predicati e il gioco è fatto.

L'ho scritto come metodo generico per chiarezza, ma è dinamico, non fortemente tipizzato in sostanza, quindi dovrebbe essere abbastanza facile cambiarlo in non generico, se necessario.



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é