# Hintergrunddienste im ASP.NET Core - Teil 1: Die Ansätze

<!--category-- ASP.NET Core, IHostedService, BackgroundService, Hangfire -->
<datetime class="hidden">2025-11-27T09:00</datetime>

Jede moderne Web-Anwendung hat Arbeit, die eine HTTP-Anfrage nicht blockieren sollte – E-Mails aussenden, Dateien verarbeiten, mit externen Diensten synchronisieren, geplante Wartung durchführen. ASP.NET Core bietet mehrere Ansätze für die Bearbeitung dieser Hintergrundarbeit, von einfachen `IHostedService` Implementierungen zu ausgeklügelten Frameworks wie Hangfire. In diesem ersten Teil werden wir die grundlegenden Muster erforschen und wann wir jedes einzelne verwenden.

# Einleitung

Confession; I like Background Services a LOT, diese Website (eine BLOG-Website!) hat mehr als eine halbe Dutzend von ihnen tun kariöse Hintergrundaufgaben, aber wie alles, was sie haben einige ODDITIES und Praktiken, die Ihren Gebrauch von ihnen viel angenehmer machen.
Hintergrunddienste sind die ungesungenen Helden moderner Web-Anwendungen. Während Ihre Controller HTTP-Anfragen im Vordergrund bearbeiten, verarbeiten Hintergrunddienste leise Warteschlangen-E-Mails, Indexinhalte für die Suche, überprüfen externe APIs, bereinigen temporäre Dateien und behandeln unzählige andere Aufgaben, die sonst Ihre Anfrage-Pipeline blockieren würden.

In dieser zweiteiligen Serie werden wir die verschiedenen Ansätze zur Implementierung von Hintergrunddiensten in ASP.NET Core aus dem integrierten `IHostedService` und `BackgroundService` Abstraktionen zu anspruchsvolleren Lösungen wie Hangfire. In Teil 1 werden wir die grundlegenden Ansätze und deren Eigenschaften untersuchen. [Teil 2](/blog/background-services-in-aspnetcore-part2)Wir tauchen in Real-World-Implementierungen von einer Produktionscodebase ein.

> **Wichtig:** Wir werden dem Lebenszyklusmanagement besondere Aufmerksamkeit widmen – vor allem den oft überblickten `StopAsync` Methode, wo viele Entwickler auf kryptische Ausnahmen stoßen, wenn ihre Anwendungen heruntergefahren werden.

[TOC]

# Warum Hintergrunddienste?

Bevor wir in das "Wie" tauchen, lassen Sie uns kurz das "Warum" betrachten. Hintergrunddienste lassen Sie:

1. **Langsamer Betrieb ausladen** - Lassen Sie Benutzer nicht warten, während Sie E-Mails senden oder PDFs generieren
2. **wiederkehrende Aufgaben planen** - Bereinigen Sie die alten Platten jeden Abend um 2 Uhr
3. **Warteschlangen verarbeiten** - Verwalten von Nachrichten von Kanälen oder Nachrichten-Broker
4. **Externen Zustand überwachen** - Poll APIs oder Dateisysteme für Änderungen ansehen
5. **Komplexe Workflows koordinieren** - Mehrschritt-Prozesse verwalten, die Minuten oder Stunden umfassen

ASP.NET Core bietet verschiedene Ansätze für die Implementierung dieser Dienste, jeweils mit unterschiedlichen Kompromissen.

# Der historische Kontext: Warum Hintergrunddienste jetzt machbar sind

In den „alten Tagen" (vor 2010) galt die Durchführung von Hintergrundarbeiten in Ihrer Web-Anwendung im Allgemeinen als schlechte Idee. Die herkömmliche Weisheit war: „Web-Server bearbeiten Web-Anfragen. Hintergrundarbeiten gehören auf einem separaten Server."

Das war nicht nur Fracht-Kult-Weisheit – es basierte auf realen technischen Einschränkungen:

## Die Ein-Kern-Ära

Frühe Web-Server (und ehrlich gesagt jetzt, 'billige' Azure-Dienste) lief in der Regel auf **Single-Core- oder Dual-Core-CPUs**. Wenn Sie eine CPU-intensive Hintergrundaufgabe ausgeführt, es direkt mit Web-Anfragen für den gleichen Kern konkurriert:

```
Single Core (2005):
┌─────────────────────┐
│  Background Task    │  ← Uses 80% CPU
│  (80% of core)      │
├─────────────────────┤
│  Web Requests       │  ← Only 20% left!
│  (20% of core)      │  ← Slow responses
└─────────────────────┘
```

Ergebnis: Ihre Website wurde träge, als die Hintergrundarbeit einsetzte.

## Fadenpool-Verhungerung

Klassisches ASP.NET verwendet **Thread-per-Request**. Der Thread-Pool war relativ klein (25-100 Threads typischerweise), und Hintergrundaufgaben würden Threads stehlen, die Web-Anfragen bearbeiten sollten:

```csharp
// Classic ASP.NET (2008)
ThreadPool.QueueUserWorkItem(_ =>
{
    // This steals a thread from the pool!
    ProcessLongRunningTask();
});

// Meanwhile, web requests are queued waiting for threads
// HTTP 503 Service Unavailable
```

## IIS-Anwendungspool-Recycling

IIS würde Anwendung Pools aggressiv recyceln (neu starten Sie Ihre App), basierend auf Speicherlimits, Request-Counts oder Zeitplänen. Hintergrundarbeit würde Mitte der Operation getötet werden:

```
00:00 - Background import starts (2 hour task)
02:00 - IIS recycles app pool (scheduled)
      - Background task killed
      - Work lost, must start again
```

## Begrenzter Async/Await-Support

Vor .NET 4.5 (2012) war die async-Programmierung (relativ) schmerzhaft. Hintergrundaufgaben blockierten Threads oft unnötig:

```csharp
// Pre-async (2008)
void ProcessEmails()
{
    foreach (var email in GetEmails())
    {
        smtp.Send(email);  // Blocks thread for 500ms per email
    }
}
// 100 emails = 50 seconds of blocked thread time
```

## Was sich verändert hat: Die Neuzeit

Die heutige Landschaft ist dramatisch anders:

### 1. Multi-Core ist erschwinglich

Die Wirtschaft ist umgedreht. Cloud-VMs mit mehreren Kernen sind preiswert, und blanke Metall-Server sind überraschend billig. Dieser Blog läuft auf einem dedizierten 8-Core-Server, der weniger kostet als ein vergleichbarer Azure VM – und ich bekomme all diese Kerne für mich, keine lauten Nachbarn. Eine Hintergrundaufgabe auf einem Kern beeinflusst Web-Anfragen auf anderen Kernen nicht signifikant:

```
8-Core Server (2024):
Core 1: ████████████████████ Web Requests
Core 2: ████████████████████ Web Requests
Core 3: ████████████████████ Web Requests
Core 4: ████████████████████ Web Requests
Core 5: ████████████████████ Background Task ← Isolated
Core 6: ████████████████████ Background Task
Core 7: ████████████████████ Background Task
Core 8: ████████████████████ Background Task
```

### 2. Async/Awai Everywhere

Moderne .NET macht die async-Programmierung trivial. Hintergrundaufgaben können auf I/O warten, ohne Threads zu blockieren:

```csharp
// Modern async (2024)
async Task ProcessEmailsAsync(CancellationToken ct)
{
    await foreach (var email in GetEmailsAsync(ct))
    {
        await smtp.SendAsync(email, ct);  // Doesn't block thread!
    }
}
// 100 emails processed efficiently, thread returns to pool during I/O
```

### 3. Bessere Prozess-Hosting

