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
Hintergrunddienste im ASP.NET Core - Teil 2: Praxisbeispiele
Thursday, 27 November 2025
Theorie ist eine Sache; Produktionscode ist eine andere. In Teil 1 haben wir die Abstraktionen behandelt – jetzt wollen wir sehen, wie sie in einer echten Codebase angewendet werden. Dieser Artikel geht durch die eigentlichen Hintergrunddienste dieser Blog-Plattform: Dateibeobachter mit Polly-Retry-Richtlinien, kanalbasierte E-Mail-Wartenschlangen mit Schaltungsbrechern, semantische Suchindexer mit Hash-basierter Änderungserkennung und vieles mehr.
Einleitung
Im Teil 1, wir erforschten die grundlegenden Ansätze für die Umsetzung von Hintergrund-Services in ASP.NET Core. IHostedService, BackgroundService, Lebenszyklus-Management und die häufigsten Fallstricke rund um die Abschaltung Handhabung.
Jetzt ist es an der Zeit, diese Muster in Aktion zu sehen. In diesem Artikel werden wir reale Hintergrunddienste von einer Produktions-Blog-Plattform untersuchen und demonstrieren:
- Dateisystembeobachter, die Markdown-Dateien mit einer Datenbank synchronisieren
- Channel-basierte E-Mail-Warteschlangen mit Polly-Retry-Richtlinien
- Analyse-Ereignis-Sender, die Batch-Hintergrund-Anfragen
- Semantische Suchindexer mit Hash-basierter Änderungserkennung
- Gebrochene Link-Checker, die regelmäßig externe URLs validieren
- Unternehmenskoordination zwischen abhängigen Dienstleistungen
- Scoping and Entity Framework Lifecycle Management
Jedes Beispiel zeigt praktische Lösungen für gemeinsame Probleme, denen Sie beim Bau von Hintergrunddiensten begegnen werden.
Beispiel 1: Dateisystembeobachter mit Polly Retry
Der erste Dienst, den wir untersuchen, ist ein Verzeichnis von Markdown-Blog-Posts und verarbeitet sie automatisch, wenn sie sich ändern. Dies ist ein perfektes Beispiel für ereignisgesteuerte Hintergrundverarbeitung.
Das Problem
Wenn eine Markdown-Datei erstellt oder geändert wird:
- Lesen Sie den Inhalt der Datei
- Parse Markdown Metadaten (Titel, Kategorien, Datum veröffentlichen)
- Render an HTML und extrahieren Sie Klartext
- In der Datenbank speichern
- Übersetzung in andere Sprachen auslösen
- Index für semantische Suche
Die Herausforderung: FileSystemWatcher Ereignisse können feuern, während die Datei noch geschrieben wird, was IOException wenn du versuchst, es zu lesen.
Die Lösung: MarkdownDirectoryWatcherService
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);
}
}
}
}
Beachten Sie einige Schlüsselmuster:
- IHostedService, nicht BackgroundService - Wir brauchen eine gute Kontrolle über das Wächter-Setup.
- Getrennte Hintergrundaufgabe - Das
StartAsyncgibt sofort zurück, die Verarbeitung erfolgt inAwaitChanges - Koordinierung der Unternehmensgründungen - Signale, wenn es für abhängige Dienste bereit ist
- Zweckmäßige Beseitigung - Deaktiviert und verfügt über den Dateisystem-Beobachter in
StopAsync
Behandlung von Dateisperrproblemen mit Polly
Der interessanteste Teil ist, wie wir mit Dateien umgehen, die noch geschrieben werden. Polly. - (EN) Nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein, nein. ist eine .NET-Resilienzbibliothek, die Retry-Richtlinien, Leistungsschalter und mehr bereitstellt:
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);
}
}
Schlüsselmuster hier:
- Polly retry Politik - Expentential Backoff (500ms, 1s, 1.5s, 2s, 2.5s)
- Nur retry IOException - Andere Ausnahmen blasen auf
- Umfangreiche Dienstleistungen - Erstellen Sie einen Bereich pro Datei, um Lebenszyklus-Probleme zu vermeiden
- Strukturierte Protokollierung - Verwendung Serilog Tätigkeiten zur Rückverfolgung
- Kaskadenverfahren - Speichern → Index → Übersetzen
Die Retry-Richtlinie behandelt den allgemeinen Fall, in dem ein Texteditor immer noch die Datei schreibt, wenn das Änderungsereignis feuert.
Visueller Fluss
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
Registrieren Sie es in Program.cs (alle gehosteten Dienste folgen diesem Muster):
builder.Services.AddHostedService<MarkdownDirectoryWatcherService>();
Die vollständige Umsetzung finden Sie unter: Mostlylucid/Blog/WatcherService/MarkdownDirectoryWatcherService.cs.
Beispiel 2: Kanalbasierte E-Mail-Abfrage
E-Mail-Senden ist ein klassischer Anwendungsfall für Hintergrunddienste. Sie möchten eine HTTP-Anfrage nicht blockieren, während Sie SMTP verhandeln, also stellen Sie die E-Mail an und senden sie im Hintergrund.
Das Problem
Wenn ein Benutzer einen Kommentar oder ein Kontaktformular absendet:
- Validierung und Speicherung der Einreichung
- Anfordern einer E-Mail-Benachrichtigung
- Return Response sofort (nicht auf SMTP blockieren)
- E-Mails im Hintergrund mit Wiederholungslogik senden
- SMTP-Ausfälle anmutig handhaben (Umschaltschalter)
Die Lösung: EmailSenderHostedService
Dieser Dienst benutzt eine Channel<T> für die Warteschlange und Polly für Resilienz:
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();
}
}
Vorgestellte Schlüsselmuster:
- Kanal als Warteschlange - Ungebundener Kanal für Nachrichten
- Politikumschlag - Retry Inside-Leistungsschalter
- Anmutiges Herunterfahren - Kanal komplett, Token absagen, auf Aufgabe warten
- Muster-Matching - Schalten Sie verschiedene E-Mail-Typen ein
- Dauer der Gültigkeitsdauer - Einmal als Singleton erstellt, hält langlebigen Zustand
Retry und Leistungsschalter visualisiert
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
Dies zeigt ein produktionsorientiertes Muster:
- Retry-Politik Behandlung von vorübergehenden Ausfällen (vorübergehende Netzwerkprobleme)
- Leistungsschalter verhindert Kaskadierungsausfälle (wenn SMTP unten ist, hämmern Sie es nicht weiter)
- Kanal stellt natürlichen Gegendruck zur Verfügung (wenn wir nicht senden können, stehen Nachrichten Schlange)
Beispiel 3: Analyse-Ereignissender (Umami)
Dieser Dienst wartet Analyseereignisse und sendet sie an eine Umami Analytics-Server. Es ist ähnlich wie der E-Mail-Dienst, aber einfacher – keine Wiederholungsrichtlinie erforderlich, nur Feuer und vergessen.
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);
}
Wesentliche Unterschiede zum E-Mail-Dienst:
- Keine Resilienzpolitiken - Analysen sind nicht kritisch, wenn es fehlschlägt, loggen wir uns einfach und fahren fort
- Doppelt während der Schleife - Innenschlaufe entwässert alle verfügbaren Artikel für Effizienz
- Erstellung von Scope-Diensten - Jede Sendung erhält einen neuen Bereich für den HTTP-Client
Dies zeigt, dass nicht alle Hintergrunddienste eine komplexe Fehlerbehandlung benötigen. Für die nicht-kritische Telemetrie reicht eine einfache Protokollierung aus.
Beispiel 4: Periodische Hintergrundarbeit mit BackgroundService
Schauen wir uns jetzt Dienste an, die periodische Arbeit verrichten, anstatt Warteschlangen zu verarbeiten. BrokenLinkCheckerBackgroundService überprüft Blog-Post-Links regelmäßig und holt archive.org URLs für defekte.
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");
}
}
}
Vorgestellte Muster:
- HintergrundService Basisklasse - Perfekt für periodische Arbeit
- Anfangsverzögerung - Warten Sie, bis andere Dienste initialisiert werden
- Batching - 20 Links auf einmal verarbeiten, nicht alle auf einmal
- Begrenzung der Steuersätze - 2-Sekunden-Verzögerung zwischen den Kontrollen höflich zu sein
- Erstellung von Scope-Diensten - Neuer Anwendungsbereich pro Charge
- Überprüfung des Annullierungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeigeichnungs-Zeigeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeigeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeigeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungs-Zeichnungen - Frühzeitige Pause, falls die Abschaltung gewünscht wird
Beispiel 5: Semantische Suchindexierung mit Hash-basierter Änderungserkennung
Der semantische Suchindexer ist ausgefeilter – er indexiert nur Posts, die sich tatsächlich geändert haben. Dies spart teure Einbettungs-API-Aufrufe.
csharp
public class SemanticIndexingBackgroundService : BackgroundService
{
private readonly ILogger
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.Plain