Hintergrundtasks mit gehosteten Diensten in ASP.NET Core

Von Jeow Li Huan

In ASP.NET Core können Hintergrundtasks als gehostete Dienste implementiert werden. Ein gehosteter Dienst ist eine Klasse mit Logik für Hintergrundaufgaben, die die Schnittstelle IHostedService implementiert. In diesem Artikel sind drei Beispiel für gehostete Dienste enthalten:

  • Hintergrundtasks, die auf einem Timer ausgeführt werden.
  • Gehostete Dienste, die einen bereichsbezogenen Dienst aktivieren. Der bereichsbezogene Dienst kann eine Abhängigkeitsinjektion (Dependency Injection, DI) verwenden.
  • Hintergrundtasks in der Warteschlange, die sequenziell ausgeführt werden.

Workerdienstvorlage

Die ASP.NET Core-Vorlage „Workerdienst“ dient als Ausgangspunkt für das Schreiben von Dienstanwendungen mit langer Laufzeit. Eine aus der Workerdienstvorlage erstellte App gibt das Worker SDK in ihrer Projektdatei an:

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

Gehen Sie folgendermaßen vor, wenn Sie die Vorlage als Grundlage für eine Hosted Services-App verwenden möchten:

  1. Erstellen Sie ein neues Projekt.
  2. Wählen Sie Workerdienst aus. Klicken Sie auf Weiter.
  3. Geben Sie im Feld Projektname einen Projektnamen ein, oder übernehmen Sie den Standardnamen. Wählen Sie Weiter aus.
  4. Wählen Sie im Dialogfeld Zusätzliche Informationen ein Framework aus. Wählen Sie Erstellen aus.

Paket

Eine App, die auf der Workerdienstvorlage basiert, verwendet das Microsoft.NET.Sdk.Worker SDK und verfügt über einen expliziten Paketverweis auf das Microsoft.Extensions.Hosting-Paket. Sehen Sie sich dazu beispielsweise die Projektdatei der Beispiel-App (BackgroundTasksSample.csproj) an.

Für Web-Apps, die das Microsoft.NET.Sdk.Web SDK verwenden, wird auf das Microsoft.Extensions.Hosting-Paket implizit über das geteilte Framework verwiesen. Es ist kein expliziter Paketverweis in der Projektdatei der App erforderlich.

Die IHostedService-Schnittstelle

Die IHostedService-Schnittstelle definiert zwei Methoden für Objekte, die vom Host verwaltet werden:

StartAsync

StartAsync(CancellationToken) enthält die Logik zum Starten der Hintergrundaufgabe. StartAsync wird vor folgenden Vorgängen aufgerufen:

StartAsync sollte auf Aufgaben mit kurzer Ausführung beschränkt werden, da gehostete Dienste sequenziell ausgeführt werden, sodass weitere Dienste erst gestartet werden, wenn die Ausführung von StartAsync beendet wurde.

StopAsync

Das Abbruchtoken hat standardmäßig ein Zeitlimit von fünf Sekunden, um zu melden, dass der Prozess des Herunterfahrens nicht mehr ordnungsgemäß ausgeführt wird. Gehen Sie wie folgt vor, wenn ein Abbruch für das Token angefordert wird:

  • Brechen Sie jegliche von der App ausgeführten Hintergrundoperationen ab.
  • Jegliche Methoden, die in StopAsync aufgerufen werden, sollten umgehend zurückgegeben werden.

Allerdings werden keine Aufgaben abgebrochen, wenn der Abbruch angefordert wird. Der Aufrufer wartet, bis alle Aufgaben abgeschlossen sind.

Wenn die App unerwartet beendet wird (weil der Prozess der App beispielsweise fehlschlägt), wird StopAsync möglicherweise nicht aufgerufen. Daher werden die in StopAsync aufgerufenen Methoden oder ausgeführten Operationen nicht durchgeführt.

Um das standardmäßig 5-sekündige Timeout beim Herunterfahren zu verlängern, legen Sie folgendes fest:

Der gehostete Dienst wird beim Start der App einmal aktiviert und beim Beenden der App wieder ordnungsgemäß heruntergefahren. Wenn während der Ausführung von Hintergrundtasks ein Fehler ausgelöst wird, sollte Dispose aufgerufen werden, auch wenn StopAsync nicht aufgerufen wird.

BackgroundService-Basisklasse

BackgroundService ist eine Basisklasse zur Implementierung eines IHostedService mit langer Laufzeit.

ExecuteAsync (CancellationToken) wird aufgerufen, um den Hintergrunddienst auszuführen. Die Implementierung gibt einen Task zurück, der die gesamte Lebensdauer des Hintergrunddiensts darstellt. Es werden keine weiteren Dienste gestartet, bis ExecuteAsync asynchron wird, etwa durch den Aufruf von await. Vermeiden Sie die Ausführung von langen, blockierenden Initialisierungsarbeiten in ExecuteAsync. Die Hostblöcke in StopAsync(CancellationToken) warten auf den Abschluss von ExecuteAsync.

