Condividi tramite


Host generico .NET

In questo articolo vengono illustrati i vari modelli per la configurazione e la compilazione di un host generico .NET disponibile nel pacchetto NuGet Microsoft.Extensions.Hosting. L'host .NET generico è responsabile della gestione dell'avvio e della durata delle app. I modelli di servizio del ruolo di lavoro creano un host generico .NET, HostApplicationBuilder. L'host generico può essere usato con altri tipi di applicazioni .NET, ad esempio app console.

Un host è un oggetto che incapsula le risorse e la durata di un'app, ad esempio:

  • Inserimento di dipendenze (DI)
  • Registrazione
  • Impostazione
  • Arresto dell'app
  • IHostedService Implementazioni

All'avvio, l’host chiama IHostedService.StartAsync ogni implementazione di IHostedService registrata nella raccolta di servizi ospitati del contenitore del servizio. In un'app di servizio del ruolo di lavoro tutte le implementazioni IHostedService che contengono istanze BackgroundService hanno le relative chiamate a un metodo BackgroundService.ExecuteAsync.

Il motivo principale per cui tutte le risorse interdipendenti dell'app sono incluse in un unico oggetto è la gestione del ciclo di vita, vale a dire il controllo sull'avvio dell'app e sull'arresto normale.

Configurare un host

L'host è in genere configurato, compilato ed eseguito da codice nella classe Program. Il metodo Main:

I modelli di servizio del ruolo di lavoro .NET generano il codice seguente per creare un host generico:

using Example.WorkerService;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

IHost host = builder.Build();
host.Run();

Per altre informazioni sui servizi del ruolo di lavoro, vedere Servizi del ruolo di lavoro in .NET.

Impostazioni del generatore host

Il metodo CreateApplicationBuilder:

  • Imposta la radice del contenuto sul percorso restituito da GetCurrentDirectory().
  • Carica la configurazione dell'host da:
    • Le variabili di ambiente con prefisso DOTNET_.
    • Argomenti della riga di comando.
  • Carica la configurazione dell'app da:
    • appsettings.json.
    • appsettings.{Environment}.json.
    • Segreti del manager quando l'app viene eseguita nell'ambiente Development.
    • variabili di ambiente.
    • Argomenti della riga di comando.
  • Aggiunge i provider di log seguenti:
    • Console
    • Debug
    • EventSource
    • EventLog (solo quando è in esecuzione su Windows)
  • Abilita la convalida dell'ambito e la convalida delle dipendenze quando l'ambiente è Development.

HostApplicationBuilder.Services è un'istanza Microsoft.Extensions.DependencyInjection.IServiceCollection. Questi servizi vengono usati per compilare un IServiceProvider che viene usato con l'inserimento delle dipendenze per risolvere i servizi registrati.

Servizi forniti dal framework

Quando si chiama IHostBuilder.Build() o HostApplicationBuilder.Build(), i servizi seguenti vengono registrati automaticamente:

Generatori host aggiuntivi basati su scenari

Se si compila per il Web o si scrive un'applicazione distribuita, potrebbe essere necessario usare un generatore host diverso. Considerare l'elenco seguente di generatori di host aggiuntivi:

IHostApplicationLifetime

Inserire il servizio IHostApplicationLifetime in qualsiasi classe per gestire le attività post-avvio e di arresto normale. Tre proprietà nell'interfaccia sono token di annullamento usati per registrare i metodi del gestore dell'evento di avvio e di arresto. L'interfaccia include anche un metodo StopApplication().

L'esempio seguente è un'implementazione IHostedService e IHostedLifecycleService che registra gli eventi IHostApplicationLifetime:

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace AppLifetime.Example;

public sealed class ExampleHostedService : IHostedService, IHostedLifecycleService
{
    private readonly ILogger _logger;

    public ExampleHostedService(
        ILogger<ExampleHostedService> logger,
        IHostApplicationLifetime appLifetime)
    {
        _logger = logger;

        appLifetime.ApplicationStarted.Register(OnStarted);
        appLifetime.ApplicationStopping.Register(OnStopping);
        appLifetime.ApplicationStopped.Register(OnStopped);
    }

