Implementieren von Hintergrundtasks in Microservices mit IHostedService und der BackgroundService-Klasse

Tipp

Diese Inhalte sind ein Auszug aus dem eBook „.NET Microservices Architecture for Containerized .NET Applications“, verfügbar unter .NET Docs oder als kostenlos herunterladbare PDF-Datei, die offline gelesen werden kann.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

Hintergrundaufgaben und geplante Aufträge können in jeder beliebigen Anwendung eingesetzt werden, unabhängig davon, ob diese auf einer Microservicearchitektur basiert. Der Unterschied bei Verwendung einer Microservicearchitektur besteht darin, dass Sie die Hintergrundaufgabe in einem separaten Hostingprozess oder -container implementieren können, damit Sie diesen je nach Bedarf hoch- oder herunterskalieren können.

In .NET werden diese Tasks als gehostete Dienste bezeichnet, da sie Dienste oder Logiken darstellen, die in einem Host, einer Anwendung oder einem Microservice gehostet werden. Beachten Sie, dass in diesem Fall der gehostete Dienst einfach einer Klasse mit der Logik des Hintergrundtasks entspricht.

Seit .NET Core 2.0 stellt das Framework die neue Schnittstelle IHostedService bereit, mit der Sie gehostete Dienste einfach implementieren können. Die Grundidee besteht darin, mehrere Hintergrundtasks (gehostete Dienste) zu registrieren, die neben dem Host oder Webhost im Hintergrund ausgeführt werden (s. Abbildung 6-26).

Diagram comparing ASP.NET Core IWebHost and .NET Core IHost.

Abbildung 6-26. Verwenden von IHostedService mit Host und WebHost

ASP.NET Core 1.x und 2.x unterstützen IWebHost für Hintergrundprozesse in Web-Apps. .NET Core-Versionen ab 2.1 unterstützen IHost für Hintergrundprozesse mit einfachen Konsolenanwendungen. Beachten Sie den Unterschied zwischen WebHost und Host.

WebHost (eine Basisklasse, die IWebHost implementiert) ist in ASP.NET Core 2.0 das Infrastrukturartefakt, mit dem Sie HTTP-Serverfunktionen für Ihren Prozess (z. B. Implementieren einer MVC-Webanwendung oder eines Web-API-Diensts) bereitstellen. Diese Klasse enthält in ASP.NET Core alle neuen Infrastrukturfunktionen, sodass Sie u. a. die Abhängigkeitsinjektion verwenden und in Anforderungspipelines Middleware einfügen können. Der WebHost verwendet genau diese IHostedServices für Hintergrundaufgaben.

Host (Basisklasse, die IHost implementiert) wurde in .NET Core 2.1 eingeführt. Mit Host können Sie eine ähnliche Infrastruktur wie mit WebHost (Dependency Injection, gehostete Dienste usw.) verwenden. Der Unterschied besteht darin, dass Sie einen einfacheren und schlankeren Prozess für den Host verwenden und auf Verknüpfungen mit MVC, einer Web-API oder HTTP-Serverfunktionen verzichten.

Sie können daher entweder einen spezialisierten Hostprozess mit IHost erstellen, um ausschließlich gehostete Dienste (beispielsweise einen Microservice zum Hosten von IHostedServices) zu verarbeiten, oder aber einen vorhandenen ASP.NET Core-WebHost wie eine ASP.NET Core-Web-API oder eine MVC-Anwendung erweitern.

Je nach Geschäfts- und Skalierbarkeitsanforderungen hat jeder Ansatz seine Vor- und Nachteile. Zusammengefasst heißt das: Falls Ihre Hintergrundtasks nichts mit HTTP (IWebHost) zu tun haben, sollten Sie IHost verwenden.

Registrieren von gehosteten Diensten in WebHost oder Host

Im Folgenden wird die IHostedService-Schnittstelle ausführlicher behandelt, da sie für WebHost oder Host auf ähnliche Weise verwendet wird.

SignalR ist ein Beispiel für ein Artefakt, das gehostete Dienste verwendet. Sie können den Dienst aber auch für einfachere Aufgaben verwenden. Hierzu zählt etwa Folgendes:

  • ein Hintergrundtask, der eine Datenbank abruft und nach Änderungen sucht
  • ein geplanter Task, der einen Cache regelmäßig aktualisiert
  • eine Implementierung von QueueBackgroundWorkItem, durch die ein Task in einem Hintergrundthread ausgeführt werden kann
  • das Verarbeiten von Nachrichten aus einer Nachrichtenwarteschlange im Hintergrund einer Webanwendung, während allgemeine Dienste wie ILogger gemeinsam genutzt werden.
  • ein Hintergrundtask, der mit Task.Run() gestartet wird

Sie können im Grunde jede dieser Aktionen in eine Hintergrundaufgabe auslagern, die IHostedService implementiert.

