Günümüz yazılım projelerinde, uzun süre devam eden işlemlerin doğru şekilde ele alınması, uygulamanın kararlı ve akıcı çalışması için büyük önem taşır. Birçok operasyon kullanıcıyı bekletmeden arka planda yürütülmelidir.
Backround Job Sorunu
Bu tür işlemler genelde cron job, scheduled job ya da background job olarak adlandırılır. Buradaki temel amaç belirli bir zamanda ya da belirli aralıklarla çalışan ve ana uygulamadan bağımsız şekilde yürütülen görevler oluşturmaktır. Ancak bu kavram sadece “arka planda çalıştırmak” anlamına gelmez, aynı zamanda bu işlerin izlenebilir ve gerektiğinde tekrar çalıştırılabilir olması beklenir.
.NET için birçok çözüm bulunuyor. En başında akla gelen BackgroundWorker olabilir. Dilerseniz sadece Task dahi kullanabilirsiniz fakat bunlar basit senaryolarda iş görse de üretim ortamında önemli eksikler barındırır. Bu yöntemlerle çalışan bir işin başlayıp başlamadığı, hala devam edip etmediği ya da hatayla mı sonlandığı kolayca takip edilemez. Ayrıca uygulama yeniden başlatıldığında ne durumda olduklarını takip edemezsiniz. IIS recycle olduğunda veya deployment yapıldığında bu işler yarıda kesilir.
Örnek olarak .NET BackgroundWorker çalışma örneği göstereceğim. Oldukça basit bir kullanımı var. Form çalıştığında arka planda bir kullanıcıya e-posta gönderme işlemi yapılacak.
using System;
using System.ComponentModel;
using System.Net.Mail;
using System.Windows.Forms;
public partial class Form1 : Form
{
BackgroundWorker bw = new BackgroundWorker();
public Form1()
{
InitializeComponent();
bw.WorkerReportsProgress = true;
bw.DoWork += bw_DoWork;
bw.ProgressChanged += bw_ProgressChanged;
bw.RunWorkerCompleted += bw_RunWorkerCompleted;
}
private void bw_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 1; i <= 100; i += 10)
{
System.Threading.Thread.Sleep(200); // bekle
bw.ReportProgress(i);
}
// Gerçek mail gönderimi (kısaca)
using (SmtpClient smtp = new SmtpClient("smtp.gmail.com", 587))
{
smtp.EnableSsl = true;
smtp.Credentials = new System.Net.NetworkCredential("mail@gmail.com", "app-password");
smtp.Send("mail@gmail.com", "receiver@recepserit.com", "Test", "BackgroundWorker job test mail");
}
}
private void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
}
private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
lblDurum.Text = "Done!";
MessageBox.Show("Mail sent!");
}
}
Bu yapıda sorunlar var. Birincisi BackgroundWorker UI thread ile bağlantılıdır. Örnekte WinForm mevcut ancak web uygulaması da olabilir. Uygulama kapanırsa veya restart olursa arka plan işlemi anında kesilir ve mail gönderilmez. Yarım kalmış işlem kaybolur, nerede kesildiği bilinmez ve tekrar başlatılmaz.
Gerçek anlamda cron job mantığına sahip sistemlerde ise işler kalıcıdır ve durum bilgileri saklanır. Hata alınması durumunda otomatik yeniden deneme (retry) mekanizmaları devreye girer, çalışma geçmişi loglanır ve gerektiğinde manuel olarak tetiklenebilir. Bu da özellikle veri işleme, rapor üretimi, e-posta gönderimi gibi zaman alan ve kritik süreçler için büyük bir avantaj sağlar.
Hangfire Çözümü
Hangfire .NET içinde sunulan, açık kaynak ve güçlü bir arka plan iş yönetim aracıdır. Zaman alan işlemleri ya da belirli periyotlarla çalışması gereken görevleri, ana uygulamanın akışını kesintiye uğratmadan çalıştırmanıza olanak tanır.
Hangfire temel farklarından biri, arka plan işlerini ayrı ve kalıcı bir veritabanında tutmasıdır. Bu sayede uygulama yeniden başlasa, servis çökse ya da deployment yapılsa bile işler kaybolmaz. Görevlerin durumu, hataları ve çalışma geçmişi veritabanında saklanır. Başarısız olan işler otomatik olarak tekrar denenebilir. Bu yaklaşım klasik BackgroundWorker veya Task tabanlı çözümlerde olmayan bir güvenilirlik sağlar.
Üstelik kendisine ait bir dashboard bulunuyor. Job yönetimi buradan yapılıyor. Yapılandırmayla bunları özelleştirebiliyorsunuz.
Zamanlanmış ve periyodik (cron) görevleri kolayca tanımlamaya imkan tanır. Gece çalışan işlemler, düzenli yedekler veya haftalık raporlar manuel müdahale olmadan otomatik şekilde çalıştırılabilir. İşler arttıkça, birden fazla worker veya sunucu eklenerek yük dağıtılabilir. Bu ise sistemi yatay olarak ölçeklenebilir hale getirir.
Hangfire mimarisinde kritik nokta, tüm işlerin ortak bir veritabanında tutulmasıdır. Her job veritabanına yazılır. Çalışan worker veritabanını dinler. Bir worker işi alır ve çalıştırır. Aynı iş aynı anda sadece tek bir worker tarafından alınabilir çünkü “lock” mekanizması var. Bu yüzden Hangfire tek bir sunucuya bağlı olmayabilir.
Birden fazla sunucuda çalışabilir kısmını detaylandıralım. Yanlış değil, gerçekten Hangfire birden fazla sunucuda çalışabilir. Örneğin şöyle bir senaryo olsun:
- Web App – Server A
- Web App – Server B
- Web App – Server C
Üçünde de Hangfire kurulu ve aynı Hangfire veritabanı kullanılıyor.
Üç sunucu da arka plan işlerini çalıştırabilir. Hangfire işleri otomatik olarak paylaştırır. Bir iş sadece tek bir sunucuda çalışır.
Bu bazı projelerde oldukça önem kazanıyor. Trafik arttığında yeni sunucu ekleyebilirsiniz. Kod değişmez ve kaydedilen job kaybolmaz, tek sunucuda boğulmazlar. Yani horizontal scaling dediğimiz şey tam olarak budur.

