En el desarrollo de aplicaciones empresariales modernas, uno de los desafíos más comunes es mantener una Web API rápida y receptiva mientras se ejecutan operaciones complejas en segundo plano. Ya sea procesar un pago, generar un informe masivo o sincronizar datos con sistemas externos, estas tareas pueden bloquear los hilos de ejecución, generar timeouts y degradar la experiencia del usuario final.

La solución a este problema no es aumentar indefinidamente los recursos del servidor, sino cambiar la arquitectura. En este artículo, exploraremos cómo implementar Azure Storage Queues dentro de una ASP.NET Core Web API para desacoplar la recepción de la solicitud de su procesamiento, construyendo un sistema más resiliente, escalable y preparado para la nube.

1. El Problema de la API Síncrona Clásica

Imaginemos un escenario cotidiano en un comercio electrónico: un cliente finaliza su compra y pulsa el botón Confirmar Pedido. En una arquitectura tradicional y puramente síncrona, el flujo sería el siguiente:

  1. El cliente envía una petición POST /api/orders.
  2. La API valida los datos del pedido.
  3. La API consulta el stock en la base de datos.
  4. La API procesa el pago a través de un proveedor externo (pasarela de pago).
  5. La API actualiza el inventario.
  6. La API envía un correo electrónico de confirmación.
  7. Finalmente, la API responde al cliente con un HTTP 200 OK.

El problema: Si la pasarela de pago tarda 3 segundos y el servicio de correo otros 2, el cliente ha esperado 5 segundos con el navegador colgado. En un pico de tráfico, como el Black Friday, los hilos del servidor se agotan rápidamente esperando respuestas de servicios externos, provocando timeouts (HTTP 408/504) y caídas del servicio.

2. ¿Por qué usar una Cola de Mensajes?

Una cola de mensajes actúa como un intermediario entre dos componentes: el productor (nuestra API) y el consumidor (un servicio de fondo o worker). La API ya no realiza el trabajo pesado; simplemente deja un mensaje en la cola y responde al cliente al instante.

Las ventajas de este patrón son inmediatas:

  • Desacoplamiento: La API no conoce los detalles del procesamiento. Puede haber uno o cien workers consumiendo de la cola sin que la API necesite cambiar una sola línea de código.
  • Resiliencia: Si el servicio de procesamiento de pagos está caído, los mensajes no se pierden. Permanecen en la cola de Azure hasta que un worker esté disponible para procesarlos.
  • Nivelación de Carga (Load Leveling): Un pico de 10,000 pedidos en un minuto no colapsa el sistema. La cola absorbe la carga y los workers la procesan a su propio ritmo constante.
  • Escalabilidad Independiente: Puedes escalar horizontalmente tu Web API para manejar más tráfico HTTP, y escalar tus workers por separado según la longitud de la cola.

3. Elegiendo la Tecnología: Azure Storage Queues

Microsoft Azure ofrece dos servicios principales de colas: Azure Storage Queues y Azure Service Bus. Aunque ambos resuelven el mismo problema fundamental, están diseñados para necesidades distintas.

Azure Storage Queues es la opción ideal cuando necesitas una solución simple y de bajo costo para crear un backlog de trabajo asíncrono, especialmente si tu aplicación ya utiliza otros servicios de Azure Storage.

4. Caso de Uso Real: Procesamiento de Órdenes de Compra

Vamos a construir un sistema para una plataforma de e-commerce. El objetivo es que, cuando un cliente confirme una compra, la experiencia sea instantánea y el procesamiento pesado ocurra tras bambalinas.

Flujo de la Arquitectura

  1. El Cliente pulsa Comprar.
  2. La Web API (Productor) recibe la solicitud, genera un OrderMessage (incluyendo un ID único) y lo envía a la cola order-processing.
  3. La Web API responde inmediatamente al cliente con un HTTP 202 Accepted y un ID de seguimiento.
  4. El Worker (Consumidor), un servicio de fondo basado en BackgroundService, lee mensajes de la cola.
  5. El Worker procesa el pago, actualiza el stock y envía la notificación.
  6. Si el worker termina con éxito, elimina el mensaje de la cola. Si falla, el mensaje vuelve a aparecer para ser reintentado.

