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

Przez Jeow Li Huan

Note

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z aktualną wersją, zobacz artykuł w wersji .NET 10.

Warning

Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz zasady pomocy technicznej platformy .NET i platformy .NET Core. Aby zapoznać się z aktualną wersją, zobacz artykuł w wersji .NET 10.

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

Szablon usługi Worker Service

Szablon ASP.NET Core Usługi Pracowniczej stanowi punkt wyjścia do pisania długo działających aplikacji serwisowych. Aplikacja utworzona na podstawie szablonu Worker Service wskazuje Worker SDK w swoim 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 robocza. 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 „Framework”. Wybierz Utwórz.

Package

Aplikacja oparta na szablonie Usługi Roboczej używa zestawu SDK Microsoft.NET.Sdk.Worker i ma 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 SDK Microsoft.NET.Sdk.Web, pakiet Microsoft.Extensions.Hosting jest odwoływany niejawnie z wspólnej 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 zadań, ponieważ hostowane usługi są uruchamiane sekwencyjnie, a żadne dalsze usługi nie są uruchamiane, dopóki StartAsync nie zostanie ukończone.

Wystąpienia usługi hostowanej są uruchamiane w kolejności, w jakiej zostały zarejestrowane w kontenerze iniekcji zależności, chyba że aplikacja włączy współbieżne uruchamianie przez ustawienie ServicesStartConcurrently na true:

builder.Services.Configure<HostOptions>(options =>
{
    options.ServicesStartConcurrently = true;
});

StopAsync

Token anulowania ma domyślny limit czasu 30 sekund, aby wskazać, że proces zamykania nie powinien być już kontrolowany. Po zażądaniu anulowania przez token.

  • Wszelkie pozostałe operacje w tle wykonywane przez aplikację powinny zostać przerwane.
  • Wszystkie metody wywoływane w StopAsync powinny zakończyć się natychmiast.

Jednak zadania nie są porzucane po żądaniu anulowania — obiekt wywołujący oczekuje na dokoń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 wywoływane metody lub przeprowadzane operacje w StopAsync 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.

Wystąpienia usług hostowanych zatrzymują się w odwrotnej kolejności do tej, w jakiej są zarejestrowane w kontenerze wstrzykiwania zależności, chyba że aplikacja włączy współbieżne zamykanie, ustawiając ServicesStopConcurrently na true:

builder.Services.Configure<HostOptions>(options =>
{
    options.ServicesStopConcurrently = true;
});

BackgroundService, klasa bazowa

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

Funkcja ExecuteAsync(CancellationToken) jest wywoływana w puli wątków w celu uruchomienia usługi w tle. Implementacja zwraca wartość Task reprezentującą cały okres istnienia usługi w tle. Blokowanie hosta w StopAsync(CancellationToken) czekającego na zakończenie ExecuteAsync.

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 inicjalizacji w ExecuteAsync. Blokowanie hosta w StopAsync(CancellationToken) czekającego na zakończenie ExecuteAsync.

Token anulowania jest wyzwalany, gdy zostanie wywołany 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 .

Zadania w tle o zaplanowanym czasie

Zadanie w tle z ustalonym czasem 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 aż wcześniejsze wykonania DoWork się zakończą, więc przedstawione podejście może nie być odpowiednie dla każdego scenariusza. Interlocked.Increment służy do inkrementacji licznika wykonywania jako operacji niepodzielnej, co gwarantuje, że wiele wątków nie aktualizuje licznika jednocześnie.

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

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.

Usługa zdefiniowana dla 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 .
  • Do usługi wstrzykiwany jest element ILogger.
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 umożliwiający rozwiązanie serwisu zadań w tle oraz wywołanie jego metody DoWork. DoWork zwraca Task, który jest oczekiwany w 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);
    }
}

Usługi są zarejestrowane w IHostBuilder.ConfigureServices (Program.cs). Hostowana usługa jest zarejestrowana za pomocą metody rozszerzenia AddHostedService.

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

Zadania w tle oczekujące na wykonanie

