# Hintergrundtjänster i ASP.NET Core - Part 2: praktiska exempel

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

Teorin är en sak.;, produktionskod är en annan.., i del, 1 tog vi upp abstraktionerna.M SK3, nu låter ', se hur de appliceras i en riktig kodbas. ., det här artikelt går igenom faktiska bakgrundstjänster från den här bloggplattformen.

# Inleiding

I [Del 1](/blog/background-services-in-aspnetcore-part1), vi undersökte de grundläggande tillvägagångssätten för att implementera bakgrundstjänster i ASP `IHostedService`, `BackgroundService`, livscykelstyrning , och de vanliga knorrarna kring avstängning handlingen M SK2

Nu är det dags att se dessa mönster i praktiken.

- Att synkronisera markerdown-filer till en databas
- Kanal-baserade e-post köer med Pollys återsökande regler
- Analytik-uppförare som tar tag i bakgrunds begäran
- Semantiska sökindexer med hash-baserad förändringsdetektion
- Breckade länkarna som periodiskt validerar externa URLs
- Koordinering av uppstart mellan beroende tjänster
- Omfattning och enhetens ramverk livscykelstyre

Varje exempel illustrerar praktiska lösningar på vanliga problem som du kommer stöta på när du bygger produktionsgrundtjänster.

[TOC]

# exempel 1: File System Watcher med Polly Retry

Den första tjänsten vi kommer att undersöka tittar på ett katalog av markerdowns bloggar och bearbetar dem automatiskt när de ändras.

## Problemet

När en markerdown-Datei skapas eller modifieras:

1. Att läsa lêers innehåll
2. Förefatta markerdown-metadata ( titel , kategorier, publiceringsdatumM SK3
3. Rendera till HTML och extrahera enkel text
4. spara till databasen
5. Trigger översättning till andra språk
6. Index för semantiska sökningar

Utmaningen [`FileSystemWatcher`](https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher) händelser kan aktiveras medan pliken fortfarande är skriven, orsakar `IOException` när du försöker läsa den.

## Lösningen: MarkdownDirectoryWatcherService

```csharp
public class MarkdownDirectoryWatcherService(
    MarkdownConfig markdownConfig,
    IServiceScopeFactory serviceScopeFactory,
    IStartupCoordinator startupCoordinator,
    ILogger<MarkdownDirectoryWatcherService> logger)
    : IHostedService
{
    private FileSystemWatcher _fileSystemWatcher;
    private Task _awaitChangeTask = Task.CompletedTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _fileSystemWatcher = new FileSystemWatcher
        {
            Path = markdownConfig.MarkdownPath,
            NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite |
                          NotifyFilters.CreationTime | NotifyFilters.Size,
            Filter = "*.md",
            IncludeSubdirectories = true
        };

        _fileSystemWatcher.EnableRaisingEvents = true;

        // Start background processing in a separate task
        _awaitChangeTask = Task.Run(() => AwaitChanges(cancellationToken), cancellationToken);

        logger.LogInformation("Started watching directory {Directory}", markdownConfig.MarkdownPath);

        // Signal ready - watcher is set up and listening
        startupCoordinator.SignalReady(StartupServiceNames.MarkdownDirectoryWatcher);

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // Proper cleanup
        _fileSystemWatcher.EnableRaisingEvents = false;
        _fileSystemWatcher.Dispose();

        logger.LogInformation("Stopped watching directory");

        return Task.CompletedTask;
    }

    private async Task AwaitChanges(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            var fileEvent = _fileSystemWatcher.WaitForChanged(WatcherChangeTypes.All);

            if (fileEvent.ChangeType == WatcherChangeTypes.Changed ||
                fileEvent.ChangeType == WatcherChangeTypes.Created)
            {
                await OnChangedAsync(fileEvent);
            }
            else if (fileEvent.ChangeType == WatcherChangeTypes.Deleted)
            {
                await OnDeletedAsync(fileEvent);
            }
            else if (fileEvent.ChangeType == WatcherChangeTypes.Renamed)
            {
                await OnRenamedAsync(fileEvent);
            }
        }
    }
}
```

Notera några nyckelmönster:

1. **IHostedService, inte BackgroundService** - Vi behöver en fin kontroll över monitoruppsättningen
2. **Separera bakgrundsuppdrag** - Den `StartAsync` återvänder omedelbart, bearbetandet sker i `AwaitChanges`
3. **Koordinering för att starta** - Signaler när ' är redo för beroende tjänster
4. **Rättvis utplåning** - Deaktiverar och avlägsnar filesystemschauern i `StopAsync`

## Att hantera filelockproblem med Polly

Den mest intressanta delen är hur vi hanterar filer som fortfarande är skrivna [Polly](https://github.com/App-vNext/Polly) är en .NET motståndskraftsbibliotek som ger policyr för att återtesta

```csharp
private async Task OnChangedAsync(WaitForChangedResult e)
{
    if (e.Name == null) return;

    // Serilog activity for distributed tracing
    using var activity = Log.Logger.StartActivity("Markdown File Changed {Name}", e.Name);

    // Define a retry policy for file access issues
    var retryPolicy = Policy
        .Handle<IOException>() // Only handle IO exceptions (like file in use)
        .WaitAndRetryAsync(5, retryAttempt => TimeSpan.FromMilliseconds(500 * retryAttempt),
            (exception, timeSpan, retryCount, context) =>
            {
                activity?.Activity?.SetTag("Retry Attempt", retryCount);
                logger.LogWarning(
                    "File is in use, retrying attempt {RetryCount} after {TimeSpan}",
                    retryCount, timeSpan);
            });

    try
    {
        var fileName = e.Name;
        var isTranslated = Path.GetFileNameWithoutExtension(e.Name).Contains(".");
        var language = MarkdownBaseService.EnglishLanguage;
        var directory = markdownConfig.MarkdownPath;

        if (isTranslated)
        {
            language = Path.GetFileNameWithoutExtension(e.Name).Split('.').Last();
            fileName = Path.GetFileName(fileName);
            directory = markdownConfig.MarkdownTranslatedPath;
        }

        var filePath = Path.Combine(directory, fileName);

        using var scope = serviceScopeFactory.CreateScope();
        var blogService = scope.ServiceProvider.GetRequiredService<IBlogService>();

        // Use the Polly retry policy
        await retryPolicy.ExecuteAsync(async () =>
        {
            // Read the file - might throw IOException if locked
            var markdown = await File.ReadAllTextAsync(filePath);

            var slug = Path.GetFileNameWithoutExtension(fileName);
            if (isTranslated)
            {
                slug = slug.Split('.').First();
            }

            // Save to database
            var savedModel = await blogService.SavePost(slug, language, markdown);

            activity?.Activity?.SetTag("Page Processed", savedModel.Slug);

            // Index in semantic search (only for main directory files)
            if (!e.Name.Contains(Path.DirectorySeparatorChar))
            {
                await IndexPostForSemanticSearchAsync(scope, savedModel, language);
            }

            // Trigger translation for English posts
            if (language == MarkdownBaseService.EnglishLanguage &&
                !string.IsNullOrEmpty(savedModel.Markdown))
            {
                var translateService = scope.ServiceProvider
                    .GetRequiredService<IBackgroundTranslateService>();
                await translateService.TranslateForAllLanguages(
                    new PageTranslationModel
                    {
                        OriginalFileName = filePath,
                        OriginalMarkdown = savedModel.Markdown,
                        Persist = true
                    });
            }
        });

        activity?.Complete();
    }
    catch (Exception exception)
    {
        activity?.Complete(LogEventLevel.Error, exception);
    }
}
```

**Nyckelmönster här:**

1. **Polly-repetitionspolicy** - Exponentiell bakåtgången 500msM SK2 1s, ♫ 1.5s\, ♫ ♫
2. **Bara pröva IOException igen** - Andra undantag bultar upp
3. **Omfattningsfulla tjänster** - skapa en skala per file för att undvika livscykelproblem
4. **Strukturerad skogning** - Användande [Serilog](https://serilog.net/) aktiviteter för spårning
5. **Kaskaderingsoperationer** - spara → index → översätta

Återprobera-policyn hanterar det vanliga fallet där en textredigerare fortfarande skriver på filet när den ändrade händelsen aktiveras.

## Visuella flöden

```mermaid
graph TD
    A[File Modified] --> B[FileSystemWatcher Event]
    B --> C{File Locked?}
    C -->|Yes| D[Wait 500ms * Attempt]
    D --> E{Retry < 5?}
    E -->|Yes| C
    E -->|No| F[Log Error]
    C -->|No| G[Read File]
    G --> H[Parse Markdown]
    H --> I[Save to Database]
    I --> J{Main Directory?}
    J -->|Yes| K[Index for Search]
    J -->|No| L{English?}
    K --> L
    L -->|Yes| M[Trigger Translation]
    L -->|No| N[Complete]
    M --> N

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style I stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style K stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
    style M stroke:#d97706,stroke-width:3px,color:#f59e0b
```

Registrera den i `Program.cs` (allt hostade tjänster följer detta mönster

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

[Se fulla implementering här.](https://github.com/scottgal/mostlylucidweb/blob/main/Mostlylucid/Blog/WatcherService/MarkdownDirectoryWatcherService.cs)

# exempel 2: Kanall -baserad e-postlinje

Att skicka e-mail är ett klassiskt sätt att använda bakgrundstjänster.

## Problemet

När en användare skickar en kommentar eller ett Kontaktformular:

1. Validatera och lagra in bidraget
2. Kväva ett e-postmeddelande
3. Ge tillbaka svaret omedelbart (don't blockera på SMTP
4. Send e-mail i bakgrunden med återprobera logik
5. Handle SMTP misslyckanden elegant (kretsbrytare)

## Lösningen: EmailSenderHostedService

Den här tjänsten använder [`Channel<T>`](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels) för köen och Polly för motståndskraft

```csharp
public class EmailSenderHostedService : IEmailSenderHostedService
{
    private readonly Channel<BaseEmailModel> _mailMessages =
        Channel.CreateUnbounded<BaseEmailModel>();
    private readonly CancellationTokenSource _cancellationTokenSource = new();
    private Task _sendTask = Task.CompletedTask;
    private readonly IEmailService _emailService;
    private readonly ILogger<EmailSenderHostedService> _logger;
    private readonly IAsyncPolicy _policyWrap;

    public EmailSenderHostedService(
        IEmailService emailService,
        ILogger<EmailSenderHostedService> logger)
    {
        _emailService = emailService;
        _logger = logger;

        // Retry policy: 3 attempts with exponential backoff
        var retryPolicy = Policy
            .Handle<SmtpException>()
            .WaitAndRetryAsync(3,
                attempt => TimeSpan.FromSeconds(2 * attempt),
                (exception, timeSpan, retryCount, context) =>
                {
                    logger.LogWarning(exception,
                        "Retry {RetryCount} for sending email failed", retryCount);
                });

        // Circuit breaker: open after 5 failures, stay open for 1 minute
        var circuitBreakerPolicy = Policy
            .Handle<SmtpException>()
            .CircuitBreakerAsync(
                5,
                TimeSpan.FromMinutes(1),
                onBreak: (exception, timespan) =>
                {
                    logger.LogError(
                        "Circuit broken due to too many failures. Breaking for {BreakDuration}",
                        timespan);
                },
                onReset: () =>
                {
                    logger.LogInformation("Circuit reset. Resuming email delivery.");
                },
                onHalfOpen: () =>
                {
                    logger.LogInformation("Circuit in half-open state. Testing connection...");
                });

        // Combine retry and circuit breaker
        _policyWrap = Policy.WrapAsync(retryPolicy, circuitBreakerPolicy);
    }

    public async Task SendEmailAsync(BaseEmailModel message)
    {
        await _mailMessages.Writer.WriteAsync(message);
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting background e-mail delivery");
        _sendTask = DeliverAsync(_cancellationTokenSource.Token);
        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Stopping background e-mail delivery");

        // Proper shutdown sequence
        await _cancellationTokenSource.CancelAsync();
        _mailMessages.Writer.Complete(); // Critical: complete the channel

        // Wait for the background task to finish
        await Task.WhenAny(_sendTask, Task.Delay(Timeout.Infinite, cancellationToken));
    }

    private async Task DeliverAsync(CancellationToken token)
    {
        _logger.LogInformation("E-mail background delivery started");

        try
        {
            // Process items as they arrive
            while (await _mailMessages.Reader.WaitToReadAsync(token))
            {
                BaseEmailModel? message = null;
                try
                {
                    message = await _mailMessages.Reader.ReadAsync(token);

                    // Execute with retry policy and circuit breaker
                    await _policyWrap.ExecuteAsync(async () =>
                    {
                        switch (message)
                        {
                            case ContactEmailModel contactEmailModel:
                                await _emailService.SendContactEmail(contactEmailModel);
                                break;
                            case CommentEmailModel commentEmailModel:
                                await _emailService.SendCommentEmail(commentEmailModel);
                                break;
                            case ConfirmEmailModel confirmEmailModel:
                                await _emailService.SendConfirmationEmail(confirmEmailModel);
                                break;
                        }
                    });

                    _logger.LogInformation("Email from {SenderEmail} sent", message.SenderEmail);
                }
                catch (OperationCanceledException)
                {
                    break; // Shutdown requested
                }
                catch (Exception exc)
                {
                    _logger.LogError(exc,
                        "Couldn't send an e-mail from {SenderEmail}",
                        message?.SenderEmail);
                }
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogWarning("E-mail background delivery canceled");
        }

        _logger.LogInformation("E-mail background delivery stopped");
    }

    public void Dispose()
    {
        _cancellationTokenSource.Dispose();
    }
}
```

**Nyckliga mönster demonstrerade:**

1. **Kanal som kö** - Kanal utan gränser för meddelanden
2. **Politisk ompackning** - Försök igen inuti strömbrytare
3. **Ganska avstängning** - Släng kanalen , avfärda token , vänta på uppgift
4. **Pattern matchning** - Switcha på olika typer av e-post
5. **Omfattningen av livslängden** - Erstellt en gång som en enda ton , håller ett långtM SK2livs tillstånd

## Retry och kretsbrytare visualiserad

```mermaid
graph TD
    A[Queue Email] --> B[Write to Channel]
    B --> C[Background Task Reads]
    C --> D{Send Email}
    D -->|Success| E[Log Success]
    D -->|SmtpException| F{Retry Count < 3?}
    F -->|Yes| G[Wait 2s * Attempt]
    G --> D
    F -->|No| H{Circuit Breaker}
    H -->|< 5 Failures| I[Log Failure]
    H -->|5+ Failures| J[Open Circuit]
    J --> K[Wait 1 Minute]
    K --> L[Half-Open: Test]
    L -->|Success| M[Close Circuit]
    L -->|Failure| J
    E --> N[Continue]
    I --> N

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

Detta visar ett produktionsmönster

- **Försök igen** hanterar tillfälliga misslyckanden (tidliga nätverksproblem
- **Serkretsbrytare** förhindrar kaskaderande misslyckanden. ( om SMTP är nedåt.
- **Kanal** tillhandahåller naturlig baktryck (om vi kan ' inte skicka

# exempel 3: Analytik Event Sender (Umami)

Den här tjänsten sätter upp queues för att analysera händelser och skickar dem till en [Umami](https://umami.is/) Analytik-servern. Det ' liknar e-posttjänsterna men är enklare

```csharp
public class UmamiBackgroundSender(
    IServiceScopeFactory scopeFactory,
    ILogger<UmamiBackgroundSender> logger) : IHostedService
{
    private readonly CancellationTokenSource _cancellationTokenSource = new();
    private readonly Channel<SendBackgroundPayload> _channel =
        Channel.CreateUnbounded<SendBackgroundPayload>();
    private Task _sendTask = Task.CompletedTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _sendTask = SendRequest(_cancellationTokenSource.Token);
        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("UmamiBackgroundSender is stopping.");

        // Standard shutdown pattern
        await _cancellationTokenSource.CancelAsync();
        _channel.Writer.Complete();

        try
        {
            await Task.WhenAny(_sendTask, Task.Delay(Timeout.Infinite, cancellationToken));
        }
        catch (OperationCanceledException)
        {
            logger.LogWarning("StopAsync operation was canceled.");
        }
    }

    public async Task TrackPageView(string url, string title, UmamiPayload? payload = null)
    {
        await using var scope = scopeFactory.CreateAsyncScope();
        var payloadService = scope.ServiceProvider.GetRequiredService<PayloadService>();
        var sendPayload = payloadService.PopulateFromPayload(payload, null);
        sendPayload.Url = url;
        sendPayload.Title = title;

        await _channel.Writer.WriteAsync(new SendBackgroundPayload("event", sendPayload));
        logger.LogInformation("Umami pageview event queued");
    }

    private async Task SendRequest(CancellationToken token)
    {
        logger.LogInformation("Umami background delivery started");

        // Double while loop: outer waits for items, inner drains all available
        while (await _channel.Reader.WaitToReadAsync(token))
        {
            while (_channel.Reader.TryRead(out var payload))
            {
                try
                {
                    using var scope = scopeFactory.CreateScope();
                    var client = scope.ServiceProvider.GetRequiredService<UmamiClient>();

                    await client.Send(payload.Payload, type: payload.EventType);

                    logger.LogInformation("Umami background event sent: {EventType}",
                        payload.EventType);
                }
                catch (OperationCanceledException)
                {
                    logger.LogWarning("Umami background delivery canceled.");
                    return;
                }
                catch (Exception ex)
                {
                    logger.LogError(ex, "Error sending Umami background event.");
                }
            }
        }
    }

    private record SendBackgroundPayload(string EventType, UmamiPayload Payload);
}
```

**Nyckelskillnader från e-posten:**

1. **Ingen motståndskraftspolitik** - Analytiker är inte kritiska, ' , om det misslyckas så går vi bara in och fortsätter
2. **Dubblering medan kretslopp** - Inre kretslopp tömmer alla tillgängliga saker för effektivitet
3. **Serviceskapande i stor skala** - Varje sändning får ett nytt utrymme för HTTP: s klient

Detta visar att inte alla bakgrundstjänster behöver komplexa felbehandlingar.

# exempel 4: periodisk bakgrundsarbete med BackgroundService

Låt oss nu titta på tjänster som utför periodisk arbete istället för att processa köer. `BrokenLinkCheckerBackgroundService` kontrollerar bloggpost länkar periodiskt och tar fram arkiven.org URLs för brutna

```csharp
public class BrokenLinkCheckerBackgroundService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<BrokenLinkCheckerBackgroundService> _logger;
    private readonly HttpClient _httpClient;
    private readonly TimeSpan _checkInterval = TimeSpan.FromHours(1);
    private const int BatchSize = 20;

    public BrokenLinkCheckerBackgroundService(
        IServiceProvider serviceProvider,
        ILogger<BrokenLinkCheckerBackgroundService> logger,
        IHttpClientFactory httpClientFactory)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
        _httpClient = httpClientFactory.CreateClient("BrokenLinkChecker");
        _httpClient.Timeout = TimeSpan.FromSeconds(30);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Broken Link Checker Background Service started");

        // Initial delay to let the application fully start
        await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await CheckLinksAsync(stoppingToken);
                await FetchArchiveUrlsAsync(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in Broken Link Checker Background Service");
            }

            _logger.LogInformation("Broken Link Checker sleeping for {Interval}", _checkInterval);
            await Task.Delay(_checkInterval, stoppingToken);
        }

        _logger.LogInformation("Broken Link Checker Background Service stopped");
    }

    private async Task CheckLinksAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting link validity check");

        using var scope = _serviceProvider.CreateScope();
        var brokenLinkService = scope.ServiceProvider.GetRequiredService<IBrokenLinkService>();

        var linksToCheck = await brokenLinkService.GetLinksToCheckAsync(BatchSize, cancellationToken);
        _logger.LogInformation("Found {Count} links to check", linksToCheck.Count);

        foreach (var link in linksToCheck)
        {
            if (cancellationToken.IsCancellationRequested) break;

            try
            {
                var (statusCode, isBroken, error) = await CheckUrlAsync(link.OriginalUrl, cancellationToken);
                await brokenLinkService.UpdateLinkStatusAsync(
                    link.Id, statusCode, isBroken, error, cancellationToken);

                if (isBroken)
                {
                    _logger.LogWarning("Link is broken: {Url} (Status: {StatusCode})",
                        link.OriginalUrl, statusCode);
                }

                // Be respectful to servers
                await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error checking link: {Url}", link.OriginalUrl);
                await brokenLinkService.UpdateLinkStatusAsync(
                    link.Id, 0, true, ex.Message, cancellationToken);
            }
        }
    }

    private async Task<(int statusCode, bool isBroken, string? error)> CheckUrlAsync(
        string url,
        CancellationToken cancellationToken)
    {
        try
        {
            using var request = new HttpRequestMessage(HttpMethod.Head, url);
            request.Headers.UserAgent.ParseAdd(
                "Mozilla/5.0 (compatible; MostlylucidBot/1.0; +https://www.mostlylucid.net)");

            using var response = await _httpClient.SendAsync(
                request,
                HttpCompletionOption.ResponseHeadersRead,
                cancellationToken);

            var statusCode = (int)response.StatusCode;
            var isBroken = response.StatusCode == HttpStatusCode.NotFound ||
                          response.StatusCode == HttpStatusCode.Gone ||
                          statusCode >= 500;

            return (statusCode, isBroken, null);
        }
        catch (HttpRequestException ex)
        {
            return (0, true, ex.Message);
        }
        catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
        {
            return (0, true, "Request timed out");
        }
    }
}
```

**Patterns demonstrerade:**

1. **BackgroundService basklass** - Perfekt för periodiskt arbete
2. **Initiellt fördröjning** - Vänta till att andra tjänster ska initialiseras
3. **Batchning** - Process 20 länkar vid en tidpunkt
4. **Limitering av hastighet** - 2-sekundlig fördröjning mellan kontroller
5. **Serviceskapande i stor skala** - Nya områden per lager
6. **Storlissementstoken-kontroll** - Breka tidigt om avstängning är begärd

# exempel 5: Semantiskt sökindexering med Hash-Integnings av förändringar baserat

Den semantiska sökindexen är mer sofistikerad.

```csharp
public class SemanticIndexingBackgroundService : BackgroundService
{
    private readonly ILogger<SemanticIndexingBackgroundService> _logger;
    private readonly IServiceProvider _serviceProvider;
    private readonly ISemanticSearchService _semanticSearchService;
    private readonly MarkdownConfig _markdownConfig;
    private readonly SemanticSearchConfig _semanticSearchConfig;
    private readonly TimeSpan _indexInterval = TimeSpan.FromHours(1);
    private readonly TimeSpan _startupDelay = TimeSpan.FromSeconds(30);

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        if (!_semanticSearchConfig.Enabled)
        {
            _logger.LogInformation("Semantic search is disabled, indexing service will not run");
            return;
        }

        _logger.LogInformation("Semantic indexing background service starting...");

        // Wait for other services to initialise
        await Task.Delay(_startupDelay, stoppingToken);

        // Initialise the semantic search service
        try
        {
            await _semanticSearchService.InitializeAsync(stoppingToken);
            _logger.LogInformation("Semantic search initialized successfully");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to initialize semantic search service");
            return;
        }

        // Initial indexing
        await IndexAllMarkdownFilesAsync(stoppingToken);

        // Periodic re-indexing to catch any changes
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(_indexInterval, stoppingToken);
                await IndexAllMarkdownFilesAsync(stoppingToken);
            }
            catch (OperationCanceledException)
            {
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error during periodic indexing");
            }
        }

        _logger.LogInformation("Semantic indexing background service stopped");
    }

    private async Task IndexAllMarkdownFilesAsync(CancellationToken stoppingToken)
    {
        var markdownPath = _markdownConfig.MarkdownPath;

        if (!Directory.Exists(markdownPath))
        {
            _logger.LogWarning("Markdown directory does not exist: {Path}", markdownPath);
            return;
        }

        // Get ONLY files in the main directory, NOT subdirectories
        var markdownFiles = Directory.GetFiles(markdownPath, "*.md", SearchOption.TopDirectoryOnly);

        _logger.LogInformation("Found {Count} markdown files to index in {Path}",
            markdownFiles.Length, markdownPath);

        var indexedCount = 0;
        var skippedCount = 0;
        var errorCount = 0;

        using var scope = _serviceProvider.CreateScope();
        var markdownRenderingService = scope.ServiceProvider
            .GetRequiredService<MarkdownRenderingService>();

        foreach (var filePath in markdownFiles)
        {
            if (stoppingToken.IsCancellationRequested)
                break;

            try
            {
                var result = await IndexMarkdownFileAsync(
                    filePath,
                    markdownRenderingService,
                    stoppingToken);

                if (result == IndexResult.Indexed)
                    indexedCount++;
                else if (result == IndexResult.Skipped)
                    skippedCount++;
            }
            catch (Exception ex)
            {
                errorCount++;
                _logger.LogError(ex, "Error indexing file: {FilePath}", filePath);
            }

            // Delay to avoid overwhelming the embedding service
            await Task.Delay(100, stoppingToken);
        }

        _logger.LogInformation(
            "Indexing complete: {Indexed} indexed, {Skipped} skipped (unchanged), {Errors} errors",
            indexedCount, skippedCount, errorCount);
    }

    private async Task<IndexResult> IndexMarkdownFileAsync(
        string filePath,
        MarkdownRenderingService markdownRenderingService,
        CancellationToken stoppingToken)
    {
        var fileName = Path.GetFileNameWithoutExtension(filePath);

        // Skip translated files (they have language suffix like .es.md, .fr.md)
        if (fileName.Contains('.'))
        {
            var parts = fileName.Split('.');
            if (parts.Length >= 2 && parts[^1].Length == 2)
            {
                return IndexResult.Skipped;
            }
        }

        var markdown = await File.ReadAllTextAsync(filePath, stoppingToken);
        var fileInfo = new FileInfo(filePath);

        var blogPost = markdownRenderingService.GetPageFromMarkdown(
            markdown,
            fileInfo.LastWriteTimeUtc,
            filePath);

        if (blogPost.IsHidden)
        {
            _logger.LogDebug("Skipping hidden post: {Slug}", blogPost.Slug);
            return IndexResult.Skipped;
        }

        // Compute content hash
        var contentHash = ComputeContentHash(blogPost.PlainTextContent);

        // Check if reindexing is needed
        var needsReindex = await _semanticSearchService.NeedsReindexingAsync(
            blogPost.Slug,
            MarkdownBaseService.EnglishLanguage,
            contentHash,
            stoppingToken);

        if (!needsReindex)
        {
            _logger.LogDebug("Skipping unchanged post: {Slug}", blogPost.Slug);
            return IndexResult.Skipped;
        }

        // Create document for indexing
        var document = new BlogPostDocument
        {
            Id = $"{blogPost.Slug}_{MarkdownBaseService.EnglishLanguage}",
            Slug = blogPost.Slug,
            Title = blogPost.Title,
            Content = blogPost.PlainTextContent,
            Language = MarkdownBaseService.EnglishLanguage,
            Categories = blogPost.Categories.ToList(),
            PublishedDate = blogPost.PublishedDate,
            ContentHash = contentHash
        };

        await _semanticSearchService.IndexPostAsync(document, stoppingToken);
        _logger.LogInformation("Indexed post: {Slug}", blogPost.Slug);

        return IndexResult.Indexed;
    }

    private static string ComputeContentHash(string content)
    {
        using var sha256 = SHA256.Create();
        var bytes = Encoding.UTF8.GetBytes(content);
        var hashBytes = sha256.ComputeHash(bytes);
        return Convert.ToBase64String(hashBytes);
    }

    private enum IndexResult
    {
        Indexed,
        Skipped
    }
}
```

**Nyckelmönster:**

1. **Konfiguration-driven** - Bara körs om den semantiska sökningen är aktiverad
2. **Hash-baserad förändringsdetektor** - SHA 256 hash av innehålletM SK2 hoppa om det inte förändras
3. **Limitering av hastighet** - 100 ms fördröjning mellan indekseringsoperationer
4. **Filterat bearbetande** - Spring över över översatta plikter och dolda meddelanden
5. **detaljerade mätvärden** - Spurindexerad / skippd/ feltal

# exempel 6: Startkoordination

Det sista exemplet visar hur tjänster kan koordinera deras startup. `MarkdownReAddPostsService` re-bearbetar alla markerdown-filer på startup. (användbar efter schemaförändringar, men signalerar när det är ready, så att andra tjänster vet att de kan gå vidare.

```csharp
public class MarkdownReAddPostsService(
    MarkdownConfig markdownConfig,
    IServiceScopeFactory serviceScopeFactory,
    IStartupCoordinator startupCoordinator,
    ILogger<MarkdownReAddPostsService> logger) : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        if (!markdownConfig.ReAddPosts)
        {
            logger.LogInformation("ReAddPosts is disabled, skipping bulk post re-add");
            // Signal ready immediately since we're not doing anything
            startupCoordinator.SignalReady(StartupServiceNames.MarkdownReAddPosts);
            return;
        }

        logger.LogWarning("ReAddPosts is ENABLED - re-processing ALL markdown posts on startup");

        // Run in background so it doesn't block startup
        _ = Task.Run(async () =>
        {
            try
            {
                // Small delay to let other services initialize
                await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
                await ReAddAllPostsAsync(cancellationToken);
            }
            finally
            {
                // Signal ready when complete (even if there were errors)
                startupCoordinator.SignalReady(StartupServiceNames.MarkdownReAddPosts);
            }
        }, cancellationToken);
    }

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

    private async Task ReAddAllPostsAsync(CancellationToken cancellationToken)
    {
        using var activity = Log.Logger.StartActivity("ReAdd All Posts");
        var stopwatch = Stopwatch.StartNew();

        try
        {
            using var scope = serviceScopeFactory.CreateScope();
            var blogService = scope.ServiceProvider.GetRequiredService<IBlogService>();
            var markdownBlogService = scope.ServiceProvider.GetRequiredService<IMarkdownBlogService>();
            var semanticSearchService = scope.ServiceProvider.GetService<ISemanticSearchService>();

            // Process English files
            var markdownFiles = Directory.GetFiles(
                markdownConfig.MarkdownPath,
                "*.md",
                SearchOption.TopDirectoryOnly);

            logger.LogInformation("Found {Count} English markdown files to process",
                markdownFiles.Length);

            int processed = 0;
            int failed = 0;

            foreach (var filePath in markdownFiles)
            {
                if (cancellationToken.IsCancellationRequested) break;

                var slug = Path.GetFileNameWithoutExtension(filePath);
                try
                {
                    var blogModel = await markdownBlogService.GetPage(filePath);
                    blogModel.Language = MarkdownBaseService.EnglishLanguage;
                    await blogService.SavePost(blogModel);

                    // Index in semantic search
                    if (semanticSearchService != null)
                    {
                        await IndexPostAsync(semanticSearchService, blogModel,
                            MarkdownBaseService.EnglishLanguage);
                    }

                    processed++;
                    if (processed % 10 == 0)
                    {
                        logger.LogInformation("Processed {Count}/{Total} English posts",
                            processed, markdownFiles.Length);
                    }
                }
                catch (Exception ex)
                {
                    failed++;
                    logger.LogError(ex, "Failed to process {Slug}", slug);
                }
            }

            // Process translated files
            var translatedFiles = Directory.GetFiles(
                markdownConfig.MarkdownTranslatedPath,
                "*.md",
                SearchOption.TopDirectoryOnly);

            logger.LogInformation("Found {Count} translated markdown files to process",
                translatedFiles.Length);

            int translatedProcessed = 0;
            int translatedFailed = 0;

            foreach (var filePath in translatedFiles)
            {
                if (cancellationToken.IsCancellationRequested) break;

                var fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
                if (!fileNameWithoutExt.Contains(".")) continue;

                var parts = fileNameWithoutExt.Split('.');
                var slug = parts[0];
                var language = parts[^1];

                try
                {
                    var blogModel = await markdownBlogService.GetPage(filePath);
                    blogModel.Language = language;
                    await blogService.SavePost(blogModel);

                    if (semanticSearchService != null)
                    {
                        await IndexPostAsync(semanticSearchService, blogModel, language);
                    }

                    translatedProcessed++;
                    if (translatedProcessed % 50 == 0)
                    {
                        logger.LogInformation("Processed {Count}/{Total} translated posts",
                            translatedProcessed, translatedFiles.Length);
                    }
                }
                catch (Exception ex)
                {
                    translatedFailed++;
                    logger.LogError(ex, "Failed to process translated post {Slug} ({Language})",
                        slug, language);
                }
            }

            stopwatch.Stop();

            activity?.Activity?.SetTag("English Processed", processed);
            activity?.Activity?.SetTag("English Failed", failed);
            activity?.Activity?.SetTag("Translated Processed", translatedProcessed);
            activity?.Activity?.SetTag("Translated Failed", translatedFailed);
            activity?.Activity?.SetTag("Duration", stopwatch.Elapsed.ToString());
            activity?.Complete();

            logger.LogWarning(
                "ReAddPosts completed in {Duration}. English: {EnglishProcessed}/{EnglishTotal}, Translated: {TranslatedProcessed}/{TranslatedTotal}",
                stopwatch.Elapsed,
                processed, markdownFiles.Length,
                translatedProcessed, translatedFiles.Length);
        }
        catch (Exception ex)
        {
            activity?.Complete(LogEventLevel.Error, ex);
            logger.LogError(ex, "Error during ReAddPosts");
        }
    }
}
```

**Nyckelmönster:**

1. **Koordinering för att starta** - Signaler när de är redo för beroende tjänster
2. **brinna och glömma med slutligen** - Uppgift .Run with finally block ensures signal
3. **Protokollering av framgångar** - logga in varenda 10 engelska Beiträge
4. **Strukturerad aktivitet** - Serilog-aktivitet med tag för observationalitet
5. **Två stadier för processning** - engelska archive först , sedan översatta archive

## Koordineringsflödet

```mermaid
graph TD
    A[App Starts] --> B[MarkdownReAddPostsService.StartAsync]
    B --> C{ReAddPosts Enabled?}
    C -->|No| D[Signal Ready Immediately]
    C -->|Yes| E[Start Background Task]
    E --> F[Wait 5 Seconds]
    F --> G[Process All English Posts]
    G --> H[Process All Translated Posts]
    H --> I[Signal Ready in Finally]
    D --> J[Other Services Can Start]
    I --> J
    J --> K[MarkdownDirectoryWatcher Starts]
    J --> L[SemanticIndexer Starts]

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style I stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style J stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
```

Ni kommer att lägga märke till varje exempel ovanför som används `IServiceScopeFactory` att skapa skalor. Det här är inte optionellt ' det är nödvändigt för att arbeta med Entity Framework och andra skalerade tjänster

# Hintergrundtjänster och utsträckning

En av de svåraste aspekterna i bakgrundstjänsterna är att hantera livslängden. [Enhetens ramskärna](https://learn.microsoft.com/en-us/ef/core/). Här, ', är vad du behöver veta.

## Problemet : Singleton vs Scoped

Hintergrundtjänster registrerade med `AddHostedService<T>()` är **Isomer:**. De ' skapas en gång och finns kvar i hela applikationens livstid. **utspända**:

- `DbContext` - Standardinriktad  och av god anledning
- `IHttpContextAccessor` - Inriktat på HTTP-ansökan
- Alla tjänster registrerade med `AddScoped<T>()`

Om du försöker injicera en scoped service i en en enda ton, får du den här undantagen.

```
Cannot consume scoped service 'YourDbContext' from singleton 'YourBackgroundService'
```

## Varför är DbContext

Enhetens ramverk's `DbContext` följer enheter och sköter databaseuppkopplingar. Den ' är designad för att vara kort

```csharp
// BAD: DbContext lives forever, accumulates tracked entities,
// connections may timeout, change tracking becomes stale
public class BrokenService : BackgroundService
{
    private readonly MyDbContext _dbContext; // Injected once, lives forever

