Attività in background con servizi ospitati in ASP.NET Core

Di Jeow Li ̉

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Per la versione corrente, vedere la versione .NET 9 di questo articolo.

In ASP.NET Core le attività in background possono essere implementate come servizi ospitati. Un servizio ospitato è una classe con logica di attività in background che implementa l'interfaccia IHostedService. Questo articolo fornisce tre esempi di servizi ospitati:

  • Attività in background eseguita su un timer.
  • Servizio ospitato che attiva un servizio con ambito. Il servizio con ambito può usare l'inserimento delle dipendenze.
  • Attività in background in coda che vengono eseguite in sequenza.

Modello di servizio di ruolo di lavoro

Il modello di servizio di ruolo di lavoro di ASP.NET Core rappresenta un punto di partenza per la scrittura di app di servizi a esecuzione prolungata. Un'app creata dal modello del servizio di lavoro specifica l'SDK del ruolo di lavoro nel file di progetto:

<Project Sdk="Microsoft.NET.Sdk.Worker">

Per usare il modello come base per un'app di servizi ospitati:

  1. Creare un nuovo progetto.
  2. Selezionare Servizio di lavoro. Selezionare Avanti.
  3. Specificare il nome di un progetto nel campo Nome progetto oppure accettare il nome predefinito. Selezionare Avanti.
  4. Nella finestra di dialogo Informazioni aggiuntive scegliere un framework. Seleziona Crea.

Pacchetto

Un'app basata sul modello del servizio di lavoro usa l'SDK Microsoft.NET.Sdk.Worker e ha un riferimento esplicito al pacchetto Microsoft.Extensions.Hosting . Ad esempio, vedi il file di progetto dell'app di esempio (BackgroundTasksSample.csproj).

Per le app Web che usano l'SDK Microsoft.NET.Sdk.Web , il pacchetto Microsoft.Extensions.Hosting viene fatto riferimento in modo implicito dal framework condiviso. Non è necessario un riferimento esplicito al pacchetto nel file di progetto dell'app.

Interfaccia IHostedService

L'interfaccia IHostedService definisce due metodi per gli oggetti gestiti dall'host:

StartAsync

StartAsync(CancellationToken) contiene la logica per avviare l'attività in background. StartAsync viene chiamato prima:

StartAsync deve essere limitato alle attività a esecuzione breve perché i servizi ospitati vengono eseguiti in sequenza e non vengono avviati altri servizi fino StartAsync al completamento.

StopAsync

Il token di annullamento ha un timeout predefinito di 30 secondi per indicare che il processo di arresto non deve più essere normale. Quando viene richiesto l'annullamento sul token:

  • Tutte le operazioni in background rimanenti che sta eseguendo l'app devono essere interrotte.
  • Tutti i metodi eventuali chiamati in StopAsync devono essere completati rapidamente.

Tuttavia, le attività non vengono abbandonate dopo la richiesta di annullamento. Il chiamante attende tuttavia il completamento di tutte le attività.

Se l'app si arresta in modo imprevisto, ad esempio, il processo dell'app ha esito negativo, il metodo StopAsync potrebbe non essere chiamato. Pertanto è possibile che i metodi chiamati o le operazioni effettuate in StopAsync non vengano eseguiti.

Per estendere il timeout di arresto predefinito di 30 secondi, impostare:

Il servizio ospitato viene attivato una volta all'avvio dell'app e arrestato normalmente all'arresto dell'applicazione. Se viene generato un errore durante l'esecuzione dell'attività in background, deve essere chiamato Dispose anche se StopAsync non viene chiamato.

Classe di base BackgroundService

BackgroundService è una classe di base per l'implementazione di un oggetto a esecuzione IHostedServiceprolungata.

ExecuteAsync(CancellationToken) viene chiamato per eseguire il servizio in background. L'implementazione restituisce un oggetto Task che rappresenta l'intera durata del servizio in background. Non vengono avviati altri servizi fino a quando ExecuteAsync non diventa asincrono, ad esempio chiamando await. Evitare di eseguire operazioni di inizializzazione lunghe e bloccate in ExecuteAsync. Blocchi host in StopAsync(CancellationToken) in attesa del ExecuteAsync completamento.