Hangfire Yapılandırması
Bu paketi kurmak için package manager console içine bunları yazabilirsiniz:
Install-Package Hangfire
Install-Package Hangfire.SqlServerDilerseniz Nuget üzerinde aratarak da kurabilirsiniz. “SqlServer” paketi önemli çünkü onunla veritabanı işlemlerini gerçekleştiriyor. Kurulumu web projenizde yapmalısınız. Örnek olarak Razor uygulaması açtım.
Program.cs için adım adım ilerleyelim:
// Host oluşturuluyor
var builder = WebApplication.CreateBuilder(args);
// Hangfire dahil ediliyor
builder.Services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(builder.Configuration.GetConnectionString("Hangfire"), new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true
}));
builder.Services.AddHangfireServer();SetDataCompatibilityLevel(CompatibilityLevel.Version_180): Hangfire için veritabanı şeması ve job ile hangi sürüm uyumluluğunda çalışacağını belirler. Eski Hangfire sürümleriyle oluşturulmuş job varsa sorunsuz çalışmasını sağlar. Version_180, Hangfire 1.8.x veri formatına uyumluluk demektir.
UseSimpleAssemblyNameTypeSerializer(): Job içindeki tip bilgilerini serialize ederken assembly tam adı yerine basit adını kullanır. Bu sebeple versiyon değişikliklerinde job bozulma ihtimali azalır.
UseRecommendedSerializerSettings(): Hangfire JSON serializer ayarıdır. Performans ve uyumluluk açısından önerilen otomatik ayarları uygular. Manuel serializer ayarı yapmamak için “best practice” kısayolu
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5): Veritabanına gönderilen batch (toplu) komutların maksimum çalışma süresini belirler. Uzun süren job update işlemleri için timeout oluşmasını engeller.
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5): Bir job bir worker tarafından alındığında o job, diğer worker öğeleri için geçici olarak görünmez olur.
QueuePollInterval = TimeSpan.Zero: Worker kuyruğu ne sıklıkla kontrol edeceğini belirler. Bu anlık olarak kontrol edeceğini gösterir. Düşük gecikme istenen sistemlerde kullanılır.
UseRecommendedIsolationLevel = true: SQL Server içindir. Hangfire için önerilen transaction isolation level kullanılır. Amaç deadlock risni azaltmak ve performansı arttırmaktır.
DisableGlobalLocks = true: Hangfire eski sürümlerde kullandığı global database lock mekanizmasını kapatır. Bu sayede multi-server (cluster) ortamlarında çok daha iyi performans sağlar ve lock contention azalır.
Hangfire için veritabanı bağlantısı ve varsayılan yapılandırma sağlandı. Şimdi içerisinde çalıştıracağımız servisleri DI içerisine dahil ettireceğim. Buradaki örneğimizde factory pattern kullanacağız.
// Hangfire içerisinde çalıştıracağım servisler
builder.Services.AddSingleton<Func<Jobs, IJob>>(provider => key =>
{
return key switch
{
Jobs.ExpirationReminder => provider.GetService<ExpirationReminderJob>(),
Jobs.ContactVerification => provider.GetService<ContactVerificationJob>(),
Jobs.BankWireCheck => provider.GetService<BankWireJob>(),
_ => throw new ArgumentException($"Job '{key}' not found")
};
});
builder.Services.AddTransient<IJobPlan, JobPlan>();Bu yönteme “factory delegate” denilir. Enum ile job sınıfının tipini belirleyip, doğru job sınıfını DI üzerinden resolve eden bir factory oluşturur ve job çalıştırma mantığını ölçeklenebilir ve test edilebilir hale getirir.