    public BrokenService(MyDbContext dbContext)
    {
        _dbContext = dbContext; // This context will never be disposed!
    }
}
```

Problemet med en lång DbContext

- **minnesmängder** - spårade enheter samlas
- **Stalla data** - Veranderaren ser inga externa förändringar
- **Koppningsproblem** - Anslutningar kan vara avstängda eller återvunna
- **samtida bugs** - DbKontext är inte rädd

## Lösningen: IServiceScopeFactory

Varje exempel i denna artikel använder samma mönster

```csharp
public class CorrectService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public CorrectService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory; // This is a singleton, safe to inject
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Create a new scope for each iteration
            using var scope = _scopeFactory.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();

            // DbContext is fresh, will be disposed when scope ends
            await ProcessWorkAsync(dbContext, stoppingToken);

            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
        // Scope disposed here - DbContext disposed, connections returned to pool
    }
}
```

## Skoopgranularitet

Hur ofta ska du skapa en ny scope? Det beror på din arbetsbela

**Varje gång** ( vanligaste

```csharp
while (!stoppingToken.IsCancellationRequested)
{
    using var scope = _scopeFactory.CreateScope();
    // Process batch...
}
```

**För varje sak** ( när man bearbetar en kö

```csharp
await foreach (var item in channel.Reader.ReadAllAsync(stoppingToken))
{
    using var scope = _scopeFactory.CreateScope();
    // Process single item...
}
```

**Per stycke** ( när du behöver transaktionens semantik

```csharp
var items = await GetBatchAsync(100);
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();

