Con Biblioteca ya reuniendo todo el catálogo y todos los socios en colecciones, la pregunta natural es: ¿cómo se responde a preguntas como "¿qué libros están disponibles?", "¿cuántos materiales hay de cada autor?" o "¿qué préstamos llevan más de dos semanas sin devolverse?" Sin más herramientas que un foreach, cada pregunta exigiría escribir un bucle manual con variables auxiliares. LINQ (Language Integrated Query, Consulta Integrada en el Lenguaje) resuelve esto con un conjunto de operadores —filtrar, transformar, ordenar, agrupar— que se combinan de forma expresiva y funcionan sobre cualquier IEnumerable<T> (la interfaz que cierra la lección anterior). Esta lección te enseña a consultar el catálogo y los préstamos de BiblioTech con LINQ, dejando atrás los bucles manuales para este tipo de tareas.

Contenido

  1. Qué es LINQ y qué problema resuelve
  2. Sintaxis de método: Where, Select, OrderBy
  3. Agrupando resultados: GroupBy
  4. Operadores que devuelven un único valor: FirstOrDefault, Any, Count
  5. Sintaxis de consulta: from...where...select
  6. Ejecución diferida frente a ejecución inmediata
  7. Consultando el catálogo y los préstamos de BiblioTech

  1. Qué es LINQ y qué problema resuelve

LINQ añade, a cualquier colección que implemente IEnumerable<T> (List<T>, arrays, Dictionary<TKey, TValue>.Values, y muchas más), un conjunto de métodos de extensión para consultarla de forma declarativa: en lugar de describir cómo recorrerla paso a paso con un foreach, describes qué resultado quieres obtener, y LINQ se encarga del recorrido por ti.

// Sin LINQ: describir "como" recorrer, con un foreach y una lista auxiliar
List<MaterialBibliotecario> disponibles = new List<MaterialBibliotecario>();
foreach (MaterialBibliotecario material in biblioteca.Catalogo)
{
    if (material.Disponible)
    {
        disponibles.Add(material);
    }
}

// Con LINQ: describir "que" resultado se quiere
List<MaterialBibliotecario> disponiblesLinq = biblioteca.Catalogo.Where(m => m.Disponible).ToList();

Ambos fragmentos producen el mismo resultado, pero el segundo expresa directamente la intención ("los materiales donde Disponible es verdadero"), sin la mecánica intermedia del bucle y la lista auxiliar.

  1. Sintaxis de método: Where, Select, OrderBy

Los operadores LINQ más usados, en su forma habitual (sintaxis de método, encadenando llamadas con .):

Operador Qué hace Ejemplo
Where Filtra elementos según una condición (Func<T, bool>) catalogo.Where(m => m.Disponible)
Select Transforma cada elemento en otra cosa (proyección) catalogo.Select(m => m.Titulo)
OrderBy Ordena de forma ascendente según una clave catalogo.OrderBy(m => m.Titulo)
OrderByDescending Ordena de forma descendente catalogo.OrderByDescending(m => m.Titulo)
List<MaterialBibliotecario> disponibles = biblioteca.Catalogo
    .Where(m => m.Disponible)
    .OrderBy(m => m.Titulo)
    .ToList();

foreach (MaterialBibliotecario material in disponibles)
{
    Console.WriteLine(material.Titulo);
}

Este encadenado se lee de izquierda a derecha como una tubería: "toma el catálogo, quédate solo con los disponibles, ordénalos por título, y conviértelo en una lista". Select transforma cada elemento, útil cuando no necesitas el objeto completo sino solo uno de sus datos:

List<string> titulos = biblioteca.Catalogo.Select(m => m.Titulo).ToList();
// ["Rayuela", "Ficciones", "National Geographic"]

  1. Agrupando resultados: GroupBy

GroupBy reparte los elementos de una colección en grupos según una clave, algo que con un foreach manual requeriría un Dictionary<TClave, List<T>> construido a mano:

var porAutor = biblioteca.Catalogo.GroupBy(m => m.Autor);

foreach (var grupo in porAutor)
{
    Console.WriteLine($"{grupo.Key}: {grupo.Count()} material(es)");
    foreach (MaterialBibliotecario material in grupo)
    {
        Console.WriteLine($"  - {material.Titulo}");
    }
}

Cada grupo es, a la vez, la clave (grupo.Key, el autor) y una colección de todos los elementos con esa clave, sobre la que se puede volver a iterar o aplicar más LINQ. var se usa aquí porque el tipo exacto que devuelve GroupBy (IEnumerable<IGrouping<string, MaterialBibliotecario>>) es largo de escribir y rara vez hace falta nombrarlo explícitamente.

  1. Operadores que devuelven un único valor: FirstOrDefault, Any, Count

No todas las consultas devuelven una colección; algunas responden con un único valor:

