Zadania w tle z hostowanymi usługami w ASP.NET Core

Przez Jeow Li Huan

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

Ważne

Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

W ASP.NET Core zadania w tle można zaimplementować jako usługi hostowane. Hostowana usługa to klasa z logiką zadań w tle, która implementuje IHostedService interfejs. Ten artykuł zawiera trzy przykłady hostowanych usług:

  • Zadanie w tle uruchamiane na czasomierzu.
  • Hostowana usługa, która aktywuje usługę o określonym zakresie. Usługa o określonym zakresie może używać wstrzykiwania zależności (DI).
  • Zadania w tle w kolejce, które są uruchamiane sekwencyjnie.

Szablon usługi procesu roboczego

Szablon usługi ASP.NET Core Worker Service stanowi punkt wyjścia do pisania długotrwałych aplikacji usługi. Aplikacja utworzona na podstawie szablonu usługi Worker Service określa zestaw SDK procesu roboczego w pliku projektu:

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

Aby użyć szablonu jako podstawy dla aplikacji usług hostowanych:

  1. Tworzenie nowego projektu.
  2. Wybierz pozycję Usługa procesu roboczego. Wybierz Dalej.
  3. Podaj nazwę projektu w polu Nazwa projektu lub zaakceptuj domyślną nazwę projektu. Wybierz Dalej.
  4. W oknie dialogowym Dodatkowe informacje wybierz strukturę. Wybierz pozycję Utwórz.

Pakiet

Aplikacja oparta na szablonie usługi roboczej używa Microsoft.NET.Sdk.Worker zestawu SDK i zawiera jawne odwołanie do pakietu Microsoft.Extensions.Hosting . Zobacz na przykład plik projektu przykładowej aplikacji (BackgroundTasksSample.csproj).

W przypadku aplikacji internetowych korzystających z Microsoft.NET.Sdk.Web zestawu SDK pakiet Microsoft.Extensions.Hosting odwołuje się niejawnie z udostępnionej platformy. Jawne odwołanie do pakietu w pliku projektu aplikacji nie jest wymagane.

Interfejs IHostedService

Interfejs IHostedService definiuje dwie metody dla obiektów zarządzanych przez hosta:

StartAsync

StartAsync(CancellationToken) zawiera logikę uruchamiania zadania w tle. StartAsync jest wywoływana przed:

StartAsync powinno być ograniczone do krótkich uruchomionych zadań, ponieważ hostowane usługi są uruchamiane sekwencyjnie, a żadne dalsze usługi nie są uruchamiane do momentu StartAsync ukończenia.

StopAsync

Token anulowania ma domyślny limit czasu 30 sekund, aby wskazać, że proces zamykania nie powinien już być wdziękny. Po zażądaniu anulowania w tokenie:

  • Wszelkie pozostałe operacje w tle wykonywane przez aplikację powinny zostać przerwane.
  • Wszystkie metody wywoływane w StopAsync metodach powinny zostać zwrócone natychmiast.

Jednak zadania nie są porzucane po żądaniu anulowania — obiekt wywołujący oczekuje na ukończenie wszystkich zadań.

Jeśli aplikacja zostanie nieoczekiwanie zamknięta (na przykład proces aplikacji zakończy się niepowodzeniem), StopAsync może nie zostać wywołana. W związku z tym wszelkie metody wywoływane lub wykonywane w programie StopAsync operacje mogą nie wystąpić.

Aby przedłużyć domyślny limit czasu zamknięcia 30 sekund, ustaw:

Hostowana usługa jest aktywowana raz podczas uruchamiania aplikacji i bezpiecznie zamykana po zamknięciu aplikacji. Jeśli podczas wykonywania zadania w tle zostanie zgłoszony błąd, powinien zostać wywołany nawet wtedy, Dispose gdy StopAsync nie zostanie wywołany.

BackgroundService, klasa bazowa

BackgroundService jest klasą bazową do implementowania długotrwałego IHostedServiceelementu .

Funkcja ExecuteAsync(CancellationToken) jest wywoływana w celu uruchomienia usługi w tle. Implementacja zwraca wartość Task reprezentującą cały okres istnienia usługi w tle. Żadne dalsze usługi nie są uruchamiane, dopóki funkcja ExecuteAsync nie stanie się asynchroniczna, na przykład przez wywołanie metody await. Unikaj wykonywania długich, blokujących pracę inicjowania w programie ExecuteAsync. Bloki hosta w stopAsync(CancellationToken) oczekujące na ExecuteAsync zakończenie.

