LINQ en C#

En este post vamos a ver una pequeña introducción a LINQ y como realizar pequeñas consultas con el mismo, debido a que LINQ es muy amplio, he decidido realizar un post introductorio y en el futuro iré a cosas más específicas. 

 

1 - Qué es LINQ ?

Más o menos en 2007 los desarrolladores de C# vieron que tenían cierto patrón, o problema, donde querían acceder a datos haciendo consultas sobre los mismos y no podían hacerlo de una forma sencilla.

El motivo por el que no se podía hacer de una forma sencilla era porque, había datos en 3 fuentes diferentes, Como son.

  • Las colecciones de datos en memoria: para las que necesitamos los diferentes métodos aportados por Generics o diferentes algorithmos para conseguir estos datos.
  • Bases de datos: para el que necesitabamos conectores de ADO.NET y escribir nuestro propio SQL.
  • Ficheros XML: Para poder iterar sobre los mismos, necesitábamos utilizar XmlDocument o Xpath.

c# antes de linq

Todas estas APIs/librerías tienen diferentes funcionalidades y sintaxis, entonces microsoft decidió introducir un lenguaje que ofreciera una sintaxis única para todas estas funcionalidades. Y aquí es donde microsoft introdujo Language Integrated Query o LINQ.

poder de linq

LINQ nos proporciona comprobaciones de tipo en consultas durante la compilación, pero estas consultas van a ser ejecutadas en tiempo de ejecución sobre datos en memoria, contra una base de datos o en xml. Pero además una vez comprendamos LINQ veremos que podemos utilizarlo contra por ejemplo el sistema de archivos de nuestro pc, una base de datos no relacional, ficheros CSV, y mucho más.

 

2 - Cómo Utilizar y escribir LINQ

Para realizar este ejemplo, utilizaremos LINQ sobre datos en memoria, en este caso un Array[] de la clase Libro. Podríamos utilizar una Lista<T> como vimos en el capítulo de arrays y listas pero, List<T> es específico del namespace System.Linq que fue específicamente creado para trabajar con él después de utilizar IEnumerable, el cual es el centro de este 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");

Antes de empezar a utilizar LINQ hay que tener muy claro y comprender que son los extension methods y sobre todo las expresiones lambda ya que LINQ esta construido a traves de estas dos tecnologias principalemnete. 

Además de hacer una sola API que pudiera consultar todas las fuentes de datos otro objetivo muy importante era hacer que estas consultas fueran fáciles de entender tanto para la persona que escribe la query, como para los que vienen después ya sea a modificarla como para hacer la review.

Lo que se consiguió fue un lenguaje muy sencillo de entender ya que utiliza extension methods que son declarados de una forma similar a las expresiones SQL.

Por ejemplo, la cláusula .Where() dentro de LINQ nos permite filtrar. 

Para realizar esta acción, tenemos dos opciones, ambas nos otorgan una sintaxis entendible y sencilla y por supuesto comprobación de los tipos en tiempo de compilación. 

Forma numero 1 de escribir consultas LINQ:
public static void LinqQueryForma1(Libro[] arrayLibros, string autor)
{
    var libros = from libro in arrayLibros
                where libro.Autor == autor
                select libro ;
} 

Como vemos disponemos de un método que recibe un array y un string autor, el cual lo utilizaremos para filtrar la lista. Si sabes SQL puedes comprobar que LINQ utiliza una sintaxis muy similar.

Al final de la consulta, indicamos una cláusula select libro la cual nos va a devolver un tipo IEnumerable<T> donde T es Libro.

Si en vez de devolver libro, quisiéramos devolver solo el título, T sería un string.

Forma numero 2 de escribir consultas LINQ

La segunda forma es utilizando extension methods llamándolos uno detrás de otro.

Replicar el ejemplo anterior es muy sencillo, únicamente tenemos que indicar el siguiente código:

public static void LinQueryForma2(Libro[] arrayLibros, string autor)
{
    var libros = arrayLibros.Where(a => a.Autor == autor);
}

Como vemos .Where es un extension method, el cual acepta un delegado Func por parametro. El cual filtrará cuando sea verdadero y falso, devolviendo la lista filtrada. 

Como podemos observar ya no incluimos el .Select y es porque por defecto el código entiende que queremos hacer un select. 

además de un .Where podemos concatenar más acciones, si por ejemplo queremos ordenar por el título podemos hacerlo de la siguiente manera: 

public static void LinQueryForma2(Libro[] arrayLibros, string autor)
{
    var libros = arrayLibros.Where(a => a.Autor == autor).OrderBy(a=>a.Titulo);
}

Ambas opciones son igual de válidas, personalmente prefiero la segunda, pero ambas nos permiten realizar consultas que lucen como consultas reales y fáciles de entender.

 

3 - La interfaz IEnumerable

Como acabamos de ver, cuando hacemos una consulta LINQ el resultado nos viene en un tipo IEnumerable

Esta interfaz es la más importante cuando estamos utilizando LINQ ya que la gran mayoría (el 98%) de extension methods que vamos a utilizar lo realizan sobre IEnumerable<T>.

Como es un tipo genérico, nos permite indicar qué tipo vamos a introducir en la colección. 

3.1 - Qué hace por detrás 

El motivo por el que podemos hacer un foreach sobre el resultado de la query o el motivo por el que podemos hacer un foreach sobre el array que hemos creado al principio, es debido a que ambos tipos implementan la interfaz IEnumerable, que tiene un método llamado GetEnumerator().