5. Implementación Paso a Paso en .NET

5.1. Prerrequisitos y Configuración en Azure

Primero, necesitamos una Cuenta de Almacenamiento de Azure y una Cola dentro de ella.

Supongamos que nuestra cola se llama order-processing.

5.2. Paquetes NuGet y Configuración

En tu proyecto de ASP.NET Core, instala el paquete oficial de Azure:

dotnet add package Azure.Storage.Queues dotnet add package Azure.Identity

En tu appsettings.json, guarda el nombre de la cuenta y la cola:

{ "AzureStorage": { "QueueName": "order-processing", "StorageAccountName": "micuentaexpositor" } }

5.3. El Modelo del Mensaje

Definimos una clase para representar el mensaje. Es crucial incluir un identificador único desde el origen para poder implementar la idempotencia más adelante.

public class OrderMessage { public Guid OrderId { get; set; } public string CustomerEmail { get; set; } public List Items { get; set; } public DateTime CreatedAt { get; set; } }

5.4. El Productor: La Web API

Vamos a registrar QueueClient en el contenedor de inyección de dependencias y a crear un endpoint que encole el mensaje.

// En Program.cs, registra el cliente de cola var storageAccountName = builder.Configuration["AzureStorage:StorageAccountName"]; var queueName = builder.Configuration["AzureStorage:QueueName"];

builder.Services.AddSingleton(x => { var queueUri = new Uri($"https://.queue.core.windows.net/"); return new QueueClient(queueUri, new DefaultAzureCredential()); });

// En OrdersController [HttpPost] public async Task CreateOrder([FromBody] CreateOrderRequest request) { var orderMessage = new OrderMessage ;

var messagePayload = JsonSerializer.Serialize(orderMessage);
await _queueClient.SendMessageAsync(messagePayload);

return Accepted(new { TrackingId = orderMessage.OrderId, Status = "Recibido" });

}

5.5. El Consumidor: BackgroundService

Ahora, el componente más importante: el worker que lee de la cola. Utilizaremos la clase base BackgroundService proporcionada por ASP.NET Core.

public class OrderProcessingWorker : BackgroundService { private readonly QueueClient _queueClient; private readonly ILogger _logger;

public OrderProcessingWorker(QueueClient queueClient, ILogger<OrderProcessingWorker> logger)
{
    _queueClient = queueClient;
    _logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        var response = await _queueClient.ReceiveMessagesAsync(
            maxMessages: 10, 
            visibilityTimeout: TimeSpan.FromMinutes(2), 
            cancellationToken: stoppingToken);

        if (response.Value.Length > 0)
        {
            foreach (QueueMessage message in response.Value)
            {
                await ProcessMessageAsync(message, stoppingToken);
            }
        }
        else
        {
            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }
}

private async Task ProcessMessageAsync(QueueMessage message, CancellationToken token)
{
    try
    {
        var orderMessage = JsonSerializer.Deserialize<OrderMessage>(message.MessageText);
        _logger.LogInformation("Procesando orden {OrderId}...", orderMessage.OrderId);
        // Lógica de negocio aquí
        await _queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error al procesar el mensaje {MessageId}. Será reintentado automáticamente.", message.MessageId);
    }
}

}

5.6. Manejo de Errores y Poison Messages

En sistemas de colas, un poison message es un mensaje que no puede ser procesado correctamente. La estrategia consiste en verificar la propiedad DequeueCount del mensaje. Si ha sido leído de la cola más de un umbral (por ejemplo, 3 veces), lo consideramos un poison message.

const int maxDequeueCount = 3;

if (message.DequeueCount > maxDequeueCount) { _logger.LogError("Mensaje marcado como poison message.", message.MessageId); await _queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt); return; }

5.7. Idempotencia: La Regla de Oro

