Achtergronddiensten in ASP.NET-kern - Deel 1: De benaderingen (Nederlands (Dutch))

Achtergronddiensten in ASP.NET-kern - Deel 1: De benaderingen

Thursday, 27 November 2025

//

24 minute read

Elke moderne webapplicatie heeft werk dat niet moet blokkeren een HTTP-verzoek het verzenden van e-mails, het verwerken van bestanden, synchroniseren met externe diensten, het uitvoeren van geplande onderhoud. ASP.NET Core biedt meerdere benaderingen voor de behandeling van deze achtergrond werk, van eenvoudige IHostedService implementaties naar verfijnde kaders zoals Hangfire. In dit eerste deel, zullen we de fundamentele patronen te verkennen en wanneer te gebruiken elk.

Inleiding

Bekentenis; Ik hou van Achtergrond Services een LOT, deze site (een BLOG site!) heeft meer dan een half dozijn van hen doen carious achtergrond taken, maar net als alles wat ze hebben een aantal ODDITEITEN en praktijken die uw gebruik van hen een stuk aangenamer maken. Achtergronddiensten zijn de unsung helden van moderne webapplicaties. Terwijl uw controllers HTTP-verzoeken op de voorgrond behandelen, verwerken achtergronddiensten stilletjes e-mails in de wachtrij, indexinhoud voor zoekopdrachten, controleren externe API's, verwijderen van tijdelijke bestanden en omgaan met talloze andere taken die anders uw aanvraagpijpleiding zouden blokkeren.

In deze twee-delige serie zullen we de verschillende benaderingen onderzoeken voor het implementeren van background services in ASP.NET Core, vanuit de ingebouwde IHostedService en BackgroundService abstracties naar meer geavanceerde oplossingen zoals Hangfire. In deel 1 zullen we de fundamentele benaderingen en hun kenmerken onderzoeken. Deel 2, we duiken in real-world implementaties vanaf een productie codebase.

Belangrijk: We zullen speciale aandacht besteden aan lifecycle management in het bijzonder de vaak bekeken StopAsync methode, dat is waar veel ontwikkelaars tegenkomen cryptische uitzonderingen wanneer hun toepassingen sluiten.

Waarom Achtergrond Services?

Voordat we in de "hoe" duiken, laten we kort kijken naar de "waarom." Achtergronddiensten laten je:

  1. Uitladen trage bewerkingen - Laat gebruikers niet wachten terwijl u e-mails verstuurt of PDF's aanmaakt
  2. Terugkerende taken plannen - Ruim elke nacht oude dossiers op om 2 uur 's nachts.
  3. Proceswachtrijen - Berichten van kanalen of berichtenmakelaars afhandelen
  4. Externe status monitoren - Poll API's of bekijk bestandssystemen voor wijzigingen
  5. Complexe workflows coördineren - Beheer multi-step processen die minuten of uren bestrijken

ASP.NET Core biedt verschillende benaderingen voor de implementatie van deze diensten, elk met verschillende trade-offs.

De historische context: Waarom achtergronddiensten nu haalbaar zijn

In de "oude dagen" (pre-2010) werd het uitvoeren van achtergrondwerk in uw webapplicatie algemeen beschouwd als een slecht idee. De conventionele wijsheid was: "Webservers behandelen webverzoeken. Achtergrondwerk hoort op een aparte server."

Dit was niet alleen vracht-cult wijsheid het was gebaseerd op echte technische beperkingen:

Het Single Core-tijdperk

Vroege webservers (en eerlijk gezegd nu, 'goedkope' Azure diensten) liepen meestal op single-core of dual-core CPU's. Als je een CPU-intensieve achtergrond taak, het rechtstreeks concurreren met web verzoeken voor dezelfde kern:

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

Resultaat: Uw website werd traag op het moment dat achtergrondwerk in werking trad.

Verhongering van Thread Pool

Classic ASP.NET gebruikt thread-per-request. De thread pool was relatief klein (25-100 threads typisch), en achtergrondtaken zouden threads stelen die webverzoeken moeten behandelen:

// 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 Application Pool Recycling

IIS zou agressief recyclen applicatie pools (herstart uw app) op basis van geheugenlimieten, verzoeken tellen, of tijdschema's. Achtergrond werk zou worden gedood halverwege de operatie:

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

Beperkte ondersteuning voor Async/Await

Vóór .NET 4.5 (2012) was async programmeren (relatief) pijnlijk. Achtergrondtaken blokkeren vaak threads onnodig:

// 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

Wat veranderde: The Modern Era

Het landschap van vandaag is dramatisch anders:

1. Multi-Core is betaalbaar

De economie is geflipt. Cloud VM's met meerdere cores zijn redelijk geprijsd, en bare metal servers zijn verrassend goedkoop. Deze blog draait op een dedicated 8-core server die minder kost dan een vergelijkbare Azure VM. En ik krijg al die cores voor mezelf, geen luidruchtige buren. Een achtergrondtaak op een core heeft geen significante impact op webverzoeken op andere cores:

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/Await Everywhere

Modern .NET maakt async programmeren triviaal. Achtergrondtaken kunnen wachten op I/O zonder threads te blokkeren:

// 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. Betere proceshosting

  • Docker - Achtergronddiensten in containers worden niet willekeurig gerecycled
  • Kubernetten - Goede sierlijke shutdown handling met SIGTERM
  • gesystemeerd - Linux services die betrouwbaar herstarten
  • Windows-diensten - Goed langlopend procesmodel

4. Kanalen en moderne primitieven

.NET heeft nu eersteklas ondersteuning voor gelijktijdige programmering met System.Threading.Channels:

// 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. Resource Limits en Cgroups

Moderne container orkestratoren laten u het gebruik van hulpbronnen beperken:

# 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

Dit betekent dat een weggelopen achtergrondtaak je web tier niet kan verhongeren.

De vraag is niet langer "Kunnen we background services uitvoeren in onze webapp?" maar "Moeten we dat doen?" We zullen deze beslissing later in de sectie "When NOT to Use Background Services" onderzoeken.

De ingebouwde opties

IHostedService: Stichting

In de kern, elke background service in ASP.NET Core implementeert IHostedService. Deze interface is prachtig eenvoudig:

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

Dat is het, twee methoden. StartAsync wordt aangeroepen wanneer uw aanvraag begint, en StopAsync Als het uitschakelt.

Registreer uw service in Program.cs:

builder.Services.AddHostedService<MyBackgroundService>();

Hier is de levenscyclus gevisualiseerd:

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: Synchronous vs Asynchrone Start

Een kritische beslissing bij de tenuitvoerlegging IHostedService is of uw StartAsync methode moet blokkeren of onmiddellijk terugkeren.

Synchroon (blokkering) start:

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

Asynchrone (niet-blokkeren) start:

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

Wanneer elke aanpak moet worden toegepast:

  • Blokkeren: Wanneer de dienst de initialisatie moet voltooien voordat de toepassing verzoeken kan behandelen (bv. het laden van kritieke configuratie, warming caches)
  • Non-blocking: Wanneer de dienst op de achtergrond kan initialiseren terwijl andere diensten beginnen (bijvoorbeeld het indexeren van bestaande inhoud, synchroniseren met externe diensten)

StopAsync: The Common Pitfall

Hier is waar dingen interessant worden en waar veel ontwikkelaars problemen tegenkomen. Wanneer uw applicatie wordt afgesloten, ASP.NET Core calls StopAsync op alle gehoste diensten. U heeft een beperkt venster (standaard 5 seconden) om elegant op te ruimen. U kunt dit uitbreiden in Program.cs:

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

De meest voorkomende fout:

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

Wanneer u deze service uitvoert en uw toepassing stopt, ziet u fouten zoals:

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

Of de toepassing zal gewoon hangen voor de shutdown timeout periode voordat krachtig beëindigen.

De juiste aanpak:

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

