# Servicios de antecedentes en ASP.NET Core - Parte 1: Los enfoques

<!--category-- ASP.NET Core, IHostedService, BackgroundService, Hangfire -->
<datetime class="hidden">2025-11-27T09:00</datetime>

Cada aplicación web moderna tiene un trabajo que no debería bloquear una solicitud HTTP: enviar correos electrónicos, procesar archivos, sincronizar con servicios externos, ejecutar mantenimiento programado. ASP.NET Core proporciona múltiples enfoques para manejar este trabajo de fondo, desde simple `IHostedService` implementaciones a marcos sofisticados como Hangfire. En esta primera parte, exploraremos los patrones fundamentales y cuándo utilizar cada uno.

# Introducción

Confesión; Me gusta Background Services a LOT, este sitio (un sitio BLOG!) tiene más de media docena de ellos haciendo tareas de fondo cariosas, pero como todo tienen algunas ODDITYS y prácticas que harán su uso de ellos mucho más agradable.
Los servicios de fondo son los héroes desconocidos de las aplicaciones web modernas. Mientras que sus controladores manejan solicitudes HTTP en primer plano, los servicios de fondo procesan silenciosamente correos electrónicos en cola, indexan contenido para buscar, comprueban API externas, limpian archivos temporales y manejan innumerables otras tareas que de otro modo bloquearían la tramitación de solicitudes.

En esta serie de dos partes, exploraremos los diferentes enfoques para implementar los servicios de fondo en ASP.NET Core, desde el `IHostedService` y `BackgroundService` En la parte 1, examinaremos los enfoques fundamentales y sus características. [Parte 2](/blog/background-services-in-aspnetcore-part2), nos sumergiremos en implementaciones del mundo real desde una base de código de producción.

> **Importante:** Prestaremos especial atención a la gestión del ciclo de vida, en particular a los `StopAsync` método, que es donde muchos desarrolladores encuentran excepciones crípticas cuando sus aplicaciones se apagan.

[TOC]

# ¿Por qué Servicios de Antecedentes?

Antes de sumergirnos en el "cómo", consideremos brevemente el "por qué". Los servicios de fondo le permiten:

1. **Operaciones de descarga lentas** - No hagas esperar a los usuarios mientras envías correos electrónicos o generas PDFs
2. **Programar tareas recurrentes** - Limpia viejos discos todas las noches a las 2 AM
3. **Colas de proceso** - Manejar mensajes de canales o corredores de mensajes
4. **Monitorear el estado externo** - Encuestar APIs o ver sistemas de archivos para los cambios
5. **Coordinar flujos de trabajo complejos** - Administrar procesos de varios pasos que abarcan minutos u horas

ASP.NET Core ofrece varios enfoques para la implementación de estos servicios, cada uno con diferentes compensaciones.

# El contexto histórico: ¿Por qué los servicios de antecedentes son ahora factibles?

En los "viejos días" (pre-2010), ejecutar el trabajo de fondo en su aplicación web fue generalmente considerado una mala idea. La sabiduría convencional era: "Los servidores web manejan las peticiones web. El trabajo de fondo pertenece a un servidor separado."

Esto no era sólo sabiduría de la carga-culto — estaba basado en limitaciones técnicas reales:

## La era de un solo núcleo

Servidores web tempranos (y honestamente ahora, servicios 'más baratos' de Azure) normalmente funcionaban en **CPU de un solo núcleo o de doble núcleo**. Si ejecutaba una tarea de fondo intensiva en CPU, compitió directamente con solicitudes web para el mismo núcleo:

```
Single Core (2005):
┌─────────────────────┐
│  Background Task    │  ← Uses 80% CPU
│  (80% of core)      │
├─────────────────────┤
│  Web Requests       │  ← Only 20% left!
│  (20% of core)      │  ← Slow responses
└─────────────────────┘
```

Resultado: Su sitio web se volvió lento en el momento en que comenzó el trabajo de fondo.

## Pool de discusión Hambre de hambre

Clásico ASP.NET utilizado **hilo por petición**. El grupo de hilos era relativamente pequeño (25-100 hilos típicamente), y las tareas de fondo robarían hilos que deberían estar manejando peticiones web:

```csharp
// Classic ASP.NET (2008)
ThreadPool.QueueUserWorkItem(_ =>
{
    // This steals a thread from the pool!
    ProcessLongRunningTask();
});

// Meanwhile, web requests are queued waiting for threads
// HTTP 503 Service Unavailable
```

## IIS Application Pool Recycling

IIS reciclaría agresivamente los pools de aplicaciones (reiniciar su aplicación) en función de los límites de memoria, los recuentos de solicitudes o los horarios.