Un principio fundamental de la mensajería distribuida es que los brokers suelen garantizar una entrega al menos una vez (at-least-once). La clave es el OrderId que generamos en la API. En el worker, verificamos si ese OrderId ya fue procesado.

public async Task ProcessOrderAsync(OrderMessage message) { var existingOrder = await _dbContext.ProcessedOrders .FirstOrDefaultAsync(o => o.OrderId == message.OrderId);

if (existingOrder != null)
{
    return; // Ya fue procesado
}

// Procesar la orden...
_dbContext.ProcessedOrders.Add(new ProcessedOrder { OrderId = message.OrderId, ProcessedAt = DateTime.UtcNow });
await _dbContext.SaveChangesAsync();

}

5.8. El Rol del Visibility Timeout

Cuando un worker lee un mensaje, Azure no lo elimina inmediatamente, sino que lo hace invisible para otros workers durante el tiempo especificado. Si el worker actual termina con éxito y llama a DeleteMessageAsync, el mensaje desaparece permanentemente.

Si el worker se cae a mitad del procesamiento, al cabo de 2 minutos el mensaje se volverá visible de nuevo en la cola y otro worker podrá recogerlo.

6. Consideraciones de Diseño y Límites Técnicos

  • Tamaño del Mensaje: El límite es de 64 KB. Si tu payload es mayor, usa Azure Blob Storage.
  • Orden de los Mensajes: A diferencia de Service Bus, Storage Queues no garantiza un orden estricto FIFO.
  • Tiempo de Vida (TTL): Por defecto, un mensaje expira a los 7 días.
  • Concurrencia: Puedes desplegar múltiples instancias de tu BackgroundService.

7. Monitoreo y Observabilidad

Un sistema basado en colas no es un agujero negro. Es crucial observar qué está pasando.

  • Logs Estructurados: Utiliza ILogger para registrar cada paso.
  • Métricas de la Cola: Puedes consultar el número aproximado de mensajes en la cola con GetPropertiesAsync().

var properties = await _queueClient.GetPropertiesAsync(); int count = properties.Value.ApproximateMessagesCount;

Esta métrica es ideal para crear alertas.

8. Conclusión y Checklist de Implementación

Hemos recorrido el camino completo: desde una API síncrona y frágil hasta una arquitectura asíncrona y resiliente utilizando Azure Storage Queues.

Checklist antes de desplegar a producción

  • Idempotencia: ¿Mi worker puede procesar el mismo mensaje varias veces sin efectos secundarios?
  • Poison Messages: ¿Tengo una cola de dead-letter y lógica para mover mensajes fallidos tras N reintentos?
  • Visibility Timeout: ¿El tiempo de invisibilidad cubre el peor escenario de procesamiento?
  • Seguridad: ¿Uso DefaultAzureCredential o Identidades Administradas?
  • Tamaño de Mensaje: ¿Mis mensajes son menores a 64 KB?
  • Monitoreo: ¿Tengo logs para rastrear el flujo de un mensaje de principio a fin?

¿Cuándo migrar a Azure Service Bus?

Considera migrar a Azure Service Bus si necesitas:

  • Orden estricto FIFO (con sesiones).
  • Detección automática de duplicados.
  • Mensajes mayores a 64 KB de forma nativa.
  • Transacciones atómicas.
  • Dead-lettering automático sin lógica manual.

Por ahora, con Storage Queues y .NET, tienes todo lo necesario para construir un backend profesional, robusto y preparado para el crecimiento.

Referencias

    1. Esposito, Software Architecture with C# 12 and .NET 8. Packt Publishing, 2023.
  1. Microsoft Learn, Compare Azure Storage queues and Service Bus queues.
  2. Microsoft Learn, Quickstart: Azure Queue Storage client library - .NET.
  3. RFC 9110, HTTP Semantics.
    1. Nagel, Apps and Services with .NET 8. Packt Publishing, 2024.
    1. Skardon, Pragmatic Microservices with C# and Azure. Apress, 2023.
  4. Microsoft Docs, .NET Microservices Architecture for Containerized .NET Applications.