Trabajar con colecciones de datos en C# se vuelve mucho más expresivo cuando dejamos de recorrer manualmente cada elemento y empezamos a declarar lo que necesitamos. Eso es exactamente lo que permite LINQ (Language Integrated Query), y a lo largo de este recorrido se implementa paso a paso un repositorio en memoria que resuelve problemas reales de inventario: búsquedas, filtros, ordenamientos, agrupaciones y cálculos estadísticos.
¿Cómo se implementa un repositorio en memoria con LINQ?
El punto de partida es la interfaz IProductoRepository, un contrato que define las operaciones disponibles. La clase InMemoryProductoRepository la implementa y almacena los productos en una lista interna, útil cuando aún no se conecta una base de datos [0:40].
Dentro del repositorio se trabaja con varios métodos clave:
- Agregar: asigna un ID único mediante un contador que se incrementa con cada inserción. No retorna nada porque modifica la colección interna [1:22].
- ObtenerPorID: usa FirstOrDefault, que retorna el primer elemento que cumple la condición o
null si no existe. Se acompaña del operador de nulabilidad (?) para manejar la ausencia de resultados [1:40].
- ObtenerTodos: convierte la lista con AsReadOnly, protegiendo los datos internos para que el consumidor no pueda modificarlos. Retorna un
IEnumerable para mantener flexibilidad [2:02].
- Actualizar: busca el producto existente, valida que no sea
null y actualiza cada propiedad (nombre, precio, cantidad, categoría, estado) sin reemplazar el objeto, manteniendo así la referencia intacta [2:22].
- Eliminar: localiza el producto, valida su existencia y lo remueve con Remove, que elimina la primera ocurrencia del elemento [3:08].
La propiedad Cantidad retorna el total de productos. Se implementa como propiedad y no como método porque no recibe parámetros y es una consulta simple [3:30].
¿Qué operadores de LINQ resuelven necesidades reales de inventario?
LINQ transforma la forma de pensar: en lugar de controlar el recorrido, definimos el criterio de selección [3:50].
¿Cómo filtrar con Where?
Where es el operador base de filtrado, equivalente a un WHERE en SQL [4:30].
- BuscarPorCategoría: compara
p.Categoría contra la categoría solicitada mediante una expresión lambda.
- BuscarPorNombre: utiliza Contains con
StringComparison.OrdinalIgnoreCase, lo que ignora mayúsculas y minúsculas. Buscar "mouse" encuentra tanto "Mouse Logitech" como "MOUSE INALÁMBRICO" [5:08].
- BuscarPorRangoDePrecio: combina dos condiciones con el operador AND (
&&) para que el precio esté entre un mínimo y un máximo, patrón habitual en aplicaciones de ecommerce [5:38].
El patrón siempre es el mismo: fuente de datos, condición, resultado filtrado.
¿Cómo transformar y consultar con Select y Any?
- Select cambia la forma del resultado.
ObtenerNombres extrae solo la propiedad Nombre de cada producto, retornando un IEnumerable<string> en lugar de objetos completos. Es equivalente a SELECT nombre FROM productos en SQL [6:10].
- Any responde una pregunta booleana:
HayStockBajo retorna true si al menos un producto tiene cantidad menor a cinco. Es eficiente porque se detiene en cuanto encuentra el primer elemento que cumple la condición [6:38].
¿Cómo ordenar y limitar resultados?
- OrderBy ordena en forma ascendente.
ObtenerOrdenadosPorPrecio retorna productos de menor a mayor precio [7:10].
- OrderByDescending junto con Take permite obtener los n productos más caros, combinación típica para implementar un top diez o best sellers [7:28].
¿Cómo agrupar datos y calcular métricas con LINQ?
GroupBy crea grupos donde cada uno comparte un mismo valor. AgruparPorCategoría retorna un IEnumerable<IGrouping>, estructura similar a un diccionario con una key (categoría) y los productos del grupo [8:00].
ContarPorCategoría convierte esos grupos en un Dictionary usando ToDictionary, donde la clave es g.Key y el valor es g.Count() [8:22].
Para cálculos de negocio se aplican operadores de agregación:
- Sum:
ObtenerValorTotalInventario multiplica precio por cantidad de cada producto y suma todo [9:00].
- Average:
ObtenerPrecioPromedio calcula el promedio validando primero que la lista no esté vacía para evitar una excepción por división entre cero [9:20].
- MaxBy:
ObtenerProductoMásCaro retorna el objeto completo con el precio máximo, no solo el valor numérico. El tipo de retorno es nulable (Producto?) porque la lista podría estar vacía [9:42].
El método ObtenerValorPorCategoría muestra la verdadera fortaleza del encadenamiento: agrupa con GroupBy y convierte con ToDictionary usando g.Sum para calcular el valor total por categoría [10:10].
Por último, ObtenerStockBajo filtra productos cuya cantidad esté por debajo de un umbral con valor por defecto de cinco. Si se pasa un parámetro diferente, se usa ese valor, lo que hace el método flexible para distintos criterios [10:32].
Si ya estás practicando con este repositorio, comparte qué otros operadores de LINQ te gustaría combinar para resolver consultas más complejas en tu inventario.