Operador Qué devuelve
FirstOrDefault(condicion) El primer elemento que cumple la condición, o default (normalmente null) si ninguno la cumple
First(condicion) Igual, pero lanza una excepción si ningún elemento la cumple
Any(condicion) true si al menos un elemento cumple la condición
Count(condicion) Cuántos elementos cumplen la condición
Count() (sin argumento) El número total de elementos
MaterialBibliotecario? primerDisponible = biblioteca.Catalogo.FirstOrDefault(m => m.Disponible);

bool hayRevistas = biblioteca.Catalogo.Any(m => m is Revista);

int totalLibros = biblioteca.Catalogo.Count(m => m is Libro);

Console.WriteLine(primerDisponible?.Titulo ?? "Ninguno disponible");
Console.WriteLine(hayRevistas);  // True
Console.WriteLine(totalLibros);  // 2

FirstOrDefault es preferible a First siempre que "no encontrar nada" sea una situación normal y esperable (como aquí); reserva First para cuando la ausencia de resultado representaría, en realidad, un error en la lógica del programa.

  1. Sintaxis de consulta: from...where...select

Además de la sintaxis de método, C# ofrece una sintaxis de consulta, más parecida a SQL, que el compilador traduce internamente a los mismos métodos (Where, Select...) vistos antes:

var disponiblesQuery =
    from material in biblioteca.Catalogo
    where material.Disponible
    orderby material.Titulo
    select material;

foreach (MaterialBibliotecario material in disponiblesQuery)
{
    Console.WriteLine(material.Titulo);
}

Esta consulta es exactamente equivalente a biblioteca.Catalogo.Where(m => m.Disponible).OrderBy(m => m.Titulo): dos formas distintas de escribir la misma operación. En la práctica, la sintaxis de método es la más habitual en código C# moderno (encaja mejor con encadenados largos y con operadores que no tienen equivalente directo en la sintaxis de consulta, como Count o Any); la sintaxis de consulta se usa sobre todo cuando hay varias fuentes combinadas (join) o cuando el equipo la prefiere por legibilidad. Basta con reconocer ambas y saber que son intercambiables.

  1. Ejecución diferida frente a ejecución inmediata

Un detalle importante, fácil de pasar por alto: la mayoría de operadores LINQ (Where, Select, OrderBy...) no se ejecutan en el momento en que se escriben, sino cuando se recorre el resultado (con foreach, o al llamar a ToList()/ToArray()/Count()). Esto se llama ejecución diferida (deferred execution):

var consulta = biblioteca.Catalogo.Where(m => m.Disponible); // todavia no se ha evaluado nada

biblioteca.AgregarMaterial(new Libro("El Aleph", "Jorge Luis Borges", "978-84-376-0497-8"));

foreach (MaterialBibliotecario material in consulta) // AQUI se ejecuta el filtro, sobre el catalogo ya actualizado
{
    Console.WriteLine(material.Titulo); // incluye tambien "El Aleph", aunque se agrego despues de "consulta"
}

Operadores como ToList(), ToArray(), Count() o FirstOrDefault() fuerzan, en cambio, una ejecución inmediata: el resultado se calcula en ese momento y queda fijo, sin verse afectado por cambios posteriores en la colección de origen. Para uso cotidiano basta con recordar esta regla práctica: si vas a recorrer el mismo resultado varias veces, o necesitas "congelarlo" en un momento concreto, añade .ToList() al final de la consulta.

  1. Consultando el catálogo y los préstamos de BiblioTech

Con todo lo anterior, así se responden las preguntas planteadas al principio de la lección sobre Biblioteca:

// ¿Que libros estan disponibles, ordenados por titulo?
List<MaterialBibliotecario> librosDisponibles = biblioteca.Catalogo
    .Where(m => m is Libro && m.Disponible)
    .OrderBy(m => m.Titulo)
    .ToList();

// ¿Cuantos materiales hay de cada autor?
var materialesPorAutor = biblioteca.Catalogo
    .GroupBy(m => m.Autor)
    .Select(g => new { Autor = g.Key, Cantidad = g.Count() });

foreach (var entrada in materialesPorAutor)
{
    Console.WriteLine($"{entrada.Autor}: {entrada.Cantidad}");
}

// ¿Que prestamos llevan mas de 14 dias sin devolverse (vencidos)?
const int DiasLimitePrestamo = 14;

List<Prestamo> prestamosVencidos = biblioteca.Prestamos
    .Where(p => p.FechaDevolucion == null && (DateTime.Now - p.FechaPrestamo).Days > DiasLimitePrestamo)
    .ToList();

foreach (Prestamo prestamo in prestamosVencidos)
{
    Console.WriteLine($"Vencido: '{prestamo.Libro.Titulo}' prestado a {prestamo.Socio.Nombre} el {prestamo.FechaPrestamo:yyyy-MM-dd}");
}

materialesPorAutor usa una característica que quizás no habías visto todavía: new { Autor = g.Key, Cantidad = g.Count() } crea un tipo anónimo, un objeto de usar y tirar con solo las propiedades que necesitas mostrar, sin declarar una clase ni un record aparte —útil precisamente para resultados intermedios de una consulta LINQ que no necesitan un tipo con nombre propio en el resto del programa.

