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

A : Jeow Li Huan

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:

Szablon usługi procesu roboczego

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

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

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

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

Pakiet

Aplikacja oparta na szablonie usługi procesu roboczego Microsoft.NET.Sdk.Worker używa zestawu SDK 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, które korzystają z Microsoft.NET.Sdk.Web zestawu SDK, pakiet Microsoft.Extensions.Hosting jest przywołyny niejawnie z udostępnionej struktury. Jawne odwołanie do pakietu w pliku projektu aplikacji nie jest wymagane.

IHostedService, interfejs

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

StartAsync

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

StartAsync powinny być ograniczone do krótko działających zadań, ponieważ hostowane usługi są uruchamiane sekwencyjnie i żadne dalsze usługi nie są uruchamiane StartAsync do momentu ukończenia działania.

StopAsync

Token anulowania ma domyślny limit czasu pięciu sekund wskazujący, że proces zamykania nie powinien już być graceful. Gdy żądanie anulowania jest żądane w tokenie:

  • Wszystkie pozostałe operacje w tle wykonywane przez aplikację powinny zostać przerwane.
  • Wszystkie metody wywoływane w powinny StopAsync zwracać natychmiast.

Zadania nie są jednak porzucone po zażądaniu anulowania — 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 metody wywoływane lub operacje przeprowadzane w programie StopAsync mogą nie wystąpić.

Aby rozszerzyć domyślny limit czasu zamknięcia systemu o pięć sekund, ustaw:

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

BackgroundService, klasa bazowa

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

Polecenie ExecuteAsync(CancellationToken) jest wywoływane w celu uruchomienia usługi w tle. Implementacja zwraca wartość Task , która reprezentuje cały okres istnienia usługi w tle. Żadne dalsze usługi nie zostaną uruchomione, dopóki executeAsync nie stanie się asynchroniczne, na przykład przez wywołanie metody await. Unikaj wykonywania długich operacji, blokując działanie inicjowania w programie ExecuteAsync. Bloki hosta w tokenie StopAsync(CancellationToken) oczekujące na ExecuteAsync zakończenie.

Token anulowania jest wyzwalany po wywołaniu IHostedService.StopAsync . Implementacja programu powinna ExecuteAsync zakończyć się natychmiast po wyzłoceniu tokenu anulowania, aby bezpiecznie zamknąć usługę. W przeciwnym razie usługa jest niełako zamykana w momencie przeoczania limitu czasu zamknięcia. Aby uzyskać więcej informacji, zobacz sekcję IHostedService interface (Interfejs IHostedService ).

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

Zadania w tle z czasem

Zadanie w tle z czasem korzysta z klasy System.Threading.Timer . Czasomierz wyzwala metodę DoWork zadania. Czasomierz jest wyłączany i StopAsync usuwany, gdy kontener usługi jest likwidowany w :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();
    }
}

Metoda Timer nie czeka na zakończenie DoWork poprzednich wykonań metody , więc przedstawione podejście może nie być odpowiednie dla każdego scenariusza. Wartość Interlocked.Increment służy do zwiększania licznika wykonywania jako niepodzielnej operacji, co gwarantuje, że wiele wątków nie będzie równocześnie aktualizowane executionCount .

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

services.AddHostedService<TimedHostedService>();

Korzystanie z usługi o zakresie w zadaniu w tle

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

Usługa zadań w tle o zakresie zawiera logikę zadania w tle. W poniższym przykładzie:

  • Usługa jest asynchroniczna. Metoda DoWork zwraca wartość Task. W celach demonstracyjnych w metodzie oczekuje się opóźnienia o 10 DoWork sekund.
  • 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);
        }
    }
}

Hostowana usługa tworzy zakres w celu rozpoznania usługi zadań w tle o zakresie do wywołania jej DoWork metody. DoWorkZwraca wartość Task, która jest oczekiwana 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 programie IHostBuilder.ConfigureServices (Program.cs). Hostowana usługa jest zarejestrowana przy użyciu AddHostedService metody 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 przykładzie QueueHostedService :

  • Metoda BackgroundProcessing zwraca wartość Task, która jest oczekiwana w .ExecuteAsync
  • Zadania w tle w kolejce są wykonywane w BackgroundProcessingkolejce.
  • Elementy robocze są oczekiwane przed zatrzymaniem usługi w usłudze 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);
    }
}

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

  • Jest IBackgroundTaskQueue wstrzykiwany do MonitorLoop usługi.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem Element jest wywoływany w celu kolejkowania elementu roboczego.
  • Element pracy symuluje długotrwałe zadanie w tle:
    • Wykonywane są trzy 5-sekundowe opóźnienia (Task.Delay).
    • Instrukcja try-catch jest wychwytywy OperationCanceledException , jeśli zadanie zostało 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 programie IHostBuilder.ConfigureServices (Program.cs). Hostowana usługa jest zarejestrowana przy użyciu AddHostedService metody 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 został uruchomiony w programie Program.cs:

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

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:

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

Szablon usługi procesu roboczego

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

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

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

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

Pakiet