Das Abbruchtoken wird beim Aufruf von IHostedService.StopAsync ausgelöst. Ihre Implementierung von ExecuteAsync sollte unverzüglich beendet werden, wenn das Abbruchtoken ausgelöst wird, um den Dienst ordnungsgemäß herunterzufahren. Andernfalls wird der Dienst beim Erreichen des Timeouts beim Herunterfahren nicht ordnungsgemäß beendet. Weitere Informationen finden Sie im Abschnitt IHostedService-Schnittstelle.

Weitere Informationen finden Sie im Quellcode für BackgroundService.

Zeitlich festgelegte Hintergrundtasks

Zeitlich festgelegte Hintergrundtasks verwenden die Klasse System.Threading.Timer. Der Timer löst die DoWork-Methode des Tasks aus. Der Timer wird durch StopAsync deaktiviert und freigegeben, wenn der Dienstcontainer durch Dispose freigegeben ist:

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

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

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

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

        return Task.CompletedTask;
    }

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

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

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

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

        return Task.CompletedTask;
    }

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

Timer wartet nicht auf den Abschluss vorheriger Ausführungen von DoWork. Die veranschaulichte Vorgehensweise eignet sich also möglicherweise nicht für alle Szenarios. Interlocked.Increment wird zum Erhöhen des Ausführungszählers mit einem atomischen Vorgang verwendet, wodurch sichergestellt wird, dass executionCount nicht durch mehrere Threads gleichzeitig aktualisiert wird.

Der Dienst wird in IHostBuilder.ConfigureServices (Program.cs) mit der Erweiterungsmethode AddHostedService registriert:

services.AddHostedService<TimedHostedService>();

Verwenden eines bereichsbezogenen Diensts in einem Hintergrundtask

Erstellen Sie einen Bereich, um bereichsbezogene Dienste in einem BackgroundService zu verwenden. Bereiche werden für einen gehosteten Dienst nicht standardmäßig erstellt.

Der bereichsbezogene Dienst für Hintergrundtasks enthält die Logik des Hintergrundtasks. Im folgenden Beispiel:

  • Der Dienst ist asynchron. Die DoWork-Methode gibt Task zurück. Zu Demonstrationszwecken wird in der DoWork-Methode eine Verzögerung von zehn Sekunden verwendet.
  • Ein ILogger wird in den Dienst eingefügt.
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);
        }
    }
}

Der gehostete Dienst erstellt einen Bereich, um den bereichsbezogenen Dienst für Hintergrundtasks aufzulösen, damit die DoWork-Methode aufgerufen wird. DoWork gibt einen Task zurück, auf den in ExecuteAsync gewartet wird:

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

Die Dienste werden in IHostBuilder.ConfigureServices (Program.cs) registriert. Der gehostete Dienst wird mit der Erweiterungsmethode AddHostedService registriert:

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

Hintergrundtasks in der Warteschlange

Eine Warteschlange für Hintergrundaufgaben basiert auf dem .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;
    }
}

Im folgenden Beispiel für QueueHostedService gilt:

  • Die BackgroundProcessing-Methode gibt einen Task zurück, auf den in ExecuteAsync gewartet wird.
  • Hintergrundtasks in der Warteschlange werden aus dieser entfernt und in BackgroundProcessing ausgeführt.
  • Auf Arbeitselemente wird gewartet, bevor der Dienst in StopAsync angehalten wird.
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);
    }
}

Ein MonitorLoop-Dienst verarbeitet das Einreihen von Tasks in die Warteschlange für den gehosteten Dienst, wenn der w-Schlüssel auf einem Eingabegerät ausgewählt wird:

  • Die IBackgroundTaskQueue wird in den MonitorLoop-Dienst eingefügt.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem wird aufgerufen, um ein Arbeitselement in die Warteschlange einzureihen.
  • Das Arbeitselement simuliert eine Hintergrundaufgabe mit langer Ausführungszeit:
    • Drei 5-Sekunden-Verzögerungen werden ausgeführt (Task.Delay).
    • Eine try-catch-Anweisung fängt OperationCanceledException auf, wenn der Task abgebrochen wird.
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);
        }
    }
}

Die Dienste werden in IHostBuilder.ConfigureServices (Program.cs) registriert. Der gehostete Dienst wird mit der Erweiterungsmethode AddHostedService registriert:

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 wird in Program.cs gestartet:

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

Zusätzliche Ressourcen

