Módulo 16: LINQ

Objetivos del módulo

  • Comprender qué es LINQ y por qué es tan potente
  • Dominar la sintaxis de métodos y la sintaxis de consulta
  • Aplicar filtros, ordenación, agrupación, proyección y agregación
  • Usar LINQ con colecciones de objetos

1. ¿Qué es LINQ?

📘 Concepto: LINQ (Language Integrated Query) permite consultar y transformar datos de cualquier fuente (colecciones, bases de datos, XML, JSON) usando una sintaxis integrada en C#. Es como tener SQL dentro de C#.

List<int> numeros = new() { 8, 3, 15, 1, 42, 7, 23, 5, 19, 2 };

// Sin LINQ: mucho código manual
List<int> paresOrdenados = new();
foreach (int n in numeros)
{
    if (n % 2 == 0) paresOrdenados.Add(n);
}
paresOrdenados.Sort();

// Con LINQ: una línea clara y expresiva
var resultado = numeros.Where(n => n % 2 == 0).OrderBy(n => n).ToList();
// [2, 8, 42]

2. Dos sintaxis de LINQ

Sintaxis de métodos (fluent): la más usada

var resultado = numeros
    .Where(n => n > 5)
    .OrderBy(n => n)
    .Select(n => n * 2)
    .ToList();

Sintaxis de consulta (query): similar a SQL

var resultado = (from n in numeros
                 where n > 5
                 orderby n
                 select n * 2).ToList();

💡 Consejo: Ambas sintaxis producen el mismo resultado. La sintaxis de métodos es más común en la práctica. Usaremos ambas en los ejemplos.


3. Operaciones LINQ fundamentales

Trabajaremos con esta lista de ejemplo:

record Alumno(string Nombre, int Edad, double Nota, string Ciudad);

List<Alumno> alumnos = new()
{
    new("Ana", 22, 8.5, "Madrid"),
    new("Luis", 25, 6.0, "Barcelona"),
    new("María", 20, 9.2, "Madrid"),
    new("Pedro", 23, 4.5, "Sevilla"),
    new("Carmen", 21, 7.8, "Barcelona"),
    new("Javier", 24, 5.0, "Madrid"),
    new("Laura", 22, 9.5, "Valencia"),
    new("Carlos", 26, 3.2, "Sevilla"),
    new("Elena", 21, 8.0, "Valencia"),
    new("Diego", 23, 6.5, "Madrid")
};

Filtrar: Where

// Alumnos aprobados (nota >= 5)
var aprobados = alumnos.Where(a => a.Nota >= 5);

foreach (var a in aprobados)
    Console.WriteLine($"{a.Nombre}: {a.Nota}");

// Filtros múltiples
var madridAprobados = alumnos
    .Where(a => a.Ciudad == "Madrid" && a.Nota >= 5);

Ordenar: OrderBy, OrderByDescending, ThenBy

// Ordenar por nota descendente
var porNota = alumnos.OrderByDescending(a => a.Nota);

// Ordenar por ciudad, y dentro de cada ciudad por nota descendente
var porCiudadYNota = alumnos
    .OrderBy(a => a.Ciudad)
    .ThenByDescending(a => a.Nota);

Proyectar: Select

// Obtener solo los nombres
List<string> nombres = alumnos.Select(a => a.Nombre).ToList();

// Crear un nuevo tipo anónimo con datos calculados
var resumen = alumnos.Select(a => new
{
    a.Nombre,
    a.Nota,
    Estado = a.Nota >= 5 ? "Aprobado" : "Suspendido",
    NotaSobre10 = $"{a.Nota}/10"
});

foreach (var r in resumen)
    Console.WriteLine($"{r.Nombre}: {r.NotaSobre10} ({r.Estado})");

Tomar y saltar: Take, Skip

// Top 3 mejores notas
var top3 = alumnos
    .OrderByDescending(a => a.Nota)
    .Take(3);