    Task IHostedLifecycleService.StartingAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("1. StartingAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedService.StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("2. StartAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedLifecycleService.StartedAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("3. StartedAsync has been called.");

        return Task.CompletedTask;
    }

    private void OnStarted()
    {
        _logger.LogInformation("4. OnStarted has been called.");
    }

    private void OnStopping()
    {
        _logger.LogInformation("5. OnStopping has been called.");
    }

    Task IHostedLifecycleService.StoppingAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("6. StoppingAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedService.StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("7. StopAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedLifecycleService.StoppedAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("8. StoppedAsync has been called.");

        return Task.CompletedTask;
    }

    private void OnStopped()
    {
        _logger.LogInformation("9. OnStopped has been called.");
    }
}

È possibile modificare il modello di servizio del ruolo di lavoro per aggiungere l'implementazione ExampleHostedService:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using AppLifetime.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<ExampleHostedService>();
using IHost host = builder.Build();

await host.RunAsync();

L'applicazione scriverà l'output di esempio seguente:

// Sample output:
//     info: AppLifetime.Example.ExampleHostedService[0]
//           1.StartingAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           2.StartAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           3.StartedAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           4.OnStarted has been called.
//     info: Microsoft.Hosting.Lifetime[0]
//           Application started. Press Ctrl+C to shut down.
//     info: Microsoft.Hosting.Lifetime[0]
//           Hosting environment: Production
//     info: Microsoft.Hosting.Lifetime[0]
//           Content root path: ..\app-lifetime\bin\Debug\net8.0
//     info: AppLifetime.Example.ExampleHostedService[0]
//           5.OnStopping has been called.
//     info: Microsoft.Hosting.Lifetime[0]
//           Application is shutting down...
//     info: AppLifetime.Example.ExampleHostedService[0]
//           6.StoppingAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           7.StopAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           8.StoppedAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           9.OnStopped has been called.

L'output mostra l'ordine di tutti i vari eventi del ciclo di vita:

  1. IHostedLifecycleService.StartingAsync
  2. IHostedService.StartAsync
  3. IHostedLifecycleService.StartedAsync
  4. IHostApplicationLifetime.ApplicationStarted

Quando l'applicazione viene arrestata, ad esempio con CTRL+C, vengono generati gli eventi seguenti:

  1. IHostApplicationLifetime.ApplicationStopping
  2. IHostedLifecycleService.StoppingAsync
  3. IHostedService.StopAsync
  4. IHostedLifecycleService.StoppedAsync
  5. IHostApplicationLifetime.ApplicationStopped

IHostLifetime

L'implementazione IHostLifetime controlla quando l'host viene avviato e quando si arresta. Viene usata l'ultima implementazione registrata. Microsoft.Extensions.Hosting.Internal.ConsoleLifetime è l'implementazione IHostLifetime predefinita. Per altre informazioni sui meccanismi di durata dell'arresto, vedere Arresto dell'host.

L'interfaccia IHostLifetime espone un metodo IHostLifetime.WaitForStartAsync, chiamato all'inizio di IHost.StartAsync che attenderà il completamento prima di continuare. Questo è utile per ritardare l'avvio fino al segnale trasmesso da un evento esterno.

Inoltre, l'interfaccia IHostLifetime espone un metodo IHostLifetime.StopAsync, chiamato da IHost.StopAsync per indicare che l'host si arresta ed è il momento di arrestare.

IHostEnvironment

Inserire il servizio IHostEnvironment in una classe per ottenere informazioni sulle impostazioni seguenti:

Inoltre, il servizio IHostEnvironment espone la possibilità di valutare l'ambiente con l'aiuto di questi metodi di estensione:

Configurazione dell'host

La configurazione host viene usata per configurare le proprietà dell'implementazione di IHostEnvironment.

La configurazione host è disponibile nella proprietà HostApplicationBuilderSettings.Configuration mentre l'implementazione dell'ambiente è disponibile nella proprietà IHostApplicationBuilder.Environment. Per configurare l'host, accedere alla proprietà Configuration e richiamare uno dei metodi di estensione disponibili.

Per aggiungere la configurazione host, considerare l'esempio seguente:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

