← maurobernal.com.ar

LINQ en .NET 9 y .NET 10: CountBy, AggregateBy, Index() y las mejoras que cambian cómo consultás datos

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

NecesidadAntesAhora (.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 inmutablenew 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 →

Comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.