Sie können WebHost- oder Host-Klassen eine oder mehrere IHostedServices-Schnittstellen hinzufügen, indem Sie sie über die AddHostedService-Erweiterungsmethode in einer WebHost-Klasse von ASP.NET Core (oder in einer Host-Klasse in .NET Core 2.1 und höher) registrieren. Grundsätzlich müssen Sie die gehosteten Dienste beim Anwendungsstart in Program.cs registrieren.

//Other DI registrations;

// Register Hosted Services
builder.Services.AddHostedService<GracePeriodManagerService>();
builder.Services.AddHostedService<MyHostedServiceB>();
builder.Services.AddHostedService<MyHostedServiceC>();
//...

Der Code für den gehosteten Dienst GracePeriodManagerService entspricht dem Code aus dem Microservice für Bestellungen in eShopOnContainers. Bei den anderen beiden Diensten handelt es sich hingegen nur um zwei zusätzliche Beispiele.

Die Ausführung des IHostedService-Hintergrundtasks wird mit der Lebensdauer der Anwendung (also des Hosts oder des Microservices) koordiniert. Die Tasks werden registriert, wenn die Anwendung gestartet und eine ordnungsgemäße Aktion ausgeführt oder wenn die Anwendung beendet wird und dabei Ressourcen bereinigt werden.

Ohne die Nutzung von IHostedService ließe sich ein Hintergrundthread erstellen, in dem ein beliebiger Task ausgeführt werden könnte. Zum Beendigungszeitpunkt der Anwendung würde der Thread dann einfach beendet werden, und ordnungsgemäße Aktionen zur Bereinigung von Ressourcen könnten nicht ausgeführt werden.

Die IHostedService-Schnittstelle

Beim Registrieren einer IHostedService-Schnittstelle ruft .NET die Methoden StartAsync() und StopAsync() des Typs IHostedService beim Anwendungsstart und -stopp auf. Weitere Informationen finden Sie unter Schnittstelle „IHostedService“.

Wie Sie sich vorstellen können, können Sie mehrere Implementierungen von IHostedService erstellen und jede davon in Program.cs registrieren, wie zuvor gezeigt. Alle gehosteten Dienste werden zusammen mit der Anwendung und dem Microservice gestartet und beendet.

Als Entwickler sind Sie dafür verantwortlich, die Beendigungsaktion Ihrer Dienste zu verarbeiten, wenn die StopAsync()-Methode vom Host ausgelöst wird.

Implementieren von IHostedService mit einer eigenen Klasse für gehostete Dienste, die von der Basisklasse BackgroundService abgeleitet wird

Sie haben die Möglichkeit, eine eigene Klasse für gehostete Dienste neu zu erstellen und IHostedService zu implementieren, was im Fall von .NET Core 2.0 und höher sogar unumgänglich ist.

Da allerdings die meisten Hintergrundtasks ähnliche Anforderungen an die Verwaltung von Abbruchtoken und an weitere typische Vorgänge stellen, gibt es hierfür die praktische abstrakte Basisklasse BackgroundService (verfügbar ab NET Core 2.1), von der eigene Klassen abgeleitet werden können.

Diese Klasse fasst die wesentlichen Aufgaben zusammen, die zum Einrichten der Hintergrundtasks erforderlich sind.

Im folgenden Codeausschnitt ist die in .NET implementierte abstrakte Basisklasse BackgroundService zu sehen.

// Copyright (c) .NET Foundation. Licensed under the Apache License, Version 2.0.
/// <summary>
/// Base class for implementing a long running <see cref="IHostedService"/>.
/// </summary>
public abstract class BackgroundService : IHostedService, IDisposable
{
    private Task _executingTask;
    private readonly CancellationTokenSource _stoppingCts =
                                                   new CancellationTokenSource();

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        // Store the task we're executing
        _executingTask = ExecuteAsync(_stoppingCts.Token);

        // If the task is completed then return it,
        // this will bubble cancellation and failure to the caller
        if (_executingTask.IsCompleted)
        {
            return _executingTask;
        }

        // Otherwise it's running
        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        // Stop called without start
        if (_executingTask == null)
        {
            return;
        }

        try
        {
            // Signal cancellation to the executing method
            _stoppingCts.Cancel();
        }
        finally
        {
            // Wait until the task completes or the stop token triggers
            await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite,
                                                          cancellationToken));
        }

    }

    public virtual void Dispose()
    {
        _stoppingCts.Cancel();
    }
}

Wenn Sie eine Klasse aus der oben gezeigten abstrakten Basisklasse ableiten, müssen Sie dank der vererbten Implementierung nur die ExecuteAsync()-Methode in Ihrer eigenen Klasse für gehostete Dienste implementieren. Dies wird im folgenden vereinfachten Codeausschnitt aus eShopOnContainers demonstriert, in dem eine Datenbank abgerufen und Integrationsereignisse bei Bedarf im Ereignisbus veröffentlicht werden.

