En esta página

LINQ: consultas integradas en el lenguaje

15 min lectura TextoCap. 3 — C# moderno

¿Qué es LINQ?

LINQ (Language Integrated Query) es una característica de C# que permite consultar y transformar colecciones directamente desde el lenguaje, con la misma sintaxis que usar métodos de objetos.

LINQ funciona sobre cualquier tipo que implemente IEnumerable<T>: listas, arrays, sets, y también bases de datos (a través de EF Core) y XML.

// Sin LINQ — imperativo
var result = new List<string>();
foreach (var nombre in nombres)
{
    if (nombre.StartsWith("A"))
        result.Add(nombre.ToUpper());
}
result.Sort();

// Con LINQ — declarativo
var result = nombres
    .Where(n => n.StartsWith("A"))
    .Select(n => n.ToUpper())
    .OrderBy(n => n)
    .ToList();

Sintaxis de métodos (Method Syntax)

La forma más común. Encadena métodos de extensión:

Where — filtrar

var productos = ObtenerProductos();

// Filtro simple
var caros = productos.Where(p => p.Precio > 500);

// Múltiples condiciones
var disponibles = productos.Where(p => p.Stock > 0 && p.Precio < 1000);

// Con índice
var impares = productos.Where((p, i) => i % 2 != 0);

Select — proyectar

// Seleccionar una propiedad
IEnumerable<string> nombres = productos.Select(p => p.Nombre);

// Objeto anónimo
var resumen = productos.Select(p => new
{
    p.Id,
    p.Nombre,
    PrecioFormateado = $"${p.Precio:F2}"
});

// SelectMany — aplanar colecciones anidadas
var tags = cursos.SelectMany(c => c.Tags);

// Con índice
var numerados = productos.Select((p, i) => $"{i + 1}. {p.Nombre}");

Ordenamiento

// OrderBy — ascendente
var porPrecio = productos.OrderBy(p => p.Precio);

// OrderByDescending — descendente
var masCaros = productos.OrderByDescending(p => p.Precio);

// ThenBy — orden secundario
var ordenCompuesto = productos
    .OrderBy(p => p.Categoria)
    .ThenByDescending(p => p.Precio)
    .ThenBy(p => p.Nombre);

GroupBy — agrupar

var grupos = productos
    .GroupBy(p => p.Categoria)
    .Select(g => new
    {
        Categoria  = g.Key,
        Cantidad   = g.Count(),
        PromedioP  = g.Average(p => p.Precio),
        MasBarato  = g.Min(p => p.Precio),
        MasCaro    = g.Max(p => p.Precio),
    });

// Agrupar por múltiples claves
var gruposMultiples = productos
    .GroupBy(p => new { p.Categoria, EsCaro = p.Precio > 100 })
    .Select(g => new
    {
        g.Key.Categoria,
        g.Key.EsCaro,
        Items = g.ToList()
    });

Join — combinar colecciones

record Pedido(int Id, int ProductoId, int Cantidad);

var pedidos = new List<Pedido>
{
    new(1, 1, 2),
    new(2, 3, 1),
    new(3, 2, 5),
};

// Inner join
var detalles = pedidos.Join(
    productos,
    pedido   => pedido.ProductoId,
    producto => producto.Id,
    (pedido, producto) => new
    {
        PedidoId  = pedido.Id,
        Producto  = producto.Nombre,
        Subtotal  = producto.Precio * pedido.Cantidad
    }
);

// Left join con GroupJoin + SelectMany
var todosConPedidos = productos.GroupJoin(
    pedidos,
    p   => p.Id,
    ped => ped.ProductoId,
    (p, peds) => new
    {
        p.Nombre,
        TotalPedidos = peds.Sum(ped => ped.Cantidad)
    }
);

Aggregate

// Acumulador genérico — como reduce en JavaScript
var listaFrases = new List<string> { "Hola", "mundo", "con", "C#" };
string frase = listaFrases.Aggregate((acc, s) => acc + " " + s);
// "Hola mundo con C#"

// Con valor semilla
decimal descuento = 0.1m;
decimal totalConDesc = productos.Aggregate(
    0m,
    (acc, p) => acc + p.Precio * (1 - descuento)
);

Métodos de cuantificación y elemento

// Verificación
bool hayProductos   = productos.Any();
bool hayBaratos     = productos.Any(p => p.Precio < 50);
bool todosConStock  = productos.All(p => p.Stock > 0);

// Conteo
int total      = productos.Count();
int totalCaro  = productos.Count(p => p.Precio > 500);
long totalLong = productos.LongCount(); // para colecciones enormes

// Elemento
var primero      = productos.First();
var primerCaro   = productos.First(p => p.Precio > 500);
var primerONull  = productos.FirstOrDefault(p => p.Precio > 9999);
var ultimo       = productos.Last();

