Back to "Services de base dans ASP.NET Core - Partie 1: Les approches"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

ASP.NET Core BackgroundService Hangfire IHostedService

Services de base dans ASP.NET Core - Partie 1: Les approches

Thursday, 27 November 2025

Toutes les applications Web modernes ont un travail qui devrait ne pas bloquer une demande HTTP—envoi d’emailsM SK2 traitement de fichiers , synchronisation avec des services externes IHostedService implémentations sur des cadres perfectionnés comme Hangfire. Dans la première partie, nous examinons les modèles fondamentaux et le moment d’utiliser chacun de ceux-ci.

Introduction

Confession; J'aime les services de base un LAU M SK1 ce site (un site de BLOG!) il y a plus d'une demi-douzaine d'entre eux qui font des tâches de fond sérieusesMSC5 mais comme tout, ils ont quelques ODDITIES et pratiques qui feront de leur utilisation beaucoup plus agréableMST6 Les services d’information sont les héros inoubliables des applications Web modernes. Alors que vos contrôleurs traitent les demandes HTTP dans l’avant-plan, les services d'information traitementnt silencieusement les courriels en rangée, le contenu indexé pour la recherche, les API externes vérifiés, les fichiers temporaires nettoyés et de nombreuses autres tâches qui bloqueraient autrement votre pipeline de requêtes.

Dans cette série de deux parties, - et ,, nous étudierons les différentes approches pour mettre en œuvre des services de base dans l’ASP, la ASP3, le Core NET et la , à partir du module intégré. IHostedService et BackgroundService abstractions à des solutions plus sophistiquées comme Hangfire. Dans la partie 1, nous examinerons les approches fondamentales et leurs caractéristiques Partie 2, nous ' nous plongons dans les mises en œuvres réelles du monde de la production à partir d’une base de codes.

Important: Nous accorderons une attention particulière à la gestion du cycle de vie, en particulier celle qui est souvent négligée. StopAsync méthode, qui est là où de nombreux développeurs rencontrent des exceptions cryptiques lorsque leurs applications s'arrêtent

Pourquoi les services de base?

Avant de plonger dans le " comment ," laissez's examiner brièvement les " pourquoiM SK4 Les services d’information vous permettent

  1. Déchargement des opérations lentes - Ne faites pas les utilisateurs attendre pendant que vous envoyez des courriels ou que vous produisez des PDF
  2. Calendrier des tâches récurrentes - Nettoyage des anciens fichiers chaque nuit à 2
  3. Processus en attente - Transmettre les messages des canaux ou des courtiers de message
  4. Surveiller l'état extérieur - APIs de sondage ou systèmes d'observation des fichiers pour les changements
  5. Coordonner les flux de travail complexes - Gérer des processus à plusieurs étapes qui s’étendent en minutes ou en heures

ASP.NET Core offre plusieurs approches pour la mise en œuvre de ces services.

Le contexte historique: Pourquoi les services d’information sont maintenant possibles

Auparavant, le travail de fond dans votre application Web était généralement considéré comme une mauvaise idée.

Il s’agissait non seulement de marchandises, mais aussi de savoir-faire et d’avoir recours à des limites techniques.

L’ère unique-Core

