IEnumerable<T> vs IQueryable<T>: el duelo que decidirá el destino de tu aplicación
\n\n¿Alguna vez te has preguntado por qué tu consulta a la base de datos va lenta, aunque los datos sean pocos? ¿O por qué después de agregar una línea de código la aplicación se vuelve una tortuga? Lo más probable es que te hayas enfrentado al clásico dilema: IEnumerable<T> o IQueryable<T>? No son solo dos interfaces del mundo de C# y LINQ. Son dos mundos diferentes, dos enfoques para trabajar con datos. Uno es para colecciones locales, el otro para fuentes remotas, como bases de datos. En este artículo no solo enumeraremos las diferencias, sino que las analizaremos con ejemplos reales, para que nunca más caigas en la trampa del rendimiento.
\n\n¿Quién es quién? Conceptos básicos
\n\nEmpecemos por lo básico. IEnumerable<T> es una interfaz que representa una secuencia de elementos en memoria. Es ideal para trabajar con colecciones como List<T>, Array o Dictionary. Cuando usas IEnumerable, todas las operaciones (filtrado, ordenamiento) se ejecutan del lado del cliente, es decir, en tu aplicación.
IQueryable<T> es una interfaz más avanzada, diseñada para trabajar con fuentes de datos externas (bases de datos, servicios web). No almacena datos, sino que construye un árbol de expresiones (Expression Tree). Esto permite que el proveedor de LINQ (por ejemplo, Entity Framework) convierta tu código C# en una consulta SQL eficiente y la ejecute del lado del servidor.
\n\nLa frase clave que debes recordar: IEnumerable trabaja en memoria, IQueryable trabaja del lado de la fuente de datos.
\n\nLa diferencia principal: dónde y cuándo se ejecuta la lógica
\n\nImagina que necesitas filtrar una lista de 10 000 productos que cuesten más de 1000 rublos y ordenarlos por precio.
\n\nEjemplo 1: IEnumerable (filtrado en memoria)
\n// Supongamos que products es un List<Product> (IEnumerable)\nvar expensiveProducts = products\n .Where(p => p.Price > 1000)\n .OrderBy(p => p.Price)\n .ToList();\n\n¿Qué sucede aquí? Primero, se cargan TODOS los 10 000 productos en memoria. Luego, C# aplica el filtro Where y la ordenación OrderBy en tu aplicación. Si tienes 10 000 registros, no es crítico. Pero si son 10 millones, la aplicación se caerá o se congelará.
Ejemplo 2: IQueryable (filtrado en el servidor de BD)
\n// Supongamos que dbContext.Products es un DbSet<Product> (IQueryable)\nvar expensiveProducts = dbContext.Products\n .Where(p => p.Price > 1000)\n .OrderBy(p => p.Price)\n .ToList();\n\nAquí está la magia: IQueryable no carga los datos de inmediato. En su lugar, construye una consulta SQL: SELECT * FROM Products WHERE Price > 1000 ORDER BY Price. Esta consulta se envía a la base de datos, y esta devuelve solo los datos filtrados y ordenados. En memoria obtienes solo lo que necesitas. El ahorro de recursos es enorme.
Ejemplo práctico: la trampa de ToList()
\n\nUno de los errores más comunes de los desarrolladores principiantes es llamar prematuramente a ToList() o ToArray(). Esto convierte IQueryable en IEnumerable.
Código malo (mata el rendimiento):
\nvar allProducts = dbContext.Products.ToList(); // ¡Cargamos TODOS los datos en memoria!\nvar filteredProducts = allProducts.Where(p => p.Price > 1000).ToList();\n\nEn este ejemplo, primero cargamos toda la tabla Products en memoria, y luego la filtramos. Es equivalente a descargar todo el almacén en el estacionamiento y luego buscar la caja correcta entre el montón.
\n\nCódigo bueno (óptimo):