Kolejka zadań w tle jest oparta na programie .NET Framework 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 Task, który jest oczekiwany w ExecuteAsync.
  • Zadania w tle w kolejce są ściągane z kolejki i wykonywane w 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 klawisz w jest wybrany na urządzeniu wejściowym.

  • Element IBackgroundTaskQueue jest wstrzykiwany do usługi MonitorLoop.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem jest wywoływany w celu kolejkowania zadania roboczego.
  • Element roboczy symuluje długotrwałe zadanie w tle:
    • Proces wykonuje 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 (Program.cs). Hostowana usługa jest zarejestrowana za pomocą metody rozszerzenia AddHostedService.

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

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

Asynchroniczne czasowe zadanie w tle

Poniższy kod tworzy asynchroniczne zadanie w tle z ustalonym czasem wykonania.

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.
        await DoWork();

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

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

    private async Task DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

        // Simulate work
        await Task.Delay(TimeSpan.FromSeconds(2));

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

Natywne 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 robocza. 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 framework.
  6. Zaznacz pole wyboru Włącz natywną publikację AOT.
  7. Wybierz 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, która implementuje interfejs IHostedService i zawiera logikę zadań w tle. Ten artykuł zawiera trzy przykłady hostowanych usług:

Szablon usługi Worker Service

Szablon ASP.NET Core Usługi Pracowniczej stanowi punkt wyjścia do pisania długo działających aplikacji serwisowych. Aplikacja utworzona na podstawie szablonu Worker Service wskazuje Worker SDK w swoim 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 robocza. 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 „Framework”. Wybierz Utwórz.

Package

Aplikacja oparta na szablonie Usługi Roboczej używa zestawu SDK Microsoft.NET.Sdk.Worker i ma 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 SDK Microsoft.NET.Sdk.Web, pakiet Microsoft.Extensions.Hosting jest odwoływany niejawnie z wspólnej 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 zadań, ponieważ hostowane usługi są uruchamiane sekwencyjnie, a żadne dalsze usługi nie są uruchamiane, dopóki StartAsync nie zostanie ukończone.

StopAsync

Token anulowania ma domyślny limit czasu 30 sekund, aby wskazać, że proces zamykania nie powinien być już kontrolowany. Po zażądaniu anulowania przez token.

  • Wszelkie pozostałe operacje w tle wykonywane przez aplikację powinny zostać przerwane.
  • Wszystkie metody wywoływane w StopAsync powinny zakończyć się natychmiast.

Jednak zadania nie są porzucane po żądaniu anulowania — obiekt wywołujący oczekuje na dokoń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 wywoływane metody lub przeprowadzane operacje w StopAsync 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 IHostedService.

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 inicjalizacji w ExecuteAsync. Blokowanie hosta w StopAsync(CancellationToken) czekającego na zakończenie ExecuteAsync.

Token anulowania jest wyzwalany, gdy zostanie wywołany 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 .

Zadania w tle o zaplanowanym czasie

Zadanie w tle z ustalonym czasem 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 aż wcześniejsze wykonania DoWork się zakończą, więc przedstawione podejście może nie być odpowiednie dla każdego scenariusza. Interlocked.Increment służy do inkrementacji licznika wykonywania jako operacji niepodzielnej, co gwarantuje, że wiele wątków nie aktualizuje licznika jednocześnie.

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

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.

Usługa zdefiniowana dla 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 .
  • Do usługi wstrzykiwany jest element ILogger.
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 umożliwiający rozwiązanie serwisu zadań w tle oraz wywołanie jego metody DoWork. DoWork zwraca Task, który jest oczekiwany w 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);
    }
}

Usługi są zarejestrowane w IHostBuilder.ConfigureServices (Program.cs). Hostowana usługa jest zarejestrowana za pomocą metody rozszerzenia AddHostedService.

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

Zadania w tle oczekujące na wykonanie

Kolejka zadań w tle jest oparta na programie .NET Framework 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 Task, który jest oczekiwany w ExecuteAsync.
  • Zadania w tle w kolejce są ściągane z kolejki i wykonywane w 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 klawisz w jest wybrany na urządzeniu wejściowym.

  • Element IBackgroundTaskQueue jest wstrzykiwany do usługi MonitorLoop.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem jest wywoływany w celu kolejkowania zadania roboczego.
  • Element roboczy symuluje długotrwałe zadanie w tle:
    • Proces wykonuje 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 (Program.cs). Hostowana usługa jest zarejestrowana za pomocą metody rozszerzenia AddHostedService.

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

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

Asynchroniczne czasowe zadanie w tle