In ASP.NET Core können Hintergrundtasks als gehostete Dienste implementiert werden. Ein gehosteter Dienst ist eine Klasse mit Logik für Hintergrundaufgaben, die die Schnittstelle IHostedService implementiert. In diesem Artikel sind drei Beispiel für gehostete Dienste enthalten:

  • Hintergrundtasks, die auf einem Timer ausgeführt werden.
  • Gehostete Dienste, die einen bereichsbezogenen Dienst aktivieren. Der bereichsbezogene Dienst kann eine Abhängigkeitsinjektion (Dependency Injection, DI) verwenden.
  • Hintergrundtasks in der Warteschlange, die sequenziell ausgeführt werden.

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

Vorlage „Workerdienst“

Die ASP.NET Core-Vorlage „Workerdienst“ dient als Ausgangspunkt für das Schreiben von Dienstanwendungen mit langer Laufzeit. Eine aus der Workerdienstvorlage erstellte App gibt das Worker SDK in ihrer Projektdatei an:

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

Gehen Sie folgendermaßen vor, wenn Sie die Vorlage als Grundlage für eine Hosted Services-App verwenden möchten:

  1. Erstellen Sie ein neues Projekt.
  2. Wählen Sie Workerdienst aus. Klicken Sie auf Weiter.
  3. Geben Sie im Feld Projektname einen Projektnamen ein, oder übernehmen Sie den Standardnamen. Klicken Sie auf Erstellen.
  4. Wählen Sie im Dialogfeld Neuen Workerdienst erstellenErstellen aus.

Paket

Eine App, die auf der Workerdienstvorlage basiert, verwendet das Microsoft.NET.Sdk.Worker SDK und verfügt über einen expliziten Paketverweis auf das Microsoft.Extensions.Hosting-Paket. Sehen Sie sich dazu beispielsweise die Projektdatei der Beispiel-App (BackgroundTasksSample.csproj) an.

Für Web-Apps, die das Microsoft.NET.Sdk.Web SDK verwenden, wird auf das Microsoft.Extensions.Hosting-Paket implizit über das geteilte Framework verwiesen. Es ist kein expliziter Paketverweis in der Projektdatei der App erforderlich.

Die IHostedService-Schnittstelle

Die IHostedService-Schnittstelle definiert zwei Methoden für Objekte, die vom Host verwaltet werden:

StartAsync

StartAsync enthält die Logik zum Starten des Hintergrundtasks. StartAsync wird vor folgenden Vorgängen aufgerufen:

Das Standardverhalten kann so geändert werden, dass der StartAsync-Vorgang des gehosteten Diensts ausgeführt wird, nachdem die Pipeline der App konfiguriert und ApplicationStarted aufgerufen wurde. Um das Standardverhalten zu ändern, fügen Sie den gehosteten Dienst (VideosWatcher im folgenden Beispiel) nach dem Aufruf von ConfigureWebHostDefaults hinzu:

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

Das Abbruchtoken hat standardmäßig ein Zeitlimit von fünf Sekunden, um zu melden, dass der Prozess des Herunterfahrens nicht mehr ordnungsgemäß ausgeführt wird. Gehen Sie wie folgt vor, wenn ein Abbruch für das Token angefordert wird:

  • Brechen Sie jegliche von der App ausgeführten Hintergrundoperationen ab.
  • Jegliche Methoden, die in StopAsync aufgerufen werden, sollten umgehend zurückgegeben werden.

Allerdings werden keine Aufgaben abgebrochen, wenn der Abbruch angefordert wird. Der Aufrufer wartet, bis alle Aufgaben abgeschlossen sind.

Wenn die App unerwartet beendet wird (weil der Prozess der App beispielsweise fehlschlägt), wird StopAsync möglicherweise nicht aufgerufen. Daher werden die in StopAsync aufgerufenen Methoden oder ausgeführten Operationen nicht durchgeführt.

Um das standardmäßig 5-sekündige Timeout beim Herunterfahren zu verlängern, legen Sie folgendes fest:

Der gehostete Dienst wird beim Start der App einmal aktiviert und beim Beenden der App wieder ordnungsgemäß heruntergefahren. Wenn während der Ausführung von Hintergrundtasks ein Fehler ausgelöst wird, sollte Dispose aufgerufen werden, auch wenn StopAsync nicht aufgerufen wird.

BackgroundService-Basisklasse

BackgroundService ist eine Basisklasse zur Implementierung eines IHostedService mit langer Laufzeit.

ExecuteAsync (CancellationToken) wird aufgerufen, um den Hintergrunddienst auszuführen. Die Implementierung gibt einen Task zurück, der die gesamte Lebensdauer des Hintergrunddiensts darstellt. Es werden keine weiteren Dienste gestartet, bis ExecuteAsync asynchron wird, etwa durch den Aufruf von await. Vermeiden Sie die Ausführung von langen, blockierenden Initialisierungsarbeiten in ExecuteAsync. Die Hostblöcke in StopAsync(CancellationToken) warten auf den Abschluss von ExecuteAsync.

