Perché utilizzare .NET Generic Host e BackgroundService nelle app desktop

· Aggiornato il: · · 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.Run che 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 blocchi finally

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 StartAsync e lo interrompi con StopAsync.
  • Servizio ospitato
    • Elaborazione residente che sospende la durata dell’host e viene avviata e interrotta con esso.
    • Implementi IHostedService o, di solito, erediti da BackgroundService.
  • 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.
  • 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

  1. Prima la conclusione (in una riga)
  2. La panoramica in un foglio
    • 2.1. Il quadro generale
    • 2.2. La tabella delle decisioni di posizionamento
  3. 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
  4. Casi in cui si adatta
  5. Un esempio di configurazione minima (WPF)
  6. Come dividere StartAsync / ExecuteAsync / StopAsync
    • 6.1. StartAsync
    • 6.2. ExecuteAsync
    • 6.3. StopAsync
    • 6.4. Una nota per .NET 10 e versioni successive
  7. Anti-pattern comuni
  8. Lista di controllo per la revisione del codice
  9. Una guida decisionale approssimativa
  10. Riepilogo
  11. 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 StartAsync breve, il corpo di lunga durata in ExecuteAsync e la pulizia del tempo di uscita in StopAsync rendono 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.

L'app desktop si avvia(WPF / WinForms)Costruisci / StartAsync l'hostPrepara DI / Registrazione / ConfigurazioneHostedService.StartAsyncBackgroundService.ExecuteAsyncPeriodicTimer / coda / riconnessione / ciclo di monitoraggioMostra MainWindow / MainFormAggiornamenti di stato / registrazione / I / O esterniL'UI utilizza Dispatcher / Invoke solo dove necessarioUscita utente / errore irreversibile / StopApplicationIHost.StopAsyncCancellationToken notificaHostedService.StopAsyncConnessioni 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.CreateApplicationBuilder assembla la base DI / configurazione / registrazione
  • appsettings.json e le variabili di ambiente sono facili da usare così come sono
  • ILogger<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.

  1. Avvia l’host prima di mostrare l’UI
  2. Attendi esplicitamente StopAsync all’uscita
  3. 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.

  1. Passa il CancellationToken dall’inizio alla fine
  2. Assicurati che un’eccezione non possa interrompere silenziosamente l’intero ciclo
  3. 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?
  • ExecuteAsync supera completamente il CancellationToken?
  • Le dipendenze scoped sono 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.Exit o 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

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.

  1. Le responsabilità di avvio e arresto possono essere consolidate in un unico posto
  2. La vita del lavoro di lunga durata può essere posseduta come un progetto
  3. 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 StopAsync e CancellationToken

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

Articoli recenti con gli stessi tag per approfondire argomenti vicini.

Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.

L’articolo è direttamente collegato ai servizi seguenti.

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.

Torna al blog