← Tüm yazılara dön

Hangfire ile Background Job Yönetimi

Hangfire ile Background Job Yönetimi

Hangfire ile Background Job Yönetimi

Neden Background Job?

Bir kullanıcı kayıt formunu doldurdu. Sunucuda şunlar olmalı:

1. Kullanıcıyı veritabanına kaydet       (50ms)
2. Hoş geldin emaili gönder              (800ms — SMTP gecikmesi)
3. CRM'e senkronize et                   (1200ms — harici API)
4. Audit log yaz                         (30ms)
5. Hoş geldin bildirimi push et          (400ms)

Hepsini senkron yaparsanız kullanıcı 2.5 saniye bekler. Email sunucusu yavaşsa veya CRM API'si yanıt vermiyorsa kullanıcı kaydı başarısız olabilir — sadece bir email gönderilemediği için.

Doğru yaklaşım: Kritik olanı (kayıt) senkron yap, gerisini arka plana at.

1. Kullanıcıyı kaydet                    (50ms) → Kullanıcıya yanıt dön
2-5. Background job kuyruğuna at         (5ms)  → Arka planda işlenir

Hangfire bunu .NET'te en az çabayla yapmanın yoludur.


Hangfire Nedir?

Hangfire, .NET için açık kaynak bir background job framework'üdür. Job'ları kalıcı olarak saklar (MSSQL, Redis, PostgreSQL), bir dashboard sunar ve retry mekanizmasıyla gelir.

Alternatiflerle karşılaştırma:

Özellik Hangfire Quartz.NET Azure Functions
Kalıcı storage ✅ MSSQL/Redis ⚠️ Ek yapılandırma ✅ Yönetilen
Dashboard UI ✅ Hazır ❌ Yok ⚠️ Azure Portal
Kurulum kolaylığı ✅ Kolay ⚠️ Orta ⚠️ Azure bağımlı
Self-hosted
Retry mekanizması ✅ Otomatik ⚠️ Manuel
.NET entegrasyonu ✅ Native ✅ Native ✅ Native

MSSQL'i zaten kullanan, on-premise IIS üzerinde çalışan .NET projeleri için Hangfire neredeyse her zaman doğru seçimdir — ek altyapı gerektirmez, mevcut veritabanını kullanır.


Kurulum

NuGet Paketleri

dotnet add package Hangfire.Core
dotnet add package Hangfire.SqlServer
dotnet add package Hangfire.AspNetCore

Veritabanı Hazırlığı

Hangfire kendi tablolarını otomatik oluşturur — ama production'da bunu kontrol altında tutmak isteyebilirsiniz.

-- Hangfire için ayrı bir şema kullanmak iyi bir pratiktir
CREATE SCHEMA HangFire;

Program.cs Kurulumu

// Program.cs
builder.Services.AddHangfire(config => config
    .SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
    .UseSimpleAssemblyNameTypeSerializer()
    .UseRecommendedSerializerSettings()
    .UseSqlServerStorage(
        builder.Configuration.GetConnectionString("DefaultConnection"),
        new SqlServerStorageOptions
        {
            CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
            SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
            QueuePollInterval = TimeSpan.Zero,      // Worker thread tabanlı, polling yok
            UseRecommendedIsolationLevel = true,
            SchemaName = "HangFire",
            PrepareSchemaIfNecessary = true,
            DisableGlobalLocks = true               // SQL Server 2017+ için önerilir
        }));

// Background job processing server
builder.Services.AddHangfireServer(options =>
{
    options.WorkerCount = Environment.ProcessorCount * 2;
    options.Queues = ["critical", "default", "low"];  // Öncelik kuyrukları
    options.ServerName = $"{Environment.MachineName}";
});

var app = builder.Build();

// Dashboard
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    Authorization = [new HangfireDashboardAuthFilter()],
    DashboardTitle = "MyApp — Background Jobs"
});

Dashboard Yetkilendirmesi — Kritik

Dashboard varsayılan olarak herkese açıktır. Production'da bu bir güvenlik açığıdır.

// HangfireDashboardAuthFilter.cs
public class HangfireDashboardAuthFilter : IDashboardAuthorizationFilter
{
    public bool Authorize(DashboardContext context)
    {
        var httpContext = context.GetHttpContext();

        // Sadece Admin rolündeki kullanıcılar erişebilir
        return httpContext.User.IsInRole("Admin");
    }
}

// Daha sıkı — IP bazlı kısıtlama ile birleştirilebilir
public class HangfireDashboardAuthFilter : IDashboardAuthorizationFilter
{
    private static readonly string[] AllowedIps = ["10.0.0.5", "192.168.1.100"];

    public bool Authorize(DashboardContext context)
    {
        var httpContext = context.GetHttpContext();

        var isAdmin = httpContext.User.IsInRole("Admin");
        var clientIp = httpContext.Connection.RemoteIpAddress?.ToString();
        var isAllowedIp = AllowedIps.Contains(clientIp);

        return isAdmin && isAllowedIp;
    }
}
// IIS arkasında çalışıyorsa gerçek IP için
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor;
});

app.UseForwardedHeaders();

Job Tipleri

1. Fire-and-Forget — Hemen Çalıştır

En yaygın kullanım. Job kuyruğa eklenir, ilk uygun worker tarafından işlenir.

public class RegisterUserCommandHandler
    : IRequestHandler<RegisterUserCommand, Guid>
{
    private readonly AppDbContext _context;
    private readonly IBackgroundJobClient _jobClient;

    public RegisterUserCommandHandler(
        AppDbContext context,
        IBackgroundJobClient jobClient)
    {
        _context = context;
        _jobClient = jobClient;
    }

    public async Task<Guid> Handle(
        RegisterUserCommand command,
        CancellationToken ct)
    {
        var user = User.Create(command.Email, command.Name);

        _context.Users.Add(user);
        await _context.SaveChangesAsync(ct);

        // Fire-and-forget — kullanıcıya hemen yanıt dön
        _jobClient.Enqueue<IWelcomeEmailService>(
            service => service.SendWelcomeEmailAsync(user.Id));

        _jobClient.Enqueue<ICrmSyncService>(
            service => service.SyncUserAsync(user.Id));

        return user.Id;
    }
}

// Servis — DI üzerinden çözümlenir, job çalıştığında instance edilir
public interface IWelcomeEmailService
{
    Task SendWelcomeEmailAsync(Guid userId);
}

public class WelcomeEmailService : IWelcomeEmailService
{
    private readonly AppDbContext _context;
    private readonly IEmailSender _emailSender;
    private readonly ILogger<WelcomeEmailService> _logger;

    public WelcomeEmailService(
        AppDbContext context,
        IEmailSender emailSender,
        ILogger<WelcomeEmailService> logger)
    {
        _context = context;
        _emailSender = emailSender;
        _logger = logger;
    }

    public async Task SendWelcomeEmailAsync(Guid userId)
    {
        var user = await _context.Users.FindAsync(userId)
            ?? throw new NotFoundException(nameof(User), userId);

        await _emailSender.SendAsync(new EmailMessage
        {
            To = user.Email,
            Subject = "Hoş Geldiniz!",
            Template = "welcome",
            Data = new { user.Name }
        });

        _logger.LogInformation(
            "Hoş geldin emaili gönderildi. UserId: {UserId}", userId);
    }
}

2. Delayed Jobs — Gecikmeli Çalıştır

Belirli bir süre sonra çalışacak job'lar:

public class OrderService
{
    private readonly IBackgroundJobClient _jobClient;

    public async Task<Guid> CreateOrderAsync(CreateOrderCommand command)
    {
        var orderId = Guid.NewGuid();
        // ... sipariş oluşturma

        // 30 dakika içinde ödeme yapılmazsa siparişi iptal et
        _jobClient.Schedule<IOrderService>(
            service => service.CancelIfUnpaidAsync(orderId),
            TimeSpan.FromMinutes(30));

        // 1 saat sonra hatırlatma emaili gönder — sepet bırakma senaryosu
        _jobClient.Schedule<ICartReminderService>(
            service => service.SendAbandonedCartReminderAsync(orderId),
            TimeSpan.FromHours(1));

        // Belirli bir tarihte çalıştır
        _jobClient.Schedule<IReportService>(
            service => service.GenerateMonthlyReportAsync(),
            new DateTimeOffset(2026, 7, 1, 0, 0, 0, TimeSpan.Zero));

        return orderId;
    }
}

public class OrderService : IOrderService
{
    private readonly AppDbContext _context;

    public async Task CancelIfUnpaidAsync(Guid orderId)
    {
        var order = await _context.Orders.FindAsync(orderId);

        if (order is null || order.Status != OrderStatus.Pending)
            return; // Zaten ödenmiş veya iptal edilmiş

        order.Cancel("Ödeme süresi doldu — otomatik iptal.");
        await _context.SaveChangesAsync();
    }
}

3. Recurring Jobs — Periyodik Çalıştır

Cron expression ile zamanlanan işler:

// Program.cs — uygulama başlarken kaydedilir
app.UseHangfireDashboard("/hangfire", ...);

RecurringJob.AddOrUpdate<IExchangeRateService>(
    "update-exchange-rates",
    service => service.UpdateRatesAsync(),
    "*/5 * * * *",                          // Her 5 dakikada
    new RecurringJobOptions { TimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Istanbul") });

RecurringJob.AddOrUpdate<IArchiveService>(
    "archive-old-orders",
    service => service.ArchiveOldDeletedRecordsAsync(),
    Cron.Daily(2, 0));                      // Her gün 02:00

RecurringJob.AddOrUpdate<IReportService>(
    "weekly-sales-report",
    service => service.GenerateWeeklySalesReportAsync(),
    Cron.Weekly(DayOfWeek.Monday, 8, 0));   // Her Pazartesi 08:00

RecurringJob.AddOrUpdate<IDatabaseMaintenanceService>(
    "rebuild-fragmented-indexes",
    service => service.RebuildFragmentedIndexesAsync(),
    Cron.Weekly(DayOfWeek.Sunday, 3, 0));   // Her Pazar 03:00

Cron Expression Referansı:

┌───────────── dakika (0-59)
│ ┌─────────── saat (0-23)
│ │ ┌───────── ayın günü (1-31)
│ │ │ ┌─────── ay (1-12)
│ │ │ │ ┌───── haftanın günü (0-6, 0=Pazar)
│ │ │ │ │
* * * * *

Örnekler:
"0 * * * *"      → Her saat başı
"*/15 * * * *"   → Her 15 dakikada
"0 9 * * 1-5"    → Hafta içi her gün 09:00
"0 0 1 * *"      → Her ayın 1'inde gece yarısı
"0 2 * * 0"      → Her Pazar 02:00
// Hangfire'ın hazır helper'ları
Cron.Minutely();
Cron.Hourly();
Cron.Daily();
Cron.Daily(hour: 2);
Cron.Weekly();
Cron.Weekly(DayOfWeek.Monday);
Cron.Monthly();
Cron.Yearly();

4. Continuations — Zincirleme Job'lar

Bir job tamamlandığında otomatik başlayacak job tanımlayın:

public class ReportGenerationService
{
    private readonly IBackgroundJobClient _jobClient;

    public void StartMonthlyReportPipeline()
    {
        // 1. Veriyi topla
        var jobId1 = _jobClient.Enqueue<IReportDataService>(
            s => s.CollectMonthlyDataAsync());

        // 2. Veri toplama bitince — PDF oluştur
        var jobId2 = _jobClient.ContinueJobWith<IReportPdfService>(
            jobId1,
            s => s.GeneratePdfAsync());

        // 3. PDF bitince — email gönder
        _jobClient.ContinueJobWith<IReportEmailService>(
            jobId2,
            s => s.SendReportEmailAsync());
    }
}

5. Batch Jobs — Toplu İşlemler

Birden fazla job'u grup olarak yönetin (Hangfire.Pro özelliği, ama açık kaynak alternatifi de mevcuttur):

// Açık kaynak versiyonda manuel batch yönetimi
public class BulkEmailService
{
    private readonly IBackgroundJobClient _jobClient;
    private readonly AppDbContext _context;

    public async Task SendBulkNotificationAsync(string campaignId, List<Guid> userIds)
    {
        // Her kullanıcı için ayrı job — paralel işlenir
        foreach (var batch in userIds.Chunk(100))
        {
            _jobClient.Enqueue<INotificationService>(
                s => s.SendBatchNotificationAsync(campaignId, batch.ToList()));
        }
    }
}

public class NotificationService : INotificationService
{
    public async Task SendBatchNotificationAsync(string campaignId, List<Guid> userIds)
    {
        foreach (var userId in userIds)
        {
            // ... bildirim gönder
            await Task.Delay(50); // Rate limiting için
        }
    }
}

Retry ve Hata Yönetimi

Otomatik Retry

Hangfire varsayılan olarak başarısız job'ları otomatik yeniden dener:

// Global retry ayarı
GlobalJobFilters.Filters.Add(new AutomaticRetryAttribute
{
    Attempts = 5,
    DelaysInSeconds = [10, 30, 60, 300, 900], // Her deneme arası artan süre
    OnAttemptsExceeded = AttemptsExceededAction.Delete // Veya: Fail
});

// Job bazında özel retry
public class CrmSyncService : ICrmSyncService
{
    [AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 5, 15, 60 })]
    public async Task SyncUserAsync(Guid userId)
    {
        var user = await _context.Users.FindAsync(userId);

        var response = await _crmClient.CreateContactAsync(new CrmContact
        {
            Email = user.Email,
            Name = user.Name
        });

        if (!response.IsSuccess)
            throw new CrmSyncException($"CRM senkronizasyonu başarısız: {response.Error}");
    }
}

Kalıcı Hatalar İçin Retry'ı Engelleme

Bazı hatalar tekrar denenmemeli — örneğin "kullanıcı bulunamadı":

public class WelcomeEmailService : IWelcomeEmailService
{
    public async Task SendWelcomeEmailAsync(Guid userId)
    {
        var user = await _context.Users.FindAsync(userId);

        if (user is null)
        {
            // Bu hata tekrar denense de düzelmez — job'u sonlandır
            // JobAbortedException Hangfire tarafından retry yapılmaz
            return; // Veya log + return
        }

        try
        {
            await _emailSender.SendAsync(...);
        }
        catch (SmtpException ex) when (ex.StatusCode == SmtpStatusCode.MailboxUnavailable)
        {
            // Geçersiz email — retry anlamsız
            _logger.LogWarning("Geçersiz email adresi: {Email}", user.Email);
            return;
        }
        // Diğer hatalar — retry mekanizması çalışır
    }
}

Failed Job İzleme ve Bildirim

// Custom filter — başarısız job'larda bildirim gönder
public class FailureNotificationAttribute : JobFilterAttribute, IElectStateFilter
{
    public void OnStateElection(ElectStateContext context)
    {
        if (context.CandidateState is FailedState failedState
            && context.GetJobParameter<int>("RetryCount") >= 5)
        {
            // Son denemede de başarısız oldu — Slack/email bildirimi
            var slackService = context.Activator.ActivateJob(typeof(ISlackNotifier)) as ISlackNotifier;

            slackService?.SendAsync(
                $"⚠️ Job kalıcı olarak başarısız oldu: {context.BackgroundJob.Job.Type.Name}.{context.BackgroundJob.Job.Method.Name}\n" +
                $"Hata: {failedState.Exception.Message}");
        }
    }
}

// Job'a uygula
[FailureNotification]
[AutomaticRetry(Attempts = 5)]
public async Task SyncCriticalDataAsync() { ... }

Öncelik Kuyrukları

Tüm job'lar aynı önemde değildir. Kritik işlemler düşük öncelikli işlemleri beklemesin.

// Program.cs — kuyruk sırası önemlidir, ilk tanımlanan öncelikli işlenir
builder.Services.AddHangfireServer(options =>
{
    options.Queues = ["critical", "default", "low"];
    options.WorkerCount = Environment.ProcessorCount * 2;
});
// Job'u belirli kuyruğa ata
public class PaymentService
{
    public void ProcessRefund(Guid orderId)
    {
        // Kritik — ödeme işlemleri öncelikli kuyrukta
        BackgroundJob.Enqueue<IRefundService>(
            "critical",
            s => s.ProcessRefundAsync(orderId));
    }
}

// Attribute ile
[Queue("critical")]
public class RefundService : IRefundService
{
    public async Task ProcessRefundAsync(Guid orderId) { ... }
}

[Queue("low")]
public class AnalyticsService : IAnalyticsService
{
    public async Task UpdateDailyStatsAsync() { ... }
}

Öneri:

critical  → Ödeme, iptal, kritik bildirimler
default   → Email, CRM senkronizasyon, genel işler
low       → Raporlama, analitik, arşivleme

Job İçinde DI Kullanımı

Hangfire her job çalıştığında DI container'dan yeni bir scope oluşturur. Scoped servisler (DbContext gibi) güvenle kullanılabilir.

// ✅ Doğru — interface üzerinden, DI çözümlenir
_jobClient.Enqueue<IWelcomeEmailService>(s => s.SendWelcomeEmailAsync(userId));

// ❌ Yanlış — closure içinde DbContext yakalamak
var context = _serviceProvider.GetRequiredService<AppDbContext>();
_jobClient.Enqueue(() => SendEmail(context, userId)); // Disposed context!
// IJobActivator — Hangfire'ın DI entegrasyonu otomatik çalışır
// AddHangfire çağrısı bunu arka planda ayarlar, ek konfigürasyon gerekmez
builder.Services.AddHangfire(config => config
    .UseSqlServerStorage(connectionString));

// Job sınıfı normal DI ile kaydedilir
builder.Services.AddScoped<IWelcomeEmailService, WelcomeEmailService>();

Job İçinde CancellationToken

Uzun süren job'lar için graceful shutdown desteği:

public class DataExportService : IDataExportService
{
    private readonly AppDbContext _context;

    public async Task ExportLargeDatasetAsync(IJobCancellationToken cancellationToken)
    {
        var totalRecords = await _context.Orders.CountAsync();
        var processed = 0;

        const int batchSize = 1000;

        while (processed < totalRecords)
        {
            // Sunucu kapanıyorsa veya job manuel durdurulduysa kontrol et
            cancellationToken.ThrowIfCancellationRequested();

            var batch = await _context.Orders
                .OrderBy(o => o.Id)
                .Skip(processed)
                .Take(batchSize)
                .ToListAsync();

            // ... işle

            processed += batch.Count;
        }
    }
}

Job Parametreleri ve Sonuçları

public class ReportService : IReportService
{
    private readonly AppDbContext _context;
    private readonly IBlobStorageService _storage;

    // Job parametreleri serileştirilebilir olmalı — basit tip veya DTO
    public async Task<string> GenerateAndUploadReportAsync(
        DateTime startDate,
        DateTime endDate,
        ReportFormat format)
    {
        var data = await _context.Orders
            .Where(o => o.CreatedAt >= startDate && o.CreatedAt <= endDate)
            .ToListAsync();

        var fileContent = format switch
        {
            ReportFormat.Excel => GenerateExcel(data),
            ReportFormat.Pdf => GeneratePdf(data),
            _ => throw new ArgumentException("Desteklenmeyen format")
        };

        var url = await _storage.UploadAsync(fileContent, $"report-{Guid.NewGuid()}.{format}");

        return url; // Sonuç Hangfire dashboard'unda görülebilir
    }
}

// Kullanım
var jobId = _jobClient.Enqueue<IReportService>(
    s => s.GenerateAndUploadReportAsync(startDate, endDate, ReportFormat.Excel));

Dashboard Özelleştirme

app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    Authorization = [new HangfireDashboardAuthFilter()],
    DashboardTitle = "MyApp Background Jobs",
    DisplayStorageConnectionString = false,  // Connection string'i gizle
    StatsPollingInterval = 5000,
    DefaultRecordsPerPage = 25
});

Dashboard'da izlenecek metrikler:

✅ Succeeded — başarıyla tamamlanan job sayısı
⚠️ Failed — başarısız job'lar (kırmızı işaret — incelenmeli)
⏳ Processing — şu anda çalışan job'lar
📋 Enqueued — kuyrukta bekleyen job'lar
🔄 Scheduled — gelecekte çalışacak job'lar
🔁 Recurring — periyodik job tanımları

Production Pratikleri

1. Job Idempotency

Bir job retry ile iki kez çalışırsa veri tutarsızlığı olmamalı:

public class InvoiceService : IInvoiceService
{
    public async Task GenerateInvoiceAsync(Guid orderId)
    {
        // İdempotency kontrolü — fatura zaten var mı?
        var existingInvoice = await _context.Invoices
            .FirstOrDefaultAsync(i => i.OrderId == orderId);

        if (existingInvoice is not null)
        {
            _logger.LogInformation(
                "Fatura zaten mevcut, atlanıyor. OrderId: {OrderId}", orderId);
            return;
        }

        var invoice = Invoice.Create(orderId);
        _context.Invoices.Add(invoice);
        await _context.SaveChangesAsync();
    }
}

2. Distributed Lock — Çoklu Sunucu

Birden fazla sunucuda Hangfire server çalışıyorsa, recurring job'lar otomatik olarak tek instance tarafından işlenir — ekstra önlem gerekmez. Ama manuel job tetiklemede dikkat:

// Hangfire recurring job'ları zaten distributed lock ile korur
// Aynı recurring job iki sunucuda aynı anda çalışmaz

// Ama uygulama kodunda aynı işi iki kez enqueue etmemeye dikkat edin
public async Task ProcessDailyOrdersAsync()
{
    var lockKey = $"daily-orders-{DateTime.UtcNow:yyyyMMdd}";

    using var distributedLock = await _lockProvider.TryAcquireAsync(lockKey, TimeSpan.FromHours(1));

    if (distributedLock is null)
    {
        _logger.LogInformation("Bu işlem başka bir instance tarafından çalıştırılıyor.");
        return;
    }

    // ... işlem
}

3. Job Timeout

[AutomaticRetry(Attempts = 3)]
public async Task LongRunningTaskAsync(IJobCancellationToken token)
{
    using var cts = CancellationTokenSource.CreateLinkedTokenSource(
        token.ShutdownToken);
    cts.CancelAfter(TimeSpan.FromMinutes(30)); // 30 dakika timeout

    await DoWorkAsync(cts.Token);
}

4. Log Retention — Eski Job Kayıtlarını Temizleme

// SqlServerStorageOptions içinde
new SqlServerStorageOptions
{
    JobExpirationCheckInterval = TimeSpan.FromHours(1),
}

// Başarılı job'lar varsayılan 24 saat sonra silinir
// Süreyi job bazında özelleştirebilirsiniz
[AutomaticRetry(Attempts = 3)]
[ExpirationTime("7.00:00:00")] // 7 gün sakla
public async Task ImportantAuditJobAsync() { ... }

5. Health Check Entegrasyonu

builder.Services.AddHealthChecks()
    .AddCheck<HangfireHealthCheck>("hangfire");

public class HangfireHealthCheck : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken ct = default)
    {
        var monitoringApi = JobStorage.Current.GetMonitoringApi();
        var failedCount = monitoringApi.FailedCount();

        if (failedCount > 100)
        {
            return Task.FromResult(HealthCheckResult.Degraded(
                $"{failedCount} başarısız job birikti."));
        }

        var servers = monitoringApi.Servers();
        if (!servers.Any())
        {
            return Task.FromResult(HealthCheckResult.Unhealthy(
                "Hiçbir Hangfire server aktif değil."));
        }

        return Task.FromResult(HealthCheckResult.Healthy());
    }
}

Gerçek Senaryo — Sipariş Sonrası İş Akışı

public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
    private readonly AppDbContext _context;
    private readonly IBackgroundJobClient _jobClient;

    public async Task<Guid> Handle(CreateOrderCommand command, CancellationToken ct)
    {
        var order = Order.Create(command.CustomerId, command.Items);

        _context.Orders.Add(order);
        await _context.SaveChangesAsync(ct);

        // Kritik — ödeme işlemi öncelikli kuyrukta, hemen başlasın
        var paymentJobId = _jobClient.Enqueue<IPaymentService>(
            "critical",
            s => s.ProcessPaymentAsync(order.Id));

        // Ödeme tamamlanınca fatura oluştur
        var invoiceJobId = _jobClient.ContinueJobWith<IInvoiceService>(
            paymentJobId,
            s => s.GenerateInvoiceAsync(order.Id));

        // Fatura oluşunca email gönder
        _jobClient.ContinueJobWith<INotificationService>(
            invoiceJobId,
            s => s.SendOrderConfirmationAsync(order.Id));

        // Paralel — stok güncelleme (bağımsız)
        _jobClient.Enqueue<IInventoryService>(
            "default",
            s => s.ReserveStockAsync(order.Id));

        // 30 dakika sonra ödeme kontrolü
        _jobClient.Schedule<IOrderService>(
            s => s.CancelIfUnpaidAsync(order.Id),
            TimeSpan.FromMinutes(30));

        // Düşük öncelik — analitik güncelleme
        _jobClient.Enqueue<IAnalyticsService>(
            "low",
            s => s.RecordOrderMetricsAsync(order.Id));

        return order.Id;
    }
}

Denetim Listesi

✅ Hangfire için ayrı şema (HangFire) kullanılıyor
✅ Dashboard yetkilendirmesi yapılandırıldı — Admin rolü zorunlu
✅ Öncelik kuyrukları tanımlandı — critical/default/low
✅ Global retry politikası ayarlandı
✅ Kalıcı hatalar için retry exception'ları ayrıştırıldı
✅ Job'lar idempotent — retry güvenli
✅ Recurring job'lar timezone ile tanımlandı
✅ Long-running job'larda CancellationToken kullanılıyor
✅ Failed job bildirimi (Slack/email) kuruldu
✅ Health check Hangfire durumunu izliyor
✅ Job log retention süresi ayarlandı
✅ Job parametreleri serileştirilebilir tipler (closure'da context yakalanmıyor)

Temel Çıkarımlar

  • HTTP isteğini bloklamayan her işlem background job adayıdır
  • Hangfire mevcut MSSQL'i kullanır — ek altyapı maliyeti yoktur
  • Dashboard'u her zaman yetkilendirin — varsayılan açık erişim güvenlik açığıdır
  • Öncelik kuyrukları kritik işlemlerin beklemesini önler
  • Her job idempotent olmalıdır — retry her zaman gerçekleşebilir
  • Continuation'lar karmaşık iş akışlarını basit zincirlere böler
  • Recurring job'lar distributed lock ile korunur — çoklu sunucuda güvenlidir
  • Failed job'ları izlemek arşiv değil, aktif bir operasyon alışkanlığı olmalıdır

Background job'lar kullanıcı deneyimini hızlandırır ama görünmez hale gelirler. Dashboard ve monitoring olmadan "çalışıyor mu, çalışmıyor mu" sorusuna kimse cevap veremez — izleme bir lüks değil, zorunluluktur.