2

In this example I have a list of People with some random data that are being filtered by a number of options.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

public class Program
{
    public static void Main()
    {
        var people = GetPeople();       
        ConsolePeople(GetPeopleFiltered(GetFilters(new FilterRequest {Male = true}), people));
        ConsolePeople(GetPeopleFiltered(GetFilters(new FilterRequest {Female= true}), people));
        ConsolePeople(GetPeopleFiltered(GetFilters(new FilterRequest {Male = true, TwentyToThirty = true}),people));
        ConsolePeople(GetPeopleFiltered(GetFilters(new FilterRequest {Male = true, Female=true, TwentyToThirty = true}),people));
    }

    public static void ConsolePeople(List<Person> people)
    {
        if(people.Count == 0)
            Console.WriteLine("No people found");
        foreach(var person in people)
        {
            Console.WriteLine(string.Format("FirstName: {0}, LastName: {1}, Age: {2}, Gender: {3}", person.FirstName, person.LastName, person.Age, person.Gender.ToString()));
        }
        Console.WriteLine(string.Empty);
    }

    public static List<Person> GetPeople()
    {
        var people = new List<Person>();
        people.Add(new Person { FirstName = "Philip", LastName = "Smith", Age = 29, Gender = GenderEnum.Male});
        people.Add(new Person { FirstName = "Joe", LastName = "Blogs", Age = 40, Gender = GenderEnum.Male});        
        people.Add(new Person { FirstName = "Mary", LastName = "Ann", Age = 10, Gender = GenderEnum.Female});
        people.Add(new Person { FirstName = "Lisa", LastName = "Dunn", Age = 60, Gender = GenderEnum.Male});
        people.Add(new Person { FirstName = "Terry", LastName = "Banks", Age = 89, Gender = GenderEnum.Male});
        people.Add(new Person { FirstName = "John", LastName = "Doe", Age = 32, Gender = GenderEnum.Male});
        people.Add(new Person { FirstName = "Sally", LastName = "Shields", Age = 19, Gender = GenderEnum.Female});
        return people;
    }

    public static List<Expression<Func<Person, bool>>> GetFilters(FilterRequest request)
    {
        var filters = new List<Expression<Func<Person, bool>>>();
        if(request.Male)
            filters.Add(x=>x.Gender == GenderEnum.Male);
        if(request.Female)
            filters.Add(x=>x.Gender == GenderEnum.Female);
        if(request.TentoTwenty)
            filters.Add(x=>x.Age >= 10 && x.Age < 20);
        if(request.TwentyToThirty)
            filters.Add(x=>x.Age >= 20 && x.Age < 30);
        if(request.ThirtyToFourty)
            filters.Add(x=>x.Age >= 30 && x.Age < 40);
        if(request.FourtyPlus)
            filters.Add(x=>x.Age >= 40);
        return filters;
    }

    public static List<Person> GetPeopleFiltered(List<Expression<Func<Person,bool>>> filters, List<Person> people)
    {
        var query = people.AsQueryable();
        foreach(var filter in filters)
        {
            query = query.Where(filter);
        }
        return query.ToList();
    }
}

public class FilterRequest
{
    public bool Male {get;set;}
    public bool Female {get;set;}
    public bool TentoTwenty {get;set;}
    public bool TwentyToThirty {get;set;}
    public bool ThirtyToFourty {get;set;}
    public bool FourtyPlus {get;set;}
}

public class Person
{
    public string FirstName {get;set;}
    public string LastName {get;set;}
    public int Age {get;set;}
    public GenderEnum Gender {get;set;}
}

public enum GenderEnum
{
    Male,
    Female
}

You can see this at DotNetFiddle

I want my List<Expression<Func<Person, bool>>> to become a list of || clauses in certain situations. So in this example if you have both male and female selected and an age range then I would expect

(x.Gender == GenderEnum.Male || x.Gender == GenderEnum.Female) 
&& ((x.Age > 10 && x.Age < 20) || (x.Age >= 20 && x.Age < 30))

How do I achieve this? I know the example could be reworked differently but it is just an example.

Note: the real piece of code will be working against several millions rows of information so it should be fairly optimized.