// Paginación: página 2 de tamaño 3
int pagina = 2;
int tamano = 3;
var pagina2 = alumnos
    .Skip((pagina - 1) * tamano)
    .Take(tamano);

Primero, último, único

// First / FirstOrDefault
Alumno primero = alumnos.First();                     // Primer elemento (excepción si vacío)
Alumno? primeroMadrid = alumnos.FirstOrDefault(a => a.Ciudad == "Madrid");  // null si no hay

// Last / LastOrDefault
Alumno ultimo = alumnos.Last();

// Single / SingleOrDefault (exactamente uno)
Alumno? unico = alumnos.SingleOrDefault(a => a.Nombre == "Ana");
// Excepción si hay más de uno o ninguno

// Any: ¿hay alguno que cumple?
bool hayAprobados = alumnos.Any(a => a.Nota >= 5);  // True

// All: ¿todos cumplen?
bool todosAprobados = alumnos.All(a => a.Nota >= 5);  // False

// Count
int totalAprobados = alumnos.Count(a => a.Nota >= 5);  // 8

4. Agregación

// Estadísticas
double notaMaxima = alumnos.Max(a => a.Nota);       // 9.5
double notaMinima = alumnos.Min(a => a.Nota);       // 3.2
double notaMedia = alumnos.Average(a => a.Nota);     // 6.82
double sumaNotas = alumnos.Sum(a => a.Nota);         // 68.2
int total = alumnos.Count();                          // 10

// MaxBy / MinBy: obtener el OBJETO con el valor máximo/mínimo
Alumno mejor = alumnos.MaxBy(a => a.Nota)!;           // Laura, 9.5
Alumno peor = alumnos.MinBy(a => a.Nota)!;            // Carlos, 3.2

Console.WriteLine($"Mejor alumno: {mejor.Nombre} ({mejor.Nota})");
Console.WriteLine($"Media: {notaMedia:F2}");

5. Agrupar: GroupBy

// Agrupar por ciudad
var porCiudad = alumnos.GroupBy(a => a.Ciudad);

foreach (var grupo in porCiudad)
{
    Console.WriteLine($"\n{grupo.Key} ({grupo.Count()} alumnos):");
    foreach (var a in grupo)
        Console.WriteLine($"  {a.Nombre}: {a.Nota}");

    Console.WriteLine($"  Media: {grupo.Average(a => a.Nota):F2}");
}

// Agrupar por aprobado/suspendido
var porEstado = alumnos.GroupBy(a => a.Nota >= 5 ? "Aprobados" : "Suspendidos");

foreach (var grupo in porEstado)
{
    Console.WriteLine($"{grupo.Key}: {grupo.Count()}");
}

6. Elementos distintos y conjuntos

// Distinct: valores únicos
List<string> ciudades = alumnos.Select(a => a.Ciudad).Distinct().ToList();
// [Madrid, Barcelona, Sevilla, Valencia]

// DistinctBy: únicos por una propiedad
var uniPorCiudad = alumnos.DistinctBy(a => a.Ciudad).ToList();

// Union, Intersect, Except
List<int> lista1 = new() { 1, 2, 3, 4, 5 };
List<int> lista2 = new() { 3, 4, 5, 6, 7 };

var union = lista1.Union(lista2).ToList();       // [1,2,3,4,5,6,7]
var inter = lista1.Intersect(lista2).ToList();   // [3,4,5]
var excepto = lista1.Except(lista2).ToList();    // [1,2]

7. Transformar colecciones

// ToList(): convertir a List
List<string> listaNombres = alumnos.Select(a => a.Nombre).ToList();

// ToArray(): convertir a array
string[] arrayNombres = alumnos.Select(a => a.Nombre).ToArray();

// ToDictionary(): convertir a Dictionary
Dictionary<string, double> dicNotas = alumnos
    .ToDictionary(a => a.Nombre, a => a.Nota);