Token anulowania jest wyzwalany po wywołaniu wywołania IHostedService.StopAsync . Implementacja polecenia ExecuteAsync powinna zakończyć się natychmiast, gdy token anulowania zostanie wyzwolony, aby bezpiecznie zamknąć usługę. W przeciwnym razie usługa niegracyjnie zamyka się po przekroczeniu limitu czasu zamknięcia. Aby uzyskać więcej informacji, zobacz sekcję interfejsu IHostedService.

Aby uzyskać więcej informacji, zobacz kod źródłowy usługi BackgroundService .

Czasowe zadania w tle

Zadanie w tle czasowe korzysta z klasy System.Threading.Timer . Czasomierz wyzwala metodę DoWork zadania. Czasomierz jest wyłączony StopAsync i usuwany po usunięciu kontenera usługi w systemie 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();
    }
}

Element Timer nie czeka na ukończenie poprzednich DoWork wykonań, więc pokazane podejście może nie być odpowiednie dla każdego scenariusza. Interlocked.Increment służy do przyrostowania licznika wykonywania jako operacji niepodzielnej, co gwarantuje, że wiele wątków nie jest aktualizowanych executionCount jednocześnie.

Usługa jest zarejestrowana w IHostBuilder.ConfigureServices (Program.cs) przy użyciu AddHostedService metody rozszerzenia:

services.AddHostedService<TimedHostedService>();

Korzystanie z usługi o określonym zakresie w zadaniu w tle

Aby używać usług o określonym zakresie w ramach usługi BackgroundService, utwórz zakres. Domyślnie dla hostowanej usługi nie jest tworzony żaden zakres.

Zakres usługi zadań w tle zawiera logikę zadania w tle. W poniższym przykładzie:

  • Usługa jest asynchroniczna. Metoda DoWork zwraca wartość Task. W celach demonstracyjnych oczekuje się na opóźnienie dziesięciu sekund w metodzie DoWork .
  • Element ILogger jest wstrzykiwany do usługi.
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);
        }
    }
}

Usługa hostowana tworzy zakres, aby rozpoznać zakres usługi zadań w tle w celu wywołania jej DoWork metody. DoWorkzwraca element , który jest oczekiwany w elemecie TaskExecuteAsync:

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);
    }
}

Usługi są zarejestrowane w IHostBuilder.ConfigureServices programie (Program.cs). Hostowana usługa jest zarejestrowana w metodzie AddHostedService rozszerzenia:

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

Zadania w tle w kolejce

Kolejka zadań w tle jest oparta na platformie .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;
    }
}

W poniższym QueueHostedService przykładzie:

  • Metoda BackgroundProcessing zwraca Taskelement , który jest oczekiwany w pliku ExecuteAsync.
  • Zadania w tle w kolejce są usuwane z kolejki i wykonywane w pliku BackgroundProcessing.
  • Elementy robocze są oczekiwane przed zatrzymanie usługi w programie 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);
    }
}

MonitorLoop Usługa obsługuje kolejkowanie zadań dla hostowanej usługi za każdym razem, gdy w klucz jest wybrany na urządzeniu wejściowym:

  • Element IBackgroundTaskQueue jest wstrzykiwany do MonitorLoop usługi.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem element jest wywoływany w celu kolejkowania elementu roboczego.
  • Element roboczy symuluje długotrwałe zadanie w tle:
    • Wykonywane są trzy 5-sekundowe opóźnienia (Task.Delay).
    • Instrukcja try-catch wychwyci OperationCanceledException , jeśli zadanie zostanie anulowane.
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);
        }
    }
}

Usługi są zarejestrowane w IHostBuilder.ConfigureServices programie (Program.cs). Hostowana usługa jest zarejestrowana w metodzie AddHostedService rozszerzenia:

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 program jest uruchamiany w pliku Program.cs:

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

Asynchroniczne zadanie w tle z czasem

Poniższy kod tworzy asynchroniczne zadanie w tle:

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);
    }
}

Natywna AOT

