Perché utilizzare .NET Generic Host e BackgroundService nelle app desktop
· Aggiornato il: · Go Komura · C#, .NET, Generic Host, BackgroundService, WPF, WinForms, Sviluppo Windows, Progettazione
Fai crescere un po’ uno strumento Windows o un’app residente e l’elaborazione al di fuori dell’UI si moltiplicherà gradualmente.
Polling periodico, sorveglianza dei file, riconnessione, elaborazione delle code, inizializzazione all’avvio, flush all’uscita.
All’inizio puoi cavartela con Form_Load, OnStartup e Task.Run, ma man mano che cresce, diventa vago chi inizia le cose, chi le ferma e chi osserva le eccezioni.
Prima ancora della questione di come scrivere async / await, questo è il punto in cui dovresti decidere a chi appartiene la vita dell’opera.
È qui che Generic Host e BackgroundService di .NET danno i loro frutti.
Sul lato del thread dell’UI di async / await, vedere
WPF / WinForms asincrono e il thread dell’UI su un foglio: dove attendono resi, Dispatcher, ConfigureAwait e dove .Result / .Wait() rimangono bloccati
e
Best practice per C# asincrono / in attesa: una tabella decisionale per Task.Run e ConfigureAwait.
Questo articolo si concentra su ciò che si trova un livello più in là: organizzare “l’avvio e l’arresto dell’intera app”.
I posti che marciscono silenziosamente nella pratica sono più o meno questi.
- Chiamate
Task.Runche spuntano ovunque nei moduli e ViewModels - Condizioni di arresto per i loop residenti sparsi come flag
bool - Il lavoro è ancora in esecuzione all’uscita, quindi l’app occasionalmente non si chiude mai
- Punti di ingresso separati per registrazione / configurazione / DI per tecnologia
- La tentazione di fare pulizia con
Environment.Exit, saltando i blocchifinally
Questo articolo presuppone principalmente app WPF / WinForms / Windows in stile residente su .NET 6 o versioni successive e organizza
perché il Generic Host / BackgroundService ripaga tranquillamente,
fino a che punto vale la pena arrivare e dove l’essere sciatti si trasforma in un pantano.
Inoltre, il codice visualizzato in questo articolo è pubblicato su GitHub come set di esempi completo compilabile ed eseguibile (una libreria, una demo della console che dimostra tutto, dall’avvio allo spegnimento regolare e test unitari).
generic-host- backgroundservice-desktop-app - komurasoft-blog-samples (GitHub)
Allineare innanzitutto la terminologia
Conversazioni come questa diventano improvvisamente difficili da seguire se il significato delle parole rimane confuso. Cerchiamo quindi di fissare in anticipo in modo approssimativo i termini utilizzati in questo articolo.
- Generic Host
- La base che si occupa insieme dell’“avvio”, delle “dipendenze”, della “configurazione”, della “registrazione” e dello “spegnimento” di un’app .NET.
- Non è un meccanismo ASP.NET solo Core; può essere utilizzato nelle app console, nei lavoratori e nelle app desktop.
- Ospite /
IHost- L’oggetto concreto dopo la costruzione.
- Lo inizi con
StartAsynce lo interrompi conStopAsync.
- Servizio ospitato
- Elaborazione residente che sospende la durata dell’host e viene avviata e interrotta con esso.
- Implementi
IHostedServiceo, di solito, erediti daBackgroundService.
BackgroundService- Un comodo aiuto per l’implementazione di
IHostedService. - È possibile scrivere il corpo a esecuzione prolungata in
ExecuteAsync, il che semplifica l’organizzazione dei cicli di monitoraggio e dell’elaborazione periodica.
- Un comodo aiuto per l’implementazione di
- a vita
- In questo articolo, significa “quando inizia il lavoro, quando finisce e chi ha la responsabilità di fermarlo”.
- Non mera durata, ma gestione della vita compresa la responsabilità di inizio e la responsabilità di fine.
- spegnimento regolare
- Piuttosto che una terminazione forzata, segnalare una fermata e uscire dopo aver riordinato il più possibile il lavoro a bordo.
- Ad esempio, “non avviare il ciclo successivo”, “decidi quanto scaricare la coda” e “attendi la chiusura e scarica” appartengono tutti qui.
- DI
- Abbreviazione di Dependency Injection: ricevere oggetti dipendenti tramite un contenitore anziché assemblarli manualmente sul sito della chiamata.
- Per questo articolo è sufficiente la comprensione “configurare logger, impostazioni e lettori insieme al punto di ingresso anziché in un festival di nuovi”.
Questa non è solo “un’introduzione alla pratica lezione BackgroundService” —
è più facile da seguire se la leggi come una storia sulla raccolta dell’avvio e dell’arresto dell’intera app nell’host e sul possesso della durata dell’elaborazione residente come progetto.
Sommario
- Prima la conclusione (in una riga)
- La panoramica in un foglio
- 2.1. Il quadro generale
- 2.2. La tabella delle decisioni di posizionamento
- Perché è conveniente nelle app desktop
- 3.1. È più semplice separare le responsabilità di elaborazione dell’UI e dei residenti
- 3.2. Un punto di ingresso per avvio, arresto ed eccezioni
- 3.3. Più facile da progettare con lo spegnimento graduale
- 3.4. DI / Registrazione / Configurazione Insieme fin dall’inizio
- Casi in cui si adatta
- Un esempio di configurazione minima (WPF)
- Come dividere
StartAsync/ExecuteAsync/StopAsync- 6.1.
StartAsync - 6.2.
ExecuteAsync - 6.3.
StopAsync - 6.4. Una nota per .NET 10 e versioni successive
- 6.1.
- Anti-pattern comuni
- Lista di controllo per la revisione del codice
- Una guida decisionale approssimativa
- Riepilogo
- Riferimenti
1. Prima la conclusione (in una riga)
- Il Generic Host è piuttosto convincente come base per l’avvio e la gestione della durata, anche nelle app desktop.
BackgroundServiceè una nave per mettere “lavoro di lunga durata” in una vita gestita piuttosto che un “fuoco e dimentica”Task.Run.- Ciò che ripaga di più nella pratica è raccogliere responsabilità di avvio / responsabilità di arresto / monitoraggio delle eccezioni / logging / DI / configurazione in un unico progetto in un unico posto.
- Mantenendo
StartAsyncbreve, il corpo di lunga durata inExecuteAsynce la pulizia del tempo di uscita inStopAsyncrendono le cose molto più leggibili. - Le app residenti, le app nella barra delle applicazioni, il monitoraggio delle apparecchiature, la sincronizzazione periodica, la post-elaborazione ordinata e i cicli di riconnessione sono soluzioni particolarmente adatte.
- Al contrario, trasformare tutto in un
BackgroundService, anche il lavoro che viene eseguito una volta alla pressione di un pulsante, diventa un po’ grandioso. StopAsyncè utile, ma non è un’assicurazione contro arresti anomali del processo o interruzione forzata. È anche importante non appoggiarsi troppo alla pulizia.
In breve, il motivo per cui Generic Host / BackgroundService ripaga nelle app desktop è
non tanto “perché c’è un’elaborazione in background”,
ma “perché vuoi possedere la durata dell’elaborazione in background come progetto, non come effetto collaterale dell’UI”.
2. Panoramica in un unico foglio
2.1. Il quadro generale
Guardare prima questo diagramma accelera notevolmente la conversazione.
flowchart LR
A["L'app desktop si avvia<br/>(WPF / WinForms)"] --> B["Costruisci / StartAsync l'host"]
B --> C["Prepara DI / Registrazione / Configurazione"]
B --> D["HostedService.StartAsync"]
D --> E["BackgroundService.ExecuteAsync"]
E --> F["PeriodicTimer / coda / riconnessione / ciclo di monitoraggio"]
C --> G["Mostra MainWindow / MainForm"]
F --> H["Aggiornamenti di stato / registrazione / I / O esterni"]
H --> I["L'UI utilizza Dispatcher / Invoke solo dove necessario"]
J["Uscita utente / errore irreversibile / StopApplication"] --> K["IHost.StopAsync"]
K --> L["CancellationToken notifica"]
L --> M["HostedService.StopAsync"]
M --> N["Connessioni strette / lavaggio / spegnimento regolare"]
Ciò che accade comunemente nelle app dell’UI è che le responsabilità si disperdono poco a poco tra Program.cs / App.xaml.cs / Form_Load / Closing / Task.Run / Timer / singleton statici.
Porta qui l’Host e potrai dividerlo all’incirca in questo modo.
- UI: schermate, input, visualizzazione
- HostedService / BackgroundService: elaborazione residente, monitoraggio, elaborazione code, lavoro periodico
- DI servizi: logica aziendale effettiva, connessioni esterne, configurazione, registrazione
Il solo fatto di poter tagliare le cose in questo modo cambia considerevolmente la possibilità di revisione.
2.2. La tabella delle decisioni di posizionamento
| Quello che vuoi | Primo candidato al tirocinio | Motivo |
|---|---|---|
| Inizializzazione leggera subito dopo l’avvio | StartAsync |
Significato chiaro come un compito breve che partecipa all’avvio |
| Monitoraggio / polling / riconnessione di lunga durata | ExecuteAsync |
Facile da gestire insieme alla durata del servizio |
| Interrompi notifica / scarica / chiudi all’uscita | StopAsync |
Facile scrivere un arresto grazioso insieme a CancellationToken |
| Cablaggio, configurazione, registrazione delle dipendenze | Host.CreateApplicationBuilder |
Un punto di ingresso consolidato |
| Aggiornamenti dello schermo | Lato UI | Meno incidenti quando i lavoratori non toccano direttamente l’UI |
| Lavoro one-shot premendo il pulsante | Un normale metodo async |
Di solito non è necessario un HostedService |
| Post-elaborazione in background ordinata | Channel<T> + BackgroundService |
Durata e limiti più facili da gestire rispetto al fuoco e dimentica |
Il valore dell’inclusione dell’Host risiede meno nella capacità di “creare qualcosa di asincrono” e più nell’interno la decisione sulla collocazione delle cose diventa chiara.
3. Perché è conveniente nelle app desktop
3.1. È più semplice separare le responsabilità di elaborazione dell’UI e dei residenti
L’UI sembra la stella di un’app desktop, ma in pratica il peso di solito si accumula all’esterno dell’UI.
Ad esempio:
- Sincronizzazione dello stato ogni 10 secondi
- Riconnessione ad apparecchiature o server
- Osservazione e acquisizione di file
- Post-elaborazione in coda per dopo
- Inoltro dei registri ed emissione di metriche
- Riscaldamento della cache all’avvio
Questi non sono “eventi sullo schermo”: sono elaborazioni che rallentano la durata dell’app nel suo insieme.
Ospitali in moduli o finestre code-behind e la responsabilità di fermarli quando lo schermo si chiude, la responsabilità di cogliere le loro eccezioni, e la responsabilità di decidere nuovi tentativi e backoff iniziare a fondersi con le preoccupazioni dell’UI.
Con BackgroundService,
la dichiarazione “questo lavoro dura finché l’app è in esecuzione”
si presenta sotto forma di codice.
Questo è silenziosamente potente.
3.2. Un punto di ingresso per avvio, arresto ed eccezioni
Anche in un’app desktop senza Host, puoi fare qualcosa di simile allineando ServiceCollection, ConfigurationBuilder e LoggerFactory individualmente.
Ma quella forma tende ad allontanarsi poco a poco.
- DI in
Program.cs - Configurazione in una statica personalizzata
- Accesso in una fabbrica separata
- Gestione dell’uscita in
ApplicationExit - Lavoro residente in
Task.Run
All’inizio funziona. Ma guardando indietro mesi dopo, diventa difficile capire chi possiede l’intera vita dell’app.
Con Generic Host,
- Registrazione del servizio
- Caricamento della configurazione
- Configurazione della registrazione
- Avvio del servizio ospitato
- Interrompi la notifica
- Spegnimento dell’intera app tramite
IHostApplicationLifetime
entrano tutti nello stesso quadro.
In altre parole, diventa facile consolidare il punto di ingresso per “come si avvia questa app e come si termina” in un unico posto. Per le app residenti, questo è ciò che ripaga in seguito.
3.3. Più facile da progettare con lo spegnimento graduale
L’elaborazione residente è più difficile da interrompere che da avviare. Veramente. L’inizio è di 3 righe; fermarsi ha il sapore del fango.
Ad esempio, allo spegnimento potresti voler:
- Annulla l’I / O in volo
- Impedire l’avvio del ciclo successivo
- Decidi fino a che punto consumare gli elementi rimanenti della coda
- Chiudi i socket e gli oggetti COM
- Attendi lo svuotamento del registro e la persistenza dello stato
Appoggia tutto questo a FormClosing, e si fonde con le preoccupazioni dello schermo e diventa doloroso.
Con l’Host / BackgroundService, hai CancellationToken e StopAsync, quindi
“la strada per fermarsi” esiste fin dall’inizio.
Non è magia, ovviamente.
In caso di incidente o uccisione, StopAsync potrebbe non essere mai chiamato.
Anche così, il semplice fatto di avere il design “all’uscita normale, ci fermiamo su questo percorso” rende le cose notevolmente più silenziose.
3.4. DI / Registrazione / Configurazione Uniti fin dall’inizio
La virtù del Generic Host non è solo BackgroundService.
Host.CreateApplicationBuilderassembla la base DI / configurazione / registrazioneappsettings.jsone le variabili di ambiente sono facili da usare così come sonoILogger<T>può essere utilizzato nello stesso stile sia dall’UI che dai lavoratori- Se necessario, le impostazioni possono essere incluse nella famiglia
IOptions<T>
In particolare nei progetti di utensili Windows, “le impostazioni e il logger che abbiamo mantenuto in modo statico perché l’app è iniziata in piccolo diventano dolorosi in seguito” è abbastanza comune.
Mettili sull’host dall’inizio, e l’app sibila meno quando inizia a ingrassare.
4. Casi in cui si adatta
Il Generic Host / BackgroundService tende a ripagare soprattutto in casi come questi.
- App residenti nel vassoio Con sincronizzazione periodica, monitoraggio, notifiche, riconnessione
- App che si collegano ad apparecchiature / fotocamere / prese Con connessione keep-alive, monitoraggio, tentativi, letture dello stato
- Strumenti di integrazione file Con sorveglianza, code di acquisizione, elaborazione ordinata
- Prevenire il gonfiamento dello strumento interno Piccolo per ora, ma la configurazione, la registrazione e l’I / O esterno sembrano destinati a crescere
- App in cui la qualità dell’uscita è importante Non vuoi lasciare indietro lo stato a metà durante la chiusura
Al contrario, ci sono casi in cui non è necessario coinvolgere immediatamente l’ospite.
- Un piccolo strumento che si avvia una volta, esegue un lavoro ed esce
- Uno schermo quasi senza lavoro in background, completo solo di eventi dell’UI
- Uno strumento di supporto interno veramente piccolo le cui dipendenze e configurazione cresceranno a malapena
L’Host non è “obbligatorio”.
Tuttavia, una volta che riesci a vedere due o più parti dell’elaborazione residente, puoi considerarla abbastanza favorevolmente.
È molto più economico che ripulire una colonia Task.Run in un secondo momento.
5. Un esempio di configurazione minima (WPF)
Ad esempio, ecco una configurazione minima che avvia un host in WPF ed esegue un BackgroundService che legge lo stato esterno ogni 5 secondi.
In WinForms il punto di ingresso cambia in Main / ApplicationContext, ma il pensiero è quasi lo stesso.
5.1. App.xaml.cs
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace DesktopHostSample;
public partial class App : Application
{
private IHost? _host;
protected override async void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
HostApplicationBuilder builder = Host.CreateApplicationBuilder(e.Args);
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(15);
});
builder.Services.AddSingleton<MainWindow>();
builder.Services.AddSingleton<StatusStore>();
builder.Services.AddScoped<IDeviceStatusReader, DeviceStatusReader>();
builder.Services.AddHostedService<DevicePollingBackgroundService>();
_host = builder.Build();
await _host.StartAsync();
MainWindow mainWindow = _host.Services.GetRequiredService<MainWindow>();
mainWindow.Show();
}
protected override async void OnExit(ExitEventArgs e)
{
if (_host is not null)
{
await _host.StopAsync();
_host.Dispose();
}
base.OnExit(e);
}
}
Ci sono tre punti in questa forma.
- Avvia l’host prima di mostrare l’UI
- Attendi esplicitamente
StopAsyncall’uscita - Bundle DI / servizi ospitati / timeout di spegnimento al punto di ingresso
Rendere asincrono OnExit richiede un po’ di attenzione a causa dei vincoli del framework dell’UI,
ma vale la pena scrivere esplicitamente il flusso “ferma l’host all’uscita”.
5.2. BackgroundService
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DesktopHostSample;
public sealed class DevicePollingBackgroundService(
IServiceScopeFactory scopeFactory,
StatusStore statusStore,
ILogger<DevicePollingBackgroundService> logger) : BackgroundService
{
public override async Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Device polling service is starting.");
await base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Device polling loop started.");
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
using IServiceScope scope = scopeFactory.CreateScope();
IDeviceStatusReader reader =
scope.ServiceProvider.GetRequiredService<IDeviceStatusReader>();
DeviceStatus status = await reader.ReadAsync(stoppingToken);
statusStore.Update(status);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
logger.LogError(ex, "Device polling failed.");
}
}
logger.LogInformation("Device polling loop finished.");
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Device polling service is stopping.");
await base.StopAsync(cancellationToken);
logger.LogInformation("Device polling service stopped.");
}
}
Ciò che conta qui è scrivere ExecuteAsync chiaramente,
come “ciclo while gestito”.
- Cadenza tramite
PeriodicTimer - Interruzione tramite
stoppingToken - Eccezioni registrate
- Se sono necessarie dipendenze
scoped, aprire un ambito a ogni iterazione
Con questa forma, “dove inizia questo lavoro residente, dove si ferma e dove diventano visibili i suoi fallimenti” diventa notevolmente più facile da leggere.
5.3. Non collegare la condivisione dello stato direttamente all’UI
Se un lavoratore tocca direttamente gli oggetti dell’UI, i problemi del thread dell’UI si ripresentano semplicemente lì.
Quindi la separazione più sicura è, in primo luogo:
- Il lavoratore aggiorna un archivio di stati o un livello di messaggistica *L’UI legge / riflette tale stato nel proprio contesto
StatusStore può essere mantenuto come un sottile strato condiviso come questo, ad esempio.
namespace DesktopHostSample;
public sealed class StatusStore
{
private readonly object _gate = new();
private DeviceStatus _current = DeviceStatus.Empty;
public DeviceStatus Current
{
get
{
lock (_gate)
{
return _current;
}
}
}
public void Update(DeviceStatus next)
{
lock (_gate)
{
_current = next;
}
}
}
public sealed record DeviceStatus(string Message)
{
public static readonly DeviceStatus Empty = new("No Data");
}
Se hai bisogno di una notifica immediata all’UI, utilizza Dispatcher / BeginInvoke / events / a messenger.
Ma questa responsabilità si fonde meno se viene mantenuta ai confini dell’UI.
6. Come dividere StartAsync / ExecuteAsync / StopAsync
Quando questi tre si fondono insieme, la testa del lettore si annebbia velocemente. La seguente suddivisione è abbastanza stabile come punto di partenza.
6.1. StartAsync
StartAsync è il luogo per lavori brevi che partecipano alla startup.
Buone misure:
- Registrazione di avvio
- Configurazione dell’abbonamento leggera
- Preparazione dello stato iniziale che termina rapidamente
- Sequenza minima attorno a
base.StartAsync
Cattivi adattamenti:
- Il riscaldamento richiede decine di secondi *Cicli infiniti
- Il corpo principale è rivestito con I / O pesanti
Rendi StartAsync pesante e l’avvio dell’intera app sembrerà lento.
Trattandolo come “il luogo dove scrivere il segnale di partenza” si mantengono bassi gli incidenti.
6.2. ExecuteAsync
ExecuteAsync è il corpo della durata del servizio.
Buone misure:
- Sondaggio
- Circuiti di monitoraggio
- Circuiti di riconnessione
- Consumatori che leggono un
Channel<T>*Lavoro periodico - Tutto ciò che “vive finché non si ferma”
Ci sono tre trucchi qui.
- Passa il
CancellationTokendall’inizio alla fine - Assicurati che un’eccezione non possa interrompere silenziosamente l’intero ciclo
- Non continuare ad accumulare tentativi ad hoc e backoff
BackgroundService è comodo, ma lasciato incustodito può anche diventare “un gigantesco anello che risucchia tutto”.
È meglio suddividere il lavoro effettivo in servizi separati e mantenere ExecuteAsync concentrato sulla gestione e orchestrazione dell’intera vita.
6.3. StopAsync
StopAsync è il luogo in cui riordinare l’uscita normale.
Buone misure:
- Interrompi la registrazione
- Eliminazione di timer / abbonamenti / osservatori
- Riordino delle risorse che desideri chiudere / svuotare in modo esplicito
- In attesa di completamento tramite
base.StopAsync
Tuttavia, è anche importante non aspettarsi tutto da StopAsync.
- Il processo si è bloccato
- È stato terminato con la forza
- L’OS l’ha ucciso
In questo tipo di uscite, potrebbe semplicemente non funzionare mai.
Quindi:
- Perseverare il più possibile con piccoli incrementi durante il normale funzionamento
- Non progettare in modo tale che la coerenza venga mantenuta solo all’uscita
- Rendi la pulizia idempotente
Questi contano. Prova a salvare il mondo solo all’uscita e le cose di solito diventano torbide.
6.4. Una nota per .NET 10 e versioni successive
Come modifica dal 2025 in poi, in .NET 10 il comportamento è cambiato in modo che l’intero BackgroundService.ExecuteAsync venga eseguito come attività in background.
In precedenza, si verificava un comportamento leggermente confuso in cui la parte sincrona prima del primo await bloccava l’avvio di altri servizi durante l’avvio.
Con questo cambiamento, l’incidente in cui “le prime righe di ExecuteAsync appesantivano l’avvio” diventa meno probabile.
Anche così, dal punto di vista progettuale, la divisione
- Breve lavoro partecipando alla startup →
StartAsync - L’organismo di lunga durata →
ExecuteAsync
rimane la disposizione più leggibile.
Se desideri un controllo più rigoroso sui tempi di avvio, viene visualizzato IHostedLifecycleService.
Questo è il tipo di argomento tranquillo che ripaga una volta che un’app residente aumenta di peso.
7. Anti-pattern comuni
7.1. Avvio di un ciclo infinito in Window_Loaded / Form_Shown
Facile all’inizio. Ma la responsabilità di arresto e la responsabilità di eccezione restano saldamente legate al lato dell’UI.
Una volta che le condizioni iniziano a moltiplicarsi – “fermati quando lo schermo si chiude” “non fermarti quando ridotto a icona nel vassoio,” “riavvia quando cambiano le impostazioni” — diventa doloroso rapidamente.
7.2. Spara e dimentica Task.Run
Task.Run in sé non è malvagio.
Ciò che è male è che nessuno possiede la vita e le eccezioni.
In particolare, avviare il lavoro residente con Task.Run(async () => { while (...) { ... } }), e
*Quando finisce? *Chi lo aspetta?
- Come vengono viste le eccezioni? *Quanto tempo dobbiamo aspettare all’uscita?
tutto diventa vago.
Il semplice fatto di metterlo su un BackgroundService rende notevolmente più semplice la risoluzione.
7.3. Toccando l’UI direttamente da BackgroundService
Questa è una mina terrestre. I problemi del thread dell’UI e quelli della durata si fondono contemporaneamente.
I lavoratori non devono accedere direttamente all’UI; posizionando un confine tramite uno di
*Stato
- Eventi
- Messaggi
- Una coda
è più sicuro.
7.4. Appoggiarsi alla logica di salvataggio critico su StopAsync da solo
StopAsync aiuta con le uscite normali, ma non è il Giudizio Universale.
Un design che salva solo all’uscita, scarica solo all’uscita, raggiunge la coerenza solo all’uscita,
crolla in un incidente.
7.5. Utilizzare l’host ma abbandonare il processo in modo approssimativo con Environment.Exit
Anche questo è comune.
Chiamando Environment.Exit “va bene, smettiamola”
taglia, con le tue mani, il grazioso percorso di spegnimento mantenuto dall’host.
Se un errore fatale dovesse terminare l’intera app,
la mossa più naturale è usare prima IHostApplicationLifetime.StopApplication() e
prendere la strada legittima per fermarsi.
8. Lista di controllo per la revisione del codice
Quando si esamina un’app desktop utilizzando Generic Host / BackgroundService, controllarli in ordine è illuminante.
- Il lavoro è un’elaborazione che rallenta la durata dell’app o è semplicemente una gestione degli eventi dell’UI?
- Le responsabilità della startup sono suddivise adeguatamente tra
StartAsync/ExecuteAsync/StopAsync? StartAsyncè diventato troppo pesante?ExecuteAsyncsupera completamente ilCancellationToken?- Le dipendenze
scopedsono mantenute direttamente da un servizio ospitato? - I lavoratori toccano direttamente gli oggetti dell’UI?
- Le eccezioni vengono inghiottite silenziosamente?
- I cicli di tentativi sono diventati illimitati e ad alta frequenza?
- Esiste un limite massimo al tempo di attesa all’uscita?
- C’è
Environment.Exito la terminazione basata sull’uccisione mescolata?
Rispetto a questa lista di controllo, la differenza tra “abbiamo in un certo senso aggiunto l’Host” e “abbiamo la vita organizzata come un progetto” diventa abbastanza visibile.
9. Una guida decisionale approssimativa
| Quello che vuoi | Prima scelta |
|---|---|
| Allinea DI / registrazione / configurazione nell’app | Host.CreateApplicationBuilder |
| Esegui un ciclo residente | BackgroundService |
| Esegui a intervalli fissi | PeriodicTimer + BackgroundService |
| Scarico ordinato post-elaborazione | Channel<T> + BackgroundService |
| Utilizzare servizi con ambito | IServiceScopeFactory.CreateScope() |
| Avvisa l’intera app di un’uscita normale | IHostApplicationLifetime.StopApplication() |
| Aggiorna l’UI | Dispatcher / Invoke sul lato dell’UI |
| Operazioni sullo schermo singolo | Un normale metodo async |
| Rigoroso controllo del ciclo di vita all’avvio | Consideriamo IHostedLifecycleService |
10. Riepilogo
Il motivo per portare Generic Host / BackgroundService in un’app desktop è
non “perché vogliamo scriverlo in modo web”.
Ciò che veramente ripaga sono queste tre cose.
- Le responsabilità di avvio e arresto possono essere consolidate in un unico posto
- La vita del lavoro di lunga durata può essere posseduta come un progetto
- L’arresto graduale può essere gestito dal punto di ingresso, non bloccato in un secondo momento
Gli strumenti Windows e le app residenti potrebbero iniziare in piccolo, ma monitoraggio, sincronizzazione, riconnessione, code, registrazione e configurazione si accumulano bit per bit. Gestiscili come un ripensamento del codice dell’UI e le cose diventano silenziosamente dolorose in seguito.
Al contrario, basta dividere
*UI come UI
- Elaborazione residente come servizi ospitati
- Il lavoro effettivo come servizi DI
- Spegnimento tramite
StopAsynceCancellationToken
riordina notevolmente le cose.
Non c’è niente di appariscente in questo. Ma questo tipo di design silenzioso ripaga ampiamente nella pratica. Riduce quella sgradevole appiccicosità del “a volte si comporta in modo strano quando si chiude” e “nessuno sa dove le cose verranno fermate.”
Se sei bloccato su uno strumento Windows o su un’app residente (conversione a BackgroundService, progettazione di avvio / arresto, cicli di monitoraggio, risoluzione della durata di COM / socket / file watcher o isolamento di difetti del tempo di uscita) non esitare a consultarci, a partire da una revisione del progetto o da un chiarimento della politica.
11. Riferimenti
- Codice di esempio completo per questo articolo (libreria, demo, test unitari) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/generic-host-backgroundservice-desktop-app
- Articolo correlato: Migliori pratiche asincrone / in attesa di C#: una tabella decisionale per Task.Run e ConfigureAwait
- Articolo correlato: WPF / WinForms asincrono e thread dell’UI su un foglio
- .NET Generic Host
- Attività in background con servizi ospitati in ASP.NET Core
- BackgroundService Classe
- Modifica importante: BackgroundService esegue tutto ExecuteAsync come attività
- Accesso C# - .NET
Articoli correlati
Articoli recenti con gli stessi tag per approfondire argomenti vicini.
Icone nella system tray e notifiche toast nelle app Windows — le insidie di NotifyIcon e come scegliere l'AppNotification giusta
Una guida pratica per mantenere un'applicazione Windows aziendale residente nella system tray (area di notifica) e avvisare l'utente tram...
Internazionalizzazione delle app WinForms/WPF — la pratica di resx, satellite assembly e cambio cultura
Analizziamo dal punto di vista pratico l'internazionalizzazione delle applicazioni desktop Windows: la differenza tra CurrentCulture e Cu...
Stampa e output PDF nelle applicazioni aziendali Windows — come scegliere tra System.Drawing.Printing, WPF e librerie di report
Organizza in una tabella decisionale la stampa WinForms con PrintDocument, la stampa WPF con FlowDocument/FixedDocument e le alternative ...
Outsourcing e sviluppo su commissione di app Windows: cosa chiarire prima di affidare l'incarico
Prima di affidare in outsourcing o su commissione lo sviluppo di un'app Windows, ecco i punti da chiarire: revisione del software esisten...
Qual è il .NET Generic Host? - La base per DI, configurazione e registrazione
Il .NET Generic Host in parole povere: un posto per DI, configurazione, registrazione, BackgroundService e arresto regolare. Esempio di c...
Argomenti correlati
Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.
Argomenti tecnici Windows
Portale su sviluppo Windows, analisi dei problemi e valorizzazione delle risorse esistenti.
Generic Host e architettura dell'applicazione
Generic Host, BackgroundService, DI, configurazione, registrazione e ciclo di vita.
Thread UI e timer
Thread UI WPF / WinForms, flussi asincroni, Dispatcher e progettazione dei timer.
Servizi collegati all’argomento
L’articolo è direttamente collegato ai servizi seguenti.
Sviluppo di applicazioni Windows
Questo tema è vicino allo sviluppo stesso di applicazioni desktop, inclusa l'elaborazione in background, l'elaborazione periodica, la riconnessione e la gestione dell'arresto.
Consulenza tecnica e revisione del progetto
Se si desidera innanzitutto esaminare la suddivisione delle responsabilità tra l'UI e l'elaborazione residente o la progettazione dell'arresto regolare, è possibile organizzare l'operazione come consulenza tecnica e impegno di revisione del progetto.
Domande frequenti
Domande che ricorrono nelle consulenze sull’argomento dell’articolo.
- Perché utilizzare .NET Generic Host in un'app desktop anziché solo Task.Run?
- Generic Host non è solo Web: riunisce avvio, inserimento delle dipendenze, configurazione, registrazione e arresto in un unico framework che funziona anche in WPF, WinForms e nelle app residenti. Il vero vantaggio è che la responsabilità di avvio, di arresto della responsabilità, il monitoraggio delle eccezioni e la configurazione risiedono tutti in un unico luogo progettato invece di essere sparsi tra Program.cs, App.xaml.cs, eventi del modulo e singleton statici. Fire-and-forget Task.Run è problematico proprio perché nessuno possiede la durata dell'opera o le sue eccezioni, quindi non è chiaro quando finisce, chi l'attende e quanto tempo attendere all'uscita.
- Qual è la differenza tra StartAsync, ExecuteAsync e StopAsync in un BackgroundService?
- StartAsync è per lavori brevi che partecipano all'avvio, come la registrazione e la configurazione leggera; renderlo pesante rende l'intera app lenta. ExecuteAsync è il corpo di lunga durata del ciclo di vita del servizio, adatto al polling, al monitoraggio, ai cicli di riconnessione e ai consumatori di code, e dovrebbe passare il CancellationToken fino in fondo. StopAsync serve per riordinare un'uscita normale, come la chiusura delle connessioni e lo svuotamento, ma non viene chiamato in caso di arresto anomalo o terminazione forzata, quindi lo stato critico dovrebbe essere persistente con piccoli incrementi durante il normale funzionamento anziché solo all'uscita.
- Un BackgroundService può aggiornare direttamente l'UI?
- Non dovrebbe. Se un lavoratore tocca direttamente gli oggetti dell'UI, i problemi del thread dell'UI e quelli della durata si fondono contemporaneamente. La separazione più sicura prevede che il lavoratore aggiorni un archivio di stati thread-safe o un livello di messaggistica, mentre l'UI legge e riflette tale stato nel proprio contesto. Se è necessaria una notifica immediata, utilizza Dispatcher, BeginInvoke, eventi o un messenger, ma mantieni tale responsabilità al limite dell'UI anziché all'interno del lavoratore.
- Quando il Generic Host è eccessivo per uno strumento Windows?
- Non ne hai bisogno per un piccolo strumento che si avvia una volta, fa un lavoro ed esce, per una schermata il cui comportamento è completo solo di eventi dell'UI o per un piccolo helper interno le cui dipendenze e configurazione cresceranno a malapena. Il lavoro che viene eseguito una volta per ogni pressione del pulsante va bene come un normale metodo asincrono piuttosto che come un servizio ospitato. Tuttavia, una volta che è possibile vedere due o più parti dell'elaborazione residente, vale la pena prendere in considerazione l'adozione dell'host, perché è molto più economico che ripulire una colonia di chiamate Task.Run in un secondo momento.
Profilo dell’autore
Pagina di presentazione dell’autore dell’articolo.
Go Komura
Rappresentante di KomuraSoft LLC
Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.