Thinking With Properties: Examining Where
Note: This post is for C# Adevent Calendar 2020 organized by Matthew Groves. Check out some of the other psots that are happening over the month of December!
What Do We Mean By Properties?
When I think about software, I will generally think about the properties that the solution has to have
Where properties are the characteristics that the code has. Sometimes, the property is obvious (for example, if you square a number, the result should always be positive). In this post, we’re going to look at LINQ’s
Where method, examine some of the properties, and come up with a more efficient way of creating filters.
For those not familiar with
Where, it’s a method that promises that it will return a subset of a list
Where each item fulfills some criteria (referred to as a predicate). The type signature for
IEnumerable<T> => Func<T, bool> => IEnumerable<T>
At face value, this sounds pretty straightforward, but there are a few more properties that
Where provides that aren’t obvious at first, but are beneficial
- The results can’t be null (worse case, it’ll be an empty list because no item fulfilled the criteria)
- The results can’t be larger than the original list
- The results are in the same order that the original list was in (i.e. if we’re trying to find the even numbers in a list that is 1..10, then you’re guaranteed to get 2, 4, 6, 8, and 10. Not, 8, 2, 6, 10, 4)
- The results will only contain elements that were in the original list (i.e. it can’t create elements out of thin air and it can’t copy elements in the original list)
Common LINQ Mistake with
That’s a ton of guarantees that you get from leveraging
Where instead of doing your own filtering inside loops. With these properties in mind, let’s take a look at a common mistake that developers make when working with
By leveraging multiple Where statements, the list will be iterated once per statement. which may not be a big deal for small lists, but for larger lists, this will become a performance hit. In order to help cut down on the iterations, it’d be ideal to combine the multiple Where statements into a single one like so
By combining the criteria in a single Where statement, we eliminate the multiple iteration problem, however, we introduce code that’s a bit harder to read and if we want to combine a non-fixed number of predicates, then this approach won’t work.
Since the goal is to take multiple predicates and combine them to a single predicate, my intuition is to leverage LINQ’s Aggregate method where we can take a List of items and reduce down to a single item.
Refactoring Multiple Where with Aggregate
In order to leverage Aggregate, we’ll first need to have a list of item to reduce down. Since all of the predicates are Func
Now that we have a list of predicates, we can go ahead and start stubbing out the Aggregate call.
In order to use Aggregate, we need to determine two pieces of information. First, what should the predicate be if there are no predicates to combine? Second, how do we we combine two predicates into a single predicate?
Defining the Base Case
When using Aggregate, the first thing that we need to think about is the base case, or in other words, what should the default value be if there are no elements to reduce down?
Given that the result needs to be a predicate, we know that the type should be
Func<int, bool>, but how do we implement that? We’ve got one of two choices for the base case, we can either filter every item out (i.e. if no predicates are specified, then no items are kept) or we keep every item.
For our use case, we want to keep every item if there are no predicates, so our base case looks like the following
Defining How To Combine Predicates
Since we’re combining predicates, our combine function will need to have the following type
Func<int, bool> => Func<int, bool> => Func<int, bool>
With this in mind, we know that for an item to be valid, it has to match every predicate in the list which implies that we’ll be leveraging the
Now that we know to use
&&, we can then use a and b to determine if the item is valid
Bringing It All Together
With the base case established and a way to combine predicates, here’s how we can solve the original problem.
Going forward, if we need to add more predicates, all we need to do is to add it to the
List and the rest of the application will work as expected
In this post, we explored LINQ’s
Where method by examining its various properties. From there, we took a look at a common mistake developers make with
Where and then showed how to resolve that issue by using
Shout out to Matthew Groves for letting me participate in C# Christmas (csadvent.christmas)