Szablony usługi Roboczej obsługują natywną platformę .NET z wyprzedzeniem (AOT) z flagą --aot :

  1. Tworzenie nowego projektu.
  2. Wybierz pozycję Usługa procesu roboczego. Wybierz Dalej.
  3. Podaj nazwę projektu w polu Nazwa projektu lub zaakceptuj domyślną nazwę projektu. Wybierz Dalej.
  4. W oknie dialogowym Dodatkowe informacje:
  5. Wybierz platformę.
  6. Zaznacz pole wyboru Włącz natywną publikację AOT.
  7. Wybierz pozycję Utwórz.

Opcja AOT dodaje <PublishAot>true</PublishAot> do pliku projektu:


<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>

Dodatkowe zasoby

W ASP.NET Core zadania w tle można zaimplementować jako usługi hostowane. Hostowana usługa to klasa z logiką zadań w tle, która implementuje IHostedService interfejs. Ten artykuł zawiera trzy przykłady hostowanych usług:

  • Zadanie w tle uruchamiane na czasomierzu.
  • Hostowana usługa, która aktywuje usługę o określonym zakresie. Usługa o określonym zakresie może używać wstrzykiwania zależności (DI).
  • Zadania w tle w kolejce, które są uruchamiane sekwencyjnie.

Szablon usługi procesu roboczego

Szablon usługi ASP.NET Core Worker Service stanowi punkt wyjścia do pisania długotrwałych aplikacji usługi. Aplikacja utworzona na podstawie szablonu usługi Worker Service określa zestaw SDK procesu roboczego w pliku projektu:

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

Aby użyć szablonu jako podstawy dla aplikacji usług hostowanych:

  1. Tworzenie nowego projektu.
  2. Wybierz pozycję Usługa procesu roboczego. Wybierz Dalej.
  3. Podaj nazwę projektu w polu Nazwa projektu lub zaakceptuj domyślną nazwę projektu. Wybierz Dalej.
  4. W oknie dialogowym Dodatkowe informacje wybierz strukturę. Wybierz pozycję Utwórz.

Pakiet

Aplikacja oparta na szablonie usługi roboczej używa Microsoft.NET.Sdk.Worker zestawu SDK i zawiera jawne odwołanie do pakietu Microsoft.Extensions.Hosting . Zobacz na przykład plik projektu przykładowej aplikacji (BackgroundTasksSample.csproj).

W przypadku aplikacji internetowych korzystających z Microsoft.NET.Sdk.Web zestawu SDK pakiet Microsoft.Extensions.Hosting odwołuje się niejawnie z udostępnionej platformy. Jawne odwołanie do pakietu w pliku projektu aplikacji nie jest wymagane.

Interfejs IHostedService

Interfejs IHostedService definiuje dwie metody dla obiektów zarządzanych przez hosta:

StartAsync

StartAsync(CancellationToken) zawiera logikę uruchamiania zadania w tle. StartAsync jest wywoływana przed:

StartAsync powinno być ograniczone do krótkich uruchomionych zadań, ponieważ hostowane usługi są uruchamiane sekwencyjnie, a żadne dalsze usługi nie są uruchamiane do momentu StartAsync ukończenia.

StopAsync

Token anulowania ma domyślny limit czasu 30 sekund, aby wskazać, że proces zamykania nie powinien już być wdziękny. Po zażądaniu anulowania w tokenie:

  • Wszelkie pozostałe operacje w tle wykonywane przez aplikację powinny zostać przerwane.
  • Wszystkie metody wywoływane w StopAsync metodach powinny zostać zwrócone natychmiast.

Jednak zadania nie są porzucane po żądaniu anulowania — obiekt wywołujący oczekuje na ukończenie wszystkich zadań.

Jeśli aplikacja zostanie nieoczekiwanie zamknięta (na przykład proces aplikacji zakończy się niepowodzeniem), StopAsync może nie zostać wywołana. W związku z tym wszelkie metody wywoływane lub wykonywane w programie StopAsync operacje mogą nie wystąpić.

Aby przedłużyć domyślny limit czasu zamknięcia 30 sekund, ustaw:

Hostowana usługa jest aktywowana raz podczas uruchamiania aplikacji i bezpiecznie zamykana po zamknięciu aplikacji. Jeśli podczas wykonywania zadania w tle zostanie zgłoszony błąd, powinien zostać wywołany nawet wtedy, Dispose gdy StopAsync nie zostanie wywołany.

BackgroundService, klasa bazowa

BackgroundService jest klasą bazową do implementowania długotrwałego IHostedServiceelementu .

