BackgroundService ile Arka Plan İşleri

BackgroundService ve IHostedService ile uzun süreli arka plan görevleri, Windows/Linux servisleri ve PeriodicTimer kullanımı anlatılır.

BackgroundService ile Arka Plan İşleri

BackgroundService & Worker Services: .NET Core'da Arka Plan İşleri

Günümüz uygulamaları sadece web API'lerinden ibaret değildir; veritabanı temizliği, e-posta gönderimi, rapor oluşturma, kuyruk tüketimi gibi uzun soluklu, periyodik veya sürekli çalışan işlemler de vardır. ASP.NET Core'dan bağımsız olarak .NET Core, IHostedService ve BackgroundService ile bu işleri profesyonelce yönetmenize olanak tanır. Bu yazıda, Windows Servisi ve Linux systemd'den, PeriodicTimer ile modern periyodik işlemlere kadar her şeyi ele alıyoruz.


1. Temel Kavramlar: IHostedService vs BackgroundService

  • IHostedService: En temel arayüzdür. StartAsync (uygulama ayağa kalkarken) ve StopAsync (uygulama kapanırken) metotlarını implemente etmenizi gerektirir. Her şeyi siz yönetirsiniz (döngü, durdurma, hata yönetimi).

  • BackgroundService: Soyut (abstract) bir sınıftır ve IHostedService'i implemente eder. Sizden sadece ExecuteAsync metotunu override etmenizi bekler. Uygulama kapanırken durdurma (stop) işlemini otomatik olarak yönetir. %99 oranında BackgroundService kullanılır.

Basit bir BackgroundService örneği:

csharp

public class TimedHostedService : BackgroundService
{
    private readonly ILogger<TimedHostedService> _logger;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Servis başlıyor.");

        // Uygulama kapanana kadar döngü (stoppingToken iptal olana kadar)
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Servis çalışıyor: {Time}", DateTimeOffset.Now);
            await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); // 10 saniye bekle
        }

        _logger.LogInformation("Servis durduruluyor.");
    }
}

Kaydetme (Program.cs):

csharp

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<TimedHostedService>();
// veya
builder.Services.AddHostedService<QueuedHostedService>();

var host = builder.Build();
await host.RunAsync(); // Uygulama çalışmaya başlar, Ctrl+C ile durdurulana kadar devam eder.

2. Modern Zamanlayıcı: PeriodicTimer (Uzun Süren İşlemler için)

Task.Delay ile periyodik işlem yapmak, her döngüde yeni bir Task oluşturur ve zamanlamada kaymalara (drift) neden olabilir. .NET 6 ile gelen PeriodicTimer, daha kararlı, düşük maliyetli ve drift'siz bir alternatiftir.

csharp

public class PeriodicTimerService : BackgroundService
{
    private readonly ILogger<PeriodicTimerService> _logger;

    public PeriodicTimerService(ILogger<PeriodicTimerService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("PeriodicTimer servisi başlıyor.");

        // 5 saniyede bir çalışacak timer (başlangıçta hemen çalışmaz)
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));

        try
        {
            // WaitForNextTickAsync, bir sonraki tick'e kadar bekler. 
            // Timer durdurulursa false döner.
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                _logger.LogInformation("Tick çalıştı: {Time}", DateTimeOffset.Now);
                // Burada uzun işlemler yapabilirsiniz (veritabanı, http çağrısı vb.)
                // Ancak tick süresi işlem süresinden uzun olmalı, yoksa overlap olur.
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Servis durdurma sinyali aldı.");
        }

        _logger.LogInformation("Servis durdu.");
    }
}

Avantajları:

  • Zamanlama kayması (drift) yoktur. Her 5 saniyede tam olarak tetiklenir (işlem süresi 5 saniyeden az olmalıdır).

  • using ile kaynak yönetimi otomatiktir.

  • CancellationToken ile temiz durdurma sağlar.

⚠️ Kritik Uyarı: PeriodicTimer içinde yaptığınız işlem, timer aralığından uzun sürerse overlap yaşanır (yani bir önceki işlem bitmeden yeni tick gelir). Bu durumu önlemek için ya işlemi Task.Run ile arka plana atın ya da timer aralığını işlem süresine göre ayarlayın.


3. Worker Service Projesi ve Windows/Linux Servisine Dönüştürme