HostApplicationBuilderSettings settings = new()
{
    Args = args,
    Configuration = new ConfigurationManager(),
    ContentRootPath = Directory.GetCurrentDirectory(),
};

settings.Configuration.AddJsonFile("hostsettings.json", optional: true);
settings.Configuration.AddEnvironmentVariables(prefix: "PREFIX_");
settings.Configuration.AddCommandLine(args);

HostApplicationBuilder builder = Host.CreateApplicationBuilder(settings);

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

Il codice precedente:

  • Imposta la radice del contenuto sul percorso restituito da GetCurrentDirectory().
  • Carica la configurazione dell'host da:
    • hostsettings.json.
    • Le variabili di ambiente con prefisso PREFIX_.
    • Argomenti della riga di comando.

Configurazione dell'app

La configurazione dell'app viene creata richiamando ConfigureAppConfiguration su un IHostApplicationBuilder. La proprietà pubblica IHostApplicationBuilder.Configuration consente ai consumer di leggere o apportare modifiche alla configurazione esistente utilizzando i metodi di estensione disponibili.

Per altre informazioni, vedi Configurazione in .NET.

Arresto dell'host

Esistono diversi modi in cui un processo ospitato viene arrestato. In genere, un processo ospitato può essere arrestato nei modi seguenti:

Il codice di hosting non è responsabile della gestione di tali scenari. Il proprietario del processo deve gestirli come qualsiasi altra app. Esistono diversi altri modi in cui è possibile arrestare un processo del servizio ospitato:

  • Se viene usato ConsoleLifetime (UseConsoleLifetime), questo rimane in ascolto dei segnali seguenti e tenta di arrestare l'host in modo corretto.
    • SIGINT (o CTRL+C).
    • SIGQUIT (o CTRL+BREAK in Windows, CTRL+\ su Unix).
    • SIGTERM (inviato da altre app, ad esempio docker stop).
  • Se l'app chiama Environment.Exit.

La logica di hosting integrata gestisce scenari di questo tipo, in particolare la classe ConsoleLifetime. ConsoleLifetime tenta di gestire i segnali di “arresto” SIGINT, SIGQUIT e SIGTERM per consentire un'uscita corretta all'applicazione.

Prima di .NET 6, il codice .NET non era in grado di gestire correttamente SIGTERM. Per ovviare a questa limitazione, ConsoleLifetime sottoscriverebbe System.AppDomain.ProcessExit. Quando ProcessExit era stato generato, ConsoleLifetime segnalava all'host di arrestare e bloccare il thread ProcessExit, in attesa che l'host si arrestasse.

Il gestore di uscita del processo avrebbe consentito l'esecuzione del codice di pulizia nell'applicazione da eseguire, ad esempio IHost.StopAsync, e il successivo codice HostingAbstractionsHostExtensions.Run nel metodo Main.

Tuttavia, ci sono stati altri problemi con questo approccio perché SIGTERM non era l'unico modo in cui ProcessExit veniva sollevato. SIGTERM viene generato anche quando il codice dell'app richiama Environment.Exit. Environment.Exit non è un modo normale per arrestare un processo nel modello Microsoft.Extensions.Hosting dell’app. Esso genera l'evento ProcessExit e quindi esce dal processo. Il termine del metodo Main non viene eseguito. I thread in background e in primo piano vengono terminati e i blocchi finally non vengono eseguiti.

Poiché ConsoleLifetime ha bloccato ProcessExit in attesa dell'arresto dell'host, tale comportamento ha portato a dei deadlock dovuti anche ai blocchi Environment.Exit in attesa della chiamata a ProcessExit. Inoltre, poiché la gestione SIGTERM tentava di arrestare normalmente il processo, ConsoleLifetime impostava ExitCode su 0, che ha eliminato il codice di uscita dell'utente passato a Environment.Exit.

In .NET 6 i segnali POSIX vengono supportati e gestiti. ConsoleLifetime gestisce SIGTERM normalmente e non viene più coinvolto quando Environment.Exit viene richiamato.

Suggerimento

Per .NET 6+, ConsoleLifetime non ha più logica per gestire lo scenario Environment.Exit. Le app che richiamano Environment.Exit e devono eseguire la logica di pulizia possono sottoscrivere se stesse a ProcessExit. In questi scenari, l'hosting non tenterà più di arrestare normalmente l'host.