Funkcja ExecuteAsync(CancellationToken) jest wywoływana w celu uruchomienia usługi w tle. Implementacja zwraca wartość Task reprezentującą cały okres istnienia usługi w tle. Żadne dalsze usługi nie są uruchamiane, dopóki funkcja ExecuteAsync nie stanie się asynchroniczna, na przykład przez wywołanie metody await. Unikaj wykonywania długich, blokujących pracę inicjowania w programie ExecuteAsync. Bloki hosta w stopAsync(CancellationToken) oczekujące na ExecuteAsync zakończenie.

Token anulowania jest wyzwalany po wywołaniu wywołania IHostedService.StopAsync . Implementacja polecenia ExecuteAsync powinna zakończyć się natychmiast, gdy token anulowania zostanie wyzwolony, aby bezpiecznie zamknąć usługę. W przeciwnym razie usługa niegracyjnie zamyka się po przekroczeniu limitu czasu zamknięcia. Aby uzyskać więcej informacji, zobacz sekcję interfejsu IHostedService.

Aby uzyskać więcej informacji, zobacz kod źródłowy usługi BackgroundService .

Czasowe zadania w tle

Zadanie w tle czasowe korzysta z klasy System.Threading.Timer . Czasomierz wyzwala metodę DoWork zadania. Czasomierz jest wyłączony StopAsync i usuwany po usunięciu kontenera usługi w systemie 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();
    }
}

Element Timer nie czeka na ukończenie poprzednich DoWork wykonań, więc pokazane podejście może nie być odpowiednie dla każdego scenariusza. Interlocked.Increment służy do przyrostowania licznika wykonywania jako operacji niepodzielnej, co gwarantuje, że wiele wątków nie jest aktualizowanych executionCount jednocześnie.

Usługa jest zarejestrowana w IHostBuilder.ConfigureServices (Program.cs) przy użyciu AddHostedService metody rozszerzenia:

services.AddHostedService<TimedHostedService>();

Korzystanie z usługi o określonym zakresie w zadaniu w tle

Aby używać usług o określonym zakresie w ramach usługi BackgroundService, utwórz zakres. Domyślnie dla hostowanej usługi nie jest tworzony żaden zakres.

Zakres usługi zadań w tle zawiera logikę zadania w tle. W poniższym przykładzie:

  • Usługa jest asynchroniczna. Metoda DoWork zwraca wartość Task. W celach demonstracyjnych oczekuje się na opóźnienie dziesięciu sekund w metodzie DoWork .
  • Element ILogger jest wstrzykiwany do usługi.
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);
        }
    }
}

Usługa hostowana tworzy zakres, aby rozpoznać zakres usługi zadań w tle w celu wywołania jej DoWork metody. DoWorkzwraca element , który jest oczekiwany w elemecie TaskExecuteAsync:

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);
    }
}

Usługi są zarejestrowane w IHostBuilder.ConfigureServices programie (Program.cs). Hostowana usługa jest zarejestrowana w metodzie AddHostedService rozszerzenia:

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

Zadania w tle w kolejce

Kolejka zadań w tle jest oparta na platformie .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;
    }
}

W poniższym QueueHostedService przykładzie:

  • Metoda BackgroundProcessing zwraca Taskelement , który jest oczekiwany w pliku ExecuteAsync.
  • Zadania w tle w kolejce są usuwane z kolejki i wykonywane w pliku BackgroundProcessing.
  • Elementy robocze są oczekiwane przed zatrzymanie usługi w programie 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);
    }
}

MonitorLoop Usługa obsługuje kolejkowanie zadań dla hostowanej usługi za każdym razem, gdy w klucz jest wybrany na urządzeniu wejściowym:

  • Element IBackgroundTaskQueue jest wstrzykiwany do MonitorLoop usługi.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem element jest wywoływany w celu kolejkowania elementu roboczego.
  • Element roboczy symuluje długotrwałe zadanie w tle:
    • Wykonywane są trzy 5-sekundowe opóźnienia (Task.Delay).
    • Instrukcja try-catch wychwyci OperationCanceledException , jeśli zadanie zostanie anulowane.
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);
        }
    }
}

Usługi są zarejestrowane w IHostBuilder.ConfigureServices programie (Program.cs). Hostowana usługa jest zarejestrowana w metodzie AddHostedService rozszerzenia:

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 program jest uruchamiany w pliku Program.cs:

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

Asynchroniczne zadanie w tle z czasem