```
00:00 - Background import starts (2 hour task)
02:00 - IIS recycles app pool (scheduled)
      - Background task killed
      - Work lost, must start again
```

## Soporte de espera/sincronización limitada

Antes de .NET 4.5 (2012), la programación async era (relativamente) dolorosa.

```csharp
// Pre-async (2008)
void ProcessEmails()
{
    foreach (var email in GetEmails())
    {
        smtp.Send(email);  // Blocks thread for 500ms per email
    }
}
// 100 emails = 50 seconds of blocked thread time
```

## Lo que cambió: La era moderna

El paisaje de hoy es dramáticamente diferente:

### 1. Multi-Core es asequible

Las máquinas virtuales de la nube con múltiples núcleos tienen un precio razonable, y los servidores de metal desnudo son sorprendentemente baratos. Este blog se ejecuta en un servidor de 8 núcleos dedicado que cuesta menos que una VM de Azure comparable — y tengo todos esos núcleos para mí mismo, sin vecinos ruidosos. Una tarea de fondo en un núcleo no afecta significativamente a las solicitudes web en otros núcleos:

```
8-Core Server (2024):
Core 1: ████████████████████ Web Requests
Core 2: ████████████████████ Web Requests
Core 3: ████████████████████ Web Requests
Core 4: ████████████████████ Web Requests
Core 5: ████████████████████ Background Task ← Isolated
Core 6: ████████████████████ Background Task
Core 7: ████████████████████ Background Task
Core 8: ████████████████████ Background Task
```

### 2. Async/Await Everywhere

Modern .NET hace trivial la programación asíncrona. Las tareas de fondo pueden esperar en E/S sin bloquear hilos:

```csharp
// Modern async (2024)
async Task ProcessEmailsAsync(CancellationToken ct)
{
    await foreach (var email in GetEmailsAsync(ct))
    {
        await smtp.SendAsync(email, ct);  // Doesn't block thread!
    }
}
// 100 emails processed efficiently, thread returns to pool during I/O
```

### 3. Mejor acogida de procesos