Les premiers serveurs Web (et honnêtement maintenantM SK1 'cheap'Les services d’AzureMSC4 fonctionnent habituellement sur CPUs single-core ou double-core. Si vous avez exécuté une tâche de fond intensive sur un CPU, - , elle a directement fait concurrence aux demandes Web pour le même noyau.

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

Résultat: Votre site Web est devenu lent dès le moment où les travaux de fond ont commencé.

La faim dans le bassin de Thread

Classic ASP.NET utilisé thread-per-request. Le bassin de threads était relativement petit, les threads typiques étaient (25-100 et les tâches de fond volaient des threads qui devaient traiter les demandes Web.

// 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 recyclerait agressivement les pools d'application (restarter votre application) selon les limites de mémoireM SK2 le nombre de demandesMSC3 ou les horaires de travailMST4 Le travail de fond serait déclenché à la mi-parcours - l'exploitation MST6

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

Async limité/Support attendu

Avant .NET 4.5 | | (2012), la programmation asynque était ♫ (relativement ♫) douloureuse ♫ |. Les tâches de fond bloquent souvent les threads inutilement |

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

Ce qui a changé: L’ère moderne

Aujourd’hui, le paysage de MSK est radicalement différent.

1. MultiM SK1Core est abordable

L’économie a changé. Les VM à nuages multiples sont raisonnablement abordables , et les serveurs de métaux vierges sont surprenantement bon marché . Ce blog fonctionne sur un serveur de noyau dédié M SK3 qui coûte moins qu’un VM Azure comparable—et je me livre tous ces noyaus à moi-mêmeMSC5 sans voisins bruyantsMNK6 Une tâche de fond sur un noyaume ne fait pas d’effet significatif sur les demandes Web sur d’autres noyauxMRK8

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

Moderne .NET rend la programmation asynque triviale . Les tâches de fond peuvent attendre sur IM SK2O sans bloquer les threads:

// 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. Un meilleur processus d'hébergement

  • Docker - Les services de base dans les conteneurs ne sont pas recyclés arbitrairement
  • Kubernetes - Manipulation correcte et gracieuse du démarrage avec SIGTERM
  • système - Les services Linux qui restartent avec fiabilité
  • Services Windows - Modèle de procédé d’exploitation correctement longM SK1

4. Channels and Modern Primitives

.NET a maintenant le premier soutien de la classe - pour la programmation concurrente avec 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. Limites de ressources et groupes C

Les orchestrateurs de conteneurs modernes vous permettent Limiter l'utilisation des ressources:

# 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

Cela signifie qu’une tâche de fond en train d’échouer peut ne pas nuire à votre niveau Web.

La question n’est plus : "Poussions-nous exploiter les services de base dans notre application Web ?Nous devrions-nousNous examinerons cette décision dans la section " Quand ne pas utiliser les services d’information " plus loin .

Les options intégrées-

IHostedService: La Fondation

À son noyau, tous les services de base dans ASP.NET Core implements IHostedService. Cette interface est très simple

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

Il s’agit de deux méthodes : StartAsync est appelée lorsque votre application commence, et StopAsync quand il s'arrête.

Inscrivez votre service dans Program.cs:

builder.Services.AddHostedService<MyBackgroundService>();

Voici la visualisation du cycle de vie de '

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: Synchrone vs Démarrage asynchrone

Une décision cruciale lors de la mise en œuvre IHostedService est si votre StartAsync la méthode doit bloquer ou retourner immédiatement.

Synchrone (blocage)démarrageM SK2

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 (nonM SK1blocage)démarrage :

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

Quand utiliser chaque approche:

  • Blocage: Lorsque le service doit terminer l'initialisation avant que l'application ne puisse traiter les demandes (e .gM SK2 chargement de la configuration critique, caches de réchauffementMSC4
  • Non-blocageM SK1 Quand le service peut initialiser dans l'arrière-plan alors que d'autres services commencent (eM SK1g., indexer les contenus existants , synchronisation avec des services externesMSC4

StopAsync: The Common Pitfall

Voici's où les choses se font intéressantes—et où de nombreux développeurs rencontrent des problèmes . Lorsque votre application s'arrêteMSC3 ASPM SK4NET Core calls StopAsync sur tous les services hébergés. Vous avez une fenêtre limitée M SK1défault 5 secondes) pour nettoyer gracieusement Program.cs:

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

Les erreurs les plus fréquentes:

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

Lorsque vous exécutez ce service et arrêtez votre application, vous verrez des erreurs comme :

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

Ou l'application sera simplement suspendue pendant la période d'interruption avant de mettre fin à force.

La bonne approche:

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

Points clés pour une mise en œuvre correcte de StopAsync:

  1. Annulation du signal - Utiliser un CancellationTokenSource et l'annuler
  2. Canals complets - Si vous 'utilisez des canaux , appelez Writer.Complete()
  3. attendre les tâches de fond - Usage Task.WhenAny avec le symbole d'annulation de la fermeture
  4. Appliquer l'opérationCanceledException - Ceci est attendu et devrait être pris
  5. Don't jeter des exceptions - Exceptions dans StopAsync peut provoquer un comportement imprévisible

Services d'information: La classe de base pratique

Écrire IHostedService les mises en oeuvre peuvent être répétitives. Vous avez toujours besoin d’une tâche de fond, une source de tokens d’annulationMSC2 et le même modèle de nettoyageM SK3 BackgroundService porte cette plaque de chaudière pour vous:

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

Il suffit de mettre en œuvre ExecuteAsync et laisser la classe de base manipuler le plomberie:

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

Quand utiliser BackgroundService vs IHostedService

Utilisation BackgroundService quand:

  • Vous avez besoin d’un cycle de fond long-
  • Vous voulez une exécution périodique simple
  • Vous n’avez pas besoin de contrôles précis StartAsync/StopAsync évaluation du temps

Utilisation IHostedService quand:

  • Vous devez contrôler exactement ce qui se passe dans StartAsync vs travail de fond
  • Vous'vous établissez des gestionnaires d'événements ou des observateurs plutôt qu'une boucle continue
  • Vous devez coordonner avec d'autres services pendant le démarrage

Advanced: Coordination de démarrage

Parfois, vous avez besoin de services pour attendre les uns des autres. Par exemple, vous voudriez peut-être que votre indexateur de recherche sémantique ait à attendre jusqu’à ce que votre processeur de fichiers désignés ait terminé sa charge initialeM SK2

Voici un modèle pour coordonner le démarrage du service'

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

Usage dans un service:

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

Ce modèle devient particulièrement utile lorsque vous avez plusieurs services de base avec des interdépendances.

Coordination répartie avec Redis

Le coordonnateur de démarrage fonctionne dans une seule instance d’application.

Redis fournit une solution simple: utilise les drapeaux M SK1 clés ) pour coordonner qui fait ce que

Élection d'un leader simple

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

Locking distribué pour les sections critiques

Pour les tâches qui ne doivent pas être exécutées simultanément à travers des instances:

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

Quand utiliser la coordination distribuée

  • Dépenses prévues - Une seule instance devrait envoyer le bulletin quotidien
  • Traitement en attente avec commande - Assurer que les messages sont traités en ordre
  • Ressources-opérations intensives - Prévenir plusieurs instances de surpeuplement d'une API externe
  • Migrations de bases de données - Une seule instance devrait lancer les migrations au démarrage

Pour des scénarios plus complexes (multi-emplois en étapesM SK2 calendrier fiable à travers les relais de réouverture ), considérez Hangfire qui gère automatiquement l'engrenage distribué avec son backend de base de données.

Quand ne pas utiliser les services d'information

Avant de nous plonger dans des outils plus sophistiqués comme Hangfire, laissez-nous vous parler 't Utiliser les services de base dans votre application Web principale.

Signes que vous devez diviser en un projet séparé

Les services d’information qui fonctionnent dans votre application Web partagent les ressources avec votre pipeline de demande HTTP. Cela peut causer des problèmes:

1. Contention sur les ressources

Problème: Votre service de référence consomme un CPU significatif, mémoire, ou des connexions à la base de donnéesM SK2

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

Lorsque des demandes Web arrivent pendant le transcodage, elles sont lentes parce que la CPU est occupée.

Solution: Passez à un service de travailleurs distinct:

# 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. Différentes exigences en matière d'étalonnage

Problème: Votre travail de base a besoin d’une échelle différente de votre niveau Web.

  • Niveau Web: Étalonnage du trafic HTTP (peut avoir besoin de 10instances pendant la journée, 2à la nuitM SK4
  • Niveau de fond: Étalonnage de la profondeur de la queue (peut-il être nécessaire d’examiner l’instance normalement ?

Si elles se trouvent dans le même processus, vous pouvez les écheller indépendamment.

Exemple de scénario:

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

Mettre des services de fond dans votre application Web signifie que vous 'd’avez à faire fonctionner les instances web 20 simplement pour gérer le bulletin d’information, gaspillage de ressourcesM SK3

3. Indépendance du déploiement

Problème: Vous voulez déployer des changements Web sans redémarrer les services de fond (ou 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);
    }
}

Chaque déploiement interrompt l’importation. Envoyez-le à un service de travail distinct que vous déployez indépendamment.

4. Domaines de défaillance différents

Problème: Un bogue dans votre service d'arrière-plan interrompt l'application Web entière.

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

Si le travail de fond se déroule dans un processus distinct, il peut démarrer et réamorcer sans affecter les demandes Web.

Comment influer sur les services de base

Lorsque vous décidez de séparer, ici ' est l'architecture recommandée:

Option 1: .NET Worker Service

Créer un nouveau projet en utilisant le modèle de Service des travailleurs:

dotnet new worker -n YourApp.Worker

Structure:

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

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

Déployer séparément:

# 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: Séparer le projet avec une liste partagée

Utiliser une liste de messages pour découpler le web et les travailleurs:

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

Travail des queues d’applications Web:

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

Consommation du travail à partir de la queue:

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

Options de la queue des messages les plus populaires:

Option 3: Travailleurs spécialisés multiples

Pour les systèmes complexes, divisé par responsabilitéM SK1

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

Chaque travailleur peut:

  • Évaluer indépendamment
  • Déployer indépendamment
  • Utiliser des ressources différentes (les travailleurs de l'email ont besoin d'un SMTPM SK1les professionnels du vidéo ont besoin de GPU)
  • Des systèmes de surveillance et d'alerte différents

Quand conserver les services d'information dans votre application Web

En dépit de ce qui précède, certains scénarios sont parfaitement bons pour les services de fond du processus

Travaux périodiques légers

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

Listeurs d'événements

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

Voie de communication-Retours basés (pour les travaux non -travail critiqueM SK3

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

Coordination du démarrage

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

Matrice de décision

Caractéristique Appuyer l'application Web
Utilisation de la CPU par opération < \100ms
Mémoire par opération 2 3 4 5 6 7
Fréquence
Évaluation critique Non
Durée Deuxièmes
Scales avec Trafic sur le Web profondeur de la queue de travail M
Exemple Échauffage de caches, Rechargement de configuration CSM3 Traitement vidéoCMS4 Importations importantes DSM5

Real-Exemple mondiale: La plateforme de blog

Dans la plateforme de blog dont le code est examiné dans la partie 2:

Conservé dans l'application Web:

  • MarkdownDirectoryWatcherService - Vérificateur de fichier léger
  • UmamiBackgroundSender - Événements d'analyse rapide
  • EmailSenderHostedService - Volume réduit, nonM SK2critique
  • MarkdownReAddPostsService - Démarrage

Il faut passer au service des travailleurs si l’échelle augmente:

  • BrokenLinkCheckerBackgroundService - Fait de nombreuses demandes HTTP
  • SemanticIndexingBackgroundService - Demande une API d'embedding externe

En service déjà séparé:

  • Mostlylucid.SchedulerService - Afficher le tableau de bord Hangfire et le bulletin d'information

Il s’agit d’une approche pragmatique.

Au-delà des bases: Hangfire

Alors que IHostedService et BackgroundService sont excellents pour les services que vous possédez et que vous contrôlez. Le feu d'engrenage .

Hangfire fournit:

  • Écarts de travail persistants - Les emplois survivent à la reprise de l'application
  • Travaux récurrents Calendrier - CronM SK1de style
  • L'interface du tableau de bord - Voir ce que 's est en train de faire, ce qu'il a fallu
  • Exécution répartie - Plusieurs serveurs peuvent traiter la même queue de travail
  • Retours automatiques - Les tâches qui ont échoué sont repérées automatiquement avec un retrait exponentiel

Voici un exemple simple :

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

Votre service est une classe normale:

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

Dispositifs de feu d'engrenage:

  • Assurer que le travail se déroule à l'heure prévue
  • Retrying if it fails
  • Enregistrement de l'historique d'exécution
  • Offrir un tableau de bord pour surveiller tout
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

Quand utiliser Hangfire:

  • Vous avez besoin d'une liste persistante de tâches qui surviendra et reprend ses activités.
  • Vous voulez un tableau de bord pour surveiller et déclencher manuellement les tâches
  • Vous avez besoin d'un processus de travail distribué à travers plusieurs serveurs
  • Vous voulez construire-in retry logic and failure handling
  • Vous avez besoin de calendrier cron-style des tâches récurrentes

Quand rester avec IHostedService/BackgroundService :

  • Vous avez besoin d'un bon contrôle sur le cycle de vie du service
  • Votre service doit réagir aux événements en temps réel
  • Vous voulez réduire au minimum les dépendances
  • Vous construisez une tâche périodique simple qui n’a pas besoin de persistance.

Autres options

Bien que Hangfire soit populaire, il y a d’autres bibliothèques à considérer:

Quartz.NET:

  • Calendrier plus souple que Hangfire
  • Appuie les expressions cron et la programmation fondée sur le calendrier-
  • Peut persister dans plusieurs bases de données
  • API plus complexe mais plus puissant

Transfert de masse/NServiceBus:

  • Mise en œuvre complète des bus de message -
  • Améliorer les systèmes distribués et les microservices
  • Support sagas (longM SK1running workflows)
  • Curve d'apprentissage accrue

Fonctions d'Azure/AWS Lambda:

  • Si vous êtes dans la numérisation, considérez que , est sans serveur.
  • Rembourser par exécution plutôt que de maintenir un service en cours
  • Étalonnage automatique
  • Une certaine latence au-dessus des coûts pour les démarrages à froid

Résumé

Dans la partie 1,, nous ' avons examiné les approches fondamentales des services de base dans l’ASP

  1. IHostedService - La baseM SK1 maximum de souplesse
  2. Service d'information - Classe de base convenable pour les longues boucles de conduite
  3. Coordination de démarrage - Faire les services attendre l’un l’autre
  4. Coordination répartie - Utilisation de Redis pour plusieurs scénarios d’instance -
  5. Le feu d'engrenage - Lorsque vous avez besoin de tâches persistantes et d’un calendrier sophistiqué

Leçons les plus importantes:

  • Remplissez toujours les canaux et annulez les tokens dans StopAsync
  • Décider si StartAsync devrait bloquer ou retourner immédiatement
  • Gérer l'opérationCanceledException gracefully
  • Utiliser Task.WhenAny avec le token de fermeture pour respecter les périodes d'interruption

À l'intérieur Partie 2Nous examinerons les mises en œuvres réelles d’une plateforme de blog sur la production.

  • Surveillants du système de fichiers qui synchronisent les fichiers de marquage à une base de données
  • Expéditeurs d'email avec des politiques de réessai et des interrupteurs de circuit
  • Enquêtes d'analyse des événements que demandent les lots
  • Indexeurs de recherche sémantiques qui traitent le contenu asynchronement
  • Contrôles des liens défectueux qui valident périodiquement les URL externes

Ces exemples démontrent les modèles de la partie 1 en action , y compris le modèle de coordination du démarrage et la bonne manipulation de shutdown

Autres lectures

logo

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