Search code examples
c#linqexpression-trees

Can this function be modified to handle multiple property comparisons?


For reference, here is the original question (Keep in mind that the Filter() function originates from this post): Dynamic Where for List<T>

Original post's function for clarity:

 public static List<T> Filter<T>
        (this List<T> source, string columnName, 
         string compValue)
    {
        ParameterExpression parameter = Expression.Parameter(typeof(T), "x");
        Expression property = Expression.Property(parameter, columnName);
        Expression constant = Expression.Constant(compValue);
        Expression equality = Expression.Equal(property, constant);
        Expression<Func<T, bool>> predicate =
            Expression.Lambda<Func<T, bool>>(equality, parameter);

        Func<T, bool> compiled = predicate.Compile();
        return source.Where(compiled).ToList();
    }

Which allows you to do this:

var people = new[] {
            new { FirstName = "John", LastName = "Smith" },
            new { FirstName = "John", LastName = "Smith" },
            new { FirstName = "John", LastName = "Noakes" },
            new { FirstName = "Linda", LastName = "Smith" },
            new { FirstName = "Richard", LastName = "Smith" },
            new { FirstName = "Richard", LastName = "Littlejohn" },
        }.ToList();

var filtered = people.Filter("LastName", "Smith");

But what if you have multiple properties you want to match on? For example if I wanted to filter for all people with FirstName = John and LastName = Smith

Pseduocode just to give the idea, I'm sure there's a more elegant way to code this:

var filtered = people.Filter(new [] {Column="FirstName", Value = "John"}, {Column="LastName", Value="Smith"});

Also curious if this could be easily adapted to handle any type for the property (i.e. object instead of always string like the example). I haven't worked with these expression trees much, I appreciate any help you can provide.


Solution

  • You can combine multiple expressions using the AndAlso extension found here. Then change your filter method to this:

    public static List<T> Filter<T>(this List<T> source, params (string columnName, string compValue)[] filters)
    {
        Expression<Func<T, bool>> exp = null;
        
        foreach (var filter in filters)
        {
            ParameterExpression parameter = Expression.Parameter(typeof(T), "x");
            Expression property = Expression.Property(parameter, filter.columnName);
            Expression constant = Expression.Constant(filter.compValue);
            Expression equality = Expression.Equal(property, constant);
            Expression<Func<T, bool>> predicate = Expression.Lambda<Func<T, bool>>(equality, parameter);
            
            exp = exp is null ? predicate : exp.AndAlso(predicate);
        }
    
        Func<T, bool> compiled = exp.Compile();
        return source.Where(compiled).ToList();
    }
    

    And call it like this:

    var results = people.Filter(("FirstName", "John"), ("LastName", "Smith"));
    

    This was very quickly written so you may prefer not to use a list of tuples.