ASP.NET Core uygulamalarinda HTTP istekleri dışında çalışan uzun omurlu isler icin hosted service altyapisi kullanılır. Kuyruk tuketimi, periyodik temizlik, outbox dispatch, cache warm-up ve rapor uretimi gibi gorevler BackgroundService sinifi veya IHostedService implementasyonu ile host yaşam dongusune baglanir. Doğru tasarlanmamis arka plan isleri bellek sizintisi, veri tutarsizligi ve sessiz hatalara yol acar. Bu yazida dayanikli background service mimarisinin temel bilesenlerini inceliyoruz.
Hosted service yaşam dongusu
IHostedService iki metot tanimlar: StartAsync ve StopAsync. BackgroundService abstract sinifi ExecuteAsync metodunu override ederek sonsuz dongu veya periyodik is mantigini kolaylastirir. Host baslatildiginda tüm hosted service'ler sirayla baslar; kapanista graceful shutdown icin cancellation token sinyali gonderilir.
public sealed class OutboxDispatcherService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<OutboxDispatcherService> _logger;
public OutboxDispatcherService(
IServiceScopeFactory scopeFactory,
ILogger<OutboxDispatcherService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
var dispatcher = scope.ServiceProvider.GetRequiredService<IOutboxDispatcher>();
await dispatcher.DispatchPendingAsync(stoppingToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogError(ex, "Outbox dispatch failed");
}
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
Scoped bağımlılık kullanımı
BackgroundService singleton olarak kaydedilir; DbContext gibi scoped servisler dogrudan inject edilemez. IServiceScopeFactory ile her is dongusu icin yeni scope acilmalidir. Bu kural ihlal edildiginde DbContext thread safety ve state sizintisi sorunlari ortaya cikar.
Graceful shutdown
Kubernetes veya IIS recycle sirasinda host kapanirken devam eden isler yarım kalabilir. stoppingToken duzenli kontrol edilmeli; uzun işlemler parcalara bolunmelidir. HostOptions.ShutdownTimeout yeterli sure vermelidir. Kritik isler icin idempotent tasarım ve retry mekanizmasi zorunludur.
Periyodik gorevler ve Timer
Basit periyodik isler icin PeriodicTimer (.NET 6+) tercih edilebilir:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await RunMaintenanceAsync(stoppingToken);
}
}
Karmaşık zamanlama (cron ifadesi) icin NCronTab veya Hangfire gibi kutuphaneler dusunulebilir. Ancak basit ihtiyaclarda ek bağımlılık gereksiz olabilir.
Kuyruk tuketimi
Message broker (RabbitMQ, Azure Service Bus, Kafka) ile çalışan worker'lar genellikle ayri worker process veya container olarak calistirilir. Web host icinde kuyruk tuketimi mumkundur ancak ölçeklendirme ayri yapilir. Channel<T> ile in-process kuyruk da hafif senaryolarda kullanılır:
- Producer HTTP istegi ile channel'a yazar.
- BackgroundService channel'dan okur ve isler.
- Bounded channel kapasitesi backpressure sağlar.
Paralellik ve throttling
Ayni anda cok fazla işlem baslatmak kaynak tuketimini artirir. SemaphoreSlim veya TPL Dataflow ile eszamanlilik sınırı konulmalidir. Örneğin outbox dispatch'te ayni anda en fazla 10 mesaj islenebilir.
Hata yönetimi ve retry
Arka plan islerinde hata yakalanmazsa servis durabilir veya sessizce başarısız olur. Polly ile exponential backoff retry uygulanabilir. Dead letter queue veya poison message tablosu tasarlanmalidir. Her hata structured log ile kaydedilmeli; alert kurallari tanimlanmalidir.
Health check entegrasyonu
Background service sagligini raporlamak icin custom health check yazilir:
public sealed class OutboxHealthCheck : IHealthCheck
{
private readonly AppDbContext _db;
public OutboxHealthCheck(AppDbContext db) => _db = db;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken ct)
{
var pending = await _db.OutboxMessages.CountAsync(m => !m.Processed, ct);
var stale = await _db.OutboxMessages.CountAsync(
m => !m.Processed && m.CreatedAt < DateTime.UtcNow.AddMinutes(-15), ct);
if (stale > 0)
return HealthCheckResult.Degraded($"{stale} stale outbox messages");
return HealthCheckResult.Healthy($"{pending} pending");
}
}
Ölçeklendirme modelleri
Web + worker ayni process'te: basit deployment, sinirli ölçeklendirme. Ayri worker servisi: bagimsiz ölçeklendirme, daha iyi kaynak ayrimi. Serverless (Azure Functions, AWS Lambda): düşük hacim, event-driven. Seçim is hacmine ve operasyonel olgunluğa baglidir.
Leader election
Birden fazla instance calistiginda ayni periyodik isin her node'da calismasi istenmeyen sonuçlar dogurabilir. Distributed lock (Redis RedLock, SQL advisory lock) veya leader election ile tek instance isi ustlenir.
Test stratejisi
Background service mantigi ayri sinifa cikarilarak birim test edilir. Entegrasyon testlerinde host baslatilir ve kisa sure beklenerek isin tamamlandigi dogrulanir. Cancellation token testleri unutulmamali.
Güvenlik
Arka plan isleri genellikle sistem yetkisiyle çalışır. Principle of least privilege uygulanmali; servis hesabi sadece gerekli kaynaklara erisebilmelidir. Hassas veri isleyen job'larda audit log tutulmalidir.
Özet
Dayanikli background service: scope factory kullan, cancellation token'a saygi goster, hatalari logla ve retry et, health check ile gozle, idempotent tasarla, paralelligi sinirla, ölçeklendirme modelini erken sec. Bu ilkeler ASP.NET Core host icinde veya ayri worker process'te güvenilir arka plan işleme sağlar.
HostedService vs Hangfire/Quartz
Basit periyodik isler icin BackgroundService yeterlidir. Cron ifadesi, DAG, retry UI ve dashboard gerekiyorsa Hangfire veya Quartz degerlendirilir. Operasyonel gorunurluk oncelikse dedicated job framework tercih edilebilir.
Resource limitleri
Container ortaminda CPU ve memory limitleri background isleri etkiler. GC pressure yüksek job'larda batch boyutu ayarlanmali. Memory leak riski olan islerde using ve dispose disiplini uygulanmali.
Observability
Her job calismasi icin baslangic/bitis logu, sure metrigi ve basarisizlik sayaci tutulmali. OpenTelemetry ActivitySource ile job span'leri trace'e eklenir. Alert: ardisik N basarisizlik veya is kuyrugu gecikmesi esik degerini astiginda.
Channel tabanli is kuyrugu
In-process Channel<T> hafif is yukleri icin yeterlidir. Bounded channel kapasitesi backpressure sağlar; HTTP istegi channel'a yazdiginda kuyruk doluysa 503 veya retry-after donulebilir. Unbounded channel bellek baskisi olusturabilir.
public sealed class EmailQueue : BackgroundService
{
private readonly Channel<EmailJob> _channel = Channel.CreateBounded<EmailJob>(100);
public ValueTask EnqueueAsync(EmailJob job, CancellationToken ct) =>
_channel.Writer.WriteAsync(job, ct);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var job in _channel.Reader.ReadAllAsync(stoppingToken))
await ProcessEmailAsync(job, stoppingToken);
}
}
IHostedLifecycleService (.NET 8+)
Yeni lifecycle arayüzü baslatma ve durdurma asamalarini daha ayrintili yönetir. Warmup islemleri StartingAsync icinde, drain islemleri StoppingAsync icinde yapilabilir. Bu sayede readiness probe baslamadan once bağımlılıklar hazir hale getirilir.
Poison message yönetimi
Defalarca başarısız olan mesajlar dead letter kuyruguna veya poison tablosuna tasinmalidir. Retry sayisi ve backoff politikasi konfigurasyonla yonetilmelidir. Operasyon ekibi icin poison mesajlari yeniden işleme araci saglanmalidir.