- [**Docker**](https://www.docker.com/) - Los servicios de fondo en contenedores no se reciclan arbitrariamente
- [**Kubernetes**](https://kubernetes.io/) - Manejo adecuado de apagado elegante con `SIGTERM`
- [**sistemated**](https://systemd.io/) - Servicios Linux que se reinician de forma fiable
- **Servicios de Windows** - Modelo de proceso adecuado de larga duración

### 4. Canales y Primitivos modernos

.NET ahora tiene soporte de primera clase para la programación concurrente con [`System.Threading.Channels`](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels):

```csharp
// System.Threading.Channels
var channel = Channel.CreateBounded<Email>(100);

// Producer (web request)
await channel.Writer.WriteAsync(email);  // Fast, non-blocking

// Consumer (background service)
await foreach (var email in channel.Reader.ReadAllAsync())
{
    await ProcessAsync(email);  // Efficient, async
}
```

### 5. Límites de recursos y grupos

Modernos orquestadores de contenedores le permiten **limitar el uso de los recursos**:

```yaml
# Kubernetes resource limits
resources:
  limits:
    cpu: "500m"        # Background task can't use more than 0.5 CPU
    memory: "512Mi"    # Or more than 512 MB RAM
```

Esto significa que una tarea de fondo descontrolada no puede pasar hambre a tu nivel web.

La pregunta ya no es "¿Podemos ejecutar servicios de fondo en nuestra aplicación web?", sino "**¿Deberíamos?**" Exploraremos esta decisión en la sección "Cuándo NO usar los servicios de fondo" más adelante.

# Las opciones integradas

## IHostedService: La Fundación

En su núcleo, cada servicio de antecedentes en ASP.NET Core implementa [`IHostedService`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice). Esta interfaz es muy simple:

```csharp
public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}
```

Eso es, dos métodos. `StartAsync` se llama cuando se inicia la aplicación, y `StopAsync` cuando se apaga.

Registre su servicio en `Program.cs`:

```csharp
builder.Services.AddHostedService<MyBackgroundService>();
```

Aquí está el ciclo de vida visualizado:

```mermaid
graph LR
    A[Application Starts] --> B[StartAsync Called]
    B --> C[Service Running]
    C --> D[Application Shutting Down]
    D --> E[StopAsync Called]
    E --> F[Application Stopped]

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style C stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style F stroke:#dc2626,stroke-width:3px,color:#ef4444
```

### StartAsync: Inicio sincrónico vs. Asincrono

Una decisión crítica en la aplicación `IHostedService` es si su `StartAsync` método debe bloquear o volver inmediatamente.

**Inicio sincrónico (bloqueo):**

```csharp
public class BlockingStartService : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // This blocks application startup until complete
        await InitializeDatabaseAsync(cancellationToken);
        await LoadConfigurationAsync(cancellationToken);

        // Only now will the application continue starting
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;
}
```

**Inicio asincrónico (no bloqueante):**

```csharp
public class NonBlockingStartService : IHostedService
{
    private Task _backgroundTask;
    private readonly CancellationTokenSource _cts = new();

    public Task StartAsync(CancellationToken cancellationToken)
    {
        // Start background work but return immediately
        _backgroundTask = Task.Run(async () =>
        {
            // Give other services time to initialise
            await Task.Delay(TimeSpan.FromSeconds(5), _cts.Token);
            await DoLongRunningWorkAsync(_cts.Token);
        }, _cts.Token);

        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _cts.Cancel();
        await _backgroundTask; // Wait for completion
    }
}
```

**Cuándo utilizar cada enfoque:**

- **Bloqueo:** Cuando el servicio debe completar la inicialización antes de que la aplicación pueda manejar peticiones (por ejemplo, cargando configuración crítica, cachés de calentamiento)
- **No bloqueo:** Cuando el servicio puede inicializarse en segundo plano mientras se inician otros servicios (por ejemplo, indexación de contenidos existentes, sincronización con servicios externos)

### StopAsync: La caída común

Aquí es donde las cosas se ponen interesantes, y donde muchos desarrolladores encuentran problemas. Cuando su aplicación se apaga, ASP.NET Core llama `StopAsync` en todos los servicios alojados. Tiene una ventana limitada (por defecto 5 segundos) para limpiar con gracia. `Program.cs`:

```csharp
builder.Services.Configure<HostOptions>(options =>
{
    options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
```

**El error más común:**

```csharp
public class BrokenService : IHostedService
{
    private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
    private Task _processingTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _processingTask = ProcessMessagesAsync();
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // WRONG: The channel is still open, ProcessMessagesAsync
        // will hang on WaitToReadAsync forever!
        return Task.CompletedTask;
    }

    private async Task ProcessMessagesAsync()
    {
        // This will never exit because the channel is never completed
        await foreach (var message in _channel.Reader.ReadAllAsync())
        {
            await ProcessAsync(message);
        }
    }
}
```

Cuando ejecutes este servicio y detengas tu aplicación, verás errores como:

```
Unable to cast object of type 'TaskCompletionSource`1[System.Threading.Tasks.VoidTaskResult]' to type 'System.Threading.Tasks.Task'
```

O la aplicación simplemente se colgará para el período de tiempo de apagado antes de terminar por la fuerza.

**El enfoque correcto:**

```csharp
public class CorrectService : IHostedService
{
    private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
    private readonly CancellationTokenSource _cts = new();
    private Task _processingTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _processingTask = ProcessMessagesAsync(_cts.Token);
        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        // CORRECT: Signal cancellation and complete the channel
        await _cts.CancelAsync();
        _channel.Writer.Complete();

        try
        {
            // Wait for processing to finish or for the shutdown timeout
            await Task.WhenAny(_processingTask,
                Task.Delay(Timeout.Infinite, cancellationToken));
        }
        catch (OperationCanceledException)
        {
            // Expected when shutdown timeout is reached
        }
    }

    private async Task ProcessMessagesAsync(CancellationToken token)
    {
        await foreach (var message in _channel.Reader.ReadAllAsync(token))
        {
            try
            {
                await ProcessAsync(message);
            }
            catch (OperationCanceledException)
            {
                // Shutdown requested, exit gracefully
                break;
            }
        }
    }
}
```

**Puntos clave para la correcta implementación de StopAsync:**

1. **Cancelación de la señal** - Use una `CancellationTokenSource` y cancelarlo
2. **Canales completos** - Si estás usando canales, llama `Writer.Complete()`
3. **Esperar las tareas de fondo** - Uso `Task.WhenAny` con el token de cancelación de apagado
4. **Manipulación OperaciónCanceladaExcepción** - Esto se espera y debe ser capturado
5. **No tires excepciones.** - Excepciones en `StopAsync` puede causar un comportamiento impredecible

## BackgroundService: La Clase Base Conveniente

Escribiendo `IHostedService` Las implementaciones pueden ser repetitivas. Siempre necesita una tarea de fondo, una fuente de token de cancelación y el mismo patrón de limpieza. [`BackgroundService`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice) maneja esta placa de caldera para usted:

```csharp
public abstract class BackgroundService : IHostedService, IDisposable
{
    private Task _executeTask;
    private CancellationTokenSource _stoppingCts;

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        _executeTask = ExecuteAsync(_stoppingCts.Token);
        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        if (_executeTask == null) return;

        try
        {
            _stoppingCts.Cancel();
        }
        finally
        {
            await Task.WhenAny(_executeTask, Task.Delay(Timeout.Infinite, cancellationToken));
        }
    }

    public virtual void Dispose()
    {
        _stoppingCts?.Cancel();
    }
}
```

Sólo tienes que implementar `ExecuteAsync` y dejar que la clase base maneje la plomería:

```csharp
public class SimpleBackgroundService : BackgroundService
{
    private readonly ILogger<SimpleBackgroundService> _logger;

    public SimpleBackgroundService(ILogger<SimpleBackgroundService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Service starting");

        // Wait for app to finish starting
        await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await DoWorkAsync(stoppingToken);
                await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
            }
            catch (OperationCanceledException)
            {
                // Shutdown requested
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in background service");
            }
        }

        _logger.LogInformation("Service stopping");
    }

    private async Task DoWorkAsync(CancellationToken token)
    {
        _logger.LogInformation("Doing work...");
        // Your actual work here
        await Task.Delay(1000, token);
    }
}
```

### Cuándo usar BackgroundService vs IHostedService

**Uso `BackgroundService` cuando:**

- Necesitas un bucle de fondo de larga duración
- Quieres una simple ejecución periódica
- No necesitas un buen control sobre `StartAsync`/`StopAsync` cronometración

**Uso `IHostedService` cuando:**

- Tienes que controlar exactamente lo que sucede en `StartAsync` versus trabajo de fondo
- Usted está configurando manejadores de eventos o vigilantes en lugar de un bucle continuo
- Necesita coordinarse con otros servicios durante el inicio

# Avanzado: Coordinación de inicio

A veces necesita servicios para esperar el uno al otro. Por ejemplo, es posible que desee que su indexador de búsqueda semántica espere hasta que su procesador de archivos Markdown haya terminado su carga inicial.

Aquí hay un patrón para coordinar el inicio del servicio:

```csharp
public interface IStartupCoordinator
{
    void RegisterService(string serviceName);
    void SignalReady(string serviceName);
    bool IsServiceReady(string serviceName);
    Task WaitForServiceAsync(string serviceName, CancellationToken cancellationToken = default);
    Task WaitForAllServicesAsync(CancellationToken cancellationToken = default);
}

