Frage

I'm building a filtering system for UserProfiles based on known properties but unknown (until runtime) combination of filtering conditions.

In my previous question How do I create a generic Expression that has an expression as a parameter, I've figured out a way to have a FilterDefinition for any value property reachable from User entity via navigation properties (i.e. (User)u=> u.NavigationProperty.AnotherNavigationProperty.SomeValue) and I have a method that can return a predicate as Expression<Func<User,bool>> for a given property, operation ( > < == etc ) and a value.

Now the time has come to filter them on collection properties as well. Say for example User has CheckedOutBooks collection (which is a total fiction, but will do) And I need to create a filter definition for Name property of CheckedOutBooks collection on User object.

What I have: A collection of Users
User class has a collection of Books
now I would like to create a method

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

That I can call like GetPredicate(u=>u.Books.Select(b=>b.Name), Operations.Contains, "C# in a nutshell")

and get an expression back similar to

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

I'm thinking maybe it will be easier to split first parameter in two to achieve this. Maybe u=>u.Books and b=>b.Name will do better?

EDIT: what I got so far:

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

    }

    }

Now this one works awesomely and does exactly what's needed, now the only thing I need to figure out is how to replace typeof(UserProfile).GetProperty("StaticSegments")) somehow to use CollectionPropertySelector that is in current example would be (UserProfile)u=>u.StaticSegments

War es hilfreich?

Lösung 2

Ok I've got it solved for myself. And I published it to gitHub:
https://github.com/Alexander-Taran/Lambda-Magic-Filters

Given the filter definition class (not reafactored to support properties other than string so far, but will do later):

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;

}

}

Andere Tipps

You're almost done. Now you just need to do a little trick - wrap your CollectionPropertySelector lambda expression in the CollectionSelector lambda expression.

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

You may have to change this a bit to be used for your particular scenario, but the idea should be clear. My test looked basically like this:

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

So you specify the collection selector, predicate and predicate operator, and you're done.

I've written it as a generic method for clarity, but it's dynamic, not strongly typed in essence, so it should be pretty easy to change it to non-generic, if you need that.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top