Enumerating through list of expressions to filter collection
Asked Answered
B

1

8

I'm pushing the limits of my knowledge in c# and linq here, so please bear with me if I'm completely off with my example or understanding of linq, c#, generic types, lambda expressions,design patterns, etc.

I have a class that holds two collections, one is the collection to be filtered: IEnumberable<InstagramUser> and the second is the collection of expressions to filter on: IEnumerable<IInstagramFilter>.

public class InstagramDisplay {

    public IEnumerable<InstagramUser> instagramUsers;
    public IEnumerable<IInstagramFilter> instagramFilters; 

    public InstagramDisplay() {
        instagramUsers = new List<InstagramUser>();
        instagramFilters = new List<IInstagramFilter>();
    }

    public IEnumerable<InstagramUser> display() {
        instagramFilters.ToList().ForEach(x => instagramUsers.Where(x.filter(instagramUsers)));
        return instagramFilters;
    }
}

public interface IInstagramFilter {
    Expression<Func<T, bool>> filter<T>(IQueryable<T> source); 
}

I would have classes extend IInstagramFilter. Each IInstagramFilter class would have a property (or function - not sure what's best) that would return the lambda expression that would be applied to IEnumerable<InstagramUser> in the display() method.

public class UserFilter : IInstagramFilter {
    public Expression<Func<T, bool>> filter<T>(IQueryable<T> source) {
        //return some expression - but how?
    }
}

I'm struggling to understand a few things:

  1. How to set the expression for each IInstagramFilter class and then call it in the display() method?

  2. Each IInstagramFilter class would have a lambda that would be used to filter IEnumerable<InstagramUser> but since the Filter class has no knowledge of IEnumerable<InstagramUser> how would I create the appropriate lambda in the first place?

  3. I think this roughly follows the Decorator Pattern but perhaps there's a better design all together that I'm not aware of.

UPDATED CODE

Based on Olivier's answer this is what I have now. On the return for display() I'm getting the error when using .Where(filter)

The type arguments for method 'System.Linq.Enumerable.Where<TSource>(System.Collections.Generic.IEnumerable<TSource>, System.Func<TSource,bool>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

public class InstagramDisplay {

    public IEnumerable<InstagramUser> instagramUsers;
    public List<Expression<Func<InstagramUser, bool>>> instagramFilters; 

    public InstagramDisplay() {
        instagramUsers = new List<InstagramUser>();
        instagramFilters = new List<Expression<Func<InstagramUser, bool>>>();
    }

    public void addFilter(Expression<Func<InstagramUser, bool>> filter) {
        instagramFilters.Add(filter);
    }

    public IEnumerable<InstagramUser> display() {
        return instagramFilters.SelectMany(filter => instagramUsers.Where(filter)).Distinct(); //error on this line
    }
}
Brozak answered 23/8, 2012 at 19:26 Comment(0)
C
6

You have to decide whether you want to perfom some action or whether you want to return something. List<T>.ForEach() performs an action on each item but has a void return type and does not return anything.

This IInstagramFilter interface seems superfluous to me. You can declare a filter list like this

var userFilters = new List<Expression<Func<InstagramUser, bool>>>();
userFilters.Add(u => u.Name.StartsWith("A"));
userFilters.Add(u => u.Score >= 100);

If you have a source of users you can do something like this to all users returned by all filters

IQueryable<InstagramUser> usersSource = ...; 
// Or IEnumerable<InstagramUser> for LINQ to objects
// if you drop the Expression<> part.

var users = userFilters.SelectMany(f => usersSource.Where(f));

SelectMany flattens the nested enumerations. The example returns all users whos name starts with "A" or who have a score >= 100. This might return a user twice, therfore I would suggest to append a .Distinct()

var users = userFilters
    .SelectMany(f => usersSource.Where(f))
    .Distinct();
Conscription answered 23/8, 2012 at 20:11 Comment(7)
I agree that IInstagramFilter is unnecessary here. Instead of SelectMany couldn't he loop over the collection of filters, appending the .Where(filter) to userSource. This would prevent duplicate users, and also chain the filters together using "and" instead of "or"Ketubim
@MikeC: Chaining the filters together is a very good idea, since it makes the Distinct() superfluous. I am not sure if the OP wants them ORed or ANDed (from his example I assumed ORed). AND is easy (just chain Wheres, OR is more difficult.Conscription
@OlivierJacot-Descombes This is an excellent answer. I've posted my updated code above, but I'm still getting an error on the return statement for display()Brozak
C# is good in inferring type arguments, but in a few cases it fails to do so. Then you must specify them explicitly return instagramFilters.SelectMany<Expression<Func<InstagramUser, bool>>, InstagramUser>(filter => instagramUsers.Where(filter)).Distinct();.Conscription
@OlivierJacot-Descombes Do I have to use Expression<Func<>>? I'm getting an error when doing .Where(filter) but if I change instagramFilters to `List<Func<InstagramUser,bool>> then I can compile. Whether I it actually filters the list I'm unsure of yet. I thought I understood that expression trees had to be used when being passed to linq as a lambda, am I wrong about that?Brozak
When I change to Func<> it doesn't filter the collection.Brozak
It depends whether you have a IEnumerable<T> or a IQueryable<T> as source. IEnumerable<T> is used for LINQ-to-objects and requires a Func<T,bool>. You can apply it to collections. IQueryable<T> is used for LINQ-to-databases like Entity Framework or LINQ-to-SQL and requires Expression<Func<T,bool>> as parameter to Where.Conscription

© 2022 - 2024 — McMap. All rights reserved.