public class StartupCoordinator : IStartupCoordinator
{
    private readonly ConcurrentDictionary<string, TaskCompletionSource> _services = new();
    private readonly ILogger<StartupCoordinator> _logger;

    public void RegisterService(string serviceName)
    {
        _services.TryAdd(serviceName, new TaskCompletionSource());
    }

    public void SignalReady(string serviceName)
    {
        if (_services.TryGetValue(serviceName, out var tcs))
        {
            tcs.TrySetResult();
            _logger.LogInformation("{Service} is ready", serviceName);
        }
    }

    public async Task WaitForServiceAsync(string serviceName, CancellationToken ct = default)
    {
        if (_services.TryGetValue(serviceName, out var tcs))
        {
            await tcs.Task.WaitAsync(ct);
        }
    }

    public async Task WaitForAllServicesAsync(CancellationToken ct = default)
    {
        await Task.WhenAll(_services.Values.Select(tcs => tcs.Task)).WaitAsync(ct);
    }
}
```

Uso en un servicio:

```csharp
public class DependentService : IHostedService
{
    private readonly IStartupCoordinator _coordinator;
    private readonly ILogger<DependentService> _logger;

    public DependentService(
        IStartupCoordinator coordinator,
        ILogger<DependentService> logger)
    {
        _coordinator = coordinator;
        _logger = logger;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // Wait for another service to be ready
        await _coordinator.WaitForServiceAsync("MarkdownProcessor", cancellationToken);

        _logger.LogInformation("Dependencies ready, starting work");

        // Do your work...

        // Signal you're ready for services that depend on you
        _coordinator.SignalReady("DependentService");
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;
}
```

Este patrón se vuelve especialmente útil cuando tiene múltiples servicios de fondo con interdependencias.

# Coordinación distribuida con Redis

El coordinador de inicio trabaja dentro de una sola instancia de aplicación. Pero, ¿qué sucede cuando escala a varias instancias? No desea que tres instancias todas ejecuten la misma tarea programada simultáneamente.

[Redis](https://redis.io/) proporciona una solución sencilla: utilice banderas (teclas) para coordinar quién hace qué.

## Elección simple de líder

```csharp
public class DistributedBackgroundService : BackgroundService
{
    private readonly IConnectionMultiplexer _redis;
    private readonly ILogger<DistributedBackgroundService> _logger;
    private readonly string _instanceId = Guid.NewGuid().ToString();
    private const string LeaderKey = "background:newsletter:leader";

