Table of Contents
In this post, we will have a brief introduction to LINQ
and how to perform small queries using it. Since LINQ
is very extensive, I decided to make an introductory post and in the future I will cover more specific topics.
1 - What is LINQ?
Around 2007, C# developers noticed a recurring pattern or problem where they wanted to access data by querying it but could not do so easily.
The reason it couldn't be done easily was because there were data in three different sources, such as:
- In-memory data collections: for which we needed different methods provided by Generics or various algorithms to get this data.
- Databases: for which we needed
ADO.NET
connectors and to write our ownSQL
. - XML files: To iterate over these, we needed to use
XmlDocument
orXpath
.
All these APIs/libraries had different functionalities and syntax, so Microsoft decided to introduce a language that would offer a unified syntax for all these functionalities. This is where Microsoft introduced the Language Integrated Query, or LINQ
.
LINQ
provides us with type checks in queries during compile time, but these queries are executed at runtime on in-memory data, against a database, or on XML. Moreover, once we understand LINQ, we'll see that we can use it against, for example, our PC's file system, a NoSQL database, CSV files, and much more.
2 - How to Use and Write LINQ
To illustrate this example, we'll use LINQ on in-memory data, in this case an Array[]
of the Libro
class. We could use a List<T>
like we saw in the chapter on arrays and lists but List<T>
is specific to the System.Linq
namespace that was specifically created for working with it after using IEnumerable
, which is the focus of this post.
public class Libro
{
public int Id { get; set; }
public string Titulo { get; set; }
public string Autor { get; set; }
public Libro(int id, string titulo, string autor)
{
Id = id;
Titulo = titulo;
Autor = autor;
}
}
Libro[] arrayLibros = new Libro[5];
arrayLibros[0] = new Libro(1, "Poeta en nueva york", "Federico García Lorca");
arrayLibros[1] = new Libro(2, "Los asesinos del emperador", "Santiago Posteguillo");
arrayLibros[2] = new Libro(3, "circo máximo", "Santiago Posteguillo");
arrayLibros[3] = new Libro(4, "La noche en que Frankenstein leyó el Quijote", "Santiago Posteguillo");
arrayLibros[4] = new Libro(5, "El origen perdido", "Matilde Asensi");
Before starting to use LINQ, you must have a clear understanding of what extension methods are and especially lambda expressions since LINQ is primarily built using these two technologies.
In addition to having one API that could query all data sources, another key goal was to make these queries easy to understand both for the person writing the query and for others coming later, whether to modify or review it.
The result was a language that is very easy to understand, as it uses extension methods written similarly to SQL expressions.
For example, the .Where()
clause in LINQ allows us to filter.
There are two options for doing this; both offer readable and simple syntax, and of course type checks at compile time.
First way to write LINQ queries:
public static void LinqQueryForma1(Libro[] arrayLibros, string autor)
{
var libros = from libro in arrayLibros
where libro.Autor == autor
select libro ;
}
As we can see, we have a method that takes an array
and a string
author, which we use to filter the list. If you know SQL, you'll notice that LINQ uses very similar syntax.
At the end of the query, we specify a select libro
clause, which returns a type IEnumerable<T>
where T
is Libro
.
If instead of returning libro we wanted to return only the title, T
would be a string
.
Second way to write LINQ queries
The second way is by using extension methods, chaining them together.
Replicating the previous example is very simple; we just need to write the following code:
public static void LinQueryForma2(Libro[] arrayLibros, string autor)
{
var libros = arrayLibros.Where(a => a.Autor == autor);
}
As we can see, .Where
is an extension method that takes a Func delegate as a parameter. It filters based on whether the condition is true or false, returning the filtered list.
Notice that we no longer include .Select
and that's because by default the code understands we want to select.
Besides .Where
, we can chain more actions. For example, if we want to sort by title, we can do it like this:
public static void LinQueryForma2(Libro[] arrayLibros, string autor)
{
var libros = arrayLibros.Where(a => a.Autor == autor).OrderBy(a=>a.Titulo);
}
Both options are equally valid; I personally prefer the second, but both allow us to perform queries that look like real queries and are easy to understand.
3 - The IEnumerable Interface
As we've just seen, when we make a LINQ query, the result comes in an IEnumerable
type.
This interface is the most important when using LINQ because the vast majority (98%) of extension methods we will use operate on IEnumerable<T>
.
Being a generic type, it allows us to specify what type we're going to insert into the collection.
3.1 - What Happens Behind the Scenes
The reason we can do a foreach
over the query result or over the array we created at the beginning is that both types implement the IEnumerable
interface, which has a method called GetEnumerator()
.
The GetEnumerator()
method of IEnumerable
comes directly from inheriting from IEnumerator
. And if we convert any of our elements, in my case arraylibros
, into IEnumerable<Libros>
we can access arrayLibros.GetEnumerator()
and once you have this enumerator, you can ask it to move to the next element using .MoveNext()
and then access it by the .Current
property, which contains a pointer to the element being iterated, so we can access its value.
IEnumerable<Libro> arrayLibros = new Libro[] {
new Libro(1, "Poeta en nueva york", "Federico García Lorca"),
new Libro(2, "Los asesinos del emperador", "Santiago Posteguillo"),
new Libro(3, "circo máximo", "Santiago Posteguillo"),
new Libro(4, "La noche en que Frankenstein leyó el Quijote", "Santiago Posteguillo"),
new Libro(5, "El origen perdido", "Matilde Asensi")
};
IEnumerator<Libro> enumeratorLibros = arrayLibros.GetEnumerator();
while (enumeratorLibros.MoveNext())
{
Console.WriteLine(enumeratorLibros.Current);
}
The good thing about using IEnumerable
is that it's an interface that can encompass an array, a list, or a database query, so it becomes completely transparent to the user, making it very easy to maintain and work with.
Before moving to the next point, it's important to emphasize that the IEnumerable
interface is "Lazy" (we'll see a post about this) which means it will not be executed until we actually need to execute it or obtain an element outside of IEnumerable
, for example, in a foreach
, or if we convert the entire IEnumerable
to a List<T>
.
4- Functionalities to Build Queries in LINQ
By default, LINQ brings us a wide range of methods to build our queries and in 99.9% of cases, they're more than sufficient. Later in this post, we'll see how to create a custom filter.
The functionalities available to build queries in LINQ are very similar to normal SQL and their names reflect this. In this post, I won't go into detail on all of them but I will cover the main ones.
The vast majority of extension methods in LINQ take a delegate as input, specifically Func<T, bool>
4.1 - Where in LINQ
The main and most important method is .Where, which allows us to filter using the parameters of our query. For example, as in the case we've seen, comparing the author's name.
var libros = arrayLibros.Where(a => a.Autor == autor);
When we use where, it returns a list, meaning it can have zero or more elements.
4.2 - Retrieving a Single Element with LINQ
To obtain a single element, we have several options. ALL of them take the Func<T, bool>
delegate as a parameter (like where) and return an element of type T
.
.First()
-> returns the first element.FristOrDefault()
- > returns the first element or one by default..Single()
-> if there is more than one element or none, it throws an exception; if there is one element, it returns it..SingleOrDefault()
-> if there is more than one element, it throws an exception, and if there is one or none, it returns the element or the default.
I could write an entire post (maybe I will) on when to use .Single
or .First
, the answer is neither, because .First
can return false positives if there's more than one element, and .Single
has to process the entire enumeration to make sure there's no other one.
It depends a bit on the scenario, but to get unique elements when we aren't certain they're a unique ID in the database, I recommend a combination of .Count == 1
and .First
. If count isn't 1, throw an exception.
4.3 - Last Element with LINQ
Similar to the above.
.Last()
-> returns the last element..LastOrDefault()
-> returns the last element or the default (empty) one.
4.4 - Sorting Elements in LINQ
To sort elements, we call the .OrderBy
or .OrderByDescending
method, which will sort in descending order.
Both methods have an additional overload where you can specify an IComparer
object to sort as you prefer.
The result we get is the sorted list as indicated:
var librosOrdenados = arrayLibros.Where(a => a.Autor == "Santiago Posteguillo").OrderBy(a=>a.Titulo);
4.5 - Grouping Elements in LINQ
To group elements, you need to use the .GroupBy()
method and the result will be an IEnumerable<Grouping<Key, T>>
list. In our case, we'll group by author, and we can iterate through the list, accessing each group. Key contains the author's name.
var agrupacion = arrayLibros.GroupBy(a => a.Autor);
foreach(var autorLibro in agrupacion)
{
Console.WriteLine(autorLibro.Key);
foreach(var libro in autorLibro)
{
Console.WriteLine(libro.Titulo);
}
}
5 - Creating a Filter for LINQ
Well, now we've seen the basics of LINQ, queries, filters, etc., but how it works underneath is very important. For that, let's create a filter ourselves.
The first thing we need to do is create a class and call it ExLinq
and inside this class, an extension method that uses IEnumerable<T>
along with an input parameter that will be a Func
delegate that will take T
and return a bool
.
If you've checked the IEnumerable
interface, you'll see that when the result returns multiple elements, the methods return IEnumerable<T>
, so we'll do the same here.
All we're going to do is loop through the initial list and compare it with the second one.
public static class ExLinq
{
public static IEnumerable<T> Filter <T> (this IEnumerable<T> source, Func<T, bool> predicado)
{
var result = new List<T>();
foreach(var item in source)
{
if (predicado(item))
{
result.Add(item);
}
}
return result;
}
}
But this is not the best way to implement filters in IEnumerable
as I mentioned before , the code is "Lazy" and will not execute until you actually need it.
C# provides us with the yield
keyword, which for those who've never seen it, is like peeking into the method. When you access the list via foreach
or ToList()
, the code says, wait, let's check this filter.
We change the code to the following:
public static class ExLinq
{
public static IEnumerable<T> Filtro<T> (this IEnumerable<T> source, Func<T, bool> predicado)
{
foreach(var item in source)
{
if (predicado(item))
{
yield return item;
}
}
}
}
This way we have created our custom filter and we can call it from the code:
IEnumerable <Libro> librosExtension = arrayLibros.Filtro(a => a.Autor == "Santiago Posteguillo");
If there is any problem you can add a comment bellow or contact me in the website's contact form