Poniższy kod tworzy asynchroniczne zadanie w tle z ustalonym czasem wykonania.

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.
        await DoWork();

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

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

    private async Task DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

        // Simulate work
        await Task.Delay(TimeSpan.FromSeconds(2));

        _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, która implementuje interfejs IHostedService i zawiera logikę zadań w tle. Ten artykuł zawiera trzy przykłady hostowanych usług:

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

Szablon usługi Worker Service

Szablon ASP.NET Core Usługi Pracowniczej stanowi punkt wyjścia do pisania długo działających aplikacji serwisowych. Aplikacja utworzona na podstawie szablonu Worker Service wskazuje Worker SDK w swoim 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 robocza. Wybierz Dalej.
  3. Podaj nazwę projektu w polu Nazwa projektu lub zaakceptuj domyślną nazwę projektu. Wybierz Utwórz.
  4. W oknie dialogowym Tworzenie nowej usługi Worker wybierz pozycję Utwórz.

Package

Aplikacja oparta na szablonie Usługi Roboczej używa zestawu SDK Microsoft.NET.Sdk.Worker i ma 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 SDK Microsoft.NET.Sdk.Web, pakiet Microsoft.Extensions.Hosting jest odwoływany niejawnie z wspólnej 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 hostowana usługa StartAsync była uruchamiana po skonfigurowaniu potoku aplikacji i wywołaniu ApplicationStarted. 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 limit czasu pięć sekund, aby wskazać, że proces zamykania nie powinien już przebiegać łagodnie. Po zażądaniu anulowania przez token.

  • Wszelkie pozostałe operacje w tle wykonywane przez aplikację powinny zostać przerwane.
  • Wszystkie metody wywoływane w StopAsync powinny zakończyć się natychmiast.

Jednak zadania nie są porzucane po żądaniu anulowania — obiekt wywołujący oczekuje na dokoń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 wywoływane metody lub przeprowadzane operacje w StopAsync mogą nie wystąpić.

Aby przedłużyć domyślny czas zamknięcia wynoszący pięć 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 IHostedService.

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 inicjalizacji w ExecuteAsync. Blokowanie hosta w StopAsync(CancellationToken) czekającego na zakończenie ExecuteAsync.

Token anulowania jest wyzwalany, gdy zostanie wywołany 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 zadań, ponieważ hostowane usługi są uruchamiane sekwencyjnie, a żadne dalsze usługi nie są uruchamiane, dopóki StartAsync nie zostanie ukończone. Długotrwałe zadania należy umieścić w pliku ExecuteAsync. Aby uzyskać więcej informacji, zobacz źródło usługi BackgroundService.

Zadania w tle o zaplanowanym czasie

Zadanie w tle z ustalonym czasem 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 aż wcześniejsze wykonania DoWork się zakończą, więc przedstawione podejście może nie być odpowiednie dla każdego scenariusza. Interlocked.Increment służy do inkrementacji licznika wykonywania jako operacji niepodzielnej, co gwarantuje, że wiele wątków nie aktualizuje licznika jednocześnie.

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

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.

Usługa zdefiniowana dla 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 .
  • Do usługi wstrzykiwany jest element ILogger.
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 umożliwiający rozwiązanie serwisu zadań w tle oraz wywołanie jego metody DoWork. DoWork zwraca Task, który jest oczekiwany w 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);
    }
}

Usługi są zarejestrowane w IHostBuilder.ConfigureServices (Program.cs). Hostowana usługa jest zarejestrowana za pomocą metody rozszerzenia AddHostedService.

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

Zadania w tle oczekujące na wykonanie

Kolejka zadań w tle jest oparta na programie .NET Framework 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 Task, który jest oczekiwany w ExecuteAsync.
  • Zadania w tle w kolejce są ściągane z kolejki i wykonywane w 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 klawisz w jest wybrany na urządzeniu wejściowym.

  • Element IBackgroundTaskQueue jest wstrzykiwany do usługi MonitorLoop.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem jest wywoływany w celu kolejkowania zadania roboczego.
  • Element roboczy symuluje długotrwałe zadanie w tle:
    • Proces wykonuje 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 (Program.cs). Hostowana usługa jest zarejestrowana za pomocą metody rozszerzenia AddHostedService.

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

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

Dodatkowe zasoby