    public DistributedBackgroundService(
        IConnectionMultiplexer redis,
        ILogger<DistributedBackgroundService> logger)
    {
        _redis = redis;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var db = _redis.GetDatabase();

        while (!stoppingToken.IsCancellationRequested)
        {
            // Try to become the leader (SET NX with expiry)
            var acquired = await db.StringSetAsync(
                LeaderKey,
                _instanceId,
                TimeSpan.FromMinutes(5),
                When.NotExists);

            if (acquired)
            {
                _logger.LogInformation("This instance is the leader, running task");

                try
                {
                    await DoScheduledWorkAsync(stoppingToken);
                }
                finally
                {
                    // Release leadership
                    await db.KeyDeleteAsync(LeaderKey);
                }
            }
            else
            {
                var leader = await db.StringGetAsync(LeaderKey);
                _logger.LogDebug("Another instance ({Leader}) is the leader", leader);
            }

            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}
```

## Bloqueo distribuido para secciones críticas

Para tareas que no deben correr simultáneamente entre instancias:

```csharp
public async Task ProcessWithLockAsync(CancellationToken cancellationToken)
{
    var db = _redis.GetDatabase();
    var lockKey = "locks:critical-task";
    var lockValue = _instanceId;

    // Try to acquire lock
    if (await db.LockTakeAsync(lockKey, lockValue, TimeSpan.FromMinutes(10)))
    {
        try
        {
            _logger.LogInformation("Lock acquired, processing...");
            await DoCriticalWorkAsync(cancellationToken);
        }
        finally
        {
            await db.LockReleaseAsync(lockKey, lockValue);
        }
    }
    else
    {
        _logger.LogDebug("Could not acquire lock, another instance is processing");
    }
}
```

## Cuándo utilizar la coordinación distribuida

- **Tareas programadas** - Sólo una instancia debe enviar el boletín diario
- **Procesamiento en cola con pedido** - Asegúrese de que los mensajes se procesan en orden
- **Operaciones intensivas en recursos** - Evitar múltiples instancias de abrumar una API externa
- **Migraciones de bases de datos** - Sólo una instancia debe ejecutar migraciones al iniciar

Para escenarios más complejos (trabajos de varios pasos, programación fiable entre reinicios), considere Hangfire que maneja el bloqueo distribuido automáticamente con su motor de base de datos.

# Cuándo NO usar los servicios de fondo

Antes de sumergirnos en herramientas más sofisticadas como Hangfire, vamos a hablar de cuando usted *No debería.* utilizar los servicios de fondo en su aplicación web principal.

## Firmas que debe dividirse en un proyecto separado

Los servicios de fondo que se ejecutan en su aplicación web comparten recursos con su línea de solicitud HTTP. Esto puede causar problemas:

### 1. Contenido de los recursos

**Problema:** Su servicio de antecedentes consume importantes conexiones de CPU, memoria o base de datos.

```csharp
// This will starve your web application
public class VideoTranscodingService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var video = await _queue.DequeueAsync();
            // This uses 100% of 4 CPU cores for 5 minutes
            await TranscodeVideoAsync(video);
        }
    }
}
```

**Cuando llegan peticiones web durante la transcodificación, son lentas porque la CPU está ocupada.**

**Solución:** Mover a un servicio de trabajador separado:

```bash
# Your solution structure
/YourApp.Web          # ASP.NET Core web app - no background services
/YourApp.Worker       # .NET Worker Service - handles background work
/YourApp.Shared       # Shared models, interfaces
```

### 2. Diferentes requisitos de escalado

**Problema:** Su trabajo de fondo necesita escala diferente a su nivel web.

- **Nivel web:** Escala para el tráfico HTTP (pueden necesitar 10 instancias durante el día, 2 por la noche)
- **Nivel de fondo:** Escala para la profundidad de la cola (puede necesitar 1 instancia normalmente, 20 cuando se procesa un lote)

Si están en el mismo proceso, no puedes escalarlos independientemente.

**Escenario de ejemplo:**

```
09:00 - High web traffic, low background work → Need 10 web instances, 1 worker
14:00 - Newsletter time! Low web traffic, high background work → Need 2 web instances, 20 workers
```

Poner los servicios de fondo en su aplicación web significa que tendría que ejecutar 20 instancias web sólo para manejar el boletín de noticias, desperdiciando recursos.

### 3. Independencia del despliegue

**Problema:** Desea implementar cambios web sin reiniciar los servicios de fondo (o viceversa).

```csharp
// If this is in your web app, deploying a CSS change restarts the service
public class LongRunningImportService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // This import takes 2 hours
        await ImportMillionsOfRecordsAsync(stoppingToken);
    }
}
```

Cada despliegue interrumpe la importación. Muévelo a un servicio de trabajador separado que despliegue de forma independiente.

### 4. Diferentes Dominios de Fracaso

**Problema:** Un error en su servicio de fondo bloquea toda la aplicación web.

```csharp
// This null reference exception crashes your web app
public class BuggyBackgroundService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        string value = null;
        // Unhandled exception - takes down the whole app
        await ProcessAsync(value.Length);
    }
}
```

Si el trabajo de fondo está en un proceso separado, puede bloquearse y reiniciarse sin afectar a las peticiones web.

## Cómo Factorizar los Servicios de Antecedentes

Cuando usted decide dividir, aquí está la arquitectura recomendada:

### Opción 1: .NET Worker Service

Crear un nuevo proyecto utilizando la plantilla Servicio de trabajador:

```bash
dotnet new worker -n YourApp.Worker
```

Estructura:

```
/YourApp.Worker
  /Services
    VideoTranscodingService.cs
    EmailSenderService.cs
  /Program.cs
  /appsettings.json