- [**Schwanzlutscher**](https://www.docker.com/) - Hintergrunddienste in Containern werden nicht willkürlich recycelt
- [**Kubernete**](https://kubernetes.io/) - Richtige anmutige Herunterfahren Handhabung mit `SIGTERM`
- [**Systemd**](https://systemd.io/) - Linux-Dienste, die zuverlässig neu starten
- **Windows-Dienste** - Richtiges langlaufendes Prozessmodell

### 4. Kanäle und moderne Primitive

.NET hat jetzt erstklassige Unterstützung für die gleichzeitige Programmierung mit [`System.Threading.Channels`](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels):

```csharp
// System.Threading.Channels
var channel = Channel.CreateBounded<Email>(100);

// Producer (web request)
await channel.Writer.WriteAsync(email);  // Fast, non-blocking

// Consumer (background service)
await foreach (var email in channel.Reader.ReadAllAsync())
{
    await ProcessAsync(email);  // Efficient, async
}
```

### 5. Ressourcengrenzen und C-Gruppen

Moderne Container-Orchester lassen Sie **Ressourcennutzung begrenzen**:

```yaml
# Kubernetes resource limits
resources:
  limits:
    cpu: "500m"        # Background task can't use more than 0.5 CPU
    memory: "512Mi"    # Or more than 512 MB RAM
```

Dies bedeutet, dass eine auslaufende Hintergrundaufgabe nicht Ihre Web-Ebene verhungern kann.

Die Frage ist nicht mehr "Können wir Hintergrunddienste in unserer Web-App ausführen?", sondern "**Sollen wir?**" Wir werden diese Entscheidung im Abschnitt "Wann nicht zu verwenden Hintergrunddienste" später untersuchen.

# Die eingebauten Optionen

## IHostedService: Die Stiftung

Jeder Hintergrunddienst in ASP.NET Core implementiert im Kern [`IHostedService`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice). Diese Schnittstelle ist sehr einfach:

```csharp
public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}
```

Das war's. Zwei Methoden. `StartAsync` wird aufgerufen, wenn Ihre Bewerbung beginnt, und `StopAsync` wenn es abschaltet.

Registrieren Sie Ihren Service in `Program.cs`:

```csharp
builder.Services.AddHostedService<MyBackgroundService>();
```

Hier ist der Lebenszyklus visualisiert:

```mermaid
graph LR
    A[Application Starts] --> B[StartAsync Called]
    B --> C[Service Running]
    C --> D[Application Shutting Down]
    D --> E[StopAsync Called]
    E --> F[Application Stopped]

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style C stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style F stroke:#dc2626,stroke-width:3px,color:#ef4444
```

### StartAsync: Synchroner vs. Asynchroner Start

Eine kritische Entscheidung bei der Umsetzung `IHostedService` ist, ob Sie `StartAsync` Die Methode sollte sofort blockieren oder zurückkehren.

**Synchroner (Blockieren) Start:**

```csharp
public class BlockingStartService : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // This blocks application startup until complete
        await InitializeDatabaseAsync(cancellationToken);
        await LoadConfigurationAsync(cancellationToken);

        // Only now will the application continue starting
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;
}
```

**Asynchroner (nicht blockierender) Beginn:**

```csharp
public class NonBlockingStartService : IHostedService
{
    private Task _backgroundTask;
    private readonly CancellationTokenSource _cts = new();

    public Task StartAsync(CancellationToken cancellationToken)
    {
        // Start background work but return immediately
        _backgroundTask = Task.Run(async () =>
        {
            // Give other services time to initialise
            await Task.Delay(TimeSpan.FromSeconds(5), _cts.Token);
            await DoLongRunningWorkAsync(_cts.Token);
        }, _cts.Token);

        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _cts.Cancel();
        await _backgroundTask; // Wait for completion
    }
}
```

**Wann wird jeder Ansatz angewendet:**

- **Blockieren:** Wenn der Dienst die Initialisierung abschließen muss, bevor die Anwendung Anfragen bearbeiten kann (z.B. Laden kritischer Konfiguration, Erwärmung von Caches)
- **Nicht blockieren:** Wenn der Dienst im Hintergrund initialisiert werden kann, während andere Dienste beginnen (z.B. Indexierung vorhandener Inhalte, Synchronisierung mit externen Diensten)

### StopAsync: Der häufige Pitfall

Hier wird es interessant – und wo viele Entwickler auf Probleme stoßen. Wenn Ihre Anwendung heruntergefahren wird, ruft ASP.NET Core an `StopAsync` auf allen gehosteten Diensten. Sie haben ein begrenztes Fenster (Standard 5 Sekunden), um anmutig aufzuräumen. Sie können dies in verlängern `Program.cs`:

```csharp
builder.Services.Configure<HostOptions>(options =>
{
    options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
```

**Der häufigste Fehler:**

```csharp
public class BrokenService : IHostedService
{
    private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
    private Task _processingTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _processingTask = ProcessMessagesAsync();
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // WRONG: The channel is still open, ProcessMessagesAsync
        // will hang on WaitToReadAsync forever!
        return Task.CompletedTask;
    }

    private async Task ProcessMessagesAsync()
    {
        // This will never exit because the channel is never completed
        await foreach (var message in _channel.Reader.ReadAllAsync())
        {
            await ProcessAsync(message);
        }
    }
}
```

Wenn Sie diesen Service ausführen und Ihre Anwendung beenden, sehen Sie Fehler wie:

```
Unable to cast object of type 'TaskCompletionSource`1[System.Threading.Tasks.VoidTaskResult]' to type 'System.Threading.Tasks.Task'
```

Oder die Anwendung hängt einfach für die Shutdown-Timeout-Periode, bevor sie gewaltsam beendet wird.

**Der richtige Ansatz:**

```csharp
public class CorrectService : IHostedService
{
    private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
    private readonly CancellationTokenSource _cts = new();
    private Task _processingTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _processingTask = ProcessMessagesAsync(_cts.Token);
        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        // CORRECT: Signal cancellation and complete the channel
        await _cts.CancelAsync();
        _channel.Writer.Complete();

        try
        {
            // Wait for processing to finish or for the shutdown timeout
            await Task.WhenAny(_processingTask,
                Task.Delay(Timeout.Infinite, cancellationToken));
        }
        catch (OperationCanceledException)
        {
            // Expected when shutdown timeout is reached
        }
    }

    private async Task ProcessMessagesAsync(CancellationToken token)
    {
        await foreach (var message in _channel.Reader.ReadAllAsync(token))
        {
            try
            {
                await ProcessAsync(message);
            }
            catch (OperationCanceledException)
            {
                // Shutdown requested, exit gracefully
                break;
            }
        }
    }
}
```

**Die wichtigsten Punkte für die korrekte StopAsync-Implementierung:**

1. **Signalannullierung** - Verwenden Sie eine `CancellationTokenSource` und stornieren
2. **Vollständige Kanäle** - Wenn Sie Kanäle benutzen, rufen Sie an. `Writer.Complete()`
3. **Warten Sie auf Hintergrundaufgaben** - Verwendung `Task.WhenAny` mit dem Abschaltungs-Token
4. **Manage OperationCanceledException** - Dies wird erwartet und sollte gefangen werden
5. **Wirf keine Ausnahmen.** - Ausnahmen in `StopAsync` kann unvorhersehbares Verhalten verursachen

## HintergrundService: Die komfortable Basisklasse

Schreiben `IHostedService` Implementationen können sich wiederholen. Sie benötigen immer eine Hintergrundaufgabe, eine Annullierungs-Token-Quelle und das gleiche Aufräummuster. [`BackgroundService`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice) behandelt diese Heizplatte für Sie:

```csharp
public abstract class BackgroundService : IHostedService, IDisposable
{
    private Task _executeTask;
    private CancellationTokenSource _stoppingCts;

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        _executeTask = ExecuteAsync(_stoppingCts.Token);
        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        if (_executeTask == null) return;

        try
        {
            _stoppingCts.Cancel();
        }
        finally
        {
            await Task.WhenAny(_executeTask, Task.Delay(Timeout.Infinite, cancellationToken));
        }
    }

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

Sie einfach nur implementieren `ExecuteAsync` und lassen Sie die Basisklasse mit den Sanitären umgehen:

```csharp
public class SimpleBackgroundService : BackgroundService
{
    private readonly ILogger<SimpleBackgroundService> _logger;

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

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Service starting");

        // Wait for app to finish starting
        await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await DoWorkAsync(stoppingToken);
                await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
            }
            catch (OperationCanceledException)
            {
                // Shutdown requested
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in background service");
            }
        }

        _logger.LogInformation("Service stopping");
    }

    private async Task DoWorkAsync(CancellationToken token)
    {
        _logger.LogInformation("Doing work...");
        // Your actual work here
        await Task.Delay(1000, token);
    }
}
```

### Wann BackgroundService vs IHostedService verwendet werden soll

**Verwendung `BackgroundService` wenn:**

- Sie brauchen eine langlaufende Hintergrundschleife
- Sie wollen eine einfache periodische Ausführung
- Du brauchst keine Kontrolle. `StartAsync`/`StopAsync` Zeitpunkt

**Verwendung `IHostedService` wenn:**

- Sie müssen genau kontrollieren, was passiert in `StartAsync` vs Hintergrundarbeit
- Du stellst eher Event-Handler oder -Beobachter als eine kontinuierliche Schleife ein
- Sie müssen sich während des Starts mit anderen Diensten abstimmen

# Fortgeschritten: Startup-Koordinierung

Manchmal benötigen Sie Dienste, um aufeinander zu warten. Zum Beispiel möchten Sie, dass Ihr semantischer Suchindexer wartet, bis Ihr Markdown-Dateiprozessor seine anfängliche Last beendet hat.

Hier ist ein Muster für die Koordination von Service-Startups:

```csharp
public interface IStartupCoordinator
{
    void RegisterService(string serviceName);
    void SignalReady(string serviceName);
    bool IsServiceReady(string serviceName);
    Task WaitForServiceAsync(string serviceName, CancellationToken cancellationToken = default);
    Task WaitForAllServicesAsync(CancellationToken cancellationToken = default);
}

public class StartupCoordinator : IStartupCoordinator
{
    private readonly ConcurrentDictionary<string, TaskCompletionSource> _services = new();
    private readonly ILogger<StartupCoordinator> _logger;

    public void RegisterService(string serviceName)
    {
        _services.TryAdd(serviceName, new TaskCompletionSource());
    }

    public void SignalReady(string serviceName)
    {
        if (_services.TryGetValue(serviceName, out var tcs))
        {
            tcs.TrySetResult();
            _logger.LogInformation("{Service} is ready", serviceName);
        }
    }

    public async Task WaitForServiceAsync(string serviceName, CancellationToken ct = default)
    {
        if (_services.TryGetValue(serviceName, out var tcs))
        {
            await tcs.Task.WaitAsync(ct);
        }
    }

    public async Task WaitForAllServicesAsync(CancellationToken ct = default)
    {
        await Task.WhenAll(_services.Values.Select(tcs => tcs.Task)).WaitAsync(ct);
    }
}
```

Nutzung in einem Service:

```csharp
public class DependentService : IHostedService
{
    private readonly IStartupCoordinator _coordinator;
    private readonly ILogger<DependentService> _logger;

    public DependentService(
        IStartupCoordinator coordinator,
        ILogger<DependentService> logger)
    {
        _coordinator = coordinator;
        _logger = logger;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // Wait for another service to be ready
        await _coordinator.WaitForServiceAsync("MarkdownProcessor", cancellationToken);

        _logger.LogInformation("Dependencies ready, starting work");

        // Do your work...

        // Signal you're ready for services that depend on you
        _coordinator.SignalReady("DependentService");
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;
}
```

Dieses Muster wird besonders nützlich, wenn Sie mehrere Hintergrunddienste mit Interdependenzen haben.

# Verteilte Koordination mit Redis

Der Startkoordinator arbeitet innerhalb einer einzelnen Anwendungsinstanz. Aber was passiert, wenn Sie auf mehrere Instanzen skalieren? Sie wollen nicht, dass alle drei Instanzen gleichzeitig die gleiche geplante Aufgabe ausführen.

[Wiedereinsetzen](https://redis.io/) bietet eine einfache Lösung: Verwenden Sie Flags (Schlüssel), um zu koordinieren, wer was tut.

## Einfache Leader-Wahl

```csharp
public class DistributedBackgroundService : BackgroundService
{
    private readonly IConnectionMultiplexer _redis;
    private readonly ILogger<DistributedBackgroundService> _logger;
    private readonly string _instanceId = Guid.NewGuid().ToString();
    private const string LeaderKey = "background:newsletter:leader";

    public DistributedBackgroundService(
        IConnectionMultiplexer redis,
        ILogger<DistributedBackgroundService> logger)
    {
        _redis = redis;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var db = _redis.GetDatabase();

        while (!stoppingToken.IsCancellationRequested)
        {
            // Try to become the leader (SET NX with expiry)
            var acquired = await db.StringSetAsync(
                LeaderKey,
                _instanceId,
                TimeSpan.FromMinutes(5),
                When.NotExists);

            if (acquired)
            {
                _logger.LogInformation("This instance is the leader, running task");

                try
                {
                    await DoScheduledWorkAsync(stoppingToken);
                }
                finally
                {
                    // Release leadership
                    await db.KeyDeleteAsync(LeaderKey);
                }
            }
            else
            {
                var leader = await db.StringGetAsync(LeaderKey);
                _logger.LogDebug("Another instance ({Leader}) is the leader", leader);
            }

            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}
```

## Verteilte Verriegelung für kritische Abschnitte

Für Aufgaben, die nicht gleichzeitig über Instanzen laufen dürfen:

```csharp
public async Task ProcessWithLockAsync(CancellationToken cancellationToken)
{
    var db = _redis.GetDatabase();
    var lockKey = "locks:critical-task";
    var lockValue = _instanceId;

    // Try to acquire lock
    if (await db.LockTakeAsync(lockKey, lockValue, TimeSpan.FromMinutes(10)))
    {
        try
        {
            _logger.LogInformation("Lock acquired, processing...");
            await DoCriticalWorkAsync(cancellationToken);
        }
        finally
        {
            await db.LockReleaseAsync(lockKey, lockValue);
        }
    }
    else
    {
        _logger.LogDebug("Could not acquire lock, another instance is processing");
    }
}
```

## Wann die verteilte Koordinierung genutzt werden soll

- **Geplante Aufgaben** - Nur eine Instanz sollte den täglichen Newsletter senden
- **Bearbeitung der Warteschlange bei Bestellung** - Stellen Sie sicher, dass Nachrichten in der Reihenfolge verarbeitet werden
- **Ressourcenintensive Operationen** - Verhindern, dass mehrere Instanzen eine externe API überwältigen
- **Datenbankmigrationen** - Nur eine Instanz sollte Migrationen beim Starten ausführen

Für komplexere Szenarien (mehrstufige Jobs, zuverlässige Terminplanung über Neustarts) sollten Sie Hangfire beachten, der mit seinem Datenbank-Backend automatisch verteilte Sperrungen verarbeitet.

# Wenn Sie Hintergrunddienste NICHT verwenden

Bevor wir in anspruchsvollere Werkzeuge wie Hangfire tauchen, lassen Sie uns darüber reden, wenn Sie *sollte nicht* verwenden Sie Hintergrunddienste in Ihrer Haupt-Web-Anwendung.

## Signiert, dass Sie ein separates Projekt aufteilen sollten

Hintergrunddienste, die in Ihrer Webanwendung laufen, teilen Ressourcen mit Ihrer HTTP-Request-Pipeline. Dies kann zu Problemen führen:

### 1. Ressourcen-Streitbeilegung

**Problemstellung:** Ihr Hintergrundservice verbraucht signifikante CPU-, Speicher- oder Datenbankverbindungen.

```csharp
// This will starve your web application
public class VideoTranscodingService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var video = await _queue.DequeueAsync();
            // This uses 100% of 4 CPU cores for 5 minutes
            await TranscodeVideoAsync(video);
        }
    }
}
```

**Wenn Web-Anfragen während der Transcodierung ankommen, sind sie langsam, weil die CPU beschäftigt ist.**

**Lösung:** Wechseln Sie zu einem separaten Arbeiterdienst:

```bash
# Your solution structure
/YourApp.Web          # ASP.NET Core web app - no background services
/YourApp.Worker       # .NET Worker Service - handles background work
/YourApp.Shared       # Shared models, interfaces
```

### 2. Verschiedene Anforderungen an die Skalierung

**Problemstellung:** Ihre Hintergrundarbeit braucht eine andere Skalierung als Ihre Web-Ebene.

- **Web-Ebene:** Skalierung für HTTP-Verkehr (vielleicht 10 Instanzen während des Tages, 2 in der Nacht)
- **Hintergrundebene:** Skala für Warteschlangentiefe (vielleicht benötigen 1 Instanz normalerweise, 20 bei der Verarbeitung einer Charge)

Wenn sie im selben Prozess sind, können Sie sie nicht unabhängig skalieren.

**Beispielszenario:**

```
09:00 - High web traffic, low background work → Need 10 web instances, 1 worker
14:00 - Newsletter time! Low web traffic, high background work → Need 2 web instances, 20 workers
```

Hintergrunddienste in Ihrer Web-App setzen bedeutet, dass Sie 20 Web-Instanzen ausführen müssen, nur um den Newsletter abzuwickeln und Ressourcen zu verschwenden.

### 3. Einsatz Unabhängigkeit

**Problemstellung:** Sie möchten Web-Änderungen bereitstellen, ohne Hintergrunddienste neu zu starten (oder umgekehrt).

```csharp
// If this is in your web app, deploying a CSS change restarts the service
public class LongRunningImportService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // This import takes 2 hours
        await ImportMillionsOfRecordsAsync(stoppingToken);
    }
}
```

Jede Bereitstellung unterbricht den Import. Verschieben Sie ihn zu einem separaten Worker-Dienst, den Sie unabhängig bereitstellen.

### 4. Verschiedene Fehler-Domains

**Problemstellung:** Ein Fehler in Ihrem Hintergrunddienst stürzt die gesamte Webanwendung ab.

```csharp
// This null reference exception crashes your web app
public class BuggyBackgroundService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        string value = null;
        // Unhandled exception - takes down the whole app
        await ProcessAsync(value.Length);
    }
}
```

Wenn Hintergrundarbeit in einem separaten Prozess ist, kann es abstürzen und neu starten, ohne Web-Anfragen zu beeinflussen.

## Wie man Hintergrunddienste beeinflusst

Wenn Sie sich entscheiden zu teilen, hier ist die empfohlene Architektur:

### Option 1: .NET Worker Service

Erstellen Sie ein neues Projekt mit der Vorlage Worker Service:

```bash
dotnet new worker -n YourApp.Worker
```

Struktur:

```
/YourApp.Worker
  /Services
    VideoTranscodingService.cs
    EmailSenderService.cs
  /Program.cs
  /appsettings.json
```

Programm.cs:

```csharp
var builder = Host.CreateApplicationBuilder(args);

// Register your background services
builder.Services.AddHostedService<VideoTranscodingService>();
builder.Services.AddHostedService<EmailSenderService>();

// Share configuration with web app
builder.Services.Configure<VideoConfig>(
    builder.Configuration.GetSection("Video"));

// Share database context
builder.Services.AddDbContext<YourDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

var host = builder.Build();
host.Run();
```

Getrennt einsetzen:

```bash
# Web app on ports 80/443
/YourApp.Web → web-server-1, web-server-2, web-server-3

# Worker service doesn't listen on any port
/YourApp.Worker → worker-server-1, worker-server-2
```

### Option 2: Getrenntes Projekt mit geteilter Warteschlange

Verwenden Sie eine Nachricht Warteschlange, um Web und Arbeiter zu entkoppeln:

```mermaid
graph LR
    A[Web App] --> B[Message Queue]
    B --> C[Worker 1]
    B --> D[Worker 2]
    B --> E[Worker N]

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style B stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style C stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
    style D stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
    style E stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
```

Web-App Warteschlangen funktionieren:

```csharp
// In your web controller
public class VideoController : ControllerBase
{
    private readonly IMessageQueue _queue;

    [HttpPost("upload")]
    public async Task<IActionResult> Upload(IFormFile video)
    {
        await _storage.SaveAsync(video);

        // Queue for processing - don't process in web app
        await _queue.PublishAsync(new VideoTranscodeJob
        {
            VideoId = video.Id,
            Priority = Priority.Normal
        });

        return Accepted(); // Return immediately
    }
}
```

Arbeiter verbraucht aus der Warteschlange:

```csharp
// In your worker service
public class VideoWorker : BackgroundService
{
    private readonly IMessageQueue _queue;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var job in _queue.SubscribeAsync<VideoTranscodeJob>(stoppingToken))
        {
            await TranscodeAsync(job);
        }
    }
}
```

**Beliebte Nachrichten Warteschlange-Optionen:**

- [**KaninchenMQ**](https://www.rabbitmq.com/) - Die beliebtesten, funktionsreichen
- [**Azure Service Bus**](https://azure.microsoft.com/en-us/products/service-bus/) - Wenn du auf Azure bist
- [**AWS-SQS**](https://aws.amazon.com/sqs/) - Wenn Sie auf AWS sind
- [**Streams wiederholen**](https://redis.io/docs/data-types/streams/) - Einfacher, gut für kleinere Größen

### Option 3: Mehrfachfacharbeiter

Für komplexe Systeme, aufgeteilt nach Verantwortung:

```
/YourApp.Web              # HTTP requests only
/YourApp.EmailWorker      # Sends emails
/YourApp.VideoWorker      # Transcodes videos
/YourApp.ReportWorker     # Generates reports
/YourApp.Scheduler        # Runs scheduled jobs (Hangfire)
```

Jeder Arbeitnehmer kann:

- unabhängig skalieren
- selbstständig einsetzen
- Verwenden Sie verschiedene Ressourcen (E-Mail-Arbeiter braucht SMTP, Video-Arbeiter braucht GPU)
- Haben Sie unterschiedliche Überwachung und Alarmierung

## Wann Sie Hintergrunddienste in Ihrer Web-App behalten

Trotz der oben genannten, einige Szenarien sind perfekt für In-Prozess-Hintergrund-Services:

### Leichte periodische Aufgaben

```csharp
// Fine to keep in web app
public class CacheWarmingService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _cache.WarmupAsync(); // Quick operation
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}
```

### Ereignishörer

```csharp
// Fine to keep in web app
public class FileWatcherService : IHostedService
{
    // Reacts to events, doesn't consume significant resources
    private FileSystemWatcher _watcher;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _watcher = new FileSystemWatcher("/config");
        _watcher.Changed += OnConfigChanged;
        _watcher.EnableRaisingEvents = true;
        return Task.CompletedTask;
    }
}
```

### Kanalgestützte Warteschlangen (für nicht-kritische Arbeiten)

```csharp
// Fine to keep in web app if work is quick and not critical
public class EmailQueueService : BackgroundService
{
    // Sends emails in background, but each email takes < 1 second
    // If the app restarts, losing a few queued emails is acceptable
}
```

### Koordinierung der Unternehmensgründungen

```csharp
// Fine to keep in web app
public class WarmupService : IHostedService
{
    // Runs once at startup, then does nothing
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await _database.WarmupConnectionPoolAsync();
        await _cache.LoadCriticalDataAsync();
    }
}
```

## Entscheidungsmatrix

Charakteristisch: Halten Sie sich in der Web-App. Bewegen Sie sich zum Worker Service.
|---------------|-----------------|------------------------|
CPU-Nutzung pro Betrieb < 100ms > 1 Sekunde
Speicher pro Betrieb < 10 MB > 100 MB
Periodische Häufigkeit (Minuten/Stunden)
Kritischität, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch
Dauer Sekunden Minuten bis Stunden
Skalen mit dem Web-Verkehr Arbeit Warteschlange Tiefe
Beispiel: Cache-Erwärmung, Config-Reload, Video-Verarbeitung, große Importe

## Real-World Beispiel: Die Blog-Plattform

In der Blog-Plattform, deren Code wir in Teil 2:

**In der Web-App beibehalten:**

- `MarkdownDirectoryWatcherService` - Leichter Dateibeobachter
- `UmamiBackgroundSender` - Schnelle Analyse-Ereignisse
- `EmailSenderHostedService` - Kleines Volumen, nicht kritisch
- `MarkdownReAddPostsService` - Start-only, Konfigurations-gated

**Sollte in den Arbeitsdienst umziehen, wenn die Skala zunimmt:**

- `BrokenLinkCheckerBackgroundService` - Macht viele HTTP-Anfragen
- `SemanticIndexingBackgroundService` - Ruft externe Einbettungs-API auf

**Bereits im eigenen Betrieb:**

- `Mostlylucid.SchedulerService` - Hangfire Dashboard und Newsletter senden

Dies ist ein pragmatischer Ansatz: Starten Sie einfach (in-Prozess), Split, wenn Sie Beweise haben, die Sie benötigen.

# Jenseits der Grundlagen: Hangfire

Während `IHostedService` und `BackgroundService` sind ausgezeichnet für Dienstleistungen, die Sie besitzen und steuern, manchmal brauchen Sie anspruchsvollere Planung. Das ist, wo Bibliotheken wie [Hangfeuer](https://www.hangfire.io/) Kommen Sie rein.

Hangfire bietet:

- **Dauerhafte Job-Warteschlangen** - Jobs überleben Anwendung neu gestartet
- **Wiederkehrende Arbeitsplätze** - Chronische Terminplanung
- **Dashboard UI** - Sehen Sie, was läuft, was fehlgeschlagen ist, versuchen Sie es noch mal.
- **Verteilte Ausführung** - Mehrere Server können die gleiche Auftragswarteschlange bearbeiten
- **Automatische Wiederholungen** - Fehlerhafte Jobs werden automatisch mit exponentiellem Backoff nachgetestet

Hier ist ein einfaches Beispiel:

```csharp
// In Program.cs
builder.Services.AddHangfire(config => config
    .UsePostgreSqlStorage(connectionString)
    .UseRecommendedSerializerSettings());

builder.Services.AddHangfireServer();

var app = builder.Build();

// Schedule recurring jobs
app.UseHangfireDashboard();
app.Services.GetRequiredService<IRecurringJobManager>()
    .AddOrUpdate<NewsletterService>(
        "send-daily-newsletter",
        x => x.SendDailyNewsletter(),
        Cron.Daily(17)); // 5 PM every day
```

Ihr Service ist nur eine normale Klasse:

```csharp
public class NewsletterService
{
    private readonly IEmailService _emailService;
    private readonly ISubscriberRepository _subscribers;

    public NewsletterService(
        IEmailService emailService,
        ISubscriberRepository subscribers)
    {
        _emailService = emailService;
        _subscribers = subscribers;
    }

    public async Task SendDailyNewsletter()
    {
        var subscribers = await _subscribers.GetDailySubscribersAsync();

        foreach (var subscriber in subscribers)
        {
            await _emailService.SendNewsletterAsync(subscriber);
        }
    }
}
```

Feuergriffe:

- Sicherstellen, dass der Job zum geplanten Zeitpunkt läuft
- Versuchen, wenn es fehlschlägt
- Verlauf der Ausführung speichern
- Bereitstellung eines Dashboards, um alles zu überwachen

```mermaid
graph TD
    A[Hangfire Server] --> B{Check Schedule}
    B -->|Job Due| C[Dequeue Job]
    C --> D[Execute Job Method]
    D -->|Success| E[Mark Complete]
    D -->|Failure| F[Retry with Backoff]
    F --> G{Max Retries?}
    G -->|No| C
    G -->|Yes| H[Mark Failed]
    E --> I[Update Dashboard]
    H --> I
    I --> B

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style D stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style E stroke:#059669,stroke-width:3px,color:#10b981
    style H stroke:#dc2626,stroke-width:3px,color:#ef4444
```

**Wann wird Hangfire verwendet:**

- Sie brauchen dauerhafte Job Warteschlangen, die Neustarts überleben
- Sie möchten, dass ein Dashboard die Aufträge überwacht und manuell auslöst
- Sie benötigen verteilte Auftragsbearbeitung über mehrere Server
- Sie wollen eingebaute Retry-Logik und Fehlerbehandlung
- Sie benötigen cron-style Terminierung von wiederkehrenden Aufgaben

**Wann Sie mit dem IHostedService/BackgroundService bleiben:**

- Sie brauchen eine gute Kontrolle über den Lebenszyklus
- Ihr Service muss auf Ereignisse in Echtzeit reagieren
- Sie wollen Abhängigkeiten minimieren
- Sie bauen eine einfache periodische Aufgabe, die keine Ausdauer braucht

# Weitere Optionen

Während Hangfire beliebt ist, gibt es noch andere Bibliotheken, die es wert sind:

[**Quarz.NET**](https://www.quartz-scheduler.net/):

- Flexiblere Planung als Hangfire
- Unterstützt Cron-Ausdrücke und kalenderbasierte Terminplanung
- Kann auf mehreren Datenbanken bestehen bleiben
- Komplexere API, aber leistungsstärker

[**MasseÜbergang**](https://masstransit.io/)/[**NServiceBus**](https://particular.net/nservicebus):

- Umsetzbare Implementierungen von Message-Bussen
- Besser für verteilte Systeme und Microservices
- Unterstützung von Sagas (Langlauf-Workflows)
- Steeper-Lernkurve

[**Azure Funktionen**](https://azure.microsoft.com/en-us/products/functions/)/[**AWS Lambda**](https://aws.amazon.com/lambda/):

- Wenn Sie in der Cloud sind, betrachten Sie serverlos
- Bezahlen Sie pro Ausführung, anstatt einen Dienst laufen zu lassen
- Automatische Skalierung
- Einige Latenz Overhead für kalte Starts

# Zusammenfassung

In Teil 1 haben wir die grundlegenden Ansätze für Hintergrunddienste in ASP.NET Core behandelt:

1. **IHostedService** - Das Fundament, maximale Flexibilität
2. **Hintergrunddienst** - Bequeme Basisklasse für Langlaufschleifen
3. **Koordinierung der Unternehmensgründungen** - Die Dienste aufeinander warten zu lassen
4. **Verteilte Koordinierung** - Verwendung von Redis für Multi-Instanz-Szenarien
5. **Hangfeuer** - Wenn Sie dauerhafte Jobs und anspruchsvolle Planung brauchen

Die wichtigsten Lektionen:

- **Vervollständigen Sie immer Kanäle und löschen Sie Token in StopAsync**
- **Entscheiden Sie, ob StartAsync blockiert oder sofort zurückgegeben werden soll**
- **Handle OperationCanceledException anmutig**
- **Verwenden Sie Task.WhenAny mit dem Shutdown-Token, um Shutdown-Timeouts zu respektieren**

Im [Teil 2](/blog/background-services-in-aspnetcore-part2), wir untersuchen Real-Welt-Implementierungen von einer Produktions-Blog-Plattform:

- Dateisystembeobachter, die Markdown-Dateien mit einer Datenbank synchronisieren
- E-Mail-Sender mit Retry-Richtlinien und Leistungsschaltern
- Analysieren Ereignis Warteschlangen, die Batch-Anfragen
- Semantische Suchindexer, die Inhalte asynchron verarbeiten
- Gebrochene Link-Checker, die regelmäßig externe URLs validieren

Diese Beispiele zeigen die Muster aus Teil 1 in Aktion, einschließlich des Startkoordinationsmusters und der ordnungsgemäßen Abschaltung.

# Literaturhinweise

- [Microsoft Docs: Hintergrundaufgaben mit gehosteten Diensten](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services)
- [Hangfire-Dokumentation](https://docs.hangfire.io/)
- [Kanäle in C#](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels)
- [Quartz.NET Dokumentation](https://www.quartz-scheduler.net/)
- [StackExchange.Redis](https://stackexchange.github.io/StackExchange.Redis/) - Redis Client für .NET