This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Thursday, 27 November 2025
Ogni moderna applicazione web ha un lavoro che non dovrebbe bloccare una richiesta HTTP email di invio, elaborazione di file, sincronizzazione con servizi esterni, esecuzione di manutenzione programmata. ASP.NET Core fornisce molteplici approcci per la gestione di questo lavoro di background, da semplice IHostedService implementazioni a quadri sofisticati come Hangfire. In questa prima parte, esploreremo i modelli fondamentali e quando utilizzare ciascuno.
Confessione; Mi piace Background Services un LOT, questo sito (un sito BLOG!) ha più di una mezza dozzina di loro che fanno compiti di background carie, ma come tutto hanno alcune ODDITY e pratiche che renderanno il vostro uso di loro molto più piacevole. I servizi di background sono gli eroi unsung delle moderne applicazioni web. Mentre i controller gestiscono le richieste HTTP in primo piano, i servizi di background elaborano silenziosamente le email in coda, indicizzano i contenuti per la ricerca, controllano le API esterne, ripuliscono i file temporanei e gestiscono innumerevoli altre attività che altrimenti bloccherebbero la pipeline di richiesta.
In questa serie in due parti, esploreremo i diversi approcci all'implementazione di servizi di background in ASP.NET Core, dal built-in IHostedService e BackgroundService Astrazioni a soluzioni più sofisticate come Hangfire. Nella Parte 1, esamineremo gli approcci fondamentali e le loro caratteristiche. Parte 2, ci immergeremo nelle implementazioni del mondo reale da una base di codici di produzione.
Importante: Faremo particolare attenzione alla gestione del ciclo di vita
StopAsyncmetodo, che è dove molti sviluppatori incontrano eccezioni criptiche quando le loro applicazioni si spengono.
Prima di tuffarsi nel "come," consideriamo brevemente il "perché." I servizi di sfondo consentono:
ASP.NET Core fornisce diversi approcci all'implementazione di questi servizi, ciascuno con diversi compromessi.
Nei "vecchi giorni" (pre-2010), eseguire il lavoro di background nella vostra applicazione web è stato generalmente considerato una cattiva idea. La saggezza convenzionale era: "I server Web gestiscono le richieste web. Il lavoro di background appartiene a un server separato."
Questo non era solo la saggezza cargo-sette si basava su reali limitazioni tecniche:
I primi server web (e onestamente ora, i servizi Azure 'a buon mercato') tipicamente eseguito su CPU single-core o dual-core. Se hai eseguito un'intensa attività di background della CPU, è direttamente in competizione con le richieste web per lo stesso core:
Single Core (2005):
┌─────────────────────┐
│ Background Task │ ← Uses 80% CPU
│ (80% of core) │
├─────────────────────┤
│ Web Requests │ ← Only 20% left!
│ (20% of core) │ ← Slow responses
└─────────────────────┘
Risultato: Il tuo sito web è diventato lento il lavoro di sfondo momento calci.
Classico ASP.NET utilizzato thread-per-request. Il thread pool era relativamente piccolo (25-100 thread in genere), e le attività di sfondo rubare thread che dovrebbero gestire le richieste 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 ricicla in modo aggressivo i pool di applicazioni (riavviare l'app) in base ai limiti di memoria, al conteggio delle richieste o agli orari.
00:00 - Background import starts (2 hour task)
02:00 - IIS recycles app pool (scheduled)
- Background task killed
- Work lost, must start again
Prima di .NET 4.5 (2012), la programmazione asincrona era (relativamente) dolorosa. Le attività di sfondo spesso bloccavano i thread inutilmente:
// 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
Il paesaggio di oggi è drammaticamente diverso:
L'economia è crollata. Le VM cloud con più core hanno un prezzo ragionevole, e i server blade metal sono sorprendentemente economici. Questo blog gira su un server dedicato a 8 core che costa meno di un analogo Azure VM e ho tutti quei core per me, nessun vicino rumoroso. Un'attività di background su un core non ha un impatto significativo sulle richieste web su altri core:
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
Moderno .NET rende banale la programmazione asincrona. Le attività di sfondo possono attendere I/O senza bloccare i thread:
// 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 ora ha il supporto di prima classe per la programmazione concorrente 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
}
Moderni orchestratori di container ti permettono limitare l'uso delle risorse:
# 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
Questo significa che un'attività di background in fuga non può far morire di fame il tuo livello web.
La domanda non è più "Possiamo eseguire servizi di background nella nostra app web?" ma "Dovremmo?" Esploreremo questa decisione nella sezione "Quando NON utilizzare i servizi di background" in seguito.
Al suo centro, ogni servizio di background in ASP.NET Core implementa IHostedService. Questa interfaccia è splendidamente semplice:
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
Ecco, due metodi. StartAsync viene chiamato quando la tua applicazione inizia, e StopAsync quando si spegne.
Registrate il vostro servizio in Program.cs:
builder.Services.AddHostedService<MyBackgroundService>();
Ecco il ciclo di vita visualizzato:
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 decisione critica all'atto dell'attuazione IHostedService è se il tuo StartAsync metodo dovrebbe bloccare o tornare immediatamente.
Inizio sincrono (blocco):
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;
}
Avvio asincrono (non bloccante):
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
}
}
Quando usare ogni approccio:
Qui è dove le cose ottengono i problemi interessanti e dove molti sviluppatori incontrano i problemi. Quando la vostra applicazione si spegne, ASP.NET core chiama StopAsync su tutti i servizi ospitati. Hai una finestra limitata (default 5 secondi) per pulire con grazia. Puoi estendere questo in Program.cs:
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
L'errore più comune:
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);
}
}
}
Quando esegui questo servizio e fermi l'applicazione, vedrai errori come:
Unable to cast object of type 'TaskCompletionSource`1[System.Threading.Tasks.VoidTaskResult]' to type 'System.Threading.Tasks.Task'
O l'applicazione sarà semplicemente appeso per il periodo di spegnimento prima di terminare con forza.
L'approccio corretto:
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;
}
}
}
}
Punti chiave per una corretta implementazione di StopAsync:
CancellationTokenSource e cancellarloWriter.Complete()Task.WhenAny con il token di annullamento di spegnimentoStopAsync può causare un comportamento imprevedibileScrittura IHostedService Le implementazioni possono essere ripetitive. Hai sempre bisogno di un'attività di background, di una sorgente token di cancellazione e dello stesso modello di pulizia. BackgroundService gestisce questa piastra caldaia per voi:
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();
}
}
Devi solo implementare ExecuteAsync e lasciare la classe base gestire l'impianto idraulico:
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 quando:
StartAsync/StopAsync tempiUso IHostedService quando:
StartAsync vs lavoro di backgroundA volte avete bisogno di servizi per aspettare l'un l'altro. Ad esempio, si potrebbe desiderare che l'indexer di ricerca semantica attendere fino a quando il processore di file markdown ha finito il suo carico iniziale.
Ecco uno schema per coordinare l'avvio del servizio:
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 in un servizio:
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;
}
Questo modello diventa particolarmente utile quando si dispone di più servizi di background con interdipendenze.
Il coordinatore startup funziona all'interno di un'unica istanza di applicazione. Ma cosa succede quando si scala a più istanze? Non si vogliono tre istanze che eseguono tutte la stessa attività programmata contemporaneamente.
RedisCity name (optional, probably does not need a translation) fornisce una soluzione semplice: utilizzare flag (chiavi) per coordinare chi fa cosa.
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);
}
}
}
Per i compiti che non devono essere eseguiti contemporaneamente tra i casi:
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");
}
}
Per scenari più complessi (job multi-step, programmazione affidabile attraverso riavvii), considerare Hangfire che gestisce il blocco distribuito automaticamente con il suo backend del database.
Prima di tuffarci in strumenti più sofisticati come Hangfire, parliamo di quando tu Non dovrebbe utilizzare i servizi di background nella tua applicazione web principale.
I servizi di background in esecuzione nell'applicazione web condividono le risorse con la pipeline di richiesta HTTP. Ciò può causare problemi:
Problema: Il tuo servizio di background consuma connessioni significative di CPU, memoria o database.
// 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);
}
}
}
Quando le richieste web arrivano durante la transcodifica, sono lenti perché la CPU è occupata.
Soluzione: Passare a un servizio di lavoro separato:
# 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: Il vostro lavoro di sfondo ha bisogno di scala diversa rispetto al vostro livello web.
Se sono nello stesso processo, non puoi scalarli in modo indipendente.
Scenario di esempio:
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
Mettere servizi di background nella tua applicazione web significa che dovresti eseguire 20 istanze web solo per gestire la newsletter, sprecando risorse.
Problema: Vuoi distribuire modifiche web senza riavviare i servizi di background (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);
}
}
Ogni spiegamento interrompe l'importazione. Spostala su un servizio operaio separato che schierate indipendentemente.
Problema: Un bug nel servizio di background blocca l'intera applicazione 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);
}
}
Se il lavoro di background è in un processo separato, può bloccarsi e riavviare senza influenzare le richieste web.
Quando si decide di dividere, ecco l'architettura consigliata:
Crea un nuovo progetto utilizzando il modello del servizio di lavoro:
dotnet new worker -n YourApp.Worker
Struttura:
/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();
Distribuisci separatamente:
# 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
Utilizzare una coda di messaggi per disaccoppiare web e lavoratori:
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
Le code delle app web funzionano:
// 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
}
}
Il lavoratore consuma dalla coda:
// 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);
}
}
}
Opzioni di coda dei messaggi popolari:
Per i sistemi complessi, suddivisi per responsabilità:
/YourApp.Web # HTTP requests only
/YourApp.EmailWorker # Sends emails
/YourApp.VideoWorker # Transcodes videos
/YourApp.ReportWorker # Generates reports
/YourApp.Scheduler # Runs scheduled jobs (Hangfire)
Ogni lavoratore può:
Nonostante quanto sopra, alcuni scenari sono perfettamente validi per i servizi di background in-process:
// 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();
}
}
| Caratteristica | Mantenere nell'app web | Spostarsi al servizio del lavoratore |
|---|---|---|
| Uso della CPU per operazione | < 100ms | > 1 secondo |
| Memoria per operazione | < 10 MB | > 100 MB |
| Frequenza | Periodico (minuti/ore) | Continuo o ad alta frequenza |
| Critica | Non critica | Critica |
| Durata | Secondi | Minuti alle ore |
| Scale con | Traffico Web | Profondità coda di lavoro |
| Esempio | Riscaldamento cache, ricarica config | Elaborazione video, grandi importazioni |
Nella piattaforma del blog il cui codice esaminiamo nella Parte 2:
Ha tenuto in app web:
MarkdownDirectoryWatcherService - Lightweight file watcherUmamiBackgroundSender - Eventi di analisi rapidaEmailSenderHostedService - Piccolo volume, non criticoMarkdownReAddPostsService - Solo avvio, configurazione-gatedDovrebbe passare al servizio dei lavoratori in caso di aumento della scala:
BrokenLinkCheckerBackgroundService - Fa molte richieste HTTPSemanticIndexingBackgroundService - Chiama API di integrazione esternaGià in servizio separato:
Mostlylucid.SchedulerService - Hangfire cruscotto e newsletter invioQuesto è un approccio pragmatico: iniziare semplice (in-process), dividere quando si dispone di prove è necessario.
Mentre IHostedService e BackgroundService sono eccellenti per i servizi che possiedi e controlli, a volte hai bisogno di una programmazione più sofisticata. HangfireCity name (optional, probably does not need a translation) Entra.
Hangfire fornisce:
Ecco un semplice esempio:
// 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
Il vostro servizio è solo una classe normale:
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);
}
}
}
Maniglie a sospensione:
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
Quando usare Hangfire:
Quando restare con IHostedService/BackgroundService:
Mentre Hangfire è popolare, ci sono altre librerie che vale la pena considerare:
Funzioni Azure/AWS LambdaCity name (optional, probably does not need a translation):
Nella Parte 1, abbiamo trattato gli approcci fondamentali ai servizi di background in ASP.NET Core:
Le lezioni più importanti:
Dentro Parte 2, esamineremo le implementazioni del mondo reale da una piattaforma del blog di produzione:
Questi esempi dimostrano i modelli della parte 1 in azione, compreso il modello di coordinamento dell'avvio e la corretta gestione dell'arresto.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.