Il token di annullamento viene attivato quando viene chiamato IHostedService.StopAsync . L'implementazione di deve terminare tempestivamente quando viene attivato il token di ExecuteAsync annullamento per arrestare normalmente il servizio. In caso contrario, il servizio si arresta in modo anomalo al timeout di arresto. Per altre informazioni, vedere la sezione interfaccia IHostedService.

Per altre informazioni, vedere il codice sorgente backgroundservice .

Attività in background programmate

Un'attività programmata in background utilizza la classe System.Threading.Timer. Il timer attiva il metodo DoWork dell'attività. Il timer viene disabilitato con StopAsync ed eliminato quando il contenitore dei servizi è eliminato con Dispose:

public class TimedHostedService : IHostedService, IDisposable
{
    private int executionCount = 0;
    private readonly ILogger<TimedHostedService> _logger;
    private Timer? _timer = null;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        var count = Interlocked.Increment(ref executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

Timer non attende il completamento delle esecuzioni precedenti di DoWork, quindi l'approccio illustrato potrebbe non essere adatto per ogni scenario. Interlocked.Increment viene usato per incrementare il contatore di esecuzione come operazione atomica, assicurando che più thread non vengano aggiornati executionCount contemporaneamente.

Il servizio viene registrato in IHostBuilder.ConfigureServices (Program.cs) con il AddHostedService metodo di estensione:

services.AddHostedService<TimedHostedService>();

Utilizzo di un servizio con ambito in un'attività in background

Per usare i servizi con ambito all'interno di backgroundService, creare un ambito. Non viene creato automaticamente alcun ambito per un servizio ospitato.

Il servizio dell'attività in background con ambito contiene la logica dell'attività in background. Nell'esempio seguente :

  • Il servizio è asincrono. Il metodo DoWork restituisce un oggetto Task. Ai fini della dimostrazione, un ritardo di dieci secondi è atteso nel metodo DoWork.
  • Un ILogger oggetto viene inserito nel servizio.
internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

internal class ScopedProcessingService : IScopedProcessingService
{
    private int executionCount = 0;
    private readonly ILogger _logger;
    
    public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
    {
        _logger = logger;
    }

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            executionCount++;

            _logger.LogInformation(
                "Scoped Processing Service is working. Count: {Count}", executionCount);

            await Task.Delay(10000, stoppingToken);
        }
    }
}

Il servizio ospitato crea un ambito per risolvere il servizio attività in background con ambito per chiamare il relativo DoWork metodo. DoWork restituisce un Task, atteso in ExecuteAsync:

public class ConsumeScopedServiceHostedService : BackgroundService
{
    private readonly ILogger<ConsumeScopedServiceHostedService> _logger;

    public ConsumeScopedServiceHostedService(IServiceProvider services, 
        ILogger<ConsumeScopedServiceHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService = 
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

I servizi vengono registrati in IHostBuilder.ConfigureServices (Program.cs). Il servizio ospitato viene registrato con il AddHostedService metodo di estensione:

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

Attività in background in coda

Una coda di attività in background si basa su .NET 4.x QueueBackgroundWorkItem:

public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);

    ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // Capacity should be set based on the expected application load and
        // number of concurrent threads accessing the queue.            
        // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task,
        // which completes only when space became available. This leads to backpressure,
        // in case too many publishers/calls start accumulating.
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}

Nell'esempio seguente QueueHostedService:

  • Il BackgroundProcessing metodo restituisce un Taskoggetto , atteso in ExecuteAsync.
  • Le attività in background nella coda vengono rimosse dalla coda ed eseguite in BackgroundProcessing.
  • Gli elementi di lavoro sono attesi prima che il servizio si arresti in StopAsync.
public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILogger<QueuedHostedService> logger)
    {
        TaskQueue = taskQueue;
        _logger = logger;
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            $"Queued Hosted Service is running.{Environment.NewLine}" +
            $"{Environment.NewLine}Tap W to add a work item to the " +
            $"background queue.{Environment.NewLine}");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                    "Error occurred executing {WorkItem}.", nameof(workItem));
            }
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

Un servizio MonitorLoop gestisce le attività di accodamento per il servizio ospitato ogni volta in cui la chiave w viene selezionata in un dispositivo di input:

  • Viene effettuato l'inserimento di IBackgroundTaskQueue nel servizio MonitorLoop.
  • Viene effettuata la chiamata di IBackgroundTaskQueue.QueueBackgroundWorkItem per accodare un elemento di lavoro.
  • L'elemento di lavoro simula un'attività in background a esecuzione prolungata:
    • Vengono eseguiti tre ritardi di 5 secondi (Task.Delay).
    • Un'istruzione try-catch intercettare OperationCanceledException se l'attività viene annullata.