// Único — lanza si hay 0 o más de 1
var unico = productos.Single(p => p.Id == 3);
var unicoONull = productos.SingleOrDefault(p => p.Id == 999);

// Elemento por índice
var tercero = productos.ElementAt(2);

// Saltar y tomar
var pagina2 = productos.Skip(10).Take(10); // paginación
var primeros3 = productos.Take(3);
var sin2primeros = productos.Skip(2);

// TakeWhile / SkipWhile — condicional
var hastaCaros = productos.TakeWhile(p => p.Precio < 500);

Sintaxis de consulta (Query Syntax)

Alternativa más declarativa, similar a SQL:

var resultado =
    from p in productos
    where p.Precio > 100 && p.Stock > 0
    orderby p.Categoria, p.Precio descending
    select new
    {
        p.Nombre,
        p.Categoria,
        PrecioStr = $"${p.Precio:F2}"
    };

// GroupBy en query syntax
var grupos =
    from p in productos
    group p by p.Categoria into g
    select new
    {
        Categoria = g.Key,
        Items     = g.ToList(),
        Promedio  = g.Average(p => p.Precio)
    };

Ejecución diferida

LINQ usa evaluación perezosa (lazy evaluation). La consulta no se ejecuta hasta que iteras el resultado:

// Esta línea solo DEFINE la consulta, no la ejecuta
IEnumerable<Producto> caros = productos.Where(p => p.Precio > 500);

// La consulta se ejecuta AQUÍ (al iterar)
foreach (var p in caros)
    Console.WriteLine(p.Nombre);

// Materializar para ejecutar de inmediato y guardar el resultado
List<Producto> carosList   = caros.ToList();   // ejecuta y almacena
Producto[]     carosArray  = caros.ToArray();  // ejecuta y almacena

// Count() también ejecuta la consulta
int cantidad = caros.Count(); // ejecuta

Práctica

  1. Top 5: Dado un List<Producto>, usa LINQ para obtener los 5 productos más caros con stock > 0, mostrando solo nombre y precio formateado.
  2. Estadísticas por categoría: Agrupa los productos por categoría y muestra el total de items, el precio promedio y el valor total en inventario (precio × stock).
  3. Join de datos: Crea listas de Pedido(Id, ClienteId, Total) y Cliente(Id, Nombre). Usa LINQ Join para mostrar pedidos con el nombre del cliente.

En la siguiente lección exploraremos async/await: cómo escribir código asíncrono en C# para manejar operaciones de I/O sin bloquear el hilo principal.

Ejecución diferida con ToList() y ToArray()
LINQ usa evaluación diferida (lazy): la consulta no se ejecuta hasta que iteras el resultado. Llama a .ToList() o .ToArray() para materializar y ejecutar la consulta inmediatamente. Esto es importante cuando la fuente de datos puede cambiar o cuando quieres medir el tiempo de ejecución con exactitud.
First vs FirstOrDefault vs Single
First() lanza excepción si la colección está vacía. FirstOrDefault() devuelve null/default si está vacía. Single() lanza si hay 0 o más de 1 elemento. SingleOrDefault() devuelve null si vacía, lanza si hay más de 1. Elige según tus expectativas sobre los datos.
LINQ y performance
LINQ es muy legible pero no siempre es la opción más rápida. Para colecciones muy grandes (millones de elementos), considera usar bucles for con arrays directamente. AsParallel() puede paralelizar consultas LINQ en múltiples núcleos para mejorar performance.
// Datos de ejemplo
record Producto(int Id, string Nombre, decimal Precio, string Categoria, int Stock);

var productos = new List<Producto>
{
    new(1, "Laptop Pro",   1299.99m, "Electrónica", 10),
    new(2, "Mouse Óptico",   29.99m, "Periféricos",  50),
    new(3, "Monitor 4K",   449.99m, "Electrónica",   8),
    new(4, "Teclado Mec.",  89.99m, "Periféricos",  30),
    new(5, "Laptop Air",   999.99m, "Electrónica",  15),
    new(6, "Audífonos BT",  79.99m, "Audio",        20),
};

// Where — filtrar
var electronicos = productos.Where(p => p.Categoria == "Electrónica");

// Select — proyectar/transformar
var nombres = productos.Select(p => p.Nombre).ToList();
var resumen = productos.Select(p => new { p.Nombre, p.Precio });

// OrderBy / OrderByDescending / ThenBy
var ordenados = productos
    .OrderBy(p => p.Categoria)
    .ThenByDescending(p => p.Precio);

// GroupBy — agrupar
var porCategoria = productos
    .GroupBy(p => p.Categoria)
    .Select(g => new
    {
        Categoria = g.Key,
        Total     = g.Count(),
        Promedio  = g.Average(p => p.Precio),
    });

foreach (var grupo in porCategoria)
    Console.WriteLine($"{grupo.Categoria}: {grupo.Total} items, avg ${grupo.Promedio:F2}");