LINQ es una de las features de C# que más uso cada día. Y con .NET 9 y .NET 10 recibió adiciones concretas que resuelven patrones que antes requerían GroupBy + Select + ToDictionary en cadena. CountBy, AggregateBy e Index son pequeños pero cambian bastante el código de consulta.
Index(): el iterador con índice que debería haber existido siempre
Cuántas veces escribiste .Select((item, i) => (i, item)) solo para tener el índice en un foreach? Index() hace exactamente eso con una API limpia.
var productos = new[] { "Monitor", "Teclado", "Mouse", "Auriculares" };
// ❌ Antes: Select con tupla o variable externa
int i = 0;
foreach (var p in productos)
Console.WriteLine($"{i++}. {p}");
// O la versión LINQ verbose:
foreach (var (indice, producto) in productos.Select((p, i) => (i, p)))
Console.WriteLine($"{indice}. {producto}");
// ✅ .NET 9: Index() — limpio y declarativo
foreach (var (indice, producto) in productos.Index())
Console.WriteLine($"{indice}. {producto}");
// También útil en proyecciones
var conIndice = productos.Index()
.Where(x => x.Index % 2 == 0) // solo los pares
.Select(x => $"[{x.Index}] {x.Item}");
CountBy(): contar por clave sin GroupBy
var pedidos = new[]
{
new { Cliente = "Ana", Estado = "Pagado", Monto = 100m },
new { Cliente = "Juan", Estado = "Pendiente", Monto = 200m },
new { Cliente = "Ana", Estado = "Pagado", Monto = 150m },
new { Cliente = "Pedro", Estado = "Pendiente", Monto = 300m },
new { Cliente = "Juan", Estado = "Cancelado", Monto = 50m },
};
// ❌ Antes: GroupBy + Count — verboso
var conteoAntes = pedidos
.GroupBy(p => p.Cliente)
.Select(g => new { Cliente = g.Key, Cantidad = g.Count() })
.ToDictionary(x => x.Cliente, x => x.Cantidad);
// ✅ .NET 9: CountBy — directo al punto
var conteoPorCliente = pedidos.CountBy(p => p.Cliente);
// { "Ana": 2, "Juan": 2, "Pedro": 1 }
// CountBy devuelve IEnumerable<KeyValuePair<TKey, int>>
foreach (var (cliente, cantidad) in conteoPorCliente)
Console.WriteLine($"{cliente}: {cantidad} pedidos");
// Contar por múltiples criterios con tipo anónimo
var conteoPorEstado = pedidos.CountBy(p => p.Estado);
// { "Pagado": 2, "Pendiente": 2, "Cancelado": 1 }
AggregateBy(): acumulación agrupada sin GroupBy
// ❌ Antes: GroupBy + Sum — varios pasos
var totalesAntes = pedidos
.GroupBy(p => p.Cliente)
.ToDictionary(g => g.Key, g => g.Sum(p => p.Monto));
// ✅ .NET 9: AggregateBy — acumulación directa
var totalPorCliente = pedidos.AggregateBy(
keySelector: p => p.Cliente,
seed: 0m,
func: (acumulado, pedido) => acumulado + pedido.Monto
);
// { "Ana": 250, "Juan": 250, "Pedro": 300 }
// Caso más complejo: acumular un objeto personalizado
var resumenPorCliente = pedidos.AggregateBy(
keySelector: p => p.Cliente,
seed: (Cantidad: 0, Total: 0m, UltimoPedido: ""),
func: (acc, p) => (
Cantidad: acc.Cantidad + 1,
Total: acc.Total + p.Monto,
UltimoPedido: p.Estado
)
);
foreach (var (cliente, resumen) in resumenPorCliente)
Console.WriteLine($"{cliente}: {resumen.Cantidad} pedidos, ${resumen.Total} total");
Mejoras de rendimiento en LINQ (.NET 9 y .NET 10)
Más allá de las nuevas APIs, .NET 9 y .NET 10 mejoran el rendimiento interno de operaciones LINQ existentes. Algunas que noté en benchmarks reales:
- Order() / OrderDescending(): versiones sin selector — más rápidas que
OrderBy(x => x) - GroupBy con span: reducción de allocations en agrupaciones de tipos por valor
- Where + Select fusionados: el JIT puede combinar estas operaciones en algunos casos
- ToFrozenDictionary() / ToFrozenSet(): colecciones inmutables optimizadas para lectura frecuente
// ToFrozenDictionary: para lookups frecuentes de datos que no cambian
// Más rápido que Dictionary en lecturas; más lento en construcción
using System.Collections.Frozen;
var codigosPais = new Dictionary<string, string>
{
{ "AR", "Argentina" }, { "BR", "Brasil" }, { "CL", "Chile" }
};
// Convertir a FrozenDictionary — operación costosa una sola vez
FrozenDictionary<string, string> lookup = codigosPais.ToFrozenDictionary();
// Lookups subsiguientes son más rápidos que Dictionary estándar
string pais = lookup["AR"]; // Acceso optimizado
Mi cheat sheet de LINQ moderno
| Necesidad | Antes | Ahora (.NET 9+) |
|---|---|---|
| Iterar con índice | .Select((x,i)=>(i,x)) | .Index() |
| Contar por clave | .GroupBy(k).Select(count) | .CountBy(k) |
| Acumular por clave | .GroupBy(k).ToDictionary(sum) | .AggregateBy(k, seed, func) |
| Ordenar elementos | .OrderBy(x => x) | .Order() |
| Lookup inmutable | new Dictionary<>() | .ToFrozenDictionary() |
← Extension Members en C# 14/15: propiedades y miembros estáticos de extensión como nunca antes | Serie .NET 8 → .NET 10 | Próximo: Rendimiento extremo en .NET 10: Stack allocation, Native AOT y el GC que trabaja menos →