← maurobernal.com.ar

Etiqueta: csharp

  • Guía práctica: cómo migré mis proyectos de .NET 8 a .NET 10 sin romper producción

    Migrar un proyecto de .NET 8 a .NET 10 es el tipo de tarea que parece riesgosa pero en la práctica es incremental y reversible. Lo hice en varios proyectos de producción sin downtime. El proceso siempre es el mismo: actualizar el TFM, correr los tests, adoptar las nuevas features de a poco.

    Paso 1: Actualizar el Target Framework

    # Verificar SDK instalado
    dotnet --list-sdks
    
    # Instalar .NET 10 SDK si no está
    # Linux/WSL:
    wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh
    bash dotnet-install.sh --channel 10.0
    
    # En cada .csproj de la solución:
    # <TargetFramework>net10.0</TargetFramework>
    
    # Para actualizar todos los .csproj de una solución en Linux:
    find . -name "*.csproj" -exec sed -i 's/net8.0/net10.0/g; s/net9.0/net10.0/g' {} \;
    
    # Restaurar y compilar
    dotnet restore
    dotnet build

    Paso 2: Actualizar paquetes NuGet

    # Ver paquetes desactualizados
    dotnet list package --outdated
    
    # Actualizar todos los paquetes de Microsoft.* a versiones compatibles con .NET 10
    dotnet add package Microsoft.AspNetCore.OpenApi
    dotnet add package Microsoft.Extensions.Http.Resilience
    
    # Paquetes que pueden desinstalarse en .NET 10:
    # - Swashbuckle.AspNetCore (reemplazado por OpenAPI nativo)
    # - Microsoft.AspNetCore.Mvc.NewtonsoftJson (System.Text.Json mejorado)
    
    # Remover Swashbuckle si usabas OpenAPI:
    dotnet remove package Swashbuckle.AspNetCore

    Paso 3: Adoptar las nuevas features de C# 14

    No hay que cambiar nada para que el proyecto compile. Las nuevas features son opt-in. La estrategia es adoptarlas gradualmente donde aporten más valor.

    // Prioridad 1: field — máximo impacto con mínimo riesgo
    // Buscar propiedades con backing fields privados y migrarlas
    
    // Antes:
    private string _nombre = string.Empty;
    public string Nombre
    {
        get => _nombre;
        set => _nombre = value?.Trim() ?? throw new ArgumentNullException();
    }
    
    // Después:
    public string Nombre
    {
        get => field;
        set => field = value?.Trim() ?? throw new ArgumentNullException();
    }
    
    // Prioridad 2: Primary Constructors en servicios con DI
    // Especialmente útil en proyectos con muchos servicios inyectados
    
    // Prioridad 3: params ReadOnlySpan en métodos variádicos de uso frecuente
    // Buscar: params int[], params string[], params object[]
    // Reemplazar por: params ReadOnlySpan<T> donde sea posible
    
    // Prioridad 4: Lock para thread safety
    // Buscar: private readonly object _lockObj = new object();
    // Reemplazar por: private readonly Lock _lockObj = new();

    Paso 4: Migrar a OpenAPI nativo (si usabas Swashbuckle)

    // Program.cs — reemplazar Swashbuckle por OpenAPI nativo
    
    // ❌ Antes con Swashbuckle:
    // builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", ...); });
    // app.UseSwagger();
    // app.UseSwaggerUI();
    
    // ✅ .NET 9/10 nativo:
    builder.Services.AddOpenApi(options =>
    {
        options.AddDocumentTransformer((document, context, ct) =>
        {
            document.Info = new()
            {
                Title = "Mi API",
                Version = "v1",
                Description = "API de gestión interna"
            };
            return Task.CompletedTask;
        });
    });
    
    // En el pipeline:
    if (app.Environment.IsDevelopment())
    {
        app.MapOpenApi();
        // Opcional: seguir usando Swagger UI solo para dev
        app.UseSwaggerUI(options =>
            options.SwaggerEndpoint("/openapi/v1.json", "Mi API v1"));
    }

    Checklist de migración completo

    PasoAcciónImpacto
    1Cambiar TFM a net10.0 en todos los .csprojObligatorio
    2dotnet restore && dotnet buildObligatorio
    3Ejecutar suite de tests completaObligatorio
    4Actualizar paquetes Microsoft.* a versión 10.xRecomendado
    5Adoptar field en propiedades con backing fieldGradual
    6Primary Constructors en servicios con DIGradual
    7Reemplazar object _lock por Lock _lockGradual
    8Migrar OpenAPI de Swashbuckle a nativoOpcional
    9Adoptar params ReadOnlySpan<T> en hot pathsOpcional
    10Evaluar Native AOT para Workers/APIs simplesOpcional avanzado

    Errores comunes y cómo resolverlos

    // ERROR 1: Paquete no compatible con net10.0
    // "Package X is not compatible with net10.0"
    // Solución: actualizar a la versión más reciente del paquete
    // Si no hay versión compatible, usar <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
    
    // ERROR 2: Reflection en Native AOT
    // "System.InvalidOperationException: Type ... is not reflection-enabled"
    // Solución: agregar source generator o usar [DynamicDependency]
    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(MiClase))]
    static void MetodoConReflection() { ... }
    
    // ERROR 3: field en C# 14 — feature no disponible
    // "error CS8652: The feature 'field keyword' is currently in Preview"
    // Solución: habilitar preview features en .csproj
    // <LangVersion>preview</LangVersion>
    // (o esperar a que salga de preview en la versión final de .NET 10)
    
    // ERROR 4: Cambios breaking en serialización JSON
    // System.Text.Json en .NET 10 es más estricto con algunos tipos
    // Solución: revisar JsonSerializerOptions y ajustar si es necesario

    Mi experiencia: cuánto tardó cada proyecto

    • API Minimal con 15 endpoints: 30 minutos (cambiar TFM, actualizar paquetes, correr tests)
    • Worker Service con threading complejo: 2 horas (migración de locks + tests de concurrencia)
    • Aplicación ASP.NET MVC grande (80 controllers): medio día (TFM + paquetes + resolver warnings)
    • Adopción gradual de C# 14 features: 2-3 semanas en paralelo con desarrollo normal

    La migración del TFM en sí es rápida. Lo que toma tiempo es la adopción de las nuevas features, y eso es completamente optativo. Podés migrar a .NET 10 hoy y adoptar field o Primary Constructors a tu ritmo, archivo por archivo, cuando tocás esa parte del código.


    ← dotnet run script.cs y las nuevas herramientas de .NET 10: OpenAPI, HttpClient y más | Fin de la Serie .NET 8 → .NET 10

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

  • Extension Members en C# 14/15: propiedades y miembros estáticos de extensión como nunca antes

    Los métodos de extensión de C# siempre fueron una de las features que más me gustaron. Pero siempre tuvieron una limitación molesta: solo podías agregar métodos, no propiedades. Si querías que lista.IsEmpty funcionara como propiedad (sin paréntesis), no había forma elegante. C# 14 lo soluciona con los bloques de extensión.

    El límite de los métodos de extensión clásicos

    // ❌ Métodos de extensión clásicos: solo métodos, sintaxis dispersa
    public static class EnumerableExtensions
    {
        // No hay forma de hacer esto una propiedad — requiere paréntesis
        public static bool IsEmpty<T>(this IEnumerable<T> source) 
            => !source.Any();
    
        public static IEnumerable<T> WhereNot<T>(
            this IEnumerable<T> source, 
            Func<T, bool> predicate) 
            => source.Where(x => !predicate(x));
    }
    
    // Uso: requiere paréntesis aunque semánticamente sea una propiedad
    var lista = new List<int>();
    bool vacia = lista.IsEmpty();  // paréntesis obligatorios

    Extension members: la nueva sintaxis de bloques

    // ✅ C# 14: bloques de extensión con propiedades reales
    public extension EnumerableExtensions<T>(IEnumerable<T> source)
    {
        // Propiedad de extensión: acceso sin paréntesis
        public bool IsEmpty => !source.Any();
        
        public int Count => source.Count();
    
        // Método de extensión en el mismo bloque
        public IEnumerable<T> WhereNot(Func<T, bool> predicate) 
            => source.Where(x => !predicate(x));
    
        public IEnumerable<T> Shuffle()
        {
            var lista = source.ToList();
            var rng = Random.Shared;
            for (int i = lista.Count - 1; i > 0; i--)
            {
                int j = rng.Next(i + 1);
                (lista[i], lista[j]) = (lista[j], lista[i]);
            }
            return lista;
        }
    }
    
    // Uso: propiedades sin paréntesis
    var numeros = new List<int>();
    bool vacia = numeros.IsEmpty;   // sin paréntesis
    int cant   = numeros.Count;     // sin paréntesis
    
    var pares = Enumerable.Range(1, 10).WhereNot(n => n % 2 == 0);

    Miembros estáticos de extensión

    Una de las capacidades nuevas más potentes: agregar métodos de fábrica estáticos a tipos que no controlamos.

    // Extender DateTime con métodos de fábrica estáticos
    public extension DateTimeExtensions(DateTime _)
    {
        // Miembro estático: se llama como DateTime.HoyArgentina()
        public static DateTime HoyArgentina()
        {
            var tz = TimeZoneInfo.FindSystemTimeZoneById("Argentina Standard Time");
            return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz);
        }
    
        // Propiedad de instancia: sin paréntesis
        public bool EsFinDeSemana => DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
    
        public string FormatoArgentino() => ToString("dd/MM/yyyy HH:mm");
    }
    
    // Uso
    var hoy = DateTime.HoyArgentina();      // método estático
    bool finde = DateTime.Now.EsFinDeSemana; // propiedad de instancia
    string fmt = DateTime.Now.FormatoArgentino(); // método de instancia

    Extensiones para tipos del framework que usamos todos los días

    // Extensiones útiles para string
    public extension StringExtensions(string source)
    {
        public bool IsNullOrEmpty => string.IsNullOrEmpty(source);
        public bool IsNullOrWhiteSpace => string.IsNullOrWhiteSpace(source);
        public string Truncar(int maxLength) 
            => source.Length <= maxLength ? source : source[..maxLength] + "...";
        public string ToCamelCase()
        {
            if (source.IsNullOrEmpty) return source;
            return char.ToLowerInvariant(source[0]) + source[1..];
        }
    }
    
    // Extensiones para HttpClient
    public extension HttpClientExtensions(HttpClient client)
    {
        public async Task<T?> GetJsonAsync<T>(string url)
        {
            var response = await client.GetAsync(url);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadFromJsonAsync<T>();
        }
    }
    
    // Uso
    string nombre = "MiServicio";
    Console.WriteLine(nombre.ToCamelCase());  // "miServicio"
    Console.WriteLine(nombre.Truncar(5));     // "MiSer..."
    
    var usuario = await httpClient.GetJsonAsync<Usuario>("/api/usuarios/1");

    Diferencias clave con los métodos de extensión clásicos

    CaracterísticaMétodos de extensión (clásico)Extension members (C# 14)
    Métodos de instancia
    Propiedades de instancia
    Miembros estáticos
    Agrupación en bloque❌ (clase estática separada)
    Compatibilidad hacia atrás✅ C# 3+C# 14+ únicamente

    ← La palabra clave field en C# 14: adiós para siempre a los backing fields repetitivos | Serie .NET 8 → .NET 10 | Próximo: LINQ en .NET 9 y .NET 10: CountBy, AggregateBy, Index() y las mejoras que cambian cómo consultás datos →

  • La palabra clave field en C# 14: adiós para siempre a los backing fields repetitivos

    Hay un patrón que repito en cada clase de dominio: propiedad con validación en el setter, campo privado de respaldo, tres líneas de código para lo que debería ser una. Con la palabra clave field en C# 14, eso terminó. El compilador genera el backing field y yo solo escribo la lógica.

    El problema que resuelve

    En C# siempre hubo una tensión: las auto-properties son concisas pero no permiten lógica en get/set. Si necesitás validar, transformar o notificar en un setter, tenés que sacrificar la sintaxis limpia y declarar un campo privado manualmente.

    // ❌ Antes de C# 14: el campo de respaldo manual es inevitable
    public class Usuario
    {
        private string _nombre = string.Empty;  // campo privado repetitivo
        private int _edad;
        private string _email = string.Empty;
    
        public string Nombre
        {
            get => _nombre;
            set => _nombre = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
        }
    
        public int Edad
        {
            get => _edad;
            set => _edad = value is >= 0 and <= 150
                ? value
                : throw new ArgumentOutOfRangeException(nameof(value));
        }
    
        public string Email
        {
            get => _email;
            set => _email = value?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(value));
        }
    }
    
    // ✅ C# 14: field — el compilador genera el backing field
    public class Usuario
    {
        public string Nombre
        {
            get => field;
            set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
        }
    
        public int Edad
        {
            get => field;
            set => field = value is >= 0 and <= 150
                ? value
                : throw new ArgumentOutOfRangeException(nameof(value));
        }
    
        public string Email
        {
            get => field;
            set => field = value?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(value));
        }
    }

    Casos de uso reales

    Valor con lazy initialization

    // Antes: campo privado para lazy init
    public class Configuracion
    {
        private string? _cadenaConexion;
    
        public string CadenaConexion
        {
            get => _cadenaConexion ??= CargarDesdeConfiguracion();
        }
    }
    
    // C# 14: mucho más limpio
    public class Configuracion
    {
        public string CadenaConexion
        {
            get => field ??= CargarDesdeConfiguracion();
        }
    }

    INotifyPropertyChanged sin campos privados

    // Muy útil en ViewModels de WPF/MAUI/Blazor
    public class ProductoViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;
    
        public string Nombre
        {
            get => field;
            set
            {
                if (field == value) return;
                field = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Nombre)));
            }
        }
    
        public decimal Precio
        {
            get => field;
            set
            {
                if (field == value) return;
                field = value > 0 ? value : throw new ArgumentException("Precio debe ser positivo");
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Precio)));
            }
        }
    }

    Propiedades de solo init con validación

    // init-only properties con validación — imposible antes sin campo privado
    public class Pedido
    {
        public required string NumeroPedido
        {
            get => field;
            init => field = string.IsNullOrWhiteSpace(value)
                ? throw new ArgumentException("Número de pedido no puede estar vacío")
                : value.ToUpperInvariant();
        }
    
        public DateTime FechaCreacion
        {
            get => field;
            init => field = value == default ? DateTime.UtcNow : value;
        }
    }
    
    // Uso con object initializer — sin cambios en el lado del consumidor
    var pedido = new Pedido { NumeroPedido = "ord-123" };
    Console.WriteLine(pedido.NumeroPedido); // "ORD-123" (uppercase automático)

    field es contextual: no rompe código existente

    field es una palabra clave contextual: solo tiene el significado especial dentro de un getter o setter de propiedad. Si tenés una variable local, parámetro o campo llamado field en otro contexto, no hay conflicto. El compilador sabe por el contexto qué significa.

    // field fuera de una propiedad: variable normal
    var field = "esto es una variable";
    Console.WriteLine(field);  // sin problema
    
    // field en parámetro de método: sin conflicto
    void Procesar(string field) => Console.WriteLine(field);
    
    // Solo dentro de get/set de una propiedad tiene el significado especial
    public string Nombre { get => field; set => field = value; }

    El resultado: código más limpio, misma semántica

    En un modelo de dominio con 12 entidades, la adopción de field eliminó 47 campos privados de respaldo. El código resultante es más fácil de leer, más fácil de mantener y genera exactamente el mismo IL que la versión con campos explícitos. Zero costo en runtime, máximo beneficio en legibilidad.


    ← C# 13: params modernos, Lock de primera clase y Task.WhenEach | Serie .NET 8 → .NET 10 | Próximo: Extension Members en C# 14/15: propiedades y miembros estáticos de extensión como nunca antes →

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

  • C# 12: Primary Constructors, Collection Expressions y el código que debería haber existido siempre

    C# 12 llegó con .NET 8 y fue la versión que más cambió mi forma de escribir código en el día a día. Los Primary Constructors me sacaron dos tercios del boilerplate de mis servicios. Las Collection Expressions me hicieron olvidar cuándo usar new List<> vs new[] vs Array.Empty(). Repaso todo lo que realmente uso.

    Primary Constructors: adiós al boilerplate de inyección de dependencias

    Antes de C# 12, cada servicio que necesitaba inyección de dependencias requería declarar campos privados, un constructor y asignaciones manuales. Con Primary Constructors, todo eso desaparece.

    // ❌ Antes (C# 11 y anteriores): mucho código repetitivo
    public class PedidoService
    {
        private readonly IRepositorio _repo;
        private readonly ILogger<PedidoService> _logger;
        private readonly IEmailService _email;
    
        public PedidoService(
            IRepositorio repo,
            ILogger<PedidoService> logger,
            IEmailService email)
        {
            _repo   = repo;
            _logger = logger;
            _email  = email;
        }
    
        public async Task ProcesarAsync(Pedido pedido)
        {
            _logger.LogInformation("Procesando pedido {Id}", pedido.Id);
            await _repo.GuardarAsync(pedido);
            await _email.NotificarAsync(pedido.ClienteEmail);
        }
    }
    
    // ✅ C# 12: Primary Constructor — el compilador genera el backing field
    public class PedidoService(
        IRepositorio repo,
        ILogger<PedidoService> logger,
        IEmailService email)
    {
        public async Task ProcesarAsync(Pedido pedido)
        {
            logger.LogInformation("Procesando pedido {Id}", pedido.Id);
            await repo.GuardarAsync(pedido);
            await email.NotificarAsync(pedido.ClienteEmail);
        }
    }

    Los parámetros del primary constructor son accesibles en todo el cuerpo de la clase. No son campos públicos — son parámetros capturados. Si necesitás exponerlos como propiedades, lo hacés explícitamente:

    // Primary constructor en record (siempre existió)
    public record Producto(string Nombre, decimal Precio, int Stock);
    
    // Primary constructor en clase con propiedad explícita
    public class Configuracion(string connectionString, int timeout)
    {
        public string ConnectionString { get; } = connectionString;
        public int TimeoutSeconds { get; } = timeout > 0 ? timeout : 30;
    }

    Collection Expressions: una sintaxis para todos los tipos de colección

    Antes de C# 12, la forma de inicializar una colección dependía de su tipo. Un array era new[] { 1, 2, 3 }, una lista era new List<int> { 1, 2, 3 }, un span era algo diferente. C# 12 unifica todo con [].

    // ✅ C# 12: misma sintaxis para todos los tipos
    int[]          array   = [1, 2, 3];
    List<int>      lista   = [4, 5, 6];
    Span<int>      span    = [7, 8, 9];
    IEnumerable<int> seq  = [10, 11, 12];
    ReadOnlySpan<int> ros  = [13, 14, 15];
    
    // Colección vacía — sin ambigüedades
    List<string> vacia = [];   // antes: new List<string>() o Array.Empty<string>()
    
    // Spread operator (..) para combinar colecciones
    int[] primeros = [1, 2, 3];
    int[] segundos = [4, 5, 6];
    int[] todos    = [..primeros, ..segundos, 7, 8];  // [1,2,3,4,5,6,7,8]
    
    // Muy útil en APIs: combinar listas de validación
    var reglas = [..reglasBase, ..reglasEspecificas, nuevaRegla];

    Using aliases para cualquier tipo

    Antes de C# 12, using solo podía crear alias para tipos nombrados. Ahora puede crear alias para cualquier tipo: tuplas, arrays, punteros, tipos genéricos.

    // C# 12: aliases para tipos complejos
    using Coordenadas   = (double Latitud, double Longitud);
    using MatrizInt     = int[][];
    using DiccionarioId = System.Collections.Generic.Dictionary<int, string>;
    using Callback      = System.Action<string, int>;
    
    // Uso en el código — mucho más legible
    Coordenadas ubicacion = (Latitud: -34.6037, Longitud: -58.3816);
    DiccionarioId usuarios = new() { { 1, "Mauro" }, { 2, "Juan" } };

    Inline Arrays: buffers de alto rendimiento sin unsafe

    Los Inline Arrays son structs con un tamaño fijo definido en compilación. Se almacenan completamente en el stack, sin asignaciones en el heap. Útiles para código de sistema o cuando necesitás buffers de tamaño conocido con rendimiento máximo.

    "[System.Runtime.CompilerServices.InlineArray(8)]
    public struct Buffer8<T>
    {
        private T _element; // El compilador genera 8 slots contiguos en memoria
    }
    
    // Uso: se comporta como un array pero sin allocations
    Buffer8<int> buffer;
    buffer[0] = 100;
    buffer[7] = 800;
    // Todo en el stack — el GC no ve nada de esto

    Lambdas con parámetros opcionales y params

    // C# 12: parámetros opcionales en lambdas
    var saludar = (string nombre, string prefijo = "Hola") => $"{prefijo}, {nombre}!";
    Console.WriteLine(saludar("Mauro"));          // "Hola, Mauro!"
    Console.WriteLine(saludar("Mauro", "Buenas")); // "Buenas, Mauro!"
    
    // params en lambdas
    var sumar = (params int[] numeros) => numeros.Sum();
    Console.WriteLine(sumar(1, 2, 3, 4, 5));  // 15

    El impacto en el código real

    En un proyecto de tamaño mediano que migré a C# 12, los Primary Constructors redujeron el código de los servicios en aproximadamente un 30%. No es solo cosmético: menos código es menos superficie de error. Las Collection Expressions eliminaron la pregunta «¿new List<> o Array.Empty<>?» de los code reviews. El spread operator simplificó docenas de patrones de concatenación de listas que antes requerían AddRange o LINQ Concat.


    ← De .NET 8 a .NET 10: qué cambió, qué mejoró y por qué conviene actualizar ya | Serie .NET 8 → .NET 10 | Próximo: C# 13: params modernos, Lock de primera clase y Task.WhenEach →

  • De .NET 8 a .NET 10: qué cambió, qué mejoró y por qué conviene actualizar ya

    Trabajo con .NET desde los tiempos de .NET Framework 4.5. Cada nueva versión traía algo interesante pero también algo que romper. El salto de .NET 8 a .NET 10 es diferente: es una consolidación. Rendimiento extremo, sintaxis más limpia, herramientas que finalmente hacen lo que uno espera. Esta serie cubre todo lo que vale la pena conocer.

    La política de versiones de .NET: qué es LTS y por qué importa

    Microsoft lanza una versión mayor de .NET cada noviembre. Las versiones pares son LTS (Long Term Support) — soporte por 3 años. Las impares son STS (Standard Term Support) — soporte por 18 meses. En producción, siempre apunto a LTS.

    VersiónTipoC#Fin de soporteEstado
    .NET 8LTSC# 12Nov 2026✅ En soporte
    .NET 9STSC# 13May 2026⚠️ Fin pronto
    .NET 10LTSC# 14Nov 2028✅ Actual recomendado

    Lo más importante de cada versión en un vistazo

    .NET 8 / C# 12 (noviembre 2023)

    • Primary constructors en clases y structs
    • Collection expressions: [1, 2, 3] para arrays, listas y spans
    • Spread operator .. para combinar colecciones
    • Inline arrays de alto rendimiento
    • Using aliases para cualquier tipo (incluso tuplas y punteros)
    • Lambdas con parámetros opcionales y params

    .NET 9 / C# 13 (noviembre 2024)

    • params ReadOnlySpan<T>: cero asignaciones en heap para métodos variádicos
    • System.Threading.Lock: tipo de primera clase para exclusión mutua
    • Task.WhenEach: procesar tareas a medida que terminan
    • LINQ: CountBy(), AggregateBy(), Index()
    • Nuevos miembros partial en clases parciales
    • OpenAPI built-in en Minimal APIs

    .NET 10 / C# 14 (noviembre 2025)

    • Palabra clave field: acceso al backing field autogenerado
    • Extension members: propiedades y miembros estáticos de extensión
    • Stack allocation inteligente para arrays pequeños
    • Native AOT mejorado: binarios más pequeños, arranque más rápido
    • dotnet run script.cs: ejecutar archivos sin proyecto
    • Mejoras profundas en el GC

    ¿Por qué actualizar desde .NET 8 a .NET 10 ahora?

    Si estás en .NET 8 LTS, no hay urgencia — tiene soporte hasta noviembre 2026. Pero .NET 10 ofrece mejoras de rendimiento concretas que se traducen en menor uso de memoria, menor latencia y binarios más chicos. En mis APIs en producción, la migración de .NET 8 a .NET 10 redujo el uso de memoria de los pods en Kubernetes entre un 15 y un 25%.

    # Verificar versión instalada
    dotnet --version
    
    # Instalar .NET 10 SDK
    # Linux/WSL:
    wget https://dot.net/v1/dotnet-install.sh
    bash dotnet-install.sh --channel 10.0
    
    # Ver SDKs disponibles
    dotnet --list-sdks
    
    # Cambiar un proyecto a .NET 10
    # En el .csproj:
    # <TargetFramework>net10.0</TargetFramework>

    El contexto: .NET unificado, multiplataforma y de alto rendimiento

    Desde .NET 5, Microsoft unificó el ecosistema: ya no hay .NET Framework, .NET Core y Xamarin como caminos separados. Todo es .NET. Corre en Windows, Linux, macOS, Android, iOS, WASM. Un solo SDK, una sola librería estándar, un solo modelo de deployment.

    Para los que venimos del mundo de .NET Framework y ASP.NET MVC clásico, este es el contexto del que partimos. Para los que arrancaron directamente con .NET Core: la dirección es la misma, pero el ritmo de mejoras se aceleró. Esta serie cubre exactamente qué cambió y cómo aprovecharlo.


    Serie .NET 8 → .NET 10 | Próximo: C# 12: Primary Constructors, Collection Expressions y el código que debería haber existido siempre →

  • Consultas Linq en Query Syntax y Method Syntax en .net con c#

    LINQ : Language Integrated Query – Wikipedia, la enciclopedia libre

    Como su nombre lo dice, es lenguaje para realizar consultas integradas, y es provisto y administrado por .NET

    Aprende a usarlo, te permitirá recorrer cualquier colección (Enumerables, Dictioraries, Lists, etc)

    También cada resultado proveniente de una consulta con el ORM de EF (Entity Framework ) te devolverá un IQueriable (Un tipo de colleción), y su manipulación, sin dudas los harás mediante LINQ.

    Para que puedas aprender en el siguiente video vemos varias consultas para que puedas realizar, y sobre todo para que puedas comparar los dos métodos disponibles que tienes:

    Method Sintax & Query Sintax

    Sin mas nada que decir, aquí está el video:

    Consultas Linq en Query Syntax y Method Syntax en .net con c# – YouTube

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)