Belangrijkste punten voor correcte StopAsync-implementatie:

  1. Signaalafbreking - Gebruik een CancellationTokenSource En annuleer het.
  2. Volledige kanalen - Als je kanalen gebruikt, bel dan. Writer.Complete()
  3. Wacht op achtergrondtaken - Gebruik Task.WhenAny met de sluiting annulering token
  4. Handle OperatieGeannuleerdUitzondering - Dit wordt verwacht en moet worden gevangen
  5. Geen uitzonderingen gooien - Uitzonderingen in StopAsync kan onvoorspelbaar gedrag veroorzaken

AchtergrondService: De comfortabele basisklasse

Schrijven IHostedService implementaties kunnen repetitief zijn. Je hebt altijd een achtergrondtaak, een annulering token bron, en hetzelfde opruimpatroon nodig. BackgroundService behandelt deze ketelplaat voor u:

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

Je implementeert gewoon ExecuteAsync en laat de basisklasse het sanitair afhandelen:

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

Wanneer Achtergrondservice vs. IHostedService gebruiken

Gebruik BackgroundService wanneer:

  • Je hebt een lang lopende achtergrondlus nodig
  • U wilt eenvoudige periodieke uitvoering
  • Je hebt geen fijne controle nodig over StartAsync/StopAsync timing

Gebruik IHostedService wanneer:

  • Je moet precies controleren wat er gebeurt in StartAsync vs achtergrondwerk
  • Je zet event handlers of watchers op in plaats van een continue lus
  • U moet coördineren met andere diensten tijdens het opstarten

Geavanceerd: Opstartcoördinatie

Soms heb je diensten nodig om op elkaar te wachten. Bijvoorbeeld, je wilt misschien dat je semantische zoekindex wacht tot je markdown bestandsprocessor klaar is met de eerste lading.

Hier is een patroon voor het coördineren van de dienst opstarten:

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

Gebruik in een dienst:

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

Dit patroon wordt vooral nuttig wanneer u meerdere background services met onderlinge afhankelijkheden.

Verdeelde coördinatie met Redis

De opstartcoördinator werkt binnen één enkele toepassing-instantie. Maar wat gebeurt er als je naar meerdere instanties schalen? Je wilt niet dat drie instanties alle dezelfde geplande taak tegelijkertijd uitvoeren.

Opnieuw biedt een eenvoudige oplossing: gebruik vlaggen (toetsen) om te coördineren wie wat doet.

Eenvoudige Leader Verkiezing

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

Gedistribueerde vergrendeling voor kritieke secties

Voor taken die niet gelijktijdig over de instanties mogen lopen:

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

Wanneer gedistribueerde coördinatie moet worden gebruikt

  • Geplande taken - Slechts één instantie dient de dagelijkse nieuwsbrief te versturen
  • Wachtrijverwerking met bestelling - Zorg ervoor dat berichten worden verwerkt in volgorde
  • Intensief gebruik van hulpbronnen - Voorkom meerdere gevallen van het overweldigen van een externe API
  • Databasemigraties - Slechts één instantie moet migraties uitvoeren bij opstarten

Voor complexere scenario's (multi-step jobs, betrouwbare planning over herstarten), overwegen Hangfire die zorgt voor gedistribueerde vergrendeling automatisch met zijn database backend.

Wanneer geen achtergronddiensten gebruiken

Voordat we in meer geavanceerde tools zoals Hangfire duiken, laten we praten over wanneer je Dat zou ik niet moeten doen. gebruik te maken van achtergronddiensten in uw belangrijkste webapplicatie.

Tekent dat u zich moet splitsen naar een apart project

Achtergronddiensten in uw webapplicatie delen resources met uw HTTP-verzoekpijplijn. Dit kan problemen veroorzaken:

1. Resource Contention

Probleem: Uw background service verbruikt belangrijke CPU, geheugen, of database verbindingen.

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

Wanneer webverzoeken tijdens transcodering arriveren, zijn ze traag omdat de CPU bezig is.

Oplossing: Verplaatsen naar een aparte dienst voor werknemers:

# 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. Verschillende schaalvereisten