El método GetEnumerator() de  IEnumerable viene directamente de heredar IEnumerator. Y si convertimos cualquiera de nuestros elementos, en mi caso arraylibros en IEnumerable<Libros> puedo acceder a arrayLibros.GetEnumerator() y una vez tienes este enumerador, puedes pedir al enumerador que se mueva al siguiente elemento utilizando .MoveNext() y podremos acceder a el con la propiedad .Current que contiene un puntero al elemento que estamos iterando, con lo que podemos acceder a su valor. 

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);
}

Lo bueno de utilizar IEnumerable es que es una interfaz que puede englobar, un array, una lista, o una consulta a una base de datos, por lo que queda totalmente transparente al usuario lo que significa que es muy fácil de mantener y trabajar con ello. 

Antes de pasar al siguiente punto, es importante remarcar que la interfaz IEnumerable es “Lazy” (ya veremos un post) lo que significa que no va a ser ejecutada hasta que necesitamos su ejecución u obtener un elemento fuera del IEnumerable, por ejemplo, en un foreach, o si convertimos todo el IEnumerable en una List<T>.

 

4- Funcionalidades para construir queries en LINQ

Por defecto LINQ nos trae una gran variedad de métodos para construir nuestras consultas, y en el 99.9% de los casos, serán más que suficientes, más adelante en este post veremos cómo crear un filtro personalizado.

Las funcionalidades de las que disponemos para construir queries en LINQ son muy similares al SQL normal y así son sus nombres, en este post no voy a entrar en detalle en todas ellas pero si en las principales.

La gran mayoría de extension methods en LINQ utilizan un delegado como parámetro de entrada que es Func<T, bool> 

4.1 - Where en LINQ

El principal y más importante es el método .Where  el cual nos permite filtrar utilizando los parámetros de nuestra consulta. por ejemplo como el caso que hemos visto, Comparando el nombre. 

var libros = arrayLibros.Where(a => a.Autor == autor);

Cuando utilizamos where nos devuelve una lista, esto quiere decir que tendrá 0 o más elementos. 

4.2 - Obtención de un único elemento con LINQ

Para obtener un único elemento tenemos varias opciones, TODAS ellas, reciben como parámetro el delegado Func<T, bool> (igual que el where) y devuelven un elemento del tipo T

  • .First() -> devvuelve el primer elemento
  • .FristOrDefault() - > devuelve el primer elemento o uno por defecto. 
  • .Single() -> si hay más de un elemento o no hay ninguno, devuelve una excepción, si hay un elemento lo devuelve.
  • .SingleOrDefault() -> en caso de haber más de un elemento salta una excepción y si hay solo uno o ningún devuelve el elemento o uno por defecto. 

Podría hacer un post entero, quizá lo haga, sobre que utilizar si .Single, o .First, la respuesta es que ninguno, ya que .First puede devolver falsos positivos si hay más de un elemento y .Single necesita leer toda la enumeración para comprobar que no hay ninguno más. 

Depende un poco del escenario, pero para obtener elementos únicos que no sabemos a ciencia cierta que son un ID único en la base de datos recomiendo una combinación entre .Count == 1 y .First. haciendo que si count no es 1 salte una excepción. 

4.3 - Último elemento con LINQ

SImilar al anterior. 

  • .Last() -> devuelve el último elemento.
  • .LastOrDefault() -> devuelve el último elemento o uno por defecto (vacío).

4.4 - Ordenar elementos en LINQ

Para ordenar elementos debemos llamar al método .OrderBy u .OrderByDescending el cual ordenará de forma descendente. 

Ambos métodos contienen un método adicional en el que puedes indicar un objeto del tipo IComparer para poder ordenar a tu gusto.

El resultado que obtendremos es la propia lista ordenada como hemos indicado:

var librosOrdenados = arrayLibros.Where(a => a.Autor == "Santiago Posteguillo").OrderBy(a=>a.Titulo);

 

4.5 - Agrupar elementos en LINQ

Para agrupar elementos debemos utilizar el método .GroupBy() y el resultado será una lista IEnumerable<Grouping<Key, T>> en nuestro caso, agrupamos por autor, y podemos iterar sobre la lista, accediendo al grupo, key contiene el nombre del autor del libro. 

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 - Creación de un filtro para LINQ

bueno ahora hemos visto lo básico de LINQ, consultas, filtros etc. pero el cómo funcionan por detrás es muy importante. para ello vamos a crear un filtro nosotros.

Lo primero que tenemos que hacer es una clase y la llamaremos ExLinq y dentro de esta clase un extension metohd que utiliza IEnumerable<T>, junto con un parámetro de entrada el cual será un delegado Func el cual contendrá T  y devolverá un bool

Si hemos consultado la interfaz IEnumerable, hemos podido observar que cuando el resultado devuelve varios elementos los métodos devuelven IEnumerable<T> con lo que aquí realizaremos lo mismo. 

Únicamente vamos a hacer un bucle sobre la lista inicial y compararla con la segunda.

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;
    }
}

Pero esta no es la forma más correcta de realizar filtros en IEnumeable como he comentado antes, el código es “Lazy” y no se va  a ejecutar hasta que lo necesitemos de verdad. 

Para ello C# nos provee de la palabra clave yield la cual para todos aquellos que no la hayan visto nunca, es algo similar a echar el ojo en el método, cuando accedemos a la lista ya sea con un foreach o un ToList(), el código dirá, espera, que hay que comprobar este filtro. 

Cambiamos el código a la siguiente manera:

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;
            }
        }
    }
}

De esta forma tenemos creado nuestro filtro personalizado y podemos llamarlo desde el código: 

IEnumerable <Libro> librosExtension = arrayLibros.Filtro(a => a.Autor == "Santiago Posteguillo");