LINQ - The building blocks
When I was introduced to the C# programming language one of the aspects I really found interesting was LINQ. It was something about the readability and the ease of use of it all. Even non-technical people would be able to read this code and semi-understand what was going on.
var adults = customers.Where(customer => customer.Age > 18);
I decided to take a look at what building blocks of the C# language that would be combined for the LINQ syntax to work. Throughout this blog post we will implement a method that works similar to Where(), a method of LINQ that is used to filter a data source. One of the major benefits of LINQ is that it can be used to query different types of data sources such as objects or relational databases. For brevity, we will not address all the aspects of such an implementation. Instead we will make do with an implementation that can be used to filter an in-memory data source, and while doing so highlight some important features of the C# language itself.
Our aim is to implement a method, ThatHas(), which can be consumed as seen below.
var adults = customers.ThatHas(customer => customer.Age > 18);
Step 1 - Extension Methods
Traditionally, a method is called with input parameters like
var adults = ThatHas(customers, customer => customer.Age > 18);
which is not the syntax we are looking for. We want our method to be callable as if it was a member of the type itself, Customers.ThatHas(). C# extension methods to the rescue! Extension methods are used to add methods to existing types, hence the name.
To achieve this, we create a separate static class that we call extensions. We also add a method stub which is the starting point of our implementation.
public static class Extensions { public static List<Customer> ThatHas(this List<Customer> customers, //otherParamters) { // implementation } }
Take note of the this keyword of the first input parameter of the method above. This allows us to use our method the way we are looking for.
var adults = customers.ThatHas(...);
Note: Extension methods must be declared in a static context.
Step 2 - Lambdas and delegates
The consumer of our method will also have to pass in a predicate to filter the list of customers by. Of course, different scenarios requires different filters and our method needs to support that. A lamda expression is perfect for this scenario. The syntax for a lambda expression is as following:
// Syntax (input-parameters) => expression // Examples number => number + 2; customer => customer.Age > 18
Luckily for us, any lamba can be converted into a delegate type. Delegate types can be passed as input parameters to methods, which is exactly what we are looking for. Below we can see a lambda expression that is assigned to the Func delegate add2. The delegate accepts an integer and returns an integer, hence the Func<int, int> syntax.
Func<int, int> add2 = number => number + 2;
For our scenario we are looking for a lambda that accepts a customer and returns a boolean value of whether a condition has been met. If the lambda returns true, the customer will be included in the result returned by ThatHas().
Func<Customer,bool> isAdult = customer => customer.Age > 18;
We now have the building blocks to implement a first working example of ThatHas(). The method accepts a predicate, iterates the list of customers and adds each customer to the result only if the predicate returns true.
public static List<Customer> ThatHas(this List<Customer> customers, Func<Customer, bool> predicate) { var result = new List<Customer>(); foreach (var customer in customers) { if (predicate(customer)) { result.Add(customer); } } return result; }
ThatHas() can now be used like this
var adults = customers.ThatHas(customer => customer.Age > 18)
There are a lot of details to this step that is too elaborate for this blog post.Please visit Microsoft official documentation for details regarding lambdas, functions and delegates.
Step 3 - IEnumerable and iterator methods
Currently, our extension method only accepts a List of customers. However, we could easily support multiple collection types of customers (lists, sets, arrays etc.) by accepting an IEnumerable
Along the same lines, the method should not return a specific class implementation and we change the return type to IEnumerable as well. To return IEnumerable instead of List, we use the yield return keyword.
public static IEnumerable<Customer> ThatHas<Customer>( this IEnumerable<Customer> customers, Func<Customer, bool> predicate) { foreach(var customer in customers) { if(predicate(customer)) { yield return customer; } } }
These last changes have turned ThatHas() into an iterator method as it now returns a source for enumeration instead of a List. Consequently, it will no longer be possible to have items accessible by index. This is of no concern to us as our filtering logic does not require such feature. Should a consumer of ThatHas() require indexing they could enumerate in a number of ways, e.g. by utilizing .ToList() or .FirstOrDefault() on the IEnumerable
var adults = customers.ThatHas(x => x.Age> 18); var firstAdults = adults[0]; //Not possible // Use ToList() to create a List from IEnumerable var adults = customers.ThatHas(x => x.Age> 18).ToList(); var firstAdults = adults[0]; //Possible // Get first element on the IEnumerable if present var first = customers.ThatHas(x => x.Age> 18).FirstOrDefault();
Step 4 - Generics
Our implementation looks decent, but we still have one major problem. The method only works for class of type Customer, but we would like it to be applicable for all C# types as seen below.
new List<int>() { 1, 2, 3 }.ThatHas(number => number > 2); new List<string>() { "one", "two", "three" }.ThatHas(str => str.Length > 2); new List<Person>().ThatHas(person => person.Birthday.Year > 1980);
Generic methods allows us to do exactly that. A generic method is a method that is called with a given type parameter. In the following example we see how the generic IsTheSame()-method is callable by both int and string as type. Take notice of IsTheSame
public class Comparer { public bool IsTheSame<T>(T value1, T value2) { return value1.Equals(value2); } } var comparer = new Comparer(); // Called with int as type var numbersMatch = comparer.IsTheSame(1,2); // Called with string as type var stringsMatch = comparer.IsTheSame("string1","string2");
Like methods, classes can also be generic. As seen in the example below, IsTheSame()-method will have the type of its input parameters set by the type parameter of the class (see Comparer
public class Comparer<T> { public bool IsTheSame(T value1, T value2) { return value1.Equals(value2); } } // Called with int as type var numberComparer = new Comparer<int>(); // Called with string as type var stringComparer = new Comparer<string>(); numberComparer.IsTheSame(1, 2); stringComparer.IsTheSame("1", "2"); numberComparer.IsTheSame("1", "2"); //compile error stringComparer.IsTheSame(1, 2); //compile error
Finally, we update ThatHas() to be generic and we end up with our final implementation.
public static IEnumerable<TSource> ThatHas<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate) { foreach(var element in source) { if(predicate(element)) { yield return element; } } }
Closing
ThatHas() is now a generic iterator extension method which accepts a generic delegate as input parameter.
Lets close this post by comparing the method signatures of our ThatHas() and Where() from LINQ:
// ThatHas() public static IEnumerable<TSource> ThatHas<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate) // Where() public static IEnumerable<TSource> Where<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate) }
It seems that we now are at a point that resembles Where() quite a bit...