public class GracePeriodManagerService : BackgroundService
{
    private readonly ILogger<GracePeriodManagerService> _logger;
    private readonly OrderingBackgroundSettings _settings;

    private readonly IEventBus _eventBus;

    public GracePeriodManagerService(IOptions<OrderingBackgroundSettings> settings,
                                     IEventBus eventBus,
                                     ILogger<GracePeriodManagerService> logger)
    {
        // Constructor's parameters validations...
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogDebug($"GracePeriodManagerService is starting.");

        stoppingToken.Register(() =>
            _logger.LogDebug($" GracePeriod background task is stopping."));

        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogDebug($"GracePeriod task doing background work.");

            // This eShopOnContainers method is querying a database table
            // and publishing events into the Event Bus (RabbitMQ / ServiceBus)
            CheckConfirmedGracePeriodOrders();

            try {
                    await Task.Delay(_settings.CheckUpdateTime, stoppingToken);
                }
            catch (TaskCanceledException exception) {
                    _logger.LogCritical(exception, "TaskCanceledException Error", exception.Message);
                }
        }

        _logger.LogDebug($"GracePeriod background task is stopping.");
    }

    .../...
}

Im Fall von eShopOnContainers wird eine Anwendungsmethode ausgeführt, durch die eine Datenbanktabelle abgefragt wird. Diese sucht nach Bestellungen mit einem bestimmten Zustand. Beim Übernehmen von Änderungen werden Integrationsereignisse über den Ereignisbus veröffentlicht (im Hintergrund kann RabbitMQ oder Azure Service Bus verwendet werden).

Sie könnten allerdings auch andere Geschäftshintergrundtasks ausführen.

Standardmäßig wird für das Abbruchtoken ein Zeitlimit von 5 Sekunden festgelegt, obwohl der Wert beim Erstellen von WebHost mithilfe der Erweiterung UseShutdownTimeout von IWebHostBuilder geändert werden kann. Der Dienst muss also innerhalb von fünf Sekunden beendet werden, da er ansonsten sofort beendet wird.

Im Folgenden Codeausschnitt wird dieses Zeitlimit geändert und auf 10 Sekunden festgelegt.

WebHost.CreateDefaultBuilder(args)
    .UseShutdownTimeout(TimeSpan.FromSeconds(10))
    ...

Zusammenfassendes Klassendiagramm

In der folgenden Abbildung werden zusammenfassend die Klassen und Schnittstellen dargestellt, die an der Implementierung von IHostedService beteiligt sind.

Diagram showing that IWebHost and IHost can host many services.

Abbildung 6-27. Klassendiagramm mit mehreren Klassen und Schnittstellen, die mit IHostedService in Verbindung stehen

Klassendiagramm: IWebHost und IHost können viele Dienste hosten, die von BackgroundService erben, der IHostedService implementiert.

Überlegungen zur Bereitstellung und wesentliche Erkenntnisse

Sie sollten beachten, dass die konkrete Bereitstellung der ASP.NET Core-Klasse WebHost oder der .NET Core-Klasse Host sich auf Ihre finale Lösung auswirken kann. Wenn Sie WebHost beispielsweise auf IIS oder in der regulären Azure App Service-Lösung bereitstellen, kann der Host durch den Neustart eines Anwendungspools heruntergefahren werden. Wenn Sie den Host jedoch als Container in einem Orchestrator wie Kubernetes bereitstellen, können Sie die zugesicherte Anzahl der aktiven Hostinstanzen anpassen. Darüber hinaus ist es empfehlenswert, sich mit anderen cloudbasierten Lösungen wie Azure Functions zu befassen, die speziell für derartige Szenarios entworfen wurden. Wenn Sie möchten, dass der Dienst permanent ausgeführt und auf einer Windows Server-Instanz bereitgestellt wird, verwenden Sie einen Windows-Dienst.

Auch wenn eine WebHost-Instanz in einem Anwendungspool bereitgestellt würde, müssten andere Szenarios wie das Leeren oder nochmalige Auffüllen des speicherinternen Anwendungscaches berücksichtigt werden.

Die IHostedService-Schnittstelle bietet eine einfache Möglichkeit, Hintergrundtasks in einer ASP.NET Core-Webanwendung ( ab .NET 2.0 Core) oder in einem Prozess oder Host zu starten (ab .NET Core 2.1 mit IHost). Ihr Hauptvorteil besteht darin, dass Sie bei einem ordnungsgemäßen Abbruch den Code Ihrer Hintergrundaufgaben bereinigen können, wenn der Host heruntergefahren wird.

Zusätzliche Ressourcen