public class MonitorLoop
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger _logger;
    private readonly CancellationToken _cancellationToken;

    public MonitorLoop(IBackgroundTaskQueue taskQueue,
        ILogger<MonitorLoop> logger,
        IHostApplicationLifetime applicationLifetime)
    {
        _taskQueue = taskQueue;
        _logger = logger;
        _cancellationToken = applicationLifetime.ApplicationStopping;
    }

    public void StartMonitorLoop()
    {
        _logger.LogInformation("MonitorAsync Loop is starting.");

        // Run a console user input loop in a background thread
        Task.Run(async () => await MonitorAsync());
    }

    private async ValueTask MonitorAsync()
    {
        while (!_cancellationToken.IsCancellationRequested)
        {
            var keyStroke = Console.ReadKey();

            if (keyStroke.Key == ConsoleKey.W)
            {
                // Enqueue a background work item
                await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
            }
        }
    }

    private async ValueTask BuildWorkItem(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid().ToString();

        _logger.LogInformation("Queued Background Task {Guid} is starting.", guid);

        while (!token.IsCancellationRequested && delayLoop < 3)
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(5), token);
            }
            catch (OperationCanceledException)
            {
                // Prevent throwing if the Delay is cancelled
            }

            delayLoop++;

            _logger.LogInformation("Queued Background Task {Guid} is running. " 
                                   + "{DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop == 3)
        {
            _logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}

I servizi vengono registrati in IHostBuilder.ConfigureServices (Program.cs). Il servizio ospitato viene registrato con il AddHostedService metodo di estensione:

services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx =>
{
    if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
        queueCapacity = 100;
    return new BackgroundTaskQueue(queueCapacity);
});

MonitorLoop viene avviato in Program.cs:

var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();

Attività in background timed asincrona

Il codice seguente crea un'attività in background timed asincrona:

namespace TimedBackgroundTasks;

public class TimedHostedService : BackgroundService
{
    private readonly ILogger<TimedHostedService> _logger;
    private int _executionCount;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        // When the timer should have no due-time, then do the work once now.
        DoWork();

        using PeriodicTimer timer = new(TimeSpan.FromSeconds(1));

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                DoWork();
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Timed Hosted Service is stopping.");
        }
    }

    // Could also be a async method, that can be awaited in ExecuteAsync above
    private void DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

        _logger.LogInformation("Timed Hosted Service is working. Count: {Count}", count);
    }
}

AOT nativo

I modelli del servizio di lavoro supportano .NET native ahead-of-time (AOT) con il --aot flag :

  1. Creare un nuovo progetto.
  2. Selezionare Servizio di lavoro. Selezionare Avanti.
  3. Specificare il nome di un progetto nel campo Nome progetto oppure accettare il nome predefinito. Selezionare Avanti.
  4. Nella finestra di dialogo Informazioni aggiuntive:
  5. Scegliere un framework.
  6. Selezionare la casella di controllo Abilita pubblicazione AOT nativa.
  7. Seleziona Crea.

L'opzione AOT aggiunge <PublishAot>true</PublishAot> al file di progetto:


<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <InvariantGlobalization>true</InvariantGlobalization>
+   <PublishAot>true</PublishAot>
    <UserSecretsId>dotnet-WorkerWithAot-e94b2</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.4.23259.5" />
  </ItemGroup>
</Project>

Risorse aggiuntive

In ASP.NET Core le attività in background possono essere implementate come servizi ospitati. Un servizio ospitato è una classe con logica di attività in background che implementa l'interfaccia IHostedService. Questo articolo fornisce tre esempi di servizi ospitati:

  • Attività in background eseguita su un timer.
  • Servizio ospitato che attiva un servizio con ambito. Il servizio con ambito può usare l'inserimento delle dipendenze.
  • Attività in background in coda che vengono eseguite in sequenza.

Modello di servizio di ruolo di lavoro

Il modello di servizio di ruolo di lavoro di ASP.NET Core rappresenta un punto di partenza per la scrittura di app di servizi a esecuzione prolungata. Un'app creata dal modello del servizio di lavoro specifica l'SDK del ruolo di lavoro nel file di progetto:

<Project Sdk="Microsoft.NET.Sdk.Worker">

Per usare il modello come base per un'app di servizi ospitati:

  1. Creare un nuovo progetto.
  2. Selezionare Servizio di lavoro. Selezionare Avanti.
  3. Specificare il nome di un progetto nel campo Nome progetto oppure accettare il nome predefinito. Selezionare Avanti.
  4. Nella finestra di dialogo Informazioni aggiuntive scegliere un framework. Seleziona Crea.

Pacchetto

Un'app basata sul modello del servizio di lavoro usa l'SDK Microsoft.NET.Sdk.Worker e ha un riferimento esplicito al pacchetto Microsoft.Extensions.Hosting . Ad esempio, vedi il file di progetto dell'app di esempio (BackgroundTasksSample.csproj).

Per le app Web che usano l'SDK Microsoft.NET.Sdk.Web , il pacchetto Microsoft.Extensions.Hosting viene fatto riferimento in modo implicito dal framework condiviso. Non è necessario un riferimento esplicito al pacchetto nel file di progetto dell'app.

Interfaccia IHostedService

L'interfaccia IHostedService definisce due metodi per gli oggetti gestiti dall'host:

StartAsync

StartAsync(CancellationToken) contiene la logica per avviare l'attività in background. StartAsync viene chiamato prima:

StartAsync deve essere limitato alle attività a esecuzione breve perché i servizi ospitati vengono eseguiti in sequenza e non vengono avviati altri servizi fino StartAsync al completamento.

StopAsync

Il token di annullamento ha un timeout predefinito di 30 secondi per indicare che il processo di arresto non deve più essere normale. Quando viene richiesto l'annullamento sul token:

  • Tutte le operazioni in background rimanenti che sta eseguendo l'app devono essere interrotte.
  • Tutti i metodi eventuali chiamati in StopAsync devono essere completati rapidamente.

Tuttavia, le attività non vengono abbandonate dopo la richiesta di annullamento. Il chiamante attende tuttavia il completamento di tutte le attività.

Se l'app si arresta in modo imprevisto, ad esempio, il processo dell'app ha esito negativo, il metodo StopAsync potrebbe non essere chiamato. Pertanto è possibile che i metodi chiamati o le operazioni effettuate in StopAsync non vengano eseguiti.

Per estendere il timeout di arresto predefinito di 30 secondi, impostare:

Il servizio ospitato viene attivato una volta all'avvio dell'app e arrestato normalmente all'arresto dell'applicazione. Se viene generato un errore durante l'esecuzione dell'attività in background, deve essere chiamato Dispose anche se StopAsync non viene chiamato.

Classe di base BackgroundService

BackgroundService è una classe di base per l'implementazione di un oggetto a esecuzione IHostedServiceprolungata.

ExecuteAsync(CancellationToken) viene chiamato per eseguire il servizio in background. L'implementazione restituisce un oggetto Task che rappresenta l'intera durata del servizio in background. Non vengono avviati altri servizi fino a quando ExecuteAsync non diventa asincrono, ad esempio chiamando await. Evitare di eseguire operazioni di inizializzazione lunghe e bloccate in ExecuteAsync. Blocchi host in StopAsync(CancellationToken) in attesa del ExecuteAsync completamento.

Il token di annullamento viene attivato quando viene chiamato IHostedService.StopAsync . L'implementazione di deve terminare tempestivamente quando viene attivato il token di ExecuteAsync annullamento per arrestare normalmente il servizio. In caso contrario, il servizio si arresta in modo anomalo al timeout di arresto. Per altre informazioni, vedere la sezione interfaccia IHostedService.

Per altre informazioni, vedere il codice sorgente backgroundservice .

Attività in background programmate

Un'attività programmata in background utilizza la classe System.Threading.Timer. Il timer attiva il metodo DoWork dell'attività. Il timer viene disabilitato con StopAsync ed eliminato quando il contenitore dei servizi è eliminato con Dispose:

public class TimedHostedService : IHostedService, IDisposable
{
    private int executionCount = 0;
    private readonly ILogger<TimedHostedService> _logger;
    private Timer? _timer = null;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        var count = Interlocked.Increment(ref executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

Timer non attende il completamento delle esecuzioni precedenti di DoWork, quindi l'approccio illustrato potrebbe non essere adatto per ogni scenario. Interlocked.Increment viene usato per incrementare il contatore di esecuzione come operazione atomica, assicurando che più thread non vengano aggiornati executionCount contemporaneamente.

Il servizio viene registrato in IHostBuilder.ConfigureServices (Program.cs) con il AddHostedService metodo di estensione:

services.AddHostedService<TimedHostedService>();

Utilizzo di un servizio con ambito in un'attività in background

Per usare i servizi con ambito all'interno di backgroundService, creare un ambito. Non viene creato automaticamente alcun ambito per un servizio ospitato.

Il servizio dell'attività in background con ambito contiene la logica dell'attività in background. Nell'esempio seguente :

  • Il servizio è asincrono. Il metodo DoWork restituisce un oggetto Task. Ai fini della dimostrazione, un ritardo di dieci secondi è atteso nel metodo DoWork.
  • Un ILogger oggetto viene inserito nel servizio.
internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

internal class ScopedProcessingService : IScopedProcessingService
{
    private int executionCount = 0;
    private readonly ILogger _logger;
    
    public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
    {
        _logger = logger;
    }

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            executionCount++;

            _logger.LogInformation(
                "Scoped Processing Service is working. Count: {Count}", executionCount);

            await Task.Delay(10000, stoppingToken);
        }
    }
}

Il servizio ospitato crea un ambito per risolvere il servizio attività in background con ambito per chiamare il relativo DoWork metodo. DoWork restituisce un Task, atteso in ExecuteAsync:

public class ConsumeScopedServiceHostedService : BackgroundService
{
    private readonly ILogger<ConsumeScopedServiceHostedService> _logger;

    public ConsumeScopedServiceHostedService(IServiceProvider services, 
        ILogger<ConsumeScopedServiceHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService = 
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

I servizi vengono registrati in IHostBuilder.ConfigureServices (Program.cs). Il servizio ospitato viene registrato con il AddHostedService metodo di estensione:

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

Attività in background in coda

Una coda di attività in background si basa su .NET 4.x QueueBackgroundWorkItem:

public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);

    ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // Capacity should be set based on the expected application load and
        // number of concurrent threads accessing the queue.            
        // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task,
        // which completes only when space became available. This leads to backpressure,
        // in case too many publishers/calls start accumulating.
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}

Nell'esempio seguente QueueHostedService:

  • Il BackgroundProcessing metodo restituisce un Taskoggetto , atteso in ExecuteAsync.
  • Le attività in background nella coda vengono rimosse dalla coda ed eseguite in BackgroundProcessing.
  • Gli elementi di lavoro sono attesi prima che il servizio si arresti in StopAsync.
public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILogger<QueuedHostedService> logger)
    {
        TaskQueue = taskQueue;
        _logger = logger;
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            $"Queued Hosted Service is running.{Environment.NewLine}" +
            $"{Environment.NewLine}Tap W to add a work item to the " +
            $"background queue.{Environment.NewLine}");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                    "Error occurred executing {WorkItem}.", nameof(workItem));
            }
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

Un servizio MonitorLoop gestisce le attività di accodamento per il servizio ospitato ogni volta in cui la chiave w viene selezionata in un dispositivo di input:

  • Viene effettuato l'inserimento di IBackgroundTaskQueue nel servizio MonitorLoop.
  • Viene effettuata la chiamata di IBackgroundTaskQueue.QueueBackgroundWorkItem per accodare un elemento di lavoro.
  • L'elemento di lavoro simula un'attività in background a esecuzione prolungata:
    • Vengono eseguiti tre ritardi di 5 secondi (Task.Delay).
    • Un'istruzione try-catch intercettare OperationCanceledException se l'attività viene annullata.