Probleem: Uw achtergrond werk moet anders schalen dan uw web tier.

  • Webniveau: Schaal voor HTTP-verkeer (kan 10 instanties nodig hebben overdag, 2 's nachts)
  • Achtergrondniveau: Schaal voor wachtrijdiepte (kan 1 instantie normaal nodig hebben, 20 bij het verwerken van een batch)

Als ze in hetzelfde proces zitten, kun je ze niet onafhankelijk opschalen.

Voorbeeldscenario:

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

Het plaatsen van background services in uw webapp betekent dat je 20 web instanties moet draaien alleen om de nieuwsbrief te verwerken, het verspillen van middelen.

3. Deployment Independence

Probleem: U wilt webwijzigingen implementeren zonder background services te herstarten (of vice versa).

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

Elke inzet onderbreekt de import. Verplaats het naar een aparte werkdienst die je onafhankelijk inzet.

4. Verschillende foutendomeinen

Probleem: Een bug in uw background service crasht de hele webapplicatie.

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

Als achtergrondwerk zich in een apart proces bevindt, kan het crashen en herstarten zonder webverzoeken te beïnvloeden.

Hoe om achtergronddiensten te Factoren

Wanneer u besluit om te splitsen, hier is de aanbevolen architectuur:

Optie 1: .NET Worker Service

Maak een nieuw project met behulp van de Worker Service sjabloon:

dotnet new worker -n YourApp.Worker

Structuur:

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

Programma.cs:

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

Separaat inzetten:

# 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

Optie 2: Afzonderlijk project met gedeelde wachtrij

Gebruik een bericht wachtrij om web en werknemers te ontkoppelen:

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

Webapp wachtrijen werken:

// 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
    }
}

Werknemer verbruikt uit de wachtrij:

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

Populaire berichtenwachtrijopties:

Optie 3: Meerdere gespecialiseerde werknemers

Voor complexe systemen, uitgesplitst naar verantwoordelijkheid:

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

Elke werknemer kan:

  • Schalen onafhankelijk
  • Zelfstandig inzetten
  • Gebruik verschillende bronnen (e-mail werknemer heeft SMTP nodig, video werknemer heeft GPU)
  • Hebben verschillende monitoring en alarmering

Wanneer achtergrondservices in uw webapp bewaren

Ondanks het bovenstaande zijn sommige scenario's perfect geschikt voor in-proces background services:

Lichtgewicht periodieke taken

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

Agendanotitieluisteraars

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

Channel-based queues (voor niet-kritieke werkzaamheden)

// 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
}

Opstartcoördinatie

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

Decision Matrix

Karakteristiek om te houden in webapp |---------------|-----------------|------------------------| CPU-gebruik per bewerking < 100ms > 1 seconde Geheugen per bewerking < 10 MB > 100 MB Frequentie (minuten/uren) continu of hoogfrequent Kritiek Niet-kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Kritiek Tijdsduur - Seconden - Minuten tot uren Scales met Webverkeer Werk wachtrijdiepte Quality over Quantity (QoQ) Releases Cache warming, config reload Video processing, large import

Real-World Voorbeeld: Het Blog Platform

In het blog platform wiens code we onderzoeken in Deel 2:

In de webapp bewaard:

  • MarkdownDirectoryWatcherService - Lichtgewicht bestandswatcher
  • UmamiBackgroundSender - Quick analytics events
  • EmailSenderHostedService - Klein volume, niet-kritisch
  • MarkdownReAddPostsService - Opstart-only, configuratie-gated

Moet naar de dienst van de werknemer gaan als de schaal toeneemt:

  • BrokenLinkCheckerBackgroundService - Maakt veel HTTP-verzoeken
  • SemanticIndexingBackgroundService - Bellen externe inbedding API

Al in aparte dienst:

  • Mostlylucid.SchedulerService - Hangfire dashboard en nieuwsbrief verzenden

Dit is een pragmatische aanpak: start eenvoudig (in proces), split wanneer je bewijs hebt dat je nodig hebt.

Beyond the Basics: Hangfire