Şimdi uygulamam oluşturuluyor ve Hangfire için dashboard paneli özelleştiriliyor.
var app = builder.Build();
app.UseExceptionHandler("/Error");
app.UseHsts();
app.UseHangfireDashboard("/panel", new DashboardOptions
{
DashboardTitle = "Çalışan Uygulamalar Paneli",
IgnoreAntiforgeryToken = true,
Authorization = new[] { new DashboardAuthorizationFilter() }
});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHangfireDashboard();
});Bu kısımda Hangfire panelini özelleştirerek yayınlıyoruz. “/panel” diyerek erişebileceğimiz URL’yi belirtiyoruz. Varsayılan olarak /hangfire kullanılır.
“IgnoreAntiforgeryToken” diyerek dashboard için CSRF token kontrolünü kapatılıyor. MVC form yapısını kullanmadığı için bunu “ignore” olarak bırakmak daha doğru. “Authorization” filtresi önemli bir nokta. Bu property ile panele kimlerin erişebileceğini belirliyoruz.
“DashboardAuthorizationFilter” adında custom bir filtre oluşturduk. Kullanıcı yetkisini denetletiyoruz. Çünkü dashboard herkese açık bırakılmamalıdır. Filtre böyle oluşturulabilir:
using Hangfire.Dashboard;
public class DashboardAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
return httpContext.User.Identity?.IsAuthenticated == true
&& httpContext.User.IsInRole("Admin");
}
}
Hangfire yapılandırmasını ve panel arayüz ile ilgili düzenlemelerimizi yaptık. Şimdi uygulamayı çalıştıralım.
var job = app.Services.GetRequiredService<IJobPlan>();
await job.ScheduleAsync();
app.MapRazorPages();
app.Run();“JobPlan” sınıfına ait kodlara buradan erişebilirsiniz. “Recurring job” türündedir ve IJob interface’e bağlı tüm sınıfları otomatik job kuyruğuna ekler. Kullanabileceğimiz attribute türlerine sonra değinebiliriz.
Job yönetimi için örnek bir paneli aşağıdaki gibi görebilirsiniz. Her gün belirli periyotlarda çalışan 11 adet “recurring job” bulunuyor. Tümünün yönetimi tek bir panel üzerinden yapabiliyoruz.

Job Türleri ve Kullanımı
Birçok farklı türde job bulunabilir. En yaygın job çalıştırma türlerini ve pratik kullanım senaryolarını inceleyelim.
Fire-and-Forget Job: Tek seferlik tetiklenen ve sonucunu beklemenize gerek olmayan işlerdir. Job kuyruğa alınır ve arka planda çalışır. Örneğin bir kullanıcı sisteme giriş yaptığında, bu olayı audit/log amaçlı kaydetmek istiyorsunuz. Ancak giriş işlemini yavaşlatmak istemiyorsunuz.
public class LoginService
{
private readonly IBackgroundJobClient _backgroundJobClient;
public LoginService(IBackgroundJobClient backgroundJobClient)
{
_backgroundJobClient = backgroundJobClient;
}
public void UserLoggedIn(string userId)
{
_backgroundJobClient.Enqueue(() => LogUserLogin(userId));
}
public void LogUserLogin(string userId)
{
Console.WriteLine($"User logged in: {userId}");
}
}Delayed Job: Bir işin hemen değil belirli bir süre sonra çalışmasını sağlar. Genellikle kullanıcı aksiyonlarından sonra “bekleme süresi” gereken durumlarda kullanılır.
Bir kullanıcı sipariş verdiğinde, ödeme kontrolünü hemen değil, 5 dakika sonra yapmak istiyorsunuz diyelim.
public class OrderService
{
private readonly IBackgroundJobClient _backgroundJobClient;
public OrderService(IBackgroundJobClient backgroundJobClient)
{
_backgroundJobClient = backgroundJobClient;
}
public void SchedulePaymentCheck(int orderId)
{
_backgroundJobClient.Schedule(
() => CheckPaymentStatus(orderId),
TimeSpan.FromMinutes(5)
);
}
public void CheckPaymentStatus(int orderId)
{
Console.WriteLine($"Checking payment for order {orderId}");
}
}Recurring Job:Belirli aralıklarla çalışan işlerdir. Bakım, raporlama ve belirli periyotlarda sürekli çalışması gereken çalışmalardır. Her gece süresi dolmuş kullanıcı oturumlarını temizlemek istiyorsunuz diyelim.
public class SessionCleanupJob
{
public void CleanExpiredSessions()
{
Console.WriteLine("Expired sessions are being cleaned.");
}
}
public void ConfigureRecurringJobs()
{
RecurringJob.AddOrUpdate<SessionCleanupJob>(
"expired-session-cleanup",
job => job.CleanExpiredSessions(),
Cron.Daily(02, 00) // Her gece 02:00
);
}
Çalıştığım projelerden ötürü en sık kullandığım tür budur.
Chained Job: Bazı işler sıralı şekilde çalışmalıdır. Bir job bitmeden diğeri başlamamalıdır. Bu durumda chained job kullanılır. Örneğin bir rapor oluşturulacak, ardından PDF’e çevrilecek ve son olarak e-posta ile gönderilecek.
public class ReportService
{
private readonly IBackgroundJobClient _backgroundJobClient;
public ReportService(IBackgroundJobClient backgroundJobClient)
{
_backgroundJobClient = backgroundJobClient;
}
public void GenerateAndSendReport(int reportId)
{
var generateJobId = _backgroundJobClient.Enqueue(
() => GenerateReport(reportId)
);
_backgroundJobClient.ContinueWith(
generateJobId,
() => SendReportByEmail(reportId)
);
}
public void GenerateReport(int reportId)
{
Console.WriteLine($"Report {reportId} generated.");
}
public void SendReportByEmail(int reportId)
{
Console.WriteLine($"Report {reportId} sent via email.");
}
}Concurrency
Hangfire’da concurrency (eşzamanlılık) kontrolü vardır ve istenirse sınırlandırılabilir, hatta belirli job sınıfları için fiilen “disable” edilebilir. Ama bu global bir kapatma değil kontrollü bir kısıtlama şeklindedir.
Concurrency burada aynı anda kaç job türünün paralel çalışabileceği anlamına gelir. Hangfire’da bu üç seviyede kontrol edilir:
- Server / Worker seviyesi
- Queue (kuyruk) seviyesi
- Job (metot) seviyesi
Worker seviyesinde concurrency (global sınır): Böyle yapılandırma yapabilirsiniz.
services.AddHangfireServer(options =>
{
options.WorkerCount = 10;
});Aynı anda en fazla 10 job çalışır. Bu sayı Environment.ProcessorCount * 5 varsayılanına göre düşürülebilir. Bu işlem concurrency eylemini sınırlar yani tamamen kapatmaz.
Job seviyesinde concurrency:
public class BankWireJob
{
[DisableConcurrentExecution(timeoutInSeconds: 300)]
public void Execute()
{
// Aynı anda sadece 1 instance çalışır
}
}Bu attribute ile aynı job metodunun aynı anda birden fazla çalışmasını engeller. Distributed lock kullanır (DB üzerinden). Süre dolarsa lock otomatik düşer. En çok kullanılan concurrency kontrolü budur.
Queue bazlı concurrency (dolaylı kontrol):
[Queue("critical")]
public void CriticalJob() { }
services.AddHangfireServer(options =>
{
options.Queues = new[] { "critical" };
options.WorkerCount = 1;
});Bu server sadece critical kuyruğunu dinler. WorkerCount = 1 yani o kuyruğa concurrency fiilen kapalıdır. Queue bazlı izolasyon sağlar.
Doğru yaklaşım concurrency’nin kapatılması değil kontrollü yönetilmesidir.

