← maurobernal.com.ar

Etiqueta: dotnet9

  • 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 →

  • C# 13: params modernos, Lock de primera clase y Task.WhenEach

    C# 13 con .NET 9 llegó con mejoras que parecen pequeñas pero cambian patrones que usamos hace años. Los params con Span eliminan allocations que ni sabíamos que estábamos haciendo. El Lock de primera clase hace que el threading sea más seguro. Y Task.WhenEach resuelve un patrón asíncrono que antes requería código contorsionado.

    params ReadOnlySpan<T>: cero allocations en métodos variádicos

    Antes de C# 13, params solo funcionaba con arrays. Cada vez que llamabas a un método con params int[], el compilador creaba un array en el heap aunque los datos pudieran vivir perfectamente en el stack. C# 13 cambia eso.

    // ❌ C# 12 y anteriores: params siempre crea un array en el heap
    public static int Sumar(params int[] numeros)
    {
        int total = 0;
        foreach (var n in numeros) total += n;
        return total;
    }
    
    // Cada llamada alloca un int[] en el heap:
    int r1 = Sumar(1, 2, 3);      // new int[] { 1, 2, 3 }
    int r2 = Sumar(10, 20);        // new int[] { 10, 20 }
    
    // ✅ C# 13: params con ReadOnlySpan — cero allocations
    public static int Sumar(params ReadOnlySpan<int> numeros)
    {
        int total = 0;
        foreach (var n in numeros) total += n;
        return total;
    }
    
    // El compilador puede poner los valores directamente en el stack
    int r1 = Sumar(1, 2, 3);   // Sin heap allocation
    int r2 = Sumar(10, 20);     // Sin heap allocation
    
    // También funciona con otros tipos de colección
    public static void LogTodos(params IEnumerable<string> mensajes) { ... }
    public static T Primero<T>(params ReadOnlySpan<T> items) => items[0];

    System.Threading.Lock: el tipo de primera clase para sincronización

    Durante años, el patrón estándar para sincronizar hilos en C# era lock (new object()). Funcionaba, pero tenía un problema: object no comunica la intención. Cualquier referencia podía usarse accidentalmente como lock. System.Threading.Lock resuelve eso con un tipo dedicado que el compilador trata de forma especial.

    // ❌ Antes: object genérico como lock
    public class Cache
    {
        private readonly object _lock = new object();
        private Dictionary<string, object> _datos = new();
    
        public void Agregar(string clave, object valor)
        {
            lock (_lock)
            {
                _datos[clave] = valor;
            }
        }
    
        public bool TryGet(string clave, out object? valor)
        {
            lock (_lock)
            {
                return _datos.TryGetValue(clave, out valor);
            }
        }
    }
    
    // ✅ C# 13 / .NET 9: System.Threading.Lock
    public class Cache
    {
        // Semánticamente claro: esto ES un lock, no cualquier objeto
        private readonly Lock _lock = new();
        private Dictionary<string, object> _datos = new();
    
        public void Agregar(string clave, object valor)
        {
            // El compilador emite código optimizado para Lock
            // Usa el patrón Dispose internamente (EnterScope)
            lock (_lock)
            {
                _datos[clave] = valor;
            }
        }
    
        // También disponible: API explícita para casos avanzados
        public bool TryAgregar(string clave, object valor)
        {
            using (_lock.EnterScope())
            {
                if (_datos.ContainsKey(clave)) return false;
                _datos[clave] = valor;
                return true;
            }
        }
    }

    El compilador emite una advertencia si intentás pasar un Lock como object a otro método — protección contra el antipatrón de usar el mismo lock para múltiples propósitos.

    Task.WhenEach: procesar tareas a medida que terminan

    Tenía un patrón que repetía en muchos proyectos: lanzar varias tareas en paralelo y procesar cada resultado apenas llegaba, sin esperar a que todas terminen. Antes requería Task.WhenAny en un loop con manejo manual. Task.WhenEach lo vuelve trivial.

    // ❌ Antes: patrón verbose con Task.WhenAny
    var tareas = new List<Task<Reporte>> { 
        ObtenerReporteAsync("ventas"), 
        ObtenerReporteAsync("stock"), 
        ObtenerReporteAsync("clientes") 
    };
    
    var pendientes = new HashSet<Task<Reporte>>(tareas);
    while (pendientes.Count > 0)
    {
        var completada = await Task.WhenAny(pendientes);
        pendientes.Remove(completada);
        var reporte = await completada;
        Console.WriteLine($"Reporte listo: {reporte.Nombre}");
    }
    
    // ✅ C# 13 / .NET 9: Task.WhenEach — elegante y eficiente
    var tareas = new List<Task<Reporte>> { 
        ObtenerReporteAsync("ventas"), 
        ObtenerReporteAsync("stock"), 
        ObtenerReporteAsync("clientes") 
    };
    
    await foreach (var completada in Task.WhenEach(tareas))
    {
        var reporte = await completada;
        Console.WriteLine($"Reporte listo: {reporte.Nombre}");
        // Procesa cada reporte apenas llega, sin bloquear a los demás
    }

    LINQ: CountBy, AggregateBy e Index

    var pedidos = new[] {
        new { Cliente = "Ana",  Monto = 100m },
        new { Cliente = "Juan", Monto = 200m },
        new { Cliente = "Ana",  Monto = 150m },
        new { Cliente = "Juan", Monto = 300m },
    };
    
    // CountBy: contar elementos agrupados por clave
    var pedidosPorCliente = pedidos.CountBy(p => p.Cliente);
    // { "Ana": 2, "Juan": 2 }
    
    // AggregateBy: acumular valores agrupados por clave
    var totalPorCliente = pedidos.AggregateBy(
        p => p.Cliente,
        seed: 0m,
        (acum, p) => acum + p.Monto
    );
    // { "Ana": 250, "Juan": 500 }
    
    // Index(): obtener índice sin usar Select((x, i) => ...)
    foreach (var (indice, pedido) in pedidos.Index())
    {
        Console.WriteLine($"Pedido #{indice}: {pedido.Cliente} - ${pedido.Monto}");
    }

    Nuevos miembros partial (C# 13)

    // C# 13: propiedades y constructores pueden ser partial
    // Muy útil con source generators (EF Core, Roslyn generators)
    
    public partial class Entidad
    {
        // Declaración en el archivo principal
        public partial string Nombre { get; set; }
        public partial void OnNombreCambiado();
    }
    
    public partial class Entidad
    {
        // Implementación en el archivo generado (o viceversa)
        public partial string Nombre
        {
            get => field;
            set { field = value; OnNombreCambiado(); }
        }
        
        public partial void OnNombreCambiado() 
            => Console.WriteLine($"Nombre cambió a: {Nombre}");
    }

    ← C# 12: Primary Constructors, Collection Expressions y el código que debería haber existido siempre | Serie .NET 8 → .NET 10 | Próximo: La palabra clave field en C# 14: adiós para siempre a los backing fields repetitivos →

Tags

tsql (27)mssql (26)sql (20)devops (20)dotnet (18)docker (15)performance (14)contenedores (11)dotnet10 (10)linux (9)csharp (8)microservicios (7)angular (7)angular21 (7)sql server (6)issabel (6)docker-compose (6)typescript (6)mysql (5).NET (5)