```

Program.cs:

```csharp
var builder = Host.CreateApplicationBuilder(args);

// Register your background services
builder.Services.AddHostedService<VideoTranscodingService>();
builder.Services.AddHostedService<EmailSenderService>();

// Share configuration with web app
builder.Services.Configure<VideoConfig>(
    builder.Configuration.GetSection("Video"));

// Share database context
builder.Services.AddDbContext<YourDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

var host = builder.Build();
host.Run();
```

Desplegar por separado:

```bash
# Web app on ports 80/443
/YourApp.Web → web-server-1, web-server-2, web-server-3

# Worker service doesn't listen on any port
/YourApp.Worker → worker-server-1, worker-server-2
```

### Opción 2: Proyecto separado con cola compartida

Utilice una cola de mensajes para desvincular la web y los trabajadores:

```mermaid
graph LR
    A[Web App] --> B[Message Queue]
    B --> C[Worker 1]
    B --> D[Worker 2]
    B --> E[Worker N]

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style B stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style C stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
    style D stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
    style E stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
```

Las colas de aplicaciones web funcionan:

```csharp
// In your web controller
public class VideoController : ControllerBase
{
    private readonly IMessageQueue _queue;

    [HttpPost("upload")]
    public async Task<IActionResult> Upload(IFormFile video)
    {
        await _storage.SaveAsync(video);

        // Queue for processing - don't process in web app
        await _queue.PublishAsync(new VideoTranscodeJob
        {
            VideoId = video.Id,
            Priority = Priority.Normal
        });

        return Accepted(); // Return immediately
    }
}
```

El trabajador consume de la cola:

```csharp
// In your worker service
public class VideoWorker : BackgroundService
{
    private readonly IMessageQueue _queue;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var job in _queue.SubscribeAsync<VideoTranscodeJob>(stoppingToken))
        {
            await TranscodeAsync(job);
        }
    }
}
```

**Opciones de cola de mensajes populares:**

- [**ConejoMQ**](https://www.rabbitmq.com/) - La mayoría de los populares, ricos en características
- [**Autobús de servicio Azure**](https://azure.microsoft.com/en-us/products/service-bus/) - Si estás en Azure
- [**AWS SQS**](https://aws.amazon.com/sqs/) - Si estás en AWS
- [**Redis Streams**](https://redis.io/docs/data-types/streams/) - Más simple, bueno para escala más pequeña

### Opción 3: Trabajadores especializados múltiples

Para sistemas complejos, divididos por responsabilidades:

```
/YourApp.Web              # HTTP requests only
/YourApp.EmailWorker      # Sends emails
/YourApp.VideoWorker      # Transcodes videos
/YourApp.ReportWorker     # Generates reports
/YourApp.Scheduler        # Runs scheduled jobs (Hangfire)
```

Cada trabajador puede:

- Escalar de forma independiente
- Desplegarse de forma independiente
- Utilizar diferentes recursos (trabajador de correo electrónico necesita SMTP, trabajador de vídeo necesita GPU)
- Tener diferentes monitoreos y alertas

## Cuándo mantener los servicios de fondo en su aplicación web

A pesar de lo anterior, algunos escenarios están perfectamente bien para los servicios de base en el proceso:

### Tareas periódicas ligeras

```csharp
// Fine to keep in web app
public class CacheWarmingService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _cache.WarmupAsync(); // Quick operation
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}
```

### Oyentes de eventos

```csharp
// Fine to keep in web app
public class FileWatcherService : IHostedService
{
    // Reacts to events, doesn't consume significant resources
    private FileSystemWatcher _watcher;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _watcher = new FileSystemWatcher("/config");
        _watcher.Changed += OnConfigChanged;
        _watcher.EnableRaisingEvents = true;
        return Task.CompletedTask;
    }
}
```

### Colas basadas en canales (para trabajos no críticos)

```csharp
// Fine to keep in web app if work is quick and not critical
public class EmailQueueService : BackgroundService
{
    // Sends emails in background, but each email takes < 1 second
    // If the app restarts, losing a few queued emails is acceptable
}
```

### Coordinación de inicio

```csharp
// Fine to keep in web app
public class WarmupService : IHostedService
{
    // Runs once at startup, then does nothing
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await _database.WarmupConnectionPoolAsync();
        await _cache.LoadCriticalDataAsync();
    }
}
```

## Matriz de decisiones

Característica  Mantener en Web App  Mover al servicio del trabajador
|---------------|-----------------|------------------------|
Uso de CPU por operación  < 100ms  > 1 segundo
Memoria por operación  < 10 MB  > 100 MB
Frecuencia Periódica (minutos/horas) Continuo o de alta frecuencia
Criticalidad Criticalidad Critical
Duración Segundos Minutos a horas
Escalas con tráfico web Profundidad de la cola de trabajo
Ejemplo Calentamiento de caché, recarga de configuración Procesamiento de vídeo, grandes importaciones

## Ejemplo del mundo real: La plataforma del blog

En la plataforma del blog cuyo código examinamos en la Parte 2:

**Mantenido en la aplicación web:**

- `MarkdownDirectoryWatcherService` - Visor de archivos ligero
- `UmamiBackgroundSender` - Eventos de análisis rápidos
- `EmailSenderHostedService` - Pequeño volumen, no crítico
- `MarkdownReAddPostsService` - Solo inicio, configuración cerrada

**Debe pasar al servicio de los trabajadores si la escala aumenta:**

- `BrokenLinkCheckerBackgroundService` - Hace muchas peticiones HTTP
- `SemanticIndexingBackgroundService` - Llama a API de integración externa

**Ya en servicio por separado:**

- `Mostlylucid.SchedulerService` - Tablero de Hangfire y envío de boletines

Este es un enfoque pragmático: comenzar simple (en el proceso), dividir cuando usted tiene evidencia que necesita.

# Más allá de lo básico: Hangfire

Mientras tanto `IHostedService` y `BackgroundService` son excelentes para los servicios que posees y controlas, a veces necesitas una programación más sofisticada. [Hangfire](https://www.hangfire.io/) Adelante.

Hangfire proporciona:

- **Las colas de trabajo persistentes** - Los trabajos sobreviven a los reinicios de aplicaciones
- **Empleos recurrentes** - Programación al estilo Cron
- **Interfaz de usuario de dashboard** - Ver lo que está funcionando, lo que ha fallado, volver a intentar trabajos
- **Ejecución distribuida** - Múltiples servidores pueden procesar la misma cola de trabajo
- **Retenes automáticos** - Los trabajos fallidos se prueban automáticamente con un retroceso exponencial

He aquí un ejemplo sencillo:

```csharp
// In Program.cs
builder.Services.AddHangfire(config => config
    .UsePostgreSqlStorage(connectionString)
    .UseRecommendedSerializerSettings());