La consulta de préstamos vencidos combina dos condiciones con &&: que no se haya devuelto todavía (FechaDevolucion == null) y que hayan pasado más días que el límite de préstamo desde FechaPrestamo. Es exactamente el tipo de pregunta de negocio ("¿qué préstamos requieren un aviso de retraso?") que LINQ permite expresar en pocas líneas, sin ningún bucle manual.

Errores Comunes y Consejos

  • Olvidar .ToList() (o similar) cuando hace falta una ejecución inmediata: si modificas la colección de origen entre la definición de la consulta y su recorrido, el resultado puede cambiar de forma inesperada por la ejecución diferida; usa .ToList() si necesitas "fijar" el resultado en un momento concreto.
  • Encadenar demasiados operadores en una sola línea ilegible: aunque técnicamente válido, una cadena larguísima de .Where().Select().OrderBy().GroupBy()... en una sola línea es difícil de leer; divide la consulta en varias líneas, una operación por línea, como en los ejemplos de esta lección.
  • Usar First cuando "no encontrado" es una situación normal: First lanza InvalidOperationException si ningún elemento cumple la condición; usa FirstOrDefault y comprueba is not null (recordando la lección de Pattern Matching) salvo que la ausencia de resultado sea, en efecto, un error de programa.
  • Confundir Count (operador LINQ) con Count (propiedad de List<T>): lista.Count (sin paréntesis) es una propiedad rápida de List<T>; lista.Count(condicion) (con paréntesis y un predicado) es el operador LINQ, que recorre la colección para contar cuántos elementos cumplen la condición. Ambos coexisten sin conflicto porque tienen firmas distintas.
  • Consejo: para tareas de filtrado, transformación y orden simples, la sintaxis de método es más habitual y más fácil de depurar paso a paso (puedes comentar una línea de la cadena para ver el resultado intermedio); reserva la sintaxis de consulta para cuando combines varias fuentes con join o cuando el equipo la prefiera explícitamente.

Ejercicios

  1. Sobre biblioteca.Catalogo, usa sintaxis de método para obtener una List<string> con los títulos de todos los materiales Disponible, ordenados alfabéticamente.

  2. Sobre biblioteca.Catalogo, usa GroupBy para agrupar los materiales por autor y muestra, para cada autor, cuántos materiales tiene en el catálogo.

  3. Sobre biblioteca.Prestamos, escribe una consulta que devuelva los préstamos sin devolver (FechaDevolucion == null) que llevan más de 14 días desde FechaPrestamo. Prueba con al menos un préstamo que cumpla la condición y otro que no.

Soluciones

List<string> titulosDisponibles = biblioteca.Catalogo
    .Where(m => m.Disponible)
    .OrderBy(m => m.Titulo)
    .Select(m => m.Titulo)
    .ToList();

foreach (string titulo in titulosDisponibles)
{
    Console.WriteLine(titulo);
}
var porAutor = biblioteca.Catalogo.GroupBy(m => m.Autor);

foreach (var grupo in porAutor)
{
    Console.WriteLine($"{grupo.Key}: {grupo.Count()} material(es)");
}
const int DiasLimitePrestamo = 14;

List<Prestamo> vencidos = biblioteca.Prestamos
    .Where(p => p.FechaDevolucion == null && (DateTime.Now - p.FechaPrestamo).Days > DiasLimitePrestamo)
    .ToList();

foreach (Prestamo prestamo in vencidos)
{
    Console.WriteLine($"Vencido: '{prestamo.Libro.Titulo}' ({prestamo.Socio.Nombre})");
}

Para comprobar el resultado, se puede crear un Prestamo con una FechaPrestamo simulada más antigua manualmente (recordando que, en el modelo actual, FechaPrestamo se fija automáticamente a DateTime.Now en el constructor), o simplemente razonar sobre préstamos recién creados, que nunca aparecerán como vencidos en el mismo instante de ejecución.

Conclusión

En esta lección has aprendido a consultar colecciones con LINQ: filtrar con Where, transformar con Select, ordenar con OrderBy, agrupar con GroupBy, y obtener valores únicos con FirstOrDefault, Any y Count; también has visto la sintaxis de consulta alternativa y la diferencia entre ejecución diferida e inmediata. Biblioteca.Catalogo y Biblioteca.Prestamos ya se pueden consultar con la misma expresividad que una base de datos, sin haber escrito ni una sola línea de SQL.

De hecho, esa comparación no es casual: en el Módulo 5 (Trabajando con Datos), Entity Framework usará LINQ como su lenguaje de consulta principal contra una base de datos real, así que todo lo aprendido aquí se reutilizará directamente. Antes de llegar allí, queda una última pieza en este módulo: todo lo visto hasta ahora ha sido código síncrono, que bloquea la ejecución mientras se completa. La última lección del módulo, Programación Asíncrona, introduce async/await y una primera versión simulada de PrestarLibroAsync, preparando a Biblioteca para las operaciones de E/S reales (ficheros, bases de datos) que llegarán en el próximo módulo.

Curso de Programación en C#

Módulo 1: Introducción a C#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados