HHangfire Handbook

ORTA

Job Filters & Middleware

Job filter'lar, job lifecycle'ının her aşamasına müdahale etmenizi sağlar — ASP.NET MVC action filter'larına benzer.

Client Enqueue() IClientFilter OnCreating / OnCreated IServerFilter OnPerforming / OnPerformed IElectStateFilter OnStateElection Storage Persist Job Server Execute Method State Change Succeeded/Failed

Filter Türleri

Interface Tetiklenme Zamanı Kullanım
IClientFilter Job oluşturulurken (Enqueue) Validation, logging, tenant tagging
IServerFilter Job execute edilirken Timing, try/catch, distributed lock
IElectStateFilter State değiştirilirken Custom retry logic, alerting
IApplyStateFilter State uygulandıktan sonra Metrics, notification

Karar Rehberi

Durum Öneri Örnek veya gerekçe
Cross-cutting logging Uygun: IServerFilter Tüm job'lara execution time ekle
Retry özelleştirme Uygun: IElectStateFilter Kalıcı hata ayrımı
Tenant context propagation Uygun: IClientFilter + IServerFilter Multi-tenant SaaS
Basit try/catch Uygun değil: Filter overkill Tek job'a özel hata yakalama
DB transaction yönetimi Uygun değil: Job içinde yap Unit of work pattern

Custom Filter: Execution Time Logger

public class LogExecutionTimeAttribute : JobFilterAttribute, IServerFilter
{
    // NOT: Filter attribute'lar DI almaz! Serilog static Log kullanın
    // veya GlobalJobFilters yerine IoC-based filter activation kullanın.

    public void OnPerforming(PerformingContext context)
    {
        context.Items["StartTime"] = Stopwatch.StartNew();
    }

    public void OnPerformed(PerformedContext context)
    {
        if (context.Items.TryGetValue("StartTime", out var obj) && obj is Stopwatch sw)
        {
            sw.Stop();
            var jobName = context.BackgroundJob.Job.Type.Name + "." +
                          context.BackgroundJob.Job.Method.Name;

            // Structured logging
            Log.Information("Job {JobName} completed in {ElapsedMs}ms. JobId: {JobId}",
                jobName, sw.ElapsedMilliseconds, context.BackgroundJob.Id);

            // Slow job alert
            if (sw.ElapsedMilliseconds > 30000)
            {
                Log.Warning("SLOW JOB: {JobName} took {ElapsedMs}ms (threshold: 30s)",
                    jobName, sw.ElapsedMilliseconds);
            }
        }
    }
}

// Kullanım — method seviyesinde
[LogExecutionTime]
public async Task ProcessLargeImportAsync(int importId) { ... }

// Veya global registration
GlobalJobFilters.Filters.Add(new LogExecutionTimeAttribute());

AutomaticRetry Attribute

// Default: 10 retry, exponential backoff
[AutomaticRetry(Attempts = 5, DelaysInSeconds = new[] { 60, 300, 900, 3600, 7200 })]
public async Task SendEmailAsync(int emailId)
{
    // 1dk, 5dk, 15dk, 1saat, 2saat arayla retry
}

// Retry kapatma (kritik — tek deneme)
[AutomaticRetry(Attempts = 0)]
public async Task ChargePaymentAsync(int paymentId)
{
    // Ödeme çift çekilmesin! Idempotency key kullan.
}

// Başarısız olunca silme yerine bekletme
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
public async Task ProcessWebhookAsync(int webhookId) { ... }

Tenant Isolation Filter

public class TenantFilterAttribute : JobFilterAttribute, IClientFilter, IServerFilter
{
    public void OnCreating(CreatingContext context)
    {
        // Job oluşturulurken mevcut tenant'ı kaydet
        var tenantId = TenantContext.CurrentTenantId;
        context.SetJobParameter("TenantId", tenantId);
    }

    public void OnCreated(CreatedContext context) { }

    public void OnPerforming(PerformingContext context)
    {
        // Job çalışırken tenant context'i restore et
        var tenantId = context.GetJobParameter<string>("TenantId");
        TenantContext.SetCurrentTenant(tenantId);
    }

    public void OnPerformed(PerformedContext context)
    {
        TenantContext.Clear();
    }
}

Örnek: Multi-tenant SaaS'ta her background job kendi tenant'ının DB connection string'ini kullanmalıdır. TenantFilter ile job oluşturulurken tenant ID kaydedilir, execute sırasında doğru connection string inject edilir.