Poniższy kod tworzy asynchroniczne zadanie w tle:

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);
    }
}

Dodatkowe zasoby

W ASP.NET Core zadania w tle można zaimplementować jako usługi hostowane. Hostowana usługa to klasa z logiką zadań w tle, która implementuje IHostedService interfejs. Ten artykuł zawiera trzy przykłady hostowanych usług:

  • Zadanie w tle uruchamiane na czasomierzu.
  • Hostowana usługa, która aktywuje usługę o określonym zakresie. Usługa o określonym zakresie może używać wstrzykiwania zależności (DI).
  • Zadania w tle w kolejce, które są uruchamiane sekwencyjnie.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Szablon usługi procesu roboczego

Szablon usługi ASP.NET Core Worker Service stanowi punkt wyjścia do pisania długotrwałych aplikacji usługi. Aplikacja utworzona na podstawie szablonu usługi Worker Service określa zestaw SDK procesu roboczego w pliku projektu:

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

Aby użyć szablonu jako podstawy dla aplikacji usług hostowanych:

  1. Tworzenie nowego projektu.
  2. Wybierz pozycję Usługa procesu roboczego. Wybierz Dalej.
  3. Podaj nazwę projektu w polu Nazwa projektu lub zaakceptuj domyślną nazwę projektu. Wybierz pozycję Utwórz.
  4. W oknie dialogowym Tworzenie nowej usługi Procesu roboczego wybierz pozycję Utwórz.

Pakiet

Aplikacja oparta na szablonie usługi roboczej używa Microsoft.NET.Sdk.Worker zestawu SDK i zawiera jawne odwołanie do pakietu Microsoft.Extensions.Hosting . Zobacz na przykład plik projektu przykładowej aplikacji (BackgroundTasksSample.csproj).

W przypadku aplikacji internetowych korzystających z Microsoft.NET.Sdk.Web zestawu SDK pakiet Microsoft.Extensions.Hosting odwołuje się niejawnie z udostępnionej platformy. Jawne odwołanie do pakietu w pliku projektu aplikacji nie jest wymagane.

Interfejs IHostedService

Interfejs IHostedService definiuje dwie metody dla obiektów zarządzanych przez hosta:

StartAsync

StartAsync zawiera logikę uruchamiania zadania w tle. StartAsync jest wywoływana przed:

Zachowanie domyślne można zmienić tak, aby usługa hostowana StartAsync była uruchamiana po skonfigurowaniu potoku aplikacji i ApplicationStarted jest wywoływana. Aby zmienić zachowanie domyślne, dodaj hostowaną usługę (VideosWatcher w poniższym przykładzie) po wywołaniu metody 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

Token anulowania ma domyślny pięć sekund limitu czasu, aby wskazać, że proces zamykania nie powinien już być wdziękny. Po zażądaniu anulowania w tokenie:

  • Wszelkie pozostałe operacje w tle wykonywane przez aplikację powinny zostać przerwane.
  • Wszystkie metody wywoływane w StopAsync metodach powinny zostać zwrócone natychmiast.

Jednak zadania nie są porzucane po żądaniu anulowania — obiekt wywołujący oczekuje na ukończenie wszystkich zadań.

Jeśli aplikacja zostanie nieoczekiwanie zamknięta (na przykład proces aplikacji zakończy się niepowodzeniem), StopAsync może nie zostać wywołana. W związku z tym wszelkie metody wywoływane lub wykonywane w programie StopAsync operacje mogą nie wystąpić.

Aby przedłużyć domyślny limit czasu zamknięcia pięciu sekund, ustaw:

Hostowana usługa jest aktywowana raz podczas uruchamiania aplikacji i bezpiecznie zamykana po zamknięciu aplikacji. Jeśli podczas wykonywania zadania w tle zostanie zgłoszony błąd, powinien zostać wywołany nawet wtedy, Dispose gdy StopAsync nie zostanie wywołany.

BackgroundService, klasa bazowa

BackgroundService jest klasą bazową do implementowania długotrwałego IHostedServiceelementu .

Funkcja ExecuteAsync(CancellationToken) jest wywoływana w celu uruchomienia usługi w tle. Implementacja zwraca wartość Task reprezentującą cały okres istnienia usługi w tle. Żadne dalsze usługi nie są uruchamiane, dopóki funkcja ExecuteAsync nie stanie się asynchroniczna, na przykład przez wywołanie metody await. Unikaj wykonywania długich, blokujących pracę inicjowania w programie ExecuteAsync. Bloki hosta w stopAsync(CancellationToken) oczekujące na ExecuteAsync zakończenie.