builder.Services.AddHangfireServer();

var app = builder.Build();

// Schedule recurring jobs
app.UseHangfireDashboard();
app.Services.GetRequiredService<IRecurringJobManager>()
    .AddOrUpdate<NewsletterService>(
        "send-daily-newsletter",
        x => x.SendDailyNewsletter(),
        Cron.Daily(17)); // 5 PM every day
```

Su servicio es sólo una clase normal:

```csharp
public class NewsletterService
{
    private readonly IEmailService _emailService;
    private readonly ISubscriberRepository _subscribers;

    public NewsletterService(
        IEmailService emailService,
        ISubscriberRepository subscribers)
    {
        _emailService = emailService;
        _subscribers = subscribers;
    }

    public async Task SendDailyNewsletter()
    {
        var subscribers = await _subscribers.GetDailySubscribersAsync();

        foreach (var subscriber in subscribers)
        {
            await _emailService.SendNewsletterAsync(subscriber);
        }
    }
}
```

Mangos del hangfire:

- Garantizar que el trabajo funcione a la hora prevista
- Reintentar si falla
- Almacenar el historial de ejecución
- Proporcionar un panel de control para monitorear todo

```mermaid
graph TD
    A[Hangfire Server] --> B{Check Schedule}
    B -->|Job Due| C[Dequeue Job]
    C --> D[Execute Job Method]
    D -->|Success| E[Mark Complete]
    D -->|Failure| F[Retry with Backoff]
    F --> G{Max Retries?}
    G -->|No| C
    G -->|Yes| H[Mark Failed]
    E --> I[Update Dashboard]
    H --> I
    I --> B

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style D stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style E stroke:#059669,stroke-width:3px,color:#10b981
    style H stroke:#dc2626,stroke-width:3px,color:#ef4444