Aplikacja oparta na szablonie usługi procesu roboczego Microsoft.NET.Sdk.Worker używa zestawu SDK 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, które korzystają z Microsoft.NET.Sdk.Web zestawu SDK, pakiet Microsoft.Extensions.Hosting jest przywołyny niejawnie z udostępnionej struktury. Jawne odwołanie do pakietu w pliku projektu aplikacji nie jest wymagane.

IHostedService, interfejs

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:

Domyślne zachowanie można zmienić, aby StartAsync hostowana usługa była uruchamiana po skonfigurowaniu potoku aplikacji i wywołaniu ApplicationStarted go. Aby zmienić zachowanie domyślne, dodaj hostowaną usługę (VideosWatcher w poniższym przykładzie) po wywołaniu funkcji 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ęciu sekund wskazujący, że proces zamykania nie powinien już być graceful. Gdy żądanie anulowania jest żądane w tokenie:

  • Wszystkie pozostałe operacje w tle wykonywane przez aplikację powinny zostać przerwane.
  • Wszystkie metody wywoływane w powinny StopAsync zwracać natychmiast.

Zadania nie są jednak porzucone po zażądaniu anulowania — 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 metody wywoływane lub operacje przeprowadzane w programie StopAsync mogą nie wystąpić.

Aby rozszerzyć domyślny limit czasu zamknięcia systemu o pięć sekund, ustaw:

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

BackgroundService, klasa bazowa

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

Polecenie ExecuteAsync(CancellationToken) jest wywoływane w celu uruchomienia usługi w tle. Implementacja zwraca wartość Task , która reprezentuje cały okres istnienia usługi w tle. Żadne dalsze usługi nie zostaną uruchomione, dopóki executeAsync nie stanie się asynchroniczne, na przykład przez wywołanie metody await. Unikaj wykonywania długich operacji, blokując działanie inicjowania w programie ExecuteAsync. Bloki hosta w tokenie StopAsync(CancellationToken) oczekujące na ExecuteAsync zakończenie.

Token anulowania jest wyzwalany po wywołaniu IHostedService.StopAsync . Implementacja programu powinna ExecuteAsync zakończyć się natychmiast po wyzłoceniu tokenu anulowania, aby bezpiecznie zamknąć usługę. W przeciwnym razie usługa jest niełako zamykana w momencie przeoczania limitu czasu zamknięcia. Aby uzyskać więcej informacji, zobacz sekcję IHostedService interface (Interfejs IHostedService ).

StartAsync powinny być ograniczone do krótko działających zadań, ponieważ hostowane usługi są uruchamiane sekwencyjnie i żadne dalsze usługi nie są uruchamiane StartAsync do momentu ukończenia działania. Długotrwałe zadania powinny być umieszczane w .ExecuteAsync Aby uzyskać więcej informacji, zobacz źródło danych BackgroundService.

Zadania w tle z czasem

Zadanie w tle z czasem korzysta z klasy System.Threading.Timer . Czasomierz wyzwala metodę DoWork zadania. Czasomierz jest wyłączany i StopAsync usuwany, gdy kontener usługi jest usuwany w usłudze 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();
    }
}

Metoda Timer nie czeka na zakończenie DoWork poprzednich wykonań, więc przedstawione podejście może nie być odpowiednie dla każdego scenariusza. Wartość Interlocked.Increment służy do zwiększania licznika wykonywania jako niepodzielnej operacji, co gwarantuje, że wiele wątków nie będzie executionCount równocześnie aktualizowane.

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

services.AddHostedService<TimedHostedService>();

Korzystanie z usługi o zakresie w zadaniu w tle

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

Usługa zadań w tle o zakresie zawiera logikę zadania w tle. W poniższym przykładzie:

  • Usługa jest asynchroniczna. Metoda DoWork zwraca wartość Task. W celach demonstracyjnych w metodzie oczekuje się na opóźnienie o 10 DoWork sekund.
  • 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);
        }
    }
}

Hostowana usługa tworzy zakres w celu rozpoznania usługi zadań w tle o zakresie w celu wywołania jej DoWork metody. DoWorkzwraca wartość Task, która jest oczekiwana 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 programie IHostBuilder.ConfigureServices (Program.cs). Hostowana usługa jest zarejestrowana przy użyciu AddHostedService metody 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 przykładzie QueueHostedService :

  • Metoda BackgroundProcessing zwraca wartość Task, która jest oczekiwana w metodzie ExecuteAsync.
  • Zadania w tle w kolejce są wykonywane w kolejce i wykonywane w .BackgroundProcessing
  • Elementy robocze są oczekiwane przed zatrzymaniem usługi w usłudze 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);
    }
}

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

  • Jest IBackgroundTaskQueue wstrzykiwany do MonitorLoop usługi.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem Element jest wywoływany w celu kolejkowania elementu roboczego.
  • Element pracy symuluje długotrwałe zadanie w tle:
    • Wykonywane są trzy 5-sekundowe opóźnienia (Task.Delay).
    • Instrukcja try-catch wychwytuje 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 programie IHostBuilder.ConfigureServices (Program.cs). Hostowana usługa jest zarejestrowana przy użyciu AddHostedService metody 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 uruchomiony w programie Program.Main:

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

Dodatkowe zasoby