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
- Qué es LINQ y qué problema resuelve
- Sintaxis de método:
Where,Select,OrderBy - Agrupando resultados:
GroupBy - Operadores que devuelven un único valor:
FirstOrDefault,Any,Count - Sintaxis de consulta:
from...where...select - Ejecución diferida frente a ejecución inmediata
- Consultando el catálogo y los préstamos de BiblioTech
- 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.
- Sintaxis de método:
Where, Select, OrderBy
Where, Select, OrderByLos 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"]
- Agrupando resultados:
GroupBy
GroupByGroupBy 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.
- Operadores que devuelven un único valor:
FirstOrDefault, Any, Count
FirstOrDefault, Any, CountNo 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); // 2FirstOrDefault 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.
- Sintaxis de consulta:
from...where...select
from...where...selectAdemá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.
- 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.
- 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
Firstcuando "no encontrado" es una situación normal:FirstlanzaInvalidOperationExceptionsi ningún elemento cumple la condición; usaFirstOrDefaulty compruebais 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) conCount(propiedad deList<T>):lista.Count(sin paréntesis) es una propiedad rápida deList<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
joino cuando el equipo la prefiera explícitamente.
Ejercicios
-
Sobre
biblioteca.Catalogo, usa sintaxis de método para obtener unaList<string>con los títulos de todos los materialesDisponible, ordenados alfabéticamente. -
Sobre
biblioteca.Catalogo, usaGroupBypara agrupar los materiales por autor y muestra, para cada autor, cuántos materiales tiene en el catálogo. -
Sobre
biblioteca.Prestamos, escribe una consulta que devuelva los préstamos sin devolver (FechaDevolucion == null) que llevan más de 14 días desdeFechaPrestamo. 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#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