```

**Cuándo usar Hangfire:**

- Necesitas colas de trabajo persistentes que sobrevivan a los reinicios
- Quieres un panel de control para monitorear y activar trabajos manualmente
- Usted necesita el procesamiento de trabajo distribuido a través de múltiples servidores
- Usted quiere lógica de reintento integrada y manejo de fallas
- Necesita programación al estilo cron de tareas recurrentes

**Cuándo permanecer con IHostedService / BackgroundService:**

- Usted necesita un control fino sobre el ciclo de vida de servicio
- Su servicio necesita reaccionar a los eventos en tiempo real
- Usted desea minimizar las dependencias
- Estás construyendo una simple tarea periódica que no necesita persistencia

# Otras opciones

Aunque Hangfire es popular, hay otras bibliotecas que vale la pena considerar:

[**Quartz.NET**](https://www.quartz-scheduler.net/):

- Programación más flexible que Hangfire
- Soporta expresiones cron y programación basada en calendario
- Puede persistir en múltiples bases de datos
- API más compleja pero más potente

[**MassTransit**](https://masstransit.io/)/[**NServiceBus**](https://particular.net/nservicebus):

- Implementaciones de buses de mensajes completos
- Mejor para los sistemas y microservicios distribuidos
- Soporta sagas (flujos de trabajo de larga duración)
- Curva de aprendizaje Steeper

[**Funciones de Azure**](https://azure.microsoft.com/en-us/products/functions/)/[**AWS Lambda**](https://aws.amazon.com/lambda/):

- Si estás en la nube, considera sin servidor
- Pagar por ejecución en lugar de mantener un servicio en funcionamiento
- Escalado automático
- Algunos gastos generales de latencia para arranques fríos

# Resumen

En la Parte 1, hemos cubierto los enfoques fundamentales de los servicios de fondo en ASP.NET Core:

1. **IHostedService** - La base, la máxima flexibilidad
2. **BackgroundService** - Clase base conveniente para bucles de larga duración
3. **Coordinación de puesta en marcha** - Hacer que los servicios se esperen el uno al otro
4. **Coordinación distribuida** - Uso de Redis para escenarios multi-instancia
5. **Hangfire** - Cuando necesita trabajos persistentes y programación sofisticada

Las lecciones más importantes:

- **Siempre complete los canales y cancele tokens en StopAsync**
- **Decidir si StartAsync debe bloquear o devolver inmediatamente**
- **Manejar la operaciónCanceledExcepción con gracia**
- **Use Task.WhenAny con el token de apagado para respetar los tiempos de apagado**

In [Parte 2](/blog/background-services-in-aspnetcore-part2), vamos a examinar implementaciones del mundo real desde una plataforma de blog de producción:

- Vigiladores del sistema de archivos que sincronizan archivos Markdown a una base de datos
- Enviadores de correo electrónico con políticas de reintento y disyuntores
- Colas de eventos de análisis que solicitan los lotes
- Indizadores de búsqueda semánticos que procesan el contenido asíncronamente
- Comprobadores de enlaces rotos que validan periódicamente URLs externas

Estos ejemplos demuestran los patrones de la Parte 1 en acción, incluyendo el patrón de coordinación de inicio y el manejo de apagado adecuado.

# Lectura adicional

- [Microsoft Docs: Tareas de fondo con servicios alojados](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services)
- [Documentación de Hangfire](https://docs.hangfire.io/)
- [Canales en C#](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels)
- [Documentación Quartz.NET](https://www.quartz-scheduler.net/)
- [StackExchange.Redis](https://stackexchange.github.io/StackExchange.Redis/) - Cliente Redis para .NET