Se l'applicazione usa l'hosting e si vuole arrestare normalmente l'host, è possibile richiamare IHostApplicationLifetime.StopApplication invece di Environment.Exit.

Processo di arresto dell'hosting

Il diagramma di sequenza seguente mostra come i segnali vengono gestiti internamente nel codice di hosting. La maggior parte degli utenti non ha necessità di comprendere questo processo. Tuttavia, per gli sviluppatori che necessitano di una comprensione approfondita, avere una buona visuale della cosa può essere utile per iniziare.

Dopo l'avvio dell'host, quando un utente richiama Run o WaitForShutdown, un gestore viene registrato per IApplicationLifetime.ApplicationStopping. L'esecuzione viene sospesa in WaitForShutdown, in attesa della generazione dell'evento ApplicationStopping. Il metodo Main non restituisce immediatamente e l'app rimane in esecuzione fino a quando non viene restituito Run o WaitForShutdown.

Quando un segnale viene inviato al processo, avvia la sequenza seguente:

Diagramma della sequenza di arresto dell'hosting.

  1. Il controllo passa da ConsoleLifetime a ApplicationLifetime per generare l'evento ApplicationStopping. Ciò segnala a WaitForShutdownAsync di sbloccare il codice di esecuzione Main. Nel frattempo, il gestore del segnale POSIX restituisce Cancel = true poiché il segnale POSIX è stato gestito.
  2. Il codice di esecuzione Main avvia nuovamente l'esecuzione e indica all'host di StopAsync(), che a sua volta arresta tutti i servizi ospitati e genera qualsiasi altro evento arrestato.
  3. Infine, WaitForShutdown esce, consentendo l'esecuzione di qualsiasi codice di pulizia dell'applicazione e la chiusura normale del metodo Main.

Arresto dell'host in scenari di server Web

Esistono vari altri scenari comuni in cui l'arresto normale funziona in Kestrel per entrambi i protocolli HTTP/1.1 e HTTP/2: come configurarlo in ambienti diversi con un servizio di bilanciamento del carico per svuotare il traffico senza problemi. Sebbene la configurazione del server Web esuli dall'ambito di questo articolo, è possibile trovare altre informazioni sulla documentazione relativa alle opzioni di configurazione del server Web Kestrel di ASP.NET Core.

Quando l'host riceve un segnale di arresto (ad esempio, CTL+C o StopAsync), invia una notifica all'applicazione segnalando ApplicationStopping. È consigliabile sottoscrivere questo evento se sono presenti operazioni a esecuzione prolungata che devono essere completate normalmente.

Successivamente, l’host chiama IServer.StopAsync con un timeout di arresto configurabile (impostazione predefinita di 30s). Kestrel (e Http.Sys) chiudono le associazioni di porte e interrompono l'accettazione di nuove connessioni. Indicano anche alle connessioni correnti di interrompere l'elaborazione di nuove richieste. Per HTTP/2 e HTTP/3, viene inviato un messaggio preliminare GOAWAY al client. Per HTTP/1.1 arrestano il ciclo di connessione perché le richieste vengono elaborate in ordine. IIS si comporta in modo diverso, rifiutando le nuove richieste con un codice di stato 503.

Le richieste attive hanno tempo fino al timeout di arresto per il completamento. Se sono tutte completate prima del timeout, il server restituisce prima il controllo all'host. Se il timeout scade, le connessioni e le richieste in sospeso vengono interrotte forzatamente, causando errori nei log e ai client.

Considerazioni sul bilanciamento del carico

Per garantire una transizione uniforme dei client a una nuova destinazione quando si usa un servizio di bilanciamento del carico, è possibile seguire questa procedura:

  • Visualizzare la nuova istanza e avviare il bilanciamento del traffico verso di esso (potrebbero essere già presenti diverse istanze a scopo di ridimensionamento).
  • Disabilitare o rimuovere l'istanza precedente nella configurazione del servizio di bilanciamento del carico in modo da arrestare la ricezione di nuovo traffico.
  • Segnalare la chiusura della vecchia istanza.
  • Attendere che si svuoti o si esaurisca.

Vedi anche