Das Abbruchtoken wird beim Aufruf von IHostedService.StopAsync ausgelöst. Ihre Implementierung von ExecuteAsync sollte unverzüglich beendet werden, wenn das Abbruchtoken ausgelöst wird, um den Dienst ordnungsgemäß herunterzufahren. Andernfalls wird der Dienst beim Erreichen des Timeouts beim Herunterfahren nicht ordnungsgemäß beendet. Weitere Informationen finden Sie im Abschnitt IHostedService-Schnittstelle.

StartAsync sollte auf Aufgaben mit kurzer Ausführung beschränkt werden, da gehostete Dienste sequenziell ausgeführt werden, sodass weitere Dienste erst gestartet werden, wenn die Ausführung von StartAsync beendet wurde. Zeitintensive Aufgaben sollten in platziert ExecuteAsync werden. Weitere Informationen finden Sie im Quellcode für BackgroundService.

Zeitlich festgelegte Hintergrundtasks

Zeitlich festgelegte Hintergrundtasks verwenden die Klasse System.Threading.Timer. Der Timer löst die DoWork-Methode des Tasks aus. Der Timer wird durch StopAsync deaktiviert und freigegeben, wenn der Dienstcontainer durch Dispose freigegeben ist:

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

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

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

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

        return Task.CompletedTask;
    }

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

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

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

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

        return Task.CompletedTask;
    }

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

Timer wartet nicht auf den Abschluss vorheriger Ausführungen von DoWork. Die veranschaulichte Vorgehensweise eignet sich also möglicherweise nicht für alle Szenarios. Interlocked.Increment wird zum Erhöhen des Ausführungszählers mit einem atomischen Vorgang verwendet, wodurch sichergestellt wird, dass executionCount nicht durch mehrere Threads gleichzeitig aktualisiert wird.

Der Dienst wird in IHostBuilder.ConfigureServices (Program.cs) mit der Erweiterungsmethode AddHostedService registriert:

services.AddHostedService<TimedHostedService>();

Verwenden eines bereichsbezogenen Diensts in einem Hintergrundtask

Erstellen Sie einen Bereich, um bereichsbezogene Dienste in einem BackgroundService zu verwenden. Bereiche werden für einen gehosteten Dienst nicht standardmäßig erstellt.

Der bereichsbezogene Dienst für Hintergrundtasks enthält die Logik des Hintergrundtasks. Im folgenden Beispiel:

  • Der Dienst ist asynchron. Die DoWork-Methode gibt Task zurück. Zu Demonstrationszwecken wird in der DoWork-Methode eine Verzögerung von zehn Sekunden verwendet.
  • Ein ILogger wird in den Dienst eingefügt.
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);
        }
    }
}

Der gehostete Dienst erstellt einen Bereich, um den bereichsbezogenen Dienst für Hintergrundtasks aufzulösen, damit die DoWork-Methode aufgerufen wird. DoWork gibt einen Task zurück, auf den in ExecuteAsync gewartet wird:

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

Die Dienste werden in IHostBuilder.ConfigureServices (Program.cs) registriert. Der gehostete Dienst wird mit der Erweiterungsmethode AddHostedService registriert:

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

Hintergrundtasks in der Warteschlange

Eine Warteschlange für Hintergrundaufgaben basiert auf dem .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;
    }
}

Im folgenden Beispiel für QueueHostedService gilt:

  • Die BackgroundProcessing-Methode gibt einen Task zurück, auf den in ExecuteAsync gewartet wird.
  • Hintergrundtasks in der Warteschlange werden aus dieser entfernt und in BackgroundProcessing ausgeführt.
  • Auf Arbeitselemente wird gewartet, bevor der Dienst in StopAsync angehalten wird.
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);
    }
}

Ein MonitorLoop-Dienst verarbeitet das Einreihen von Tasks in die Warteschlange für den gehosteten Dienst, wenn der w-Schlüssel auf einem Eingabegerät ausgewählt wird:

  • Die IBackgroundTaskQueue wird in den MonitorLoop-Dienst eingefügt.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem wird aufgerufen, um ein Arbeitselement in die Warteschlange einzureihen.
  • Das Arbeitselement simuliert eine Hintergrundaufgabe mit langer Ausführungszeit:
    • Drei 5-Sekunden-Verzögerungen werden ausgeführt (Task.Delay).
    • Eine try-catch-Anweisung fängt OperationCanceledException auf, wenn der Task abgebrochen wird.
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);
        }
    }
}

Die Dienste werden in IHostBuilder.ConfigureServices (Program.cs) registriert. Der gehostete Dienst wird mit der Erweiterungsmethode AddHostedService registriert:

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 wird in Program.Main gestartet:

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

Zusätzliche Ressourcen