9

I have a long Linq Where clause that I would like to populate with a predicate list.

List<Expression<Func<Note, bool>>> filters = new List<Expression<Func<Note, bool>>>();

filters.Add(p => p.Title != null && p.Title.ToLower().Contains(searchString));
filters.Add(p => p.Notes != null && p.Notes.ToLower().Contains(searchString));
filters.Add(GlobalSearchUser((List < User > users = new List<User>() { p.user1, p.user2, p.user3, p.user4 }), searchString));

notes = dataAccess.GetList<Note>(pn => pn.ProjectVersionID == projectVersionID, filterExtensions.ToArray())
      .Where(filters.ToArray()).Take(10).ToList();

However I'm getting this error:

cannot convert from 'System.Linq.Expressions.Expression<System.Func<project.Contracts.DTOs.Note,bool>>[]' to 'System.Func<project.Contracts.DTOs.Note,bool>'

Which is an error on the .where clause. Pulling out the .where compiles just fine.

PiotrWolkowski
  • 8,408
  • 6
  • 48
  • 68
user3754124
  • 240
  • 2
  • 4
  • 13

4 Answers4

11

I think great answer from Hogan can be simplified and shorten a bit by use of Any and All Linq methods.

To get items that fulfill all the conditions:

var resultAll = listOfItems.Where(p => filters.All(f => f(p)));

And to get the items that fulfill any condition:

var resultAny = listOfItems.Where(p => filters.Any(f => f(p)));
PiotrWolkowski
  • 8,408
  • 6
  • 48
  • 68
  • This technique evaluates one condition per time, not all at same time. If you need evaluate all filters together, look at PredicateBuilder – Fer R Mar 25 '20 at 17:57
8

There are at least two errors in your code:

List<Expression<Func<Note, bool>>> filters = new List<Expression<Func<Note, bool>>>();

change it to

List<Func<Note, bool>> filters = new List<Func<Note, bool>>();

You don't need Expression trees here. You are using IEnumerable<>, not IQueryable<>

notes = dataAccess.GetList<Note>(pn => pn.ProjectVersionID == projectVersionID, filterExtensions.ToArray())
  .Where(filters.ToArray()).Take(10).ToList();

There .Where() accepts a single predicate at a time. You could:

notes = dataAccess.GetList<Note>(pn => pn.ProjectVersionID == projectVersionID, filterExtensions.ToArray())
  .Where(x => filters.All(x)).Take(10).ToList();

or various other solutions, like:

var notesEnu = dataAccess.GetList<Note>(pn => pn.ProjectVersionID == projectVersionID, filterExtensions.ToArray())
              .AsEnumerable();

foreach (var filter in filters)
{
    notesEmu = notesEmu.Where(filter);
}

notes = notesEnu.Take(10).ToList();

Because all the .Where() conditions are implicitly in &&.

xanatos
  • 109,618
  • 12
  • 197
  • 280
4

You have to loop over your filters and run a test on each one.

You can do it with linq like this to return true if any of your filters are true:

.Where(p => { foreach(f in filters) if (f(p) == true) return(true); return(false)}) 

or like this to to return true if all of your filters are true:

.Where(p => { foreach(f in filters) if (f(p) == false) return(false); return(true)}) 
Hogan
  • 69,564
  • 10
  • 76
  • 117
  • Add something about the fact that he is using `List – xanatos Apr 29 '15 at 20:50
  • @xanatos yeah I added some comments :) – Hogan Apr 29 '15 at 20:51
  • May be better served using `.Aggregate()` as well – moarboilerplate Apr 29 '15 at 20:51
  • @moarboilerplate - I've no idea how Aggregate() would help. – Hogan Apr 29 '15 at 20:53
  • `.Aggregate(GetList()..., (list, func) => list.Where(func));` progressively filtering the list – moarboilerplate Apr 29 '15 at 20:58
  • @moarboilerplate - that would involve re-writing the entire data access framework -- seems like a lot of work to use `.Aggregate()`. I do agree that `.Aggregate()` is cool, but I don't think I would go that far. – Hogan Apr 29 '15 at 20:59
  • Good point since it needs to go through a query provider and not just linq to objects. I should mention you can streamline your answer into one example by simply replacing the conditionals and booleans with `return f(p);` – moarboilerplate Apr 29 '15 at 21:02
  • @moarboilerplate - no that replace won't work, the if statement is inside the foreach loop. – Hogan Apr 29 '15 at 21:05
  • I know what I was thinking. `.Where(p => filters.Aggregate(true, (result, func) => result && func(p));` which shouldn't cause the translation issue. Your foreach is more performant, though, because it breaks out of the loop instead of running all the filters. – moarboilerplate Apr 29 '15 at 21:16
  • @moarboilerplate - oh yeah that works. It has a higher coolness factor because it uses Aggregate, but I agree it will be slower. I could be the basis for a solution that requires all filters to be run. – Hogan Apr 30 '15 at 13:51
0

You can't just pass an array of predicates to the where method. You need to either iterate over the array and keep calling Where() for each expression in the array, or find a way to merge them all together into one expression and use that. You'll want to use LinqKit if you go the second route.

moarboilerplate
  • 1,633
  • 9
  • 23