Philiop
  • 473
  • 3
  • 14
  • You should take a look at predicate builder for things like that : http://www.albahari.com/nutshell/predicatebuilder.aspx – Raphaël Althaus Apr 10 '15 at 15:43
  • Id suggest to write function like that: `static IEnumerable MyWhere(IEnumerable dataSource, Func predicate)` Now, you're able to pass parameters this way: `var qry = MyWhere(Persons, p => p.FirstName.Contains("ee") || p.LastName.Contains("ic") || p.Age > 21)` – Maciej Los Apr 10 '15 at 15:52
  • That example doesn't make it very clear but doing that would result in a massive amount code working out every possibility of || . For a bit of reference this is supposed to convert a SearchRequest from a MVC view into a return of data. The structure to the search isn't very friendly (client decided on this and no amount of complaining will budge them) which results in a number of bool's that represent multiple search terms. – Philiop Apr 10 '15 at 16:01
  • This was a duplicate and the answer given in the other post does work. The example I have given working is this https://dotnetfiddle.net/4mumPD it can be tidied up obviously to be more generic. Answer taken from http://www.albahari.com/nutshell/predicatebuilder.aspx – Philiop Apr 13 '15 at 09:02

2 Answers2

3

Here is an implementation of a PredicateBuilder that is able to Or two expressions together:

public static class PredicateBuilder
{
    public static Expression<Func<T, bool>> True<T>() { return f => true; }
    public static Expression<Func<T, bool>> False<T>() { return f => false; }

    public static Expression<Func<T, bool>> Or<T>(
        this Expression<Func<T, bool>> expr1,
        Expression<Func<T, bool>> expr2)
    {
        var secondBody = expr2.Body.Replace(
            expr2.Parameters[0], expr1.Parameters[0]);
        return Expression.Lambda<Func<T, bool>>
              (Expression.OrElse(expr1.Body, secondBody), expr1.Parameters);
    }

    public static Expression<Func<T, bool>> And<T>(
        this Expression<Func<T, bool>> expr1,
        Expression<Func<T, bool>> expr2)
    {
        var secondBody = expr2.Body.Replace(
            expr2.Parameters[0], expr1.Parameters[0]);
        return Expression.Lambda<Func<T, bool>>
              (Expression.AndAlso(expr1.Body, secondBody), expr1.Parameters);
    }
}

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}
internal class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

This allows you to write:

var predicate = listOfPredicateExpressions.Aggregate(PredicateBuilder.Or);
Servy
  • 202,030
  • 26
  • 332
  • 449
-1

What you will want to do is group the filters of the same category. For example two gender filters should be or'ed, while a gender filter and an age filter should be and'ed. Therefore you will need to replace the method that returns the filter to return an enumerable of enumerables of filters. Each enumerable represents a category of filters you can have.

In the example you gave above, you will get an object that looks something like this:

{
    {
        x => x.Gender == GenderEnum.Male,
        x => x.Gender == GenderEnum.Female
    },
    {
        x => x.Age >= 10 && x.Age < 20,
        x => x.Age >= 20 && x.Age < 30
    }
}

Your query will then change into the following method:

public static List<Person> GetPeopleFiltered(IEnumerable<IEnumerable<Func<Person,bool>>> filterCategories, List<Person> people)
{
    var query = people;
    foreach(var filterCat in filterCategories)
    {
        query = query.Where(x => filterCat.Any(f => f(x)));
    }
    return query.ToList();
}

Or, getting rid of the outer foreach loop as well:

public static List<Person> GetPeopleFiltered(IEnumerable<IEnumerable<Func<Person,bool>>> filterCategories, List<Person> people)
{
    return people.Where(x => filterCategories.All(cat => cat.Any(f => f(x)))).ToList();
}

The Any method iterates through all elements of a set and if it encounters an element that yields true, it returns true. All does the same, but returns false if it enounters an element that yields false.

Tom
  • 532
  • 5
  • 19
  • I've added that to the example https://dotnetfiddle.net/Z1UqBo and I can't seem to get it to work. just complains that f is used like a method – Philiop Apr 10 '15 at 15:59
  • My apologies. If you take the approach I proposed, you will have to use bare `Func`'s instead of wrapping them in `Expression`s (in which case casting the data to queryable is also not really necessary any more). I have altered my examples to take that into account. If you still want to use `Expressions`, you will have to first iterate through all filter categories, turning them in one expression per category (so grouping all the things that are or'ed in one expression), and then take the previous approach. – Tom Apr 10 '15 at 16:22