Daha fazla bilgi için Hangfire dokümanını inceleyebilirsiniz: https://docs.hangfire.io/en/latest/background-processing/throttling.html
Sonuç
Hangfire her ne kadar .NET dünyasında arka plan job yönetimi için en yaygın çözümlerden biri olsa da her proje için tek seçenek değildir. Daha karmaşık zamanlama ihtiyaçlarında Quartz.NET tercih edilebilirken, bulut tabanlı ve sunucusuz mimarilerde Azure Functions veya AWS Lambda öne çıkar. Dağıtık sistemlerde mesajlaşma ve iş kontrolü ön plandaysa RabbitMQ daha uygun bir yaklaşım sunar. Kısacası sistemin mimarisine, ölçeklenme ihtiyacına ve operasyonel beklentilere göre bu yaklaşımlar seçilmelidir.
Her paketi kendi ihtiyacınıza göre seçmelisiniz. BackgroundWorker veya bir Timer kullanırken daha esnek bir kütüphane için Quartz tercih edebiliyorsunuz. Farklı bir projede uçtan uca yönetilebilir bir yapı gerekli olabilir. Job geçmişini tutmak, retry seçeneğini olması, concurrency için bir yapılandırma olması ve bir dashboard aracılığıyla yönetiminin yapılabilmesi gerçekten harika. Desteklediği bu özelliklerle operasyonel yükü önemli ölçüde azaltır. Quartz bu özelliklerin çoğu için ek altyapı ve geliştirme gerekirken Hangfire daha az yapılandırmayla hızlıca production-ready bir çözüm sağlar.



Yorum bırakın