await using var transaction = await dbContext.Database.BeginTransactionAsync();
foreach (var item in items)
{
    // Process within same transaction...
}
await transaction.CommitAsync();
```

## Async- utrymme

För async-operationer prefererar `CreateAsyncScope()` som ordentligt hanterar async hantering:

```csharp
await using var scope = _scopeFactory.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
await dbContext.SaveChangesAsync(stoppingToken);
// Scope disposed asynchronously
```

## Vanliga misstag

**1. Capturing scoped services in closures**

```csharp
// BAD: dbContext captured, will outlive its scope
var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
_ = Task.Run(async () =>
{
    await Task.Delay(10000);
    await dbContext.SaveChangesAsync(); // Scope already disposed!
});
```

**2. Att återanvända ett område tvärsöver asynka gränser**

```csharp
// BAD: Scope should match unit of work
using var scope = _scopeFactory.CreateScope();
while (!stoppingToken.IsCancellationRequested)
{
    var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
    await ProcessAsync(dbContext); // Same stale context every iteration
    await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
```

**3. Att glömma bort att lagra bort**

```csharp
// BAD: Scope never disposed, DbContext leaks
var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
await ProcessAsync(dbContext);
// Where's the dispose?
```

Använd alltid `using` påståenden eller `await using` för async skalor.

# Summary: Patterns över alla exempel

När man tittar över alla dessa verkliga ”-” -tjänster, ”,“ -utvisar sig flera mönster.

## 1. Rättvis avstängde hantering

Varje tjänst följer samma avstängningsmönster:

```csharp
public async Task StopAsync(CancellationToken cancellationToken)
{
    // 1. Cancel the cancellation token
    await _cancellationTokenSource.CancelAsync();

    // 2. Complete any channels
    _channel.Writer.Complete();

    // 3. Wait for background tasks with timeout
    await Task.WhenAny(_backgroundTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
```

## 2. Serviceskapande i utsträckning

Hintergrundtjänsterna är singletons, men de behöver ofta avgränsade tjänster ( som DbContext ). Varje exempel skapar avgränsningar

```csharp
using var scope = _serviceProvider.CreateScope();
var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();
```

## 3. Stabilitetsmönster

Olika tjänster använder olika motståndskraftsstrategier:

- **Polly försöker igen** - File system watcher ( löser låstade filer)
- **Polly-kretsbrytare** - E-post skickare ( förhindrar SMTP-hammering )
- **Limitering av hastighet** - Linkchecker och indexer  var snäll mot externa tjänster
- **Hash-baserad förändringsdetektor** - Semantisk indexator  undvika dyra omställningar

## 4. Observerbarhet

Varje tjänst inklusive komplett loging:

- **Startup/avstängningar** - Den är alltid logged
- **Framsteg mätningar** - Batch processing logs counts
- **Serilog- aktiviteter** - För fördelade spårning
- **Fout med att logga in i sammanhanget** - Nog information för att diagnostisera problem

## 5. Konfiguration

Servicekontroll Konfiguration:

```csharp
if (!_config.Enabled)
{
    _logger.LogInformation("Service is disabled");
    return;
}
```

Det gör att du kan aktivera de här egenskaperna utan kodförändringar

# Slutsatsen

De här verkliga exemplen visar att bakgrundstjänsterna i ASP kan hantera avancerade scenarior

- **Event-driven processing** med filesystems observatörer
- **Processning baserat på queue** med kanaler
- **Tydliga uppgifter** med BackgroundService
- **Stabila operationer** med Polly-politiker
- **Koordinerad uppstart** med egna koordineringstjänster
- **Effektiv förändringsdetektion** med innehållshackning

De viktigaste lärdomarna:

1. **Att alltid komplettera kanaler och avfärda tokens i StopAsync**
2. **Skep skalor för skalerade tjänster**
3. **Välj den rätta motståndskraftsmönstret för ditt scenario**
4. **Protokollera fullständigt för observabilitet**
5. **Handle OperationCanceledException elegant**
6. **Använd startkoordination när tjänster är beroende av varandra**

Med dessa mönster, kan du bygga produktionen - klassificera bakgrundstjänster som är motståndskraftiga , synliga M SK3 och enkla att underhålla

# Fortsättning avläsning

- [Del 1: Serviceförhållningssätt](/blog/background-services-in-aspnetcore-part1)
- [Polly Dokumentation](https://github.com/App-vNext/Polly)
- [System.Threading.Channel](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels)
- [Microsoft Docs: arbetartjänster](https://learn.microsoft.com/en-us/dotnet/core/extensions/workers)