Visual Studio'dan "Worker Service" şablonu ile başladığınızda, zaten BackgroundService'ten türetilmiş bir sınıf ve IHost yapılanması gelir. Bu proje, konsol uygulaması gibi görünse de aslında bir Windows Servisi veya Linux systemd servisi olarak çalıştırılmaya hazırdır.

Windows Servisi Olarak Yayınlama:

csharp

// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

// Windows servisi olarak çalışması için:
builder.Services.AddWindowsService(options =>
{
    options.ServiceName = "MyBackgroundService";
});

// veya Linux systemd için:
// builder.Services.AddSystemd();

var host = builder.Build();
await host.RunAsync();

Yayınlama ve Yükleme:

  • dotnet publish --configuration Release --output ./publish

  • Windows'ta sc create MyBackgroundService binPath= "C:\path\to\MyApp.exe" (veya PowerShell ile)

  • Linux'ta /etc/systemd/system/ altına .service dosyası oluşturup systemctl start MyApp


4. Queue ve Channel ile Arka Plan İş Kuyruğu (Producer-Consumer)

Web API'den gelen istekleri arka planda işlemek istiyorsanız (ör. e-posta gönderme), BackgroundService ile bir kuyruk tüketicisi oluşturabilirsiniz.

csharp

// Kuyruk servisi (Singleton)
public interface IBackgroundTaskQueue
{
    void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
    Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, Task>> _queue;

    public BackgroundTaskQueue(int capacity = 100)
    {
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, Task>>(options);
    }

    public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
    {
        if (workItem == null) throw new ArgumentNullException(nameof(workItem));
        _queue.Writer.TryWrite(workItem);
    }

    public async Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
    {
        return await _queue.Reader.ReadAsync(cancellationToken);
    }
}

// Tüketici BackgroundService
public class QueuedHostedService : BackgroundService
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger<QueuedHostedService> logger)
    {
        _taskQueue = taskQueue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = await _taskQueue.DequeueAsync(stoppingToken);
            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "İşlem sırasında hata oluştu.");
            }
        }
    }
}

// Controller'da kullanım:
[ApiController]
[Route("api/[controller]")]
public class EmailController : ControllerBase
{
    private readonly IBackgroundTaskQueue _taskQueue;

    public EmailController(IBackgroundTaskQueue taskQueue)
    {
        _taskQueue = taskQueue;
    }

    [HttpPost]
    public IActionResult SendEmail([FromBody] EmailRequest request)
    {
        _taskQueue.QueueBackgroundWorkItem(async (ct) =>
        {
            // Bu kod arka planda, isteğin dışında çalışır.
            await SendEmailAsync(request, ct);
        });
        return Accepted(); // İstemciye hemen dön, iş arka planda yapılsın.
    }
}

Bu model, web API'lerinde background processing için standarttır ve uygulamanızı tıkamaz.


5. Performans ve Dikkat Edilmesi Gerekenler

  • ExecuteAsync içinde sonsuz döngü (while(true)) kullanmayın; her zaman stoppingToken kontrolü yapın.

  • Uzun süren işlemler için Task.Delay veya PeriodicTimer dışında Thread.Sleep KULLANMAYIN – çünkü bu thread'i bloklar ve stoppingToken iptalini kontrol edemezsiniz.

  • DI ile Scope Yönetimi: BackgroundService'in kendisi genelde Singleton'dır. İçinde Scoped bir servis (ör. DbContext) kullanacaksanız, bir IServiceScopeFactory ile her işlem için yeni bir scope oluşturun.

csharp

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        using (var scope = _serviceScopeFactory.CreateScope())
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            // dbContext ile işlem yap
        }
        await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
    }
}
  • Exception Yönetimi: ExecuteAsync içinde fırlayan yakalanmamış exception, servisin çökmesine neden olur. Bu nedenle ana döngüyü bir try-catch ile sarmalayın ve hatayı loglayıp devam edin (yeniden deneme mekanizması ekleyin).

Sonuç:

BackgroundService ve IHostedService, .NET Core ile arka plan işlemlerini profesyonelce yapmanızın olmazsa olmazıdır.

  • Basit periyodik işler için PeriodicTimer + BackgroundService.

  • İş kuyruğu (queue) için Channel + BackgroundService.

  • Windows/Linux servisi olarak çalıştırmak için AddWindowsService veya AddSystemd.

Unutmayın: Arka plan servisleri, uygulamanızın can damarıdır; hata yönetimini, loglamayı ve durdurma sinyallerini (graceful shutdown) asla ihmal etmeyin.

Tüm yazılar