Bakgrundstjänster i ASP.NET Core - Del 1: Metoderna (Svenska (Swedish))

Bakgrundstjänster i ASP.NET Core - Del 1: Metoderna

Thursday, 27 November 2025

//

23 minute read

Varje modern webbapplikation har arbete som inte ska blockera en HTTP-begäran – att skicka e-post, behandla filer, synkronisera med externa tjänster, köra schemalagt underhåll. ASP.NET Core ger flera metoder för att hantera detta bakgrundsarbete, från enkla IHostedService implementationer till sofistikerade ramar som Hangfire. I denna första del kommer vi att utforska de grundläggande mönstren och när man ska använda var och en.

Inledning

Bekännelse; Jag gillar bakgrundstjänster en LOT, denna webbplats (en BLOG webbplats!) har över en halv-dussin av dem gör karga bakgrundsuppgifter, men som allt de har några ODDITYER och praxis som kommer att göra din användning av dem mycket trevligare. Bakgrundstjänster är de osung hjältar av moderna webbapplikationer. Medan dina controllers hanterar HTTP-förfrågningar i förgrunden, bakgrundstjänster tyst behandla köade e-postmeddelanden, index innehåll för sökning, kontrollera externa API:er, rensa upp tillfälliga filer och hantera otaliga andra uppgifter som annars skulle blockera din begäran rörledning.

I denna tvådelade serie kommer vi att utforska de olika tillvägagångssätten för att implementera bakgrundstjänster i ASP.NET Core, från den inbyggda IHostedService och BackgroundService I del 1 ska vi undersöka de grundläggande tillvägagångssätten och deras egenskaper. Häfte 2, kommer vi dyka in i verkliga implementeringar från en produktionskodbas.

Viktigt: Vi kommer att ägna särskild uppmärksamhet åt livscykelhantering – särskilt de ofta förbisedda StopAsync metod, där många utvecklare stöter på kryptiska undantag när deras program stängs av.

Varför bakgrundstjänster?

Innan du dyker in i "hur", låt oss helt kort överväga "varför." Bakgrundstjänster låter dig:

  1. Avlasta långsamma operationer - Låt inte användare vänta medan du skickar e-post eller generera PDF-filer
  2. Schemalägg återkommande uppgifter - Städa upp gamla skivor varje kväll vid 2 AM
  3. Processköer - Hantera meddelanden från kanaler eller meddelandemäklare
  4. Övervaka det yttre tillståndet - Polera API:er eller titta på filsystem för ändringar
  5. Samordna komplexa arbetsflöden - Hantera flerstegsprocesser som spänner över minuter eller timmar

ASP.NET Core tillhandahåller flera metoder för att genomföra dessa tjänster, var och en med olika kompromisser.

Den historiska kontexten: Varför bakgrundstjänster är nu möjliga

Under de "gamla dagarna" (före 2010) ansågs det allmänt vara en dålig idé att köra bakgrundsarbete i din webbapplikation. Den konventionella visdomen var: "Webbservrar hanterar webbförfrågningar. Bakgrundsarbete hör hemma på en separat server."

Detta var inte bara last-kult visdom – det var baserat på verkliga tekniska begränsningar:

Enkoreansk tid

Tidiga webbservrar (och ärligt talat nu, "billiga" Azure-tjänster) körde normalt på enkärniga eller tvåkärniga processorer. Om du körde en CPU-intensiv bakgrundsuppgift, konkurrerade den direkt med webbförfrågningar om samma kärna:

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

Resultat: Din webbplats blev trög när bakgrundsarbetet började.

Trådpoolssvält

Klassiskt ASP.NET använt tråd per begäran. Trådpoolen var relativt liten (25-100 trådar typiskt), och bakgrundsuppgifter skulle stjäla trådar som bör hantera webbförfrågningar:

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

Återvinning av IIS-tillämpningar

IIS skulle aggressivt återvinna programpooler (återstarta din app) baserat på minnesgränser, begäran räknas, eller tidsscheman. Bakgrundsarbete skulle dödas mitt i operationen:

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

Begränsat stöd för Async/Avänta

Före .NET 4.5 (2012) var async programmering (relativt) smärtsam. Bakgrundsuppgifter ofta blockerade trådar i onödan:

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

Vad som förändrades: Den moderna tiden

Dagens landskap är dramatiskt annorlunda:

1. Multi-Core är prisvärd

Ekonomin har vänt. Cloud VMs med flera kärnor är rimligt prissatta, och bare metal servrar är förvånansvärt billigt. Denna blogg körs på en dedikerad 8-core server som kostar mindre än en jämförbar Azure VM-och jag får alla dessa kärnor för mig själv, inga bullriga grannar. En bakgrundsuppgift på en kärna påverkar inte avsevärt webbförfrågningar på andra kärnor:

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/Avänta överallt

Modern .NET gör async programmering trivial. Bakgrundsuppgifter kan vänta på I/O utan att blockera trådar:

// 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. Bättre processvärdskap

  • Docka - Bakgrundstjänster i containrar återvinns inte godtyckligt.
  • Kuberneter - Rätt graciös avstängningshantering med SIGTERM
  • systemd - Linuxtjänster som startar om tillförlitligt
  • Windows-tjänster - Rätt långsiktig processmodell

4. Kanaler och moderna primitives

.NET har nu förstklassigt stöd för samtidig programmering med 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. Resursgränser och C-grupper

Moderna container orkestratorer låter dig begränsa resursanvändning:

# 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

Detta innebär att en flyktig bakgrundsuppgift inte kan svälta din webbnivå.

Frågan är inte längre "Kan vi köra bakgrundstjänster i vår webbapp?" utan "Ska vi?" Vi kommer att undersöka detta beslut i avsnittet "När du INTE ska använda bakgrundstjänster" senare.

Inbyggda alternativ

IHostedService: Stiftelsen

I sin kärna, varje bakgrundstjänst i ASP.NET Core implementerar IHostedService. Detta gränssnitt är vackert enkelt:

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

Två metoder. StartAsync kallas när din ansökan börjar, och StopAsync När den stängs av.

Registrera din tjänst i Program.cs:

builder.Services.AddHostedService<MyBackgroundService>();

Här är livscykeln visualiserade:

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: Synkron vs Asynkron start

Ett avgörande beslut vid genomförandet IHostedService är om din StartAsync metoden bör blockera eller återvända omedelbart.

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

Asynkron (icke-blockerande) 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
    }
}

När varje metod ska användas:

  • Blockering: När tjänsten måste slutföra initiering innan ansökan kan hantera förfrågningar (t.ex. ladda kritisk konfiguration, värma cache)
  • Icke-blockerande: När tjänsten kan initieras i bakgrunden medan andra tjänster börjar (t.ex. indexering av befintligt innehåll, synkning med externa tjänster)

StopAsync: Den vanliga fallskärmen

Här blir det intressant – och där många utvecklare stöter på problem. När din ansökan stängs, ASP.NET Core samtal StopAsync på alla värdtjänster. Du har ett begränsat fönster (standard 5 sekunder) att städa upp graciöst. Du kan förlänga detta i Program.cs:

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

Det vanligaste misstaget:

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

När du kör denna tjänst och stoppa din ansökan, kommer du att se fel som:

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

Eller programmet kommer helt enkelt hänga för avstängning timeout period innan kraftfullt avsluta.

Det korrekta tillvägagångssättet:

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

Nyckelpunkter för korrekt StopAsync-implementation:

  1. Signalavstängning - Använd en CancellationTokenSource och annullera den
  2. Fullständiga kanaler - Om du använder kanaler, ring Writer.Complete()
  3. Vänta på bakgrundsuppgifter - Användning Task.WhenAny med avstängningssymbolen
  4. Hantera OperationCanceledException - Detta förväntas och bör fångas
  5. Kasta inte undantag - Undantag i StopAsync kan orsaka oförutsägbart beteende

Bakgrundstjänst: Den bekväma basklassen

Skriva IHostedService implementationer kan vara repetitiva. Du behöver alltid en bakgrundsuppgift, en annullering token källa, och samma rensning mönster. BackgroundService hanterar denna pannplatta för dig:

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

Du bara implementerar ExecuteAsync och låt basklassen hantera VVS:

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

När du ska använda bakgrundstjänst vs IHostedService

Användning BackgroundService när:

  • Du behöver en långvarig bakgrundsloop
  • Du vill ha enkel periodisk körning
  • Du behöver inte bra kontroll över StartAsync/StopAsync Tidpunkt

Användning IHostedService när:

  • Du måste kontrollera exakt vad som händer i StartAsync vs bakgrundsarbete
  • Du sätter upp händelsehanterare eller åskådare istället för en kontinuerlig loop
  • Du måste samordna med andra tjänster under start

Avancerat: Uppstartssamordning

Ibland behöver du tjänster för att vänta på varandra. Du kanske till exempel vill att din semantiska sökindexerare ska vänta tills din filprocessor har avslutat sin ursprungliga laddning.

Här är ett mönster för samordning av servicestart:

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

Användning i en tjänst:

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

Detta mönster blir särskilt användbart när du har flera bakgrundstjänster med ömsesidigt beroende.

Distribuerad samordning med Redis

Startsamordnaren fungerar i en enda programinstans. Men vad händer när du skalar till flera instanser? Du vill inte att tre instanser alla kör samma schemalagda aktivitet samtidigt.

Redis Ordförande ger en enkel lösning: använd flaggor (nycklar) för att samordna vem som gör vad.

Enkelt ledarval

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

Distribuerad låsning för kritiska sektioner

För uppgifter som inte får utföras samtidigt mellan olika instanser:

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

När man ska använda distribuerad samordning

  • Schemalagda uppgifter - Endast en instans ska skicka det dagliga nyhetsbrevet
  • Köbehandling med beställning - Se till att meddelanden behandlas i ordning
  • Resursintensiva verksamheter - Förhindra flera instanser från att överväldigande en extern API
  • Databasmigreringar - Endast ett fall bör köra migreringar vid start

För mer komplexa scenarier (flerstegsjobb, tillförlitlig schemaläggning över omstarter), överväga Hangfire som hanterar distribuerad låsning automatiskt med sin databas backend.

När du inte ska använda bakgrundstjänster

Innan vi dyker in i mer sofistikerade verktyg som Hangfire, låt oss prata om när du - Det borde jag inte göra. använda bakgrundstjänster i din huvudsakliga webbapplikation.

Signerar du bör dela upp dig i ett separat projekt

Bakgrundstjänster som körs i din webbapplikation delar resurser med din HTTP- begäran pipeline. Detta kan orsaka problem:

1. Resursinnehåll

Problem: Din bakgrundstjänst förbrukar betydande CPU, minne, eller databasanslutningar.

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

När webbförfrågningar anländer under omkodning, de är långsamma eftersom CPU är upptagen.

Lösning: Flytta till en separat arbetstagartjänst:

# 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. Olika skalkrav

Problem: Ditt bakgrundsarbete behöver en annan skala än din webbnivå.

  • Webbnivå: Skala för HTTP-trafik (kan behöva 10 instanser under dagen, 2 på natten)
  • Bakgrundsnivå: Skala för ködjup (kan behöva 1 instans normalt, 20 vid bearbetning av ett parti)

Om de är i samma process, kan du inte skala dem självständigt.

Exempel på scenario:

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

Att sätta bakgrundstjänster i din webbapp innebär att du måste köra 20 webbinstanser bara för att hantera nyhetsbrevet, slösa resurser.

3. Deployment oberoende

Problem: Du vill distribuera webbändringar utan att starta om bakgrundstjänster (eller tvärtom).

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

Varje utplacering avbryter importen. Flytta den till en separat arbetstjänst som du använder självständigt.

4. Olika feldomäner

Problem: En bugg i din bakgrundstjänst kraschar hela webbprogrammet.

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

Om bakgrundsarbetet är i en separat process kan det krascha och starta om utan att påverka webbförfrågningar.

Hur man skapar bakgrundstjänster

När du bestämmer dig för att dela, här är den rekommenderade arkitekturen:

Alternativ 1: NET Worker Service

Skapa ett nytt projekt med hjälp av mallen Worker Service:

dotnet new worker -n YourApp.Worker

Struktur:

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

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

Distribuera separat:

# 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

Alternativ 2: Separat projekt med delad kö

Använd en meddelandekö för att koppla bort webben och arbetstagare:

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

Webbappen köer fungerar:

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

Arbetaren konsumerar från kön:

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

Populära alternativ för brevköer:

Alternativ 3: Flera specialiserade arbetstagare

För komplexa system, delat på ansvar:

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

Varje arbetstagare kan:

  • Skala självständigt
  • Distribuera självständigt
  • Använd olika resurser (e-postarbetare behöver SMTP, videoarbetare behöver GPU)
  • Har olika övervakning och varning

När du ska behålla bakgrundstjänster i din webbapp

Trots det ovanstående är vissa scenarier helt bra för processbakgrundstjänster:

Lätta periodiska uppgifter

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

Händelselyssnare

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

Kanalbaserade köer (för icke-kritiskt arbete)

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

Uppstartssamordning

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

Beslutsmatris

en karaktäristisk och behålla i webbappen och flytta till tjänsten för arbetare |---------------|-----------------|------------------------| på CPU-användning per operation på < 100ms på > 1 sekund Memorandum per operation på < 10 MB > 100 MB på regelbunden (minuter/timmar) på kontinuerlig eller högfrekvent "Kritikalitet" Icke-kritiska "Kritisk" Varaktighet .sekunder Protokoll till timmar Skala med webbtrafik på arbetsködjup på Exemple på Cache uppvärmning, config reload på video bearbetning, stor import

Real-World Exempel: Bloggplattformen

I bloggplattformen vars kod vi undersöker i del 2:

Sparad i webbappen:

  • MarkdownDirectoryWatcherService - Lättviktig filskådare
  • UmamiBackgroundSender - Snabba analyshändelser
  • EmailSenderHostedService - Liten volym, icke-kritisk
  • MarkdownReAddPostsService - Endast start, konfigurationsstyrd

Bör gå över till arbetartjänst om skalan ökar:

  • BrokenLinkCheckerBackgroundService - Gör många HTTP-förfrågningar
  • SemanticIndexingBackgroundService - Anropar extern inbäddning API

Redan i separat tjänst:

  • Mostlylucid.SchedulerService - Hangfire instrumentpanelen och nyhetsbrev skicka

Detta är ett pragmatiskt tillvägagångssätt: starta enkelt (i processen), dela när du har bevis du behöver.

Bortom grunderna: Hangfire

Medan IHostedService och BackgroundService är utmärkt för tjänster du äger och kontrollerar, ibland behöver du mer sofistikerade schemaläggning. Det är där bibliotek gillar Hangfire Hoppa in.

Hangfire ger:

  • Ihållande arbetsköer - Jobb överlever programstarter
  • Återkommande arbetstillfällen - Cron-stil schemaläggning
  • Dashboard- användargränssnitt - Se vad som körs, vad som misslyckas, försök igen.
  • Distribuerad körning - Flera servrar kan behandla samma jobbkö
  • Automatiska regressioner - Misslyckade jobb återställs automatiskt med exponentiell backoff

Här är ett enkelt exempel:

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

Din tjänst är bara en normal klass:

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

Handtag för hangfire:

  • Att se till att arbetet körs vid utsatt tid
  • Försöka igen om det misslyckas
  • Lagring av exekveringshistorik
  • Tillhandahålla en instrumentpanel för att övervaka allt
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

När du ska använda Hangfire:

  • Du behöver ihållande jobbköer som överlever omstarter
  • Du vill ha en instrumentpanel för att övervaka och manuellt trigga jobb
  • Du behöver distribuerad jobbhantering över flera servrar
  • Du vill ha inbyggd försökslogik och felhantering
  • Du behöver schemaläggning av återkommande uppgifter i cron-stil

När du ska hålla dig till IHostedService/BakgrundService:

  • Du behöver fin kontroll över servicens livscykel
  • Din tjänst måste reagera på händelser i realtid
  • Du vill minimera beroenden
  • Du bygger en enkel periodisk uppgift som inte behöver envishet.

Andra alternativ

Medan Hangfire är populärt, finns det andra bibliotek värda att överväga:

Kvarts.NET:

  • Flexibelare schemaläggning än Hangfire
  • Stöder cron- uttryck och kalenderbaserad schemaläggning
  • Kan finnas kvar i flera databaser
  • Mer komplext API men kraftfullare

Massöverföring/NServiceBus:

  • Fullfjädrad message bus implementationer
  • Bättre för distribuerade system och mikrotjänster
  • Stöd sagas (långgående arbetsflöden)
  • Steeper inlärningskurva

Azure-funktioner/AWS Lambda:

  • Om du är i molnet, anser serverless
  • Betala per utförande i stället för att hålla en tjänst igång
  • Automatisk skalning
  • Lite latens över huvudet för kallstart

Sammanfattning

I del 1 har vi behandlat de grundläggande tillvägagångssätten för bakgrundstjänster i ASP.NET Core:

  1. IHostedService - Grunden, maximal flexibilitet
  2. Bakgrundstjänst - Bekväm basklass för långgående loopar
  3. Uppstartssamordning - Se till att tjänsterna väntar på varandra.
  4. Fördelad samordning - Använda Redis för multi-instance scenarier
  5. Hangfire - När du behöver ihållande jobb och sofistikerad schemaläggning

De viktigaste lärdomarna:

  • Fyll alltid i kanaler och avbryt polletter i StopAsync
  • Bestäm om StartAsync ska blockera eller återvända omedelbart
  • Handtag OperationCanceledException graciöst
  • Använd Aktivitet.När någon med avstängning token för att respektera avstängning timeouts

Till Häfte 2, Vi ska undersöka verkliga implementeringar från en produktionsblogg plattform:

  • Filsystemväktare som synkroniserar nermarkerade filer till en databas
  • E-postavsändare med regresspolicyer och strömbrytare
  • Analys händelse köer som batch begäran
  • Semantiska sökindexörer som behandlar innehåll asynkront
  • Brutna länkkontroller som regelbundet validerar externa webbadresser

Dessa exempel visar mönstren från del 1 i praktiken, inklusive mönstret för startsamordning och korrekt hantering av avstängning.

Ytterligare läsning

Finding related posts...
logo

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