public class MonitorLoop
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger _logger;
    private readonly CancellationToken _cancellationToken;

    public MonitorLoop(IBackgroundTaskQueue taskQueue,
        ILogger<MonitorLoop> logger,
        IHostApplicationLifetime applicationLifetime)
    {
        _taskQueue = taskQueue;
        _logger = logger;
        _cancellationToken = applicationLifetime.ApplicationStopping;
    }

    public void StartMonitorLoop()
    {
        _logger.LogInformation("MonitorAsync Loop is starting.");

        // Run a console user input loop in a background thread
        Task.Run(async () => await MonitorAsync());
    }

    private async ValueTask MonitorAsync()
    {
        while (!_cancellationToken.IsCancellationRequested)
        {
            var keyStroke = Console.ReadKey();

            if (keyStroke.Key == ConsoleKey.W)
            {
                // Enqueue a background work item
                await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
            }
        }
    }

    private async ValueTask BuildWorkItem(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid().ToString();

        _logger.LogInformation("Queued Background Task {Guid} is starting.", guid);

        while (!token.IsCancellationRequested && delayLoop < 3)
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(5), token);
            }
            catch (OperationCanceledException)
            {
                // Prevent throwing if the Delay is cancelled
            }

            delayLoop++;

            _logger.LogInformation("Queued Background Task {Guid} is running. " 
                                   + "{DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop == 3)
        {
            _logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}

I servizi vengono registrati in IHostBuilder.ConfigureServices (Program.cs). Il servizio ospitato viene registrato con il AddHostedService metodo di estensione:

services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx =>
{
    if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
        queueCapacity = 100;
    return new BackgroundTaskQueue(queueCapacity);
});

MonitorLoop viene avviato in Program.cs:

var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();

Attività in background timed asincrona

Il codice seguente crea un'attività in background timed asincrona:

namespace TimedBackgroundTasks;

public class TimedHostedService : BackgroundService
{
    private readonly ILogger<TimedHostedService> _logger;
    private int _executionCount;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        // When the timer should have no due-time, then do the work once now.
        DoWork();

        using PeriodicTimer timer = new(TimeSpan.FromSeconds(1));

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                DoWork();
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Timed Hosted Service is stopping.");
        }
    }

    // Could also be a async method, that can be awaited in ExecuteAsync above
    private void DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

        _logger.LogInformation("Timed Hosted Service is working. Count: {Count}", count);
    }
}

Risorse aggiuntive

In ASP.NET Core le attività in background possono essere implementate come servizi ospitati. Un servizio ospitato è una classe con logica di attività in background che implementa l'interfaccia IHostedService. Questo articolo fornisce tre esempi di servizi ospitati:

  • Attività in background eseguita su un timer.
  • Servizio ospitato che attiva un servizio con ambito. Il servizio con ambito può usare l'inserimento delle dipendenze.
  • Attività in background in coda che vengono eseguite in sequenza.

Visualizzare o scaricare il codice di esempio (procedura per il download)

Modello di servizio di ruolo di lavoro

Il modello di servizio di ruolo di lavoro di ASP.NET Core rappresenta un punto di partenza per la scrittura di app di servizi a esecuzione prolungata. Un'app creata dal modello del servizio di lavoro specifica l'SDK del ruolo di lavoro nel file di progetto:

<Project Sdk="Microsoft.NET.Sdk.Worker">

Per usare il modello come base per un'app di servizi ospitati:

  1. Creare un nuovo progetto.
  2. Selezionare Servizio di lavoro. Selezionare Avanti.
  3. Specificare il nome di un progetto nel campo Nome progetto oppure accettare il nome predefinito. Seleziona Crea.
  4. Nella finestra di dialogo Crea un nuovo servizio di lavoro selezionare Crea.

Pacchetto

Un'app basata sul modello del servizio di lavoro usa l'SDK Microsoft.NET.Sdk.Worker e ha un riferimento esplicito al pacchetto Microsoft.Extensions.Hosting . Ad esempio, vedi il file di progetto dell'app di esempio (BackgroundTasksSample.csproj).

Per le app Web che usano l'SDK Microsoft.NET.Sdk.Web , il pacchetto Microsoft.Extensions.Hosting viene fatto riferimento in modo implicito dal framework condiviso. Non è necessario un riferimento esplicito al pacchetto nel file di progetto dell'app.

Interfaccia IHostedService

L'interfaccia IHostedService definisce due metodi per gli oggetti gestiti dall'host:

StartAsync

StartAsync contiene la logica per avviare l'attività in background. StartAsync viene chiamato prima:

Il comportamento predefinito può essere modificato in modo che il servizio StartAsync ospitato venga eseguito dopo la configurazione della pipeline dell'app e ApplicationStarted venga chiamato. Per modificare il comportamento predefinito, aggiungere il servizio ospitato (VideosWatcher nell'esempio seguente) dopo aver chiamato ConfigureWebHostDefaults:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            })
            .ConfigureServices(services =>
            {
                services.AddHostedService<VideosWatcher>();
            });
}