Console.WriteLine(dicNotas["Ana"]);  // 8.5

// ToLookup(): como Dictionary pero con múltiples valores por clave
var lookup = alumnos.ToLookup(a => a.Ciudad);
foreach (var a in lookup["Madrid"])
    Console.WriteLine(a.Nombre);

8. Encadenar consultas complejas

// Informe completo: alumnos aprobados de Madrid, ordenados por nota descendente
var informe = alumnos
    .Where(a => a.Ciudad == "Madrid")     // Filtrar por ciudad
    .Where(a => a.Nota >= 5)              // Filtrar aprobados
    .OrderByDescending(a => a.Nota)       // Ordenar por nota
    .Select(a => new                      // Proyectar datos
    {
        a.Nombre,
        NotaFormateada = $"{a.Nota:F1}/10",
        Calificacion = a.Nota switch
        {
            >= 9 => "Sobresaliente",
            >= 7 => "Notable",
            >= 5 => "Aprobado",
            _ => "Suspendido"
        }
    })
    .ToList();

foreach (var item in informe)
    Console.WriteLine($"{item.Nombre}: {item.NotaFormateada} ({item.Calificacion})");
// María: 9.2/10 (Sobresaliente)
// Ana: 8.5/10 (Notable)
// Diego: 6.5/10 (Aprobado)
// Javier: 5.0/10 (Aprobado)

9. Ejecución diferida (lazy evaluation)

⚠️ Importante: LINQ usa ejecución diferida: la consulta no se ejecuta hasta que accedes a los resultados (con foreach, ToList(), Count(), etc.). Esto mejora el rendimiento.

var consulta = alumnos.Where(a => a.Nota >= 5);
// ¡Aquí NO se ha ejecutado nada todavía!

// Se ejecuta al recorrer o materializar
var lista = consulta.ToList();      // Se ejecuta ahora
int total = consulta.Count();        // Se ejecuta de nuevo
foreach (var a in consulta) { }      // Se ejecuta de nuevo

10. Ejercicios

Ejercicio 1: Análisis de ventas

Crea una lista de registros Venta(string Producto, string Categoria, double Precio, int Cantidad, DateTime Fecha). Usa LINQ para: las 5 ventas más caras, total por categoría, producto más vendido, ventas por mes.

Ejercicio 2: Análisis de texto

Dado un texto largo, usa LINQ para: contar cada palabra (agrupar), encontrar la más frecuente, listar palabras de más de 5 letras ordenadas alfabéticamente, contar vocales totales.

Ejercicio 3: Gestión de películas

Lista de Pelicula(string Titulo, int Año, double Puntuacion, string Genero, string Director). Consultas: mejores por género, directores con más películas, media por década, filtros combinados.

Ejercicio 4: Join entre colecciones

Crea listas de Departamento y Empleado (con DepartamentoId). Usa .Join() para: listar empleados con su departamento, contar empleados por departamento, salario total por departamento.


Resumen

Operación Método Ejemplo rápido
Filtrar Where .Where(x => x > 5)
Ordenar ↑ OrderBy .OrderBy(x => x.Nombre)
Ordenar ↓ OrderByDescending .OrderByDescending(x => x.Nota)
Proyectar Select .Select(x => x.Nombre)
Tomar N Take .Take(5)
Saltar N Skip .Skip(10)
Primero FirstOrDefault .FirstOrDefault(x => ...)
Alguno cumple Any .Any(x => x > 0)
Todos cumplen All .All(x => x > 0)
Contar Count .Count(x => x > 5)
Máximo Max / MaxBy .Max(x => x.Nota)
Media Average .Average(x => x.Nota)
Agrupar GroupBy .GroupBy(x => x.Ciudad)
Distintos Distinct .Distinct()
A lista ToList .ToList()
A diccionario ToDictionary .ToDictionary(k => ..., v => ...)