Un BackgroundService es un servicio hospedado que ejecuta trabajo en segundo plano dentro de una aplicación .NET.
Cuando un usuario se registra, quizá tengamos que hacer tres cosas:
- Guardar el usuario en la base de datos (10ms).
- Enviar un email de bienvenida (1s - 3s).
- Redimensionar su foto de perfil (500ms).
Si haces todo esto de forma secuencial (await), el usuario estará mirando una pantalla de carga durante casi 4 segundos. En tiempos de internet, eso es una eternidad.
Lo ideal sería:
Guardar en BBDD.
Responder al usuario: “200 OK - ¡Bienvenido!”.
En segundo plano: enviar el email y procesar la foto.
En ASP.NET Core, esto se hace mediante los Hosted Services (servicios hospedados). Son pequeños procesos que corren junto a tu API web, dentro del mismo proceso.
Vamos a implementarlos usando el patrón productor-consumidor con Channels.
El concepto básico: BackgroundService
.NET nos da una clase base llamada BackgroundService. Cualquier clase que herede de ella y se registre en el sistema, arrancará cuando arranque la API y morirá cuando se pare la API.
Es perfecto para tareas cronometradas (ej: “Borrar logs viejos cada hora”).
using Microsoft.Extensions.Hosting;
public class LimpiadorService : BackgroundService
{
private readonly ILogger<LimpiadorService> _logger;
public LimpiadorService(ILogger<LimpiadorService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Limpiando archivos temporales...");
// Simula trabajo
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
}Para activarlo, lo añadimos en Program.cs:
builder.Services.AddHostedService<LimpiadorService>();Comunicar la API con el servicio
El ejemplo anterior es un bucle infinito. Pero, ¿cómo le decimos al servicio: “Oye, envía un email a ESTE usuario ahora” desde un Controlador?
No conviene inyectar el BackgroundService directamente en el controlador. Necesitamos un intermediario, un buzón donde el controlador deje mensajes y el servicio los recoja.
Ese buzón es una Cola (Queue). En .NET podemos crear esta cola en memoria con System.Threading.Channels.
Implementar la cola
Vamos a crear una clase que actúe de cola. Será un Singleton para que sea la misma instancia para toda la aplicación.
using System.Threading.Channels;
public interface IBackgroundTaskQueue
{
// Método para que el Controlador meta tareas (Productor)
ValueTask QueueBackgroundWorkItemAsync(
Func<CancellationToken, ValueTask> workItem,
CancellationToken cancellationToken = default);
// Método para que el Servicio saque tareas (Consumidor)
ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(CancellationToken cancellationToken);
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
// Channel es una colección thread-safe optimizada
private readonly Channel<Func<CancellationToken, ValueTask>> _queue;
public BackgroundTaskQueue(int capacity)
{
// Bounded: Si la cola se llena, el productor espera. Evita que la RAM explote.
var options = new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.Wait
};
_queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
}
public async ValueTask QueueBackgroundWorkItemAsync(
Func<CancellationToken, ValueTask> workItem,
CancellationToken cancellationToken = default)
{
await _queue.Writer.WriteAsync(workItem, cancellationToken);
}
public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(CancellationToken cancellationToken)
{
return await _queue.Reader.ReadAsync(cancellationToken);
}
}El worker que procesa la cola
Ahora creamos el BackgroundService que espera nuevos elementos y los ejecuta.
public class QueuedHostedService : BackgroundService
{
private readonly IBackgroundTaskQueue _taskQueue;
private readonly ILogger<QueuedHostedService> _logger;
public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger<QueuedHostedService> logger)
{
_taskQueue = taskQueue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Servicio de Cola Iniciado");
while (!stoppingToken.IsCancellationRequested)
{
// Espera aquí hasta que haya algo en la cola
var workItem = await _taskQueue.DequeueAsync(stoppingToken);
try
{
// Ejecuta la tarea
await workItem(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error ejecutando la tarea en segundo plano");
}
}
}
}Registrar todo en Program.cs
// 1. La cola debe ser Singleton (Buzón único)
builder.Services.AddSingleton<IBackgroundTaskQueue>(ctx =>
{
return new BackgroundTaskQueue(100); // Capacidad de 100 tareas
});
// 2. El servicio que procesa (Worker)
builder.Services.AddHostedService<QueuedHostedService>();Uso desde el controlador
¡Ya está todo listo! Ahora, cuando un usuario se registra, encolamos el envío de email y devolvemos respuesta inmediata.
[HttpPost("registro")]
public async Task<IActionResult> RegistrarUsuario(
UsuarioDto dto,
CancellationToken requestAborted)
{
// 1. Guardar en BBDD (Rápido)
_repo.Guardar(dto);
// 2. Encolar tarea pesada (Email)
// Inyectamos IBackgroundTaskQueue en el constructor
await _queue.QueueBackgroundWorkItemAsync(
async token =>
{
// Simular envío de email lento
await Task.Delay(2000, token);
Console.WriteLine($"Email de bienvenida enviado a {dto.Email}");
},
requestAborted);
// 3. Responder al usuario YA
return Accepted(); // 202 Accepted (La petición fue aceptada para procesamiento)
}No captures servicios Scoped de la petición dentro de una tarea en segundo plano. Si la tarea necesita un DbContext u otro servicio scoped, crea un scope dentro del worker con IServiceScopeFactory.
Cuándo usar esto o una cola persistente
Lo que hemos hecho es una solución In-Memory.
- Ventaja: Es gratis, rápido y no necesita infraestructura extra.
- Desventaja: Si reinicias el servidor (o se cae la API), las tareas que estaban en la cola se pierden.
Usa esta solución para:
- Enviar emails no críticos.
- Procesar imágenes.
- Tareas de mantenimiento sencillas.
Usa Hangfire o RabbitMQ (Colas persistentes) para:
- Cobros de tarjetas de crédito.
- Procesos críticos que NO se pueden perder bajo ningún concepto.
- Tareas que deben reintentarse automáticamente si fallan 5 veces.