← maurobernal.com.ar

Etiqueta: .NET 10

  • .NET Aspire: Desde su Origen hasta .NET 10 — La Guía Práctica

    Cuando Microsoft anunció .NET Aspire a finales de 2023, mi primera reacción fue escéptica. «Otro framework de orquestación», pensé. Me equivoqué bastante. Después de usarlo en proyectos reales —desde una API con tres microservicios hasta una arquitectura con colas, caché y modelos de IA local— cambió radicalmente cómo arranco proyectos distribuidos. En este artículo repaso qué es, cómo evolucionó de .NET 8 a .NET 10 y cómo puede convivir con tu stack de Kubernetes existente.

    ¿Qué es .NET Aspire y para quién está destinado?

    .NET Aspire es un stack opinado (opinionated) de herramientas, plantillas y paquetes NuGet diseñado para construir aplicaciones distribuidas, observables y listas para producción con .NET. La palabra clave ahí es opinado: toma decisiones por vos (OpenTelemetry por defecto, health checks incluidos, service discovery automático) para que vos no tengas que hacerlo.

    ¿Para quién es?

    • Equipos sin un DevOps dedicado: Facilita enormemente la orquestación local y el despliegue a la nube sin tener que escribir complejos archivos docker-compose.yml o manifiestos de K8s.
    • Desarrolladores que buscan productividad local: Permite levantar 5 microservicios, una base de datos PostgreSQL, un caché Redis y un broker RabbitMQ con un solo F5, autoconfigurando las cadenas de conexión y la inyección de dependencias entre ellos.
    • Proyectos que requieren observabilidad inmediata: Trae un Dashboard local que muestra logs, métricas, trazas distribuidas (OpenTelemetry) y variables de entorno en tiempo real, sin necesidad de configurar Grafana, Prometheus o Jaeger localmente.

    ¿Para quién NO es (o no es obligatorio)?

    Si sos un arquitecto que necesita control absoluto sobre el plano de red, service mesh y configuraciones avanzadas de orquestación, Aspire no tiene que reemplazar tu setup de producción. Pero sí puede ser tu entorno de desarrollo local mientras seguís gestionando todo con Helm y Kubernetes en prod. Más sobre esto al final.

    Línea de Tiempo: De .NET 8 a .NET 10

    El Origen: .NET 8 (Finales de 2023)

    Nació como la respuesta de Microsoft a la complejidad de configurar microservicios localmente. Se introdujeron los conceptos de AppHost (el proyecto orquestador) y Service Defaults (configuraciones base de OpenTelemetry y resiliencia). En esta primera versión estaba bastante acoplado al ecosistema de Azure, lo que generó resistencia en quienes trabajaban con otros proveedores cloud. Aun así, la experiencia de desarrollo local era notablemente mejor que cualquier alternativa existente.

    La Madurez: .NET 9 (Finales de 2024)

    Aspire se desvinculó de ser «solo para Azure». La comunidad explotó: aparecieron decenas de integraciones para AWS, GCP y contenedores genéricos. El Dashboard mejoró considerablemente en rendimiento y se introdujo una gestión más robusta del ciclo de vida de los contenedores locales. Para este momento ya lo estaba usando en proyectos reales sin dudar.

    El Estado Actual: .NET 10 (2025/2026)

    Con .NET 10, Aspire se convirtió en el estándar de facto para arquitecturas distribuidas en .NET. Las novedades principales:

    • Integración profunda con IA: Componentes nativos para orquestar contenedores de Ollama (modelos de IA locales), bases de datos vectoriales (Qdrant, Milvus) y configuración automática de Semantic Kernel.
    • Event-Driven & Dapr: Integración fluida con arquitecturas dirigidas por eventos y soporte mejorado para Dapr.
    • Manifiestos K8s Nativos: Mejoras en la exportación de la topología de Aspire hacia clústeres de Kubernetes, lo que facilita el trabajo de quienes administran sus propios servidores.

    Conceptos Clave

    AppHost: Tu docker-compose en C#

    El AppHost es un proyecto de consola de C# normal que actúa como el orquestador local. En lugar de YAML, usás C# para definir qué contenedores o proyectos de .NET deben ejecutarse juntos. Si sabés C#, ya sabés escribir tu orquestación.

    Service Discovery: Sin hardcodear puertos

    Aspire inyecta automáticamente las URLs de los servicios. Si tu API «A» necesita llamar a la API «B», no hardcodeás localhost:5001. Aspire resuelve los nombres dinámicamente usando el nombre que vos le asignaste. Esto elimina una categoría entera de bugs que antes aparecían en desarrollo («funciona en mi máquina»).

    Hosting & Client Integrations

    Son paquetes NuGet que vienen en pares: uno para el AppHost que levanta el contenedor (Aspire.Hosting.Redis), y otro para el microservicio que lo consume (Aspire.StackExchange.Redis). Este segundo paquete configura automáticamente el cliente con resiliencia, health checks y telemetría. Sin boilerplate.

    Ejemplos de Código (.NET 10)

    A. El Orquestador: Proyecto AppHost

    Este es el código que reemplaza a un docker-compose.yml. Cuando presionás F5 sobre este proyecto, Aspire levanta todos los contenedores, configura las variables de entorno y arranca los proyectos de .NET automáticamente.

    var builder = DistributedApplication.CreateBuilder(args);
    
    // 1. Contenedor de PostgreSQL con pgAdmin integrado
    var postgres = builder.AddPostgres("postgres-server")
        .WithPgAdmin()
        .AddDatabase("MiBaseDeDatos");
    
    // 2. Contenedor de Redis
    var cache = builder.AddRedis("redis-cache");
    
    // 3. Modelo de IA local con Ollama
    var ollama = builder.AddOllama("ia-local")
        .AddModel("llama3");
    
    // 4. API Backend: recibe referencias a todos los recursos
    var apiBackend = builder.AddProject<Projects.MiApiBackend>("backend-api")
        .WithReference(postgres)
        .WithReference(cache)
        .WithReference(ollama);
    
    // 5. Frontend Blazor: solo necesita saber dónde está la API
    builder.AddProject<Projects.MiFrontendBlazor>("frontend-web")
        .WithReference(apiBackend)
        .WithExternalHttpEndpoints();
    
    builder.Build().Run();

    Lo que más me llamó la atención la primera vez: no hay ninguna cadena de conexión hardcodeada. Aspire las inyecta automáticamente en las variables de entorno de cada proyecto al momento de arrancar.

    B. Consumiendo los recursos en la API

    En el Program.cs de tu microservicio, usás las integraciones de Aspire. El nombre "redis-cache" o "MiBaseDeDatos" se resuelve automáticamente al contenedor correspondiente.

    var builder = WebApplication.CreateBuilder(args);
    
    // Configura OpenTelemetry, health checks y resiliencia por defecto
    builder.AddServiceDefaults();
    
    // Se conecta al Redis del AppHost por nombre, sin IP ni puerto
    builder.AddRedisClient("redis-cache");
    
    // Se conecta a Postgres, también por nombre
    builder.AddNpgsqlDbContext<MiDbContext>("MiBaseDeDatos");
    
    var app = builder.Build();
    
    // Expone /health y /alive automáticamente
    app.MapDefaultEndpoints();
    
    app.MapGet("/datos", async (IDistributedCache cache, MiDbContext db) =>
    {
        // Usás los servicios como siempre, sin saber nada de cómo se conectaron
        return Results.Ok(new { Mensaje = "Conectado por Aspire!" });
    });
    
    app.Run();

    C. Service Discovery entre microservicios

    En el Frontend, si queremos llamar al Backend por HTTP, usamos el nombre registrado en el AppHost. Aspire intercepta y resuelve el puerto real en tiempo de ejecución.

    // En el Program.cs del Frontend Blazor
    builder.Services.AddHttpClient<BackendClient>(client =>
    {
        // "backend-api" es el nombre del proyecto en el AppHost
        // Aspire lo traduce al puerto correcto automáticamente
        client.BaseAddress = new Uri("http://backend-api"); 
    });

    El primer día que esto funcionó sin tocar un solo archivo de configuración ni abrir el launchSettings.json, entendí por qué la gente dice que Aspire cambia la forma de trabajar.

    ¿Y si uso Kubernetes en producción?

    Esta es la pregunta que más me hacen cuando presento Aspire. La respuesta corta: no son excluyentes.

    Podés adoptar .NET Aspire únicamente como tu entorno de desarrollo local. Tus desarrolladores clonan el repositorio, presionan F5 y Aspire se encarga de levantar las bases de datos en Docker, configurar las variables de entorno y mostrar las trazas de OpenTelemetry en el Dashboard. Luego, en el pipeline de CI/CD, el proyecto AppHost simplemente se ignora: compilás tus contenedores como siempre y los desplegás en K8s con tus manifiestos o Helm Charts.

    El resultado: productividad local extrema sin ceder ni un milímetro de control en producción. Para equipos que administran sus propios clústeres, es una combinación muy práctica.

    Conclusión

    .NET Aspire no es hype. Es una solución concreta a un problema real: configurar microservicios localmente es tedioso, propenso a errores y consume tiempo valioso de desarrollo. Con la evolución de .NET 8 a .NET 10 pasó de ser una herramienta interesante a ser el punto de partida obvio para cualquier arquitectura distribuida en .NET.

    Si todavía no lo probaste, mi recomendación es crear un proyecto nuevo con la plantilla de Aspire, agregar un Redis y un Postgres, y ver el Dashboard en acción. En menos de 20 minutos vas a entender por qué está cambiando la forma en que trabajamos.

    ¿Lo estás usando en algún proyecto? ¿Tenés dudas sobre cómo integrarlo con tu setup actual? Dejalo en los comentarios.

  • Novedades en Entity Framework Core: Todo lo que cambió de .NET 8 a .NET 10

    Si hay algo que me gusta de trabajar con .NET es que el ecosistema no se detiene. Hace unos años, cuando arranqué a usar Entity Framework, la discusión era «¿EF o Dapper?». Hoy esa discusión todavía existe, pero EF Core evolucionó tanto que la balanza se inclinó bastante. En este artículo repaso todas las novedades que llegaron desde .NET 8 hasta .NET 10, con ejemplos concretos y el contexto necesario para entender por qué cada feature importa.

    Novedades en EF Core 8 (.NET 8)

    La versión 8 fue la que me hizo replantear varios patrones que venía usando desde hace tiempo. Especialmente los Tipos Complejos, que resolvieron algo que siempre me molestó de las Owned Entities.

    1. Tipos Complejos (Complex Types)

    Cuando modelamos Value Objects en DDD, la lógica indica que un objeto como Dirección no tiene identidad propia: se define por sus atributos. Antes de EF8, si querías mapear esto usabas Owned Entities, que internamente requerían una clave primaria oculta y te generaban joins innecesarios. Con los Complex Types en EF Core 8, las propiedades del objeto se mapean directamente como columnas en la tabla del padre. Sin clave, sin tabla aparte, sin overhead.

    [ComplexType]
    public class Direccion
    {
        public string Calle { get; set; }
        public string Ciudad { get; set; }
    }
    
    public class Cliente
    {
        public int Id { get; set; }
        public string Nombre { get; set; }
        
        // Se mapeará como columnas Calle y Ciudad en la tabla Clientes
        public required Direccion DireccionResidencia { get; set; } 
    }

    En producción esto se traduce en columnas como DireccionResidencia_Calle y DireccionResidencia_Ciudad directamente en la tabla Clientes. Mucho más limpio y sin el overhead de las relaciones.

    2. Colecciones de Tipos Primitivos

    Otro punto de dolor histórico: guardar una lista de strings o enteros. La solución clásica era crear una tabla auxiliar o serializar a mano. EF Core 8 lo resuelve nativamente: en SQL Server serializa a JSON automáticamente, en PostgreSQL usa tipos de array nativos.

    public class Producto
    {
        public int Id { get; set; }
        public string Nombre { get; set; }
        public List<string> Etiquetas { get; set; } = new();
    }
    
    // EF Core traduce esto a funciones JSON en SQL Server
    var productos = context.Productos
        .Where(p => p.Etiquetas.Contains("Urgente"))
        .ToList();

    La primera vez que vi esto funcionar en un query real me sorprendió. EF genera el JSON_VALUE correspondiente en SQL Server y la consulta funciona sin traer entidades a memoria.

    3. Consultas SQL Nativas para Tipos No Mapeados (SqlQuery)

    Cuando necesitás ejecutar SQL crudo que retorna un escalar o un DTO que no está en tu DbContext, antes tenías que recurrir a ADO.NET o trucos con FromSqlRaw. Con SqlQuery<T> ya no.

    var salarios = await context.Database
        .SqlQuery<decimal>($"SELECT AVG(Salario) AS Value FROM Empleados")
        .ToListAsync();

    Lo que más me gusta es que podés componer LINQ encima de este resultado, combinando la potencia del SQL manual con las ventajas del query pipeline de EF.

    Novedades en EF Core 9 (.NET 9)

    Con .NET 9, el foco se desplazó hacia rendimiento extremo e inteligencia artificial. Dos temas que, honestamente, no esperaba ver tan rápido en el ORM.

    4. Búsqueda Vectorial (Vector Search)

    Si estás construyendo aplicaciones con IA generativa, tarde o temprano necesitás almacenar y consultar embeddings. EF Core 9 introduce soporte de primera clase para esto en SQL Server, PostgreSQL (con pgvector) y Cosmos DB. Esto es clave para patrones como RAG (Retrieval Augmented Generation).

    public class Documento
    {
        public int Id { get; set; }
        public string Contenido { get; set; }
        public ReadOnlyMemory<float> Embedding { get; set; }
    }
    
    ReadOnlyMemory<float> vectorBusqueda = ObtenerVector("¿cómo configuro autenticación?");
    
    var docs = await context.Documentos
        .OrderBy(d => EF.Functions.VectorDistance(d.Embedding, vectorBusqueda)) 
        .Take(5)
        .ToListAsync();

    En un proyecto donde implementé un chatbot sobre documentación interna, esto redujo el tiempo de integración a la mitad. Antes había que saltar a SDKs separados; ahora EF Core maneja el acceso al store vectorial junto con el resto de la lógica de datos.

    5. Optimizaciones de Rendimiento y Native AOT

    EF Core 9 habilita compatibilidad con compilación Native AOT, lo que significa arranques de aplicación drásticamente más rápidos y menor consumo de memoria. En microservicios y funciones serverless esto no es un detalle: es la diferencia entre ser viable o no. El modelo compilado pre-genera el mapeo en tiempo de build, eliminando la reflexión en runtime.

    6. Mejoras en Actualizaciones y Borrados Masivos

    ExecuteUpdate y ExecuteDelete se introdujeron en EF7, pero en EF9 se potenciaron para soportar actualizaciones de Complex Types y traducir expresiones LINQ más avanzadas. Lo importante: siguen sin traer entidades a memoria.

    await context.Productos
        .Where(p => p.Categoria == "Herramientas")
        .ExecuteUpdateAsync(s => s.SetProperty(p => p.Precio, p => p.Precio * 1.10m));

    Una sola query SQL de UPDATE. Sin SaveChanges, sin tracking, sin roundtrips innecesarios.

    Novedades en EF Core 10 (.NET 10)

    .NET 10 se lanzó en noviembre de 2025 y la versión de EF Core que lo acompaña terminó de pulir varias aristas que la comunidad venía pidiendo. Algunas son pequeñas pero hacen una diferencia real en el día a día.

    7. Operadores Explícitos LeftJoin y RightJoin

    ¿Cuántas veces escribiste un GroupJoin + SelectMany + DefaultIfEmpty solo para hacer un LEFT JOIN? Demasiadas. EF Core 10 lo resuelve con operadores directos.

    var clientesConYsinPedidos = await context.Clientes
        .LeftJoin(
            context.Pedidos,
            c => c.Id,
            p => p.ClienteId,
            (c, p) => new { c.Nombre, PedidoId = p?.Id }
        ).ToListAsync();

    Mucho más legible. La intención queda clara en el código sin tener que descifrar la cascada de lambdas.

    8. Flexibilidad en ExecuteUpdateAsync (Lambdas)

    Las actualizaciones masivas ahora permiten lambdas regulares en lugar de árboles de expresión estrictos. Esto permite agrupar setters en bloques más limpios, especialmente cuando actualizás múltiples propiedades a la vez.

    await context.Productos.Where(p => p.Stock < 10)
        .ExecuteUpdateAsync(s => {
            s.SetProperty(p => p.RequiereReabastecimiento, true);
            s.SetProperty(p => p.FechaAviso, DateTime.UtcNow);
        });

    9. Columnas JSON Avanzadas para Tipos Complejos

    Combinando lo mejor de dos mundos: los Complex Types de EF8 ahora pueden mapearse directamente como documentos JSON en una sola columna, con capacidad de consulta LINQ nativa dentro de ese JSON.

    modelBuilder.Entity<Usuario>()
        .ComplexProperty(u => u.Preferencias, p => p.ToJson());
    
    // Consulta dentro del JSON sin traer todo a memoria
    var usuarios = await context.Usuarios
        .Where(u => u.Preferencias.Tema == "Dark")
        .ToListAsync();

    10. Filtros de Consulta Nombrados (Named Query Filters)

    Los global query filters (soft delete, multitenancy) son muy útiles, pero el problema siempre fue que para ignorar uno tenías que ignorarlos todos. EF Core 10 le pone nombre a cada filtro, permitiéndote desactivar solo el que necesitás.

    modelBuilder.Entity<Blog>().HasQueryFilter("FiltroBorrados", b => !b.EstaBorrado);
    
    // Solo ignoro el filtro de borrados, el de tenant sigue activo
    var blogs = await context.Blogs
        .IgnoreQueryFilter("FiltroBorrados")
        .ToListAsync();

    En aplicaciones multitenant con soft delete, esto es oro puro. Ya no hay que elegir entre ver los registros borrados o perder el filtro de tenant.

    11. Búsqueda Híbrida y Full-Text (Cosmos DB)

    Para los que trabajan con Cosmos DB y motores de búsqueda basados en IA, EF Core 10 introduce soporte nativo para Full-Text Search y Hybrid Search. Esta última combina similitud vectorial con búsqueda de palabras clave usando Reciprocal Rank Fusion, obteniendo lo mejor de ambos enfoques en una sola query.

    var resultados = await context.Documentos
        .OrderBy(x => EF.Functions.VectorDistance(x.Embedding, vectorUsuario)) 
        .ThenBy(x => EF.Functions.FullTextRank(x.Contenido, "término exacto")) 
        .Take(5)
        .ToListAsync();

    Resumen: La Evolución de EF Core en Tres Versiones

    Mirando el camino recorrido de .NET 8 a .NET 10, la evolución es clara:

    • .NET 8: Mejoras en modelado DDD (Complex Types), soporte nativo de colecciones primitivas, SQL más flexible.
    • .NET 9: Rendimiento extremo con Native AOT, primer soporte real para IA con embeddings vectoriales.
    • .NET 10: Ergonomía del desarrollador: sintaxis más limpia, JSON avanzado, filtros nombrados, búsqueda híbrida.

    Si tu proyecto aún corre en EF Core 6 o 7, te recomiendo migrar. No solo por las features nuevas, sino porque el rendimiento en operaciones masivas mejoró notablemente. Personalmente, lo que más uso en el día a día son los ExecuteUpdate/ExecuteDelete para evitar cargar entidades innecesariamente, y los Complex Types para modelar Value Objects sin el overhead de las Owned Entities.

    ¿Tenés dudas sobre alguna feature específica o querés ver un caso de uso más complejo? Dejalo en los comentarios.

Tags

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