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.
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, 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
StopAsyncmétodo, que es donde muchos desarrolladores encuentran excepciones crípticas cuando sus aplicaciones se apagan.
Antes de sumergirnos en el "cómo", consideremos brevemente el "por qué". Los servicios de fondo le permiten:
ASP.NET Core ofrece varios enfoques para la implementación de estos servicios, cada uno con diferentes compensaciones.
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:
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.
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:
// 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 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
Antes de .NET 4.5 (2012), la programación async era (relativamente) dolorosa.
// 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
El paisaje de hoy es dramáticamente diferente:
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
Modern .NET hace trivial la programación asíncrona. Las tareas de fondo pueden esperar en E/S sin bloquear hilos:
// 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
SIGTERM.NET ahora tiene soporte de primera clase para la programación concurrente con System.Threading.Channels:
// 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
}
Modernos orquestadores de contenedores le permiten limitar el uso de los recursos:
# 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.
En su núcleo, cada servicio de antecedentes en ASP.NET Core implementa IHostedService. Esta interfaz es muy simple:
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:
builder.Services.AddHostedService<MyBackgroundService>();
Aquí está el ciclo de vida visualizado:
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
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):
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):
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:
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:
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
El error más común:
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:
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:
CancellationTokenSource y cancelarloWriter.Complete()Task.WhenAny con el token de cancelación de apagadoStopAsync puede causar un comportamiento impredecibleEscribiendo 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 maneja esta placa de caldera para usted:
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:
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);
}
}
Uso BackgroundService cuando:
StartAsync/StopAsync cronometraciónUso IHostedService cuando:
StartAsync versus trabajo de fondoA 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:
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:
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.
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 proporciona una solución sencilla: utilice banderas (teclas) para coordinar quién hace qué.
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);
}
}
}
Para tareas que no deben correr simultáneamente entre instancias:
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");
}
}
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.
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.
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:
Problema: Su servicio de antecedentes consume importantes conexiones de CPU, memoria o base de datos.
// 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:
# 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
Problema: Su trabajo de fondo necesita escala diferente a su nivel web.
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.
Problema: Desea implementar cambios web sin reiniciar los servicios de fondo (o viceversa).
// 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.
Problema: Un error en su servicio de fondo bloquea toda la aplicación web.
// 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.
Cuando usted decide dividir, aquí está la arquitectura recomendada:
Crear un nuevo proyecto utilizando la plantilla Servicio de trabajador:
dotnet new worker -n YourApp.Worker
Estructura:
/YourApp.Worker
/Services
VideoTranscodingService.cs
EmailSenderService.cs
/Program.cs
/appsettings.json
Program.cs:
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:
# 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
Utilice una cola de mensajes para desvincular la web y los trabajadores:
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:
// 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:
// 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:
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:
A pesar de lo anterior, algunos escenarios están perfectamente bien para los servicios de base en el proceso:
// 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);
}
}
}
// 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;
}
}
// 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
}
// 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();
}
}
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
En la plataforma del blog cuyo código examinamos en la Parte 2:
Mantenido en la aplicación web:
MarkdownDirectoryWatcherService - Visor de archivos ligeroUmamiBackgroundSender - Eventos de análisis rápidosEmailSenderHostedService - Pequeño volumen, no críticoMarkdownReAddPostsService - Solo inicio, configuración cerradaDebe pasar al servicio de los trabajadores si la escala aumenta:
BrokenLinkCheckerBackgroundService - Hace muchas peticiones HTTPSemanticIndexingBackgroundService - Llama a API de integración externaYa en servicio por separado:
Mostlylucid.SchedulerService - Tablero de Hangfire y envío de boletinesEste es un enfoque pragmático: comenzar simple (en el proceso), dividir cuando usted tiene evidencia que necesita.
Mientras tanto IHostedService y BackgroundService son excelentes para los servicios que posees y controlas, a veces necesitas una programación más sofisticada. Hangfire Adelante.
Hangfire proporciona:
He aquí un ejemplo sencillo:
// 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:
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:
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:
Cuándo permanecer con IHostedService / BackgroundService:
Aunque Hangfire es popular, hay otras bibliotecas que vale la pena considerar:
Funciones de Azure/AWS Lambda:
En la Parte 1, hemos cubierto los enfoques fundamentales de los servicios de fondo en ASP.NET Core:
Las lecciones más importantes:
In Parte 2, vamos a examinar implementaciones del mundo real desde una plataforma de blog de producción:
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.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.