← maurobernal.com.ar

Etiqueta: ASP.NET Core

  • GraphQL en .NET con Hot Chocolate: De los Conceptos Básicos a DataLoaders y Subscriptions

    Durante años trabajé exclusivamente con REST. Funciona, escala, es predecible — no tenía razones para cambiar. Hasta que me tocó construir una API para una app móvil que consumía cinco endpoints distintos para armar una sola pantalla. Ahí entendí el problema de under-fetching en carne propia. Empecé a explorar GraphQL y, en el ecosistema .NET, el camino lleva inevitablemente a Hot Chocolate. En este artículo cuento cómo funciona, con ejemplos que van desde la configuración básica hasta DataLoaders y Subscriptions en tiempo real.

    ¿Qué es GraphQL y por qué importa?

    GraphQL es un lenguaje de consulta para APIs que expone un único endpoint. El cliente define exactamente qué datos necesita — ni más, ni menos. Esto resuelve dos problemas clásicos de REST:

    • Over-fetching: el endpoint devuelve 30 campos y el cliente usa 5.
    • Under-fetching: para armar una pantalla necesitás llamar a 4 endpoints distintos.

    Hot Chocolate es la implementación de GraphQL para .NET más completa y activa que existe. Es modular, se integra nativamente con ASP.NET Core y Entity Framework Core, y su enfoque Code-First (definir el esquema desde clases C#) hace que el onboarding sea natural para cualquier desarrollador .NET.

    1. Configuración Inicial y Queries Básicas

    El punto de entrada: registrar el servidor GraphQL en el contenedor de DI y mapear el endpoint. Una Query en GraphQL es el equivalente a GET en REST — define qué datos pueden leer los clientes.

    // Program.cs
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services
        .AddGraphQLServer()
        .AddQueryType<Query>();
    
    var app = builder.Build();
    
    app.MapGraphQL(); // Endpoint por defecto: /graphql
    
    app.Run();
    
    // Query.cs
    public class Query
    {
        public string GetHolaMundo() => "¡Hola desde Hot Chocolate!";
    
        public Usuario GetUsuarioActual() => new Usuario { Id = 1, Nombre = "Admin" };
    }
    
    public class Usuario
    {
        public int Id { get; set; }
        public string Nombre { get; set; }
    }

    Hot Chocolate genera automáticamente el esquema GraphQL a partir de las clases C#. Al navegar a /graphql, tenés acceso al playground interactivo Banana Cake Pop donde podés explorar y ejecutar queries.

    2. Relaciones Anidadas con Projections y EF Core

    Acá empieza la magia real. El atributo [UseProjection] intercepta la query GraphQL del cliente y genera dinámicamente el SELECT SQL correspondiente. Si el cliente pide solo nombre y titulo, la base de datos devuelve solo esas columnas. Sin traer entidades completas a memoria.

    public class Autor
    {
        public int Id { get; set; }
        public string Nombre { get; set; }
        public ICollection<Libro> Libros { get; set; }
    }
    
    public class Libro
    {
        public int Id { get; set; }
        public string Titulo { get; set; }
        public int AutorId { get; set; }
    }
    
    public class Query
    {
        [UseProjection]
        public IQueryable<Autor> GetAutores([Service] AppDbContext context)
            => context.Autores;
    }
    
    /* Query del cliente — una sola petición, datos exactos:
    query {
      autores {
        nombre
        libros {
          titulo
        }
      }
    }
    */

    3. Filtros y Ordenamiento Dinámico

    Con [UseFiltering] y [UseSorting], el cliente puede enviar argumentos complejos de búsqueda que Hot Chocolate traduce automáticamente a expresiones LINQ. El orden de los atributos importa: primero Projections, luego Filtering, luego Sorting.

    // Program.cs — registrar las capacidades
    builder.Services.AddGraphQLServer()
        .AddFiltering()
        .AddSorting();
    
    // Query.cs
    public class Query
    {
        [UseProjection]
        [UseFiltering]
        [UseSorting]
        public IQueryable<Libro> GetLibros([Service] AppDbContext context)
            => context.Libros;
    }
    
    /* Query del cliente con filtro y ordenamiento:
    query {
      libros(
        where: { titulo: { contains: "GraphQL" } }
        order: [ { id: DESC } ]
      ) {
        id
        titulo
      }
    }
    */

    Todo esto se traduce a una sola query SQL optimizada. Sin código adicional en el servidor.

    4. Mutations: Escritura de Datos

    Las Mutations son el equivalente a POST/PUT/DELETE en REST. Lo interesante es que devuelven un tipo de dato (Payload), permitiendo al cliente mutar y consultar el nuevo estado en la misma operación.

    // Program.cs
    builder.Services.AddGraphQLServer()
        .AddMutationType<Mutation>();
    
    // Mutation.cs
    public class Mutation
    {
        public async Task<LibroPayload> CrearLibroAsync(
            CrearLibroInput input,
            [Service] AppDbContext context)
        {
            var nuevoLibro = new Libro
            {
                Titulo = input.Titulo,
                AutorId = input.AutorId
            };
    
            context.Libros.Add(nuevoLibro);
            await context.SaveChangesAsync();
    
            return new LibroPayload(nuevoLibro);
        }
    }
    
    public record CrearLibroInput(string Titulo, int AutorId);
    public record LibroPayload(Libro Libro);
    
    /* Mutation del cliente — crea y consulta en una sola operación:
    mutation {
      crearLibro(input: { titulo: "Mastering Hot Chocolate", autorId: 1 }) {
        libro {
          id
          titulo
        }
      }
    }
    */

    5. DataLoaders: Eliminando el Problema N+1

    El talón de Aquiles de cualquier implementación GraphQL ingenua es el problema N+1: si consultás 10 libros y cada libro necesita resolver su autor, terminás con 1 query para los libros + 10 queries individuales para los autores = 11 queries en total. Con un dataset real de 1000 libros, eso es un desastre.

    Los DataLoaders resuelven esto agrupando todas las peticiones de autores en un solo lote y ejecutándolas en una única query a la base de datos, con caché automático por ciclo de vida de la petición.

    public class AutorBatchDataLoader : BatchDataLoader<int, Autor>
    {
        private readonly AppDbContext _dbContext;
    
        public AutorBatchDataLoader(
            IBatchScheduler batchScheduler,
            DataLoaderOptions options,
            AppDbContext dbContext)
            : base(batchScheduler, options)
        {
            _dbContext = dbContext;
        }
    
        protected override async Task<IReadOnlyDictionary<int, Autor>> LoadBatchAsync(
            IReadOnlyList<int> keys,
            CancellationToken cancellationToken)
        {
            // 1000 libros = 1 sola query con WHERE Id IN (...)
            return await _dbContext.Autores
                .Where(a => keys.Contains(a.Id))
                .ToDictionaryAsync(a => a.Id, cancellationToken);
        }
    }
    
    // Extensión de tipo para resolver el autor de cada libro usando el DataLoader
    [ExtendObjectType(typeof(Libro))]
    public class LibroExtensions
    {
        public async Task<Autor> GetAutorAsync(
            [Parent] Libro libro,
            AutorBatchDataLoader dataLoader,
            CancellationToken cancellationToken)
            => await dataLoader.LoadAsync(libro.AutorId, cancellationToken);
    
        [GraphQLName("tituloEnMayusculas")]
        public string GetTituloMayusculas([Parent] Libro libro)
            => libro.Titulo.ToUpper();
    }
    
    // Program.cs — registrar la extensión
    // builder.Services.AddGraphQLServer().AddType<LibroExtensions>();

    6. Subscriptions: Datos en Tiempo Real

    Esto es lo que REST no puede hacer de forma nativa: el cliente se suscribe a un evento y el servidor le notifica automáticamente cuando ocurre. Hot Chocolate implementa Subscriptions sobre WebSockets, lo que es ideal para notificaciones en vivo, dashboards en tiempo real o chats.

    // Program.cs — habilitar WebSockets y Subscriptions
    app.UseWebSockets();
    
    builder.Services.AddGraphQLServer()
        .AddSubscriptionType<Subscription>()
        .AddInMemorySubscriptions(); // Para un solo servidor; en cluster usar Redis
    
    // Subscription.cs
    public class Subscription
    {
        [Subscribe]
        [Topic("LibroCreado")]
        public Libro OnLibroCreado([EventMessage] Libro libro) => libro;
    }
    
    // En la Mutation, publicar el evento al crear un libro
    public class Mutation
    {
        public async Task<LibroPayload> CrearLibroAsync(
            CrearLibroInput input,
            [Service] AppDbContext context,
            [Service] ITopicEventSender eventSender,
            CancellationToken cancellationToken)
        {
            var nuevoLibro = new Libro { Titulo = input.Titulo, AutorId = input.AutorId };
            context.Libros.Add(nuevoLibro);
            await context.SaveChangesAsync(cancellationToken);
    
            // Notificar a todos los clientes suscritos
            await eventSender.SendAsync("LibroCreado", nuevoLibro, cancellationToken);
    
            return new LibroPayload(nuevoLibro);
        }
    }
    
    /* Suscripción del cliente (WebSocket):
    subscription {
      onLibroCreado {
        id
        titulo
      }
    }
    */

    La primera vez que vi esto funcionar en un dashboard en tiempo real, conectado a una app móvil y una web simultáneamente, entendí por qué GraphQL cambió la forma en que pensamos las APIs.

    GraphQL vs. REST: ¿Cuándo usar cada uno?

    No es una competencia — son herramientas para contextos distintos. Mi criterio:

    • Usá GraphQL cuando: tenés múltiples clientes (web, móvil, terceros) con necesidades de datos distintas, o cuando necesitás tiempo real con Subscriptions.
    • Usá REST cuando: la API es pública y necesita ser consumida por herramientas genéricas (cURL, Postman básico, integraciones externas simples), o cuando el equipo no tiene experiencia con GraphQL y el deadline no da margen.

    En proyectos complejos, también conviven: REST para endpoints públicos y webhooks, GraphQL para el frontend propio.

    Conclusión

    Hot Chocolate es, en mi opinión, la implementación más completa de GraphQL en cualquier ecosistema. La integración con EF Core, los DataLoaders para resolver N+1, las Subscriptions sobre WebSockets y el enfoque Code-First lo hacen una opción sólida para proyectos empresariales en .NET.

    Si venís de REST y nunca probaste GraphQL, el primer paso es levantar un proyecto con la configuración básica y explorar el playground Banana Cake Pop. La curva de aprendizaje inicial es real, pero se amortiza rápido en proyectos con múltiples clientes o necesidades de datos complejas.

    ¿Tenés dudas sobre cómo migrar una API REST existente a GraphQL o cómo manejar autenticación y autorización en el esquema? Dejalo en los comentarios.

  • Novedades en .NET 11 Preview 2 (Marzo 2026): IA, Rendimiento y Blazor

    Microsoft lanzó en marzo de 2026 la segunda Preview de .NET 11, y la lista de novedades es suficientemente interesante como para que valga la pena revisarla ahora, sin esperar al release final de noviembre. Lo que se ve en esta preview marca la dirección del ecosistema: más IA integrada en el ORM, mejor observabilidad por defecto, y optimizaciones del runtime que van a impactar en aplicaciones de alta concurrencia. Acá te cuento lo más relevante.

    1. Entity Framework Core: IA y Funciones Avanzadas de SQL Server

    Índices Vectoriales DiskANN y VECTOR_SEARCH()

    Las versiones anteriores introdujeron soporte básico para embeddings vectoriales. .NET 11 da el siguiente paso: integración nativa con DiskANN, el algoritmo de búsqueda vectorial aproximada ultrarrápido de SQL Server y Azure SQL. Esto permite correr consultas de tipo RAG (Retrieval Augmented Generation) a escala masiva sin degradar el rendimiento transaccional de la base de datos. En proyectos donde combinamos búsqueda semántica con lógica de negocio tradicional, esto es un cambio significativo.

    // Configuración del índice vectorial DiskANN en el DbContext
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Articulo>()
            .HasIndex(a => a.Embedding)
            .HasMethod("diskann"); // Índice de alto rendimiento
    }
    
    // Consulta LINQ que usa la función nativa VECTOR_SEARCH
    ReadOnlyMemory<float> vectorUsuario = ObtenerVectorDelPrompt();
    
    var articulosSimilares = await context.Articulos
        .Where(a => EF.Functions.VectorSearch(a.Embedding, vectorUsuario))
        .Take(10)
        .ToListAsync();

    JSON_CONTAINS y Soporte Full-Text

    Trabajar con columnas JSON en bases de datos relacionales siempre fue un poco incómodo: o traías todo a memoria y deserializabas, o escribías SQL crudo. EF Core 11 expone EF.Functions.JsonContains() que se traduce directamente a la instrucción optimizada de SQL Server. Además, ya podés crear catálogos e índices Full-Text directamente desde las migraciones, sin tocar SQL a mano.

    // Busca dentro del documento JSON sin traerlo a memoria
    var usuariosConPreferencia = await context.Usuarios
        .Where(u => EF.Functions.JsonContains(u.Configuraciones, "{\"Tema\":\"Oscuro\"}"))
        .ToListAsync();

    Soporte nativo para MaxBy y MinBy

    Pequeño pero muy práctico. Históricamente, obtener la entidad completa con el valor máximo de una propiedad requería un .OrderByDescending().FirstOrDefault() que generaba una query más pesada. Ahora .MaxByAsync() y .MinByAsync() se traducen directamente a la consulta SQL más eficiente según el proveedor.

    // Obtiene la entidad completa del empleado con el salario más alto
    var empleadoTop = await context.Empleados
        .Where(e => e.DepartamentoId == 5)
        .MaxByAsync(e => e.Salario);

    2. ASP.NET Core y Blazor: Observabilidad y Manejo de Estado

    Trazabilidad Nativa de OpenTelemetry

    OpenTelemetry ya era compatible con versiones anteriores, pero la integración requería configuración manual. En .NET 11 Preview 2, el rastreo de métricas, logs y trazas distribuidas viene activado desde los templates base. Esto es especialmente valioso en arquitecturas orquestadas con .NET Aspire: el viaje completo de una petición entre microservicios queda trazado sin escribir una línea extra de configuración.

    TempData en Blazor

    En MVC clásico, TempData era la forma estándar de pasar mensajes efímeros («Guardado exitosamente») que sobrevivían a una redirección HTTP. Blazor no tenía un equivalente nativo sencillo, lo que obligaba a usar workarounds con servicios de estado o query params. .NET 11 lo resuelve de forma limpia.

    @page "/formulario"
    @inject NavigationManager Nav
    
    <button class="btn" @onclick="Guardar">Guardar Perfil</button>
    
    @code {
        [TempData]
        public string MensajeExito { get; set; }
    
        private void Guardar()
        {
            // Lógica de guardado...
            MensajeExito = "¡El perfil se ha actualizado correctamente!";
            Nav.NavigateTo("/perfil"); // El mensaje sobrevive a esta redirección
        }
    }

    Simple, declarativo, y consistente con lo que ya conocían los desarrolladores que venían de MVC.

    Soporte para OpenAPI 3.2.0

    Las APIs creadas con .NET 11 soportan de fábrica el estándar OpenAPI 3.2.0 mediante el paquete interno Microsoft.AspNetCore.OpenApi. Documentación automática de contratos REST alineada con las mejores prácticas modernas, sin dependencias externas.

    3. Mejoras en el Runtime y Bibliotecas Base

    System.Text.Json Genérico y Optimizaciones AOT

    El soporte de JSON para compilación nativa (Native AOT) sigue madurando. La novedad es un selector genérico para metadatos (GetTypeInfo<T>) que permite a los desarrolladores de librerías navegar árboles de serialización de forma ultrarrápida, sin las penalizaciones clásicas del boxing ni la reflexión lenta. Si publicás paquetes NuGet, esto te va a interesar.

    Optimizaciones del JIT y Runtime Async V2

    Esta es la mejora que más me entusiasma a nivel de plataforma. El equipo de .NET está evolucionando cómo se compila el modelo async/await bajo el capó (la state machine que genera el compilador). El nuevo modelo Async V2 reduce significativamente las asignaciones de memoria en operaciones asíncronas, lo que se traduce en menos presión sobre el Garbage Collector en aplicaciones web de alta concurrencia. Combinado con el nuevo Cached Interface Dispatch (que mejora la velocidad del código genérico), el runtime de .NET 11 promete ser notablemente más eficiente.

    En producción, en APIs que manejan miles de requests por segundo, estas optimizaciones no son menores.

    4. Contenedores, F#, MAUI y más

    • Imágenes Docker más livianas: Las imágenes SDK de .NET para Linux y macOS se redujeron un 17%. Menos tiempo de descarga en CI/CD, pipelines más rápidos.
    • F# modernizado: La directiva #elif para compilación condicional, caché de resolución de sobrecargas y simplificaciones en la herencia de interfaces con Default Interface Methods (DIM). La comunidad de F# lo venía pidiendo hace tiempo.
    • .NET MAUI: Optimizaciones en TypedBindings y marcadores de inmutabilidad para elementos como Font y Color. El efecto práctico es que los motores de interfaz evitan re-renderizar pantallas cuando los valores no cambiaron, mejorando la fluidez visual.

    Conclusión

    .NET 11 Preview 2 no es solo una preview de mantenimiento: marca claramente las apuestas de Microsoft para el ciclo siguiente. IA integrada a nivel de ORM, observabilidad como ciudadano de primera clase, y un runtime async más eficiente son señales de hacia dónde va el ecosistema.

    No recomendaría usar una preview en producción, pero sí vale la pena explorarla en proyectos de laboratorio ya. Varias de estas features van a cambiar hábitos que tenemos muy arraigados —especialmente MaxByAsync, JsonContains y el soporte nativo de OpenTelemetry.

    ¿Alguna de estas novedades te genera dudas o querés ver un caso de uso más concreto? 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)aot (6)