Terwijl IHostedService en BackgroundService zijn uitstekend voor diensten die u bezit en controle, soms heb je meer geavanceerde planning nodig. Dat is waar bibliotheken zoals Hangvuur Kom binnen.

Hangfire biedt:

  • Aanhoudende wachtrijen - Jobs survive application herstart
  • Terugkerende banen - Cron-style planning
  • Dashboard-UI - Kijken wat draait, wat mislukt is, opnieuw proberen van jobs
  • Verdeelde uitvoering - Meerdere servers kunnen dezelfde wachtrij verwerken
  • Automatische herhalingen - Mislukte taken worden automatisch opnieuw opgepakt met exponentiële backoff

Hier is een eenvoudig voorbeeld:

// 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

Uw service is gewoon een normale klasse:

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

Hangvuurhandvatten:

  • Ervoor zorgen dat de baan loopt op het geplande tijdstip
  • Opnieuw proberen als het mislukt
  • Opslaan van executiegeschiedenis
  • Het verstrekken van een dashboard om alles te controleren
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

Wanneer moet u Hangfire gebruiken:

  • Je hebt aanhoudende wachtrijen nodig die overleven herstarten
  • U wilt een dashboard om taken te monitoren en handmatig te activeren
  • Je hebt gedistribueerde jobverwerking nodig over meerdere servers
  • U wilt ingebouwde retry logica en falen behandeling
  • Je hebt cron-stijl planning van terugkerende taken nodig

Wanneer te plakken met IHostedService/AchtergrondService:

  • Je hebt fijne controle nodig over de levensduur van de dienst
  • Uw service moet real-time reageren op gebeurtenissen
  • U wilt afhankelijkheden minimaliseren
  • Je bouwt een eenvoudige periodieke taak die niet persistent hoeft te zijn.

Andere opties

Terwijl Hangfire populair is, zijn er andere bibliotheken die het overwegen waard zijn:

Quartz.NET:

  • Flexibelere planning dan Hangfire
  • Ondersteunt cron expressies en agenda-gebaseerde planning
  • Kan blijven bestaan naar meerdere databases
  • Meer complexe API maar krachtiger

MassTransit/NServiceBus:

  • Complete berichtenbus-implementaties
  • Beter voor gedistribueerde systemen en microdiensten
  • Ondersteuning van sagas (langlopende workflows)
  • Steeper learning curve

Azure functies/AWS Lambda:

  • Als je in de cloud bent, overweeg dan serverless
  • Betalen per uitvoering in plaats van een dienst draaiende te houden
  • Automatisch schalen
  • Wat latency overhead voor koude begint

Samenvatting

In deel 1 hebben we de fundamentele benaderingen van background services behandeld in ASP.NET Core:

  1. IHostedService - De fundering, maximale flexibiliteit
  2. Achtergronddienst - Handige basisklasse voor lange loops
  3. Coördinatie van het opstarten - De diensten op elkaar laten wachten
  4. Verdeelde coördinatie - Het gebruik van Redis voor multi-instance scenario's
  5. Hangvuur - Wanneer u hardnekkige banen en geavanceerde planning nodig hebt

De belangrijkste lessen:

  • Altijd kanalen voltooien en tokens annuleren in StopAsync
  • Beslis of StartAsync moet blokkeren of onmiddellijk moet terugkeren
  • Handle OperationGeannuleerdUitzondering sierlijk
  • Gebruik Taak.WanneerElke met de shutdown token om time-outs te respecteren

In Deel 2, zullen we real-world implementaties van een productie blog platform te onderzoeken:

  • Bestandssysteem watchers die markdown bestanden synchroniseren naar een database
  • E-mail afzenders met retry beleid en stroomonderbrekers
  • Analytics event wachtrijen die batch-verzoeken
  • Semantische zoekindexers die inhoud asynchroon verwerken
  • Gebroken koppelingscheckers die periodiek externe URL's valideren

Deze voorbeelden tonen de patronen uit deel 1 in actie, met inbegrip van het opstartcoördinatiepatroon en de juiste shutdown handling.

Meer lezen

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.