Token anulowania jest wyzwalany po wywołaniu wywołania IHostedService.StopAsync . Implementacja polecenia ExecuteAsync powinna zakończyć się natychmiast, gdy token anulowania zostanie wyzwolony, aby bezpiecznie zamknąć usługę. W przeciwnym razie usługa niegracyjnie zamyka się po przekroczeniu limitu czasu zamknięcia. Aby uzyskać więcej informacji, zobacz sekcję interfejsu IHostedService.

StartAsync powinno być ograniczone do krótkich uruchomionych zadań, ponieważ hostowane usługi są uruchamiane sekwencyjnie, a żadne dalsze usługi nie są uruchamiane do momentu StartAsync ukończenia. Długotrwałe zadania należy umieścić w pliku ExecuteAsync. Aby uzyskać więcej informacji, zobacz źródło usługi BackgroundService.

Czasowe zadania w tle

Zadanie w tle czasowe korzysta z klasy System.Threading.Timer . Czasomierz wyzwala metodę DoWork zadania. Czasomierz jest wyłączony StopAsync i usuwany po usunięciu kontenera usługi w systemie 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();
    }
}

Element Timer nie czeka na ukończenie poprzednich DoWork wykonań, więc pokazane podejście może nie być odpowiednie dla każdego scenariusza. Interlocked.Increment służy do przyrostowania licznika wykonywania jako operacji niepodzielnej, co gwarantuje, że wiele wątków nie jest aktualizowanych executionCount jednocześnie.

Usługa jest zarejestrowana w IHostBuilder.ConfigureServices (Program.cs) przy użyciu AddHostedService metody rozszerzenia:

services.AddHostedService<TimedHostedService>();

Korzystanie z usługi o określonym zakresie w zadaniu w tle

Aby używać usług o określonym zakresie w ramach usługi BackgroundService, utwórz zakres. Domyślnie dla hostowanej usługi nie jest tworzony żaden zakres.

Zakres usługi zadań w tle zawiera logikę zadania w tle. W poniższym przykładzie:

  • Usługa jest asynchroniczna. Metoda DoWork zwraca wartość Task. W celach demonstracyjnych oczekuje się na opóźnienie dziesięciu sekund w metodzie DoWork .
  • Element ILogger jest wstrzykiwany do usługi.
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);
        }
    }
}

Usługa hostowana tworzy zakres, aby rozpoznać zakres usługi zadań w tle w celu wywołania jej DoWork metody. DoWorkzwraca element , który jest oczekiwany w elemecie TaskExecuteAsync:

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);
    }
}

Usługi są zarejestrowane w IHostBuilder.ConfigureServices programie (Program.cs). Hostowana usługa jest zarejestrowana w metodzie AddHostedService rozszerzenia:

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

Zadania w tle w kolejce

Kolejka zadań w tle jest oparta na platformie .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;
    }
}

W poniższym QueueHostedService przykładzie:

  • Metoda BackgroundProcessing zwraca Taskelement , który jest oczekiwany w pliku ExecuteAsync.
  • Zadania w tle w kolejce są usuwane z kolejki i wykonywane w pliku BackgroundProcessing.
  • Elementy robocze są oczekiwane przed zatrzymanie usługi w programie 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);
    }
}

MonitorLoop Usługa obsługuje kolejkowanie zadań dla hostowanej usługi za każdym razem, gdy w klucz jest wybrany na urządzeniu wejściowym:

  • Element IBackgroundTaskQueue jest wstrzykiwany do MonitorLoop usługi.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem element jest wywoływany w celu kolejkowania elementu roboczego.
  • Element roboczy symuluje długotrwałe zadanie w tle:
    • Wykonywane są trzy 5-sekundowe opóźnienia (Task.Delay).
    • Instrukcja try-catch wychwyci OperationCanceledException , jeśli zadanie zostanie anulowane.
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);
        }
    }
}

Usługi są zarejestrowane w IHostBuilder.ConfigureServices programie (Program.cs). Hostowana usługa jest zarejestrowana w metodzie AddHostedService rozszerzenia:

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 program jest uruchamiany w pliku Program.Main:

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

Dodatkowe zasoby