StopAsync

Il token di annullamento ha un timeout predefinito di cinque secondi che indica che il processo di arresto non è più normale. Quando viene richiesto l'annullamento sul token:

  • Tutte le operazioni in background rimanenti che sta eseguendo l'app devono essere interrotte.
  • Tutti i metodi eventuali chiamati in StopAsync devono essere completati rapidamente.

Tuttavia, le attività non vengono abbandonate dopo la richiesta di annullamento. Il chiamante attende tuttavia il completamento di tutte le attività.

Se l'app si arresta in modo imprevisto, ad esempio, il processo dell'app ha esito negativo, il metodo StopAsync potrebbe non essere chiamato. Pertanto è possibile che i metodi chiamati o le operazioni effettuate in StopAsync non vengano eseguiti.

Per estendere il timeout di arresto predefinito di cinque secondi, impostare:

Il servizio ospitato viene attivato una volta all'avvio dell'app e arrestato normalmente all'arresto dell'applicazione. Se viene generato un errore durante l'esecuzione dell'attività in background, deve essere chiamato Dispose anche se StopAsync non viene chiamato.

Classe di base BackgroundService

BackgroundService è una classe di base per l'implementazione di un oggetto a esecuzione IHostedServiceprolungata.

ExecuteAsync(CancellationToken) viene chiamato per eseguire il servizio in background. L'implementazione restituisce un oggetto Task che rappresenta l'intera durata del servizio in background. Non vengono avviati altri servizi fino a quando ExecuteAsync non diventa asincrono, ad esempio chiamando await. Evitare di eseguire operazioni di inizializzazione lunghe e bloccate in ExecuteAsync. Blocchi host in StopAsync(CancellationToken) in attesa del ExecuteAsync completamento.

Il token di annullamento viene attivato quando viene chiamato IHostedService.StopAsync . L'implementazione di deve terminare tempestivamente quando viene attivato il token di ExecuteAsync annullamento per arrestare normalmente il servizio. In caso contrario, il servizio si arresta in modo anomalo al timeout di arresto. Per altre informazioni, vedere la sezione interfaccia IHostedService.

StartAsync deve essere limitato alle attività a esecuzione breve perché i servizi ospitati vengono eseguiti in sequenza e non vengono avviati altri servizi fino StartAsync al completamento. Le attività a esecuzione prolungata devono essere inserite in ExecuteAsync. Per altre informazioni, vedere l'origine in BackgroundService.

Attività in background programmate

Un'attività programmata in background utilizza la classe System.Threading.Timer. Il timer attiva il metodo DoWork dell'attività. Il timer viene disabilitato con StopAsync ed eliminato quando il contenitore dei servizi è eliminato con Dispose:

public class TimedHostedService : IHostedService, IDisposable
{
    private int executionCount = 0;
    private readonly ILogger<TimedHostedService> _logger;
    private Timer _timer;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero, 
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        var count = Interlocked.Increment(ref executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

Timer non attende il completamento delle esecuzioni precedenti di DoWork, quindi l'approccio illustrato potrebbe non essere adatto per ogni scenario. Interlocked.Increment viene usato per incrementare il contatore di esecuzione come operazione atomica, assicurando che più thread non vengano aggiornati executionCount contemporaneamente.

Il servizio viene registrato in IHostBuilder.ConfigureServices (Program.cs) con il AddHostedService metodo di estensione:

services.AddHostedService<TimedHostedService>();

Utilizzo di un servizio con ambito in un'attività in background

Per usare i servizi con ambito all'interno di backgroundService, creare un ambito. Non viene creato automaticamente alcun ambito per un servizio ospitato.

Il servizio dell'attività in background con ambito contiene la logica dell'attività in background. Nell'esempio seguente :

  • Il servizio è asincrono. Il metodo DoWork restituisce un oggetto Task. Ai fini della dimostrazione, un ritardo di dieci secondi è atteso nel metodo DoWork.
  • Un ILogger oggetto viene inserito nel servizio.
internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

internal class ScopedProcessingService : IScopedProcessingService
{
    private int executionCount = 0;
    private readonly ILogger _logger;
    
    public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
    {
        _logger = logger;
    }

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            executionCount++;

            _logger.LogInformation(
                "Scoped Processing Service is working. Count: {Count}", executionCount);

            await Task.Delay(10000, stoppingToken);
        }
    }
}

Il servizio ospitato crea un ambito per risolvere il servizio attività in background con ambito per chiamare il relativo DoWork metodo. DoWork restituisce un Task, atteso in ExecuteAsync:

public class ConsumeScopedServiceHostedService : BackgroundService
{
    private readonly ILogger<ConsumeScopedServiceHostedService> _logger;

    public ConsumeScopedServiceHostedService(IServiceProvider services, 
        ILogger<ConsumeScopedServiceHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService = 
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

I servizi vengono registrati in IHostBuilder.ConfigureServices (Program.cs). Il servizio ospitato viene registrato con il AddHostedService metodo di estensione:

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

Attività in background in coda

Una coda di attività in background si basa su .NET 4.x QueueBackgroundWorkItem:

public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);

    ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // Capacity should be set based on the expected application load and
        // number of concurrent threads accessing the queue.            
        // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task,
        // which completes only when space became available. This leads to backpressure,
        // in case too many publishers/calls start accumulating.
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}

Nell'esempio seguente QueueHostedService:

  • Il BackgroundProcessing metodo restituisce un Taskoggetto , atteso in ExecuteAsync.
  • Le attività in background nella coda vengono rimosse dalla coda ed eseguite in BackgroundProcessing.
  • Gli elementi di lavoro sono attesi prima che il servizio si arresti in StopAsync.
public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILogger<QueuedHostedService> logger)
    {
        TaskQueue = taskQueue;
        _logger = logger;
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            $"Queued Hosted Service is running.{Environment.NewLine}" +
            $"{Environment.NewLine}Tap W to add a work item to the " +
            $"background queue.{Environment.NewLine}");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                    "Error occurred executing {WorkItem}.", nameof(workItem));
            }
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

Un servizio MonitorLoop gestisce le attività di accodamento per il servizio ospitato ogni volta in cui la chiave w viene selezionata in un dispositivo di input:

  • Viene effettuato l'inserimento di IBackgroundTaskQueue nel servizio MonitorLoop.
  • Viene effettuata la chiamata di IBackgroundTaskQueue.QueueBackgroundWorkItem per accodare un elemento di lavoro.
  • L'elemento di lavoro simula un'attività in background a esecuzione prolungata:
    • Vengono eseguiti tre ritardi di 5 secondi (Task.Delay).
    • Un'istruzione try-catch intercettare OperationCanceledException se l'attività viene annullata.
public class MonitorLoop
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger _logger;
    private readonly CancellationToken _cancellationToken;

    public MonitorLoop(IBackgroundTaskQueue taskQueue, 
        ILogger<MonitorLoop> logger, 
        IHostApplicationLifetime applicationLifetime)
    {
        _taskQueue = taskQueue;
        _logger = logger;
        _cancellationToken = applicationLifetime.ApplicationStopping;
    }

    public void StartMonitorLoop()
    {
        _logger.LogInformation("MonitorAsync Loop is starting.");

        // Run a console user input loop in a background thread
        Task.Run(async () => await MonitorAsync());
    }

    private async ValueTask MonitorAsync()
    {
        while (!_cancellationToken.IsCancellationRequested)
        {
            var keyStroke = Console.ReadKey();

            if (keyStroke.Key == ConsoleKey.W)
            {
                // Enqueue a background work item
                await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
            }
        }
    }

    private async ValueTask BuildWorkItem(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid().ToString();

        _logger.LogInformation("Queued Background Task {Guid} is starting.", guid);

        while (!token.IsCancellationRequested && delayLoop < 3)
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(5), token);
            }
            catch (OperationCanceledException)
            {
                // Prevent throwing if the Delay is cancelled
            }

            delayLoop++;

            _logger.LogInformation("Queued Background Task {Guid} is running. " + "{DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop == 3)
        {
            _logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}

I servizi vengono registrati in IHostBuilder.ConfigureServices (Program.cs). Il servizio ospitato viene registrato con il AddHostedService metodo di estensione:

services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx => {
    if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
        queueCapacity = 100;
    return new BackgroundTaskQueue(queueCapacity);
});

MonitorLoop viene avviato in Program.Main:

var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();

Risorse aggiuntive