Her üretim uygulamasının zamanlanmış görevlere, kuyruk tüketicilere veya başlangıç sırasında çalışan işlere ihtiyacı vardır. .NET'in built-in mekanizması olan IHostedService ve BackgroundService bu ihtiyacı harici bir kütüphane olmadan karşılar.
IHostedService: Düşük Seviyeli Kontrol
public class DatabaseMigrationService(
IServiceScopeFactory scopeFactory,
ILogger<DatabaseMigrationService> logger) : IHostedService
{
public async Task StartAsync(CancellationToken ct)
{
logger.LogInformation("Migration başlatılıyor...");
await using var scope = scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.MigrateAsync(ct);
logger.LogInformation("Migration tamamlandı");
}
public Task StopAsync(CancellationToken ct)
{
// Uygulama kapanırken temizlik
logger.LogInformation("DatabaseMigrationService durduruluyor");
return Task.CompletedTask;
}
}
// Program.cs
builder.Services.AddHostedService<DatabaseMigrationService>();
BackgroundService: Uzun Süre Çalışan İşler
BackgroundService, IHostedService'i implement eden soyut bir sınıftır. Sadece ExecuteAsync metodunu override etmeniz yeterlidir:
public class HealthReportService(
IServiceScopeFactory scopeFactory,
ILogger<HealthReportService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("HealthReportService başladı");
// İlk çalışmayı biraz geciktir (uygulama tam başlasın)
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await DoWorkAsync(stoppingToken);
}
catch (OperationCanceledException)
{
// Normal shutdown — sessizce çık
break;
}
catch (Exception ex)
{
// Hatayı logla ama döngüyü öldürme
logger.LogError(ex, "HealthReport çalışırken hata oluştu");
}
// 5 dakikada bir çalış
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
private async Task DoWorkAsync(CancellationToken ct)
{
await using var scope = scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var stats = new SystemHealth
{
Timestamp = DateTime.UtcNow,
OrderCount = await db.Orders.CountAsync(ct),
PendingCount = await db.Orders.CountAsync(o => o.Status == OrderStatus.Pending, ct)
};
db.SystemHealthLogs.Add(stats);
await db.SaveChangesAsync(ct);
logger.LogInformation("Health raporu kaydedildi: {Count} sipariş", stats.OrderCount);
}
}
Kuyruk Tüketici: Channel<T> ile
// Kuyruk servisi — Singleton
public class EmailQueue
{
private readonly Channel<EmailMessage> _channel =
Channel.CreateBounded<EmailMessage>(new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = true
});
public ValueTask EnqueueAsync(EmailMessage msg, CancellationToken ct = default)
=> _channel.Writer.WriteAsync(msg, ct);
public IAsyncEnumerable<EmailMessage> ReadAllAsync(CancellationToken ct)
=> _channel.Reader.ReadAllAsync(ct);
}
// Tüketici background service
public class EmailSenderService(
EmailQueue queue,
IServiceScopeFactory scopeFactory,
ILogger<EmailSenderService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var msg in queue.ReadAllAsync(stoppingToken))
{
try
{
await using var scope = scopeFactory.CreateAsyncScope();
var emailSvc = scope.ServiceProvider.GetRequiredService<IEmailService>();
await emailSvc.SendAsync(msg, stoppingToken);
logger.LogDebug("E-posta gönderildi: {To}", msg.To);
}
catch (Exception ex)
{
logger.LogError(ex, "E-posta gönderilemedi: {To}", msg.To);
// Dead letter queue'ya eklenebilir
}
}
}
}
// Kayıt
builder.Services.AddSingleton<EmailQueue>();
builder.Services.AddHostedService<EmailSenderService>();
// Controller'dan kuyruğa ekle
public class OrderController(EmailQueue emailQueue) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderRequest req)
{
var order = await _orderService.CreateAsync(req);
// Fire and forget — yanıt beklemeden kuyruğa ekle
await emailQueue.EnqueueAsync(new EmailMessage
{
To = order.CustomerEmail,
Subject = $"Siparişiniz alındı: #{order.Id}",
Body = $"Toplam: {order.Total:C}"
});
return Created($"/orders/{order.Id}", order);
}
}
Periyodik Görev: PeriodicTimer ile
.NET 6'da gelen PeriodicTimer, Task.Delay'e göre daha doğru aralık sağlar:
public class CacheWarmupService(
IServiceScopeFactory scopeFactory,
ILogger<CacheWarmupService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Task.Delay'in aksine, PeriodicTimer iş süresi ne kadar sürerse sürsün
// bir sonraki tick'i doğru zamanda verir
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(10));
// İlk çalışma hemen
await WarmupCacheAsync(stoppingToken);
// Sonraki çalışmalar periyodik
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await WarmupCacheAsync(stoppingToken);
}
}
private async Task WarmupCacheAsync(CancellationToken ct)
{
logger.LogInformation("Cache ısıtılıyor...");
await using var scope = scopeFactory.CreateAsyncScope();
var cache = scope.ServiceProvider.GetRequiredService<ICacheService>();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var categories = await db.Categories.AsNoTracking().ToListAsync(ct);
foreach (var cat in categories)
await cache.SetAsync($"category:{cat.Id}", cat, TimeSpan.FromHours(1));
logger.LogInformation("{Count} kategori önbelleğe alındı", categories.Count);
}
}
Graceful Shutdown
// appsettings.json — kapatma için bekleme süresi
{
"ShutdownTimeout": "00:00:30"
}
// Program.cs
builder.Services.Configure<HostOptions>(options =>
{
// 30 saniye bekle, sonra zorla kapat
options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
// BackgroundService içinde temiz kapanış
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
while (!stoppingToken.IsCancellationRequested)
{
await ProcessNextItemAsync(stoppingToken);
}
}
finally
{
// stoppingToken iptal edilse de bu blok çalışır
logger.LogInformation("Service temiz bir şekilde kapandı");
await FlushPendingAsync(); // Son işlemleri tamamla
}
}
BackgroundService ile Hangfire veya Quartz gibi kütüphanelere gerek kalmadan basit zamanlanmış görevler, kuyruk tüketiciler ve başlangıç işlemleri rahatlıkla yönetilebilir. Karmaşık cron ifadeleri veya dağıtık job yönetimi gerektiren senaryolarda ise bir job kütüphanesi daha uygun olacaktır.