Hangfire Nedir? .NET Core Hangfire Kullanımı

Can Yüksel

Erdoğancan Yüksel

5 dakika 5 saniye okuma süresi

Hangfire, arka plan görevlerini güvenli bir şekilde planlayarak ilgili arka plan işlerini yürütmemiz ve yönetmemiz konusunda oldukça kolaylık sağlayan açık kaynaklı bir kütüphanedir.

hangfire-nedir

Hangfire Nedir?

Hangfire, arka plan görevlerini planlayıp yürütmemizi sağlayan açık kaynaklı bir kütüphanedir. Hangfire ile .NET uygulamalarında asenkron işlemleri zamanlayabilir, kalıcı hale getirebilir ve ilgili işlemleri bir dashboard üzerinden izleyebilirsiniz.

Hangfire'ın başlıca özellikleri şu şekilde sıralanabilir:

  • Arka plan görevlerini sürdürülebilir(persist) şekilde çalıştırır.
  • İlgili görevin başarısız olması durumunda retry(yeniden deneme) mekanizması sunmaktadır.
  • Zamanlanmış işler (Scheduled), periyodik işler (Recurring) ve anlık işler (Fire-and-Forget) gibi farklı görev türlerini destekler.
  • Dashboard arayüz özelliği sayesinde görevlerin durumlarını izleme imkanı sunar.

Hangfire, e-posta gönderimi, günlük, haftalık, aylık veya yıllık raporlama işlemlerinin otomatik hale getirilmesi, dış servislere periyodik istekler gönderilmesi ve veritabanı veya dosya sistemleri üzerinden veri temizliği yapılması gibi işlemlerde kullanılabilir. İlgili görevleri gerçekleştirmek için Quartz.NET veya Windows service'ler de kullanabilmektedir.

.NET Uygulamalarında Hangifre Kurulumu

NuGet paket yöneticisi kullanılarak Hangfire kütüphanesini projeye eklemek için aşağıdaki komut kullanılabilir:

-> Install-Package Hangfire

.NET CLI kullanrak terminal üzerinden yüklemek için:

-> dotnet add package Hangfire

Veya Solution üzerinden “Manage Nuget Packages for Solution” seçeneğine tıklayarak aşağıdaki görseldeki kütüphaneleri projenize ekleyebilirsiniz.

hangfire-kurulumu

Not: Kurulum örneği SQL üzerinden yapılmaktadır fakat Hangfire SQL dışında Redis, PostgreSQL gibi farklı çözümleri de desteklenmektedir.

Hangifre Kullanımı

Hangfire konfigürasyonu için kendi isteğimize göre ismini belirlediğimiz HangfireConfigurationExtensions isimli genişletilebilir metot(extension method) yazıyoruz:

// Hangfire Configuration Extension
public static class HangfireConfigurationExtensions
{
    public static IServiceCollection AddHangfireScheduler(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddHangfire(cfg => cfg
            .SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
            .UseSimpleAssemblyNameTypeSerializer()
            .UseRecommendedSerializerSettings()
            .UseSqlServerStorage(configuration.GetConnectionString("HangfireDbConnection"), new SqlServerStorageOptions
                {
                    CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
                    SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
                    QueuePollInterval = TimeSpan.FromSeconds(15),
                    UseRecommendedIsolationLevel = true,
                    DisableGlobalLocks = true
                })
        );

        // Global retry policy
        GlobalJobFilters.Filters.Add(new AutomaticRetryAttribute
        {
            Attempts = 3, // Attempt count
            DelaysInSeconds = new[] { 30, 60, 90 }, // 30 second, 60 second, 90 second
            OnAttemptsExceeded = AttemptsExceededAction.Fail
        });

        services.AddHangfireServer(option =>
        {
            option.WorkerCount = Environment.ProcessorCount * 5;
            option.SchedulePollingInterval = TimeSpan.FromSeconds(15);
        });

        //Hangfire background schedule job manager
        services.AddScoped<IBackgroundJobManager, BackgroundJobManager>();

        return services;
    }
}

Yukarıdaki kodda AutomaticRetryAttribute alanı içerisine genel bir retry policy kuralı eklenmiştir. İlgili attribute override edilebilir. Override etmek istediğimiz görevimize [AutomaticRetry(Attempts = 0)] attribute'u eklenerek retry sayısını özelleştirebiliriz.

Extension method içerisindeki bazı alanların anlamları ve işlevleri şu şekilde sıralanabilir:

  • SetDataCompatibilityLevel (CompatibilityLevel.Version_180): Hangfire'ın veriyi nasıl yazıp okuyacağına dair sürüm uyumluluğunu tanımlamaktadır. 1.8 sürümü ile eklenen yeni davranışları bu şekilde aktif etmiş olur.
  • CommandBatchMaxTimeout = TimeSpan.FromMinutes(5): Hangfire'ın SQL Server'a gönderdiği grup(batch) komutun maksimum çalışma süresini belirler. Bu sayede paralel yürütme verimliliği artar, db round-trip(isteğin ağ üzerinden geçerek hedefe gitmesi ve yanıtın geri dönmesi için geçen toplam süreyi ifade eder.) süresi azalır.
  • SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5): İlgili iş tamamlanana dek işin tekrar kuyruğa dönmesi riskini azaltır.
  • UseRecommendedIsolationLevel = true: Job verilerinin her zaman tutarlı ve güvenli bir şekilde okunmasını sağlayarak stale read(eski verilerin okunarak tutarsızlıklara yol açması) riskini ortadan kaldırır.
  • DisableGlobalLocks = true: Bazı SQL Server application lock mekanizmalarını devre dışı bırakır. Eski sürümlerde kapatıldığında deadlock riski bulunmaktaydı fakat güncel sürüm şemasında güvenle devre dışı bırakılabilir.

Hangfire veritabanı bağlantı bilgisini appsettings.json dosyasına ekleyelim:

"ConnectionStrings": {
  "HangfireDbConnection": "Host=localhost;Port=5432;Database=your_db_name;Username=your_username;Password=your_password"
}

İlgili özelliğin programın derlemesi sırasında görünmesi için yazmış olduğumuz extension metodunu Program.cs sınıfının içerisine tanımlayalım:

using Hangfire;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHangfireScheduler(builder.Configuration);

var app = builder.Build();

app.UseHangfireDashboard();

Zamanlanmış işler(Scheduled), Periyodik işler(Recurring) ve Anlık işler(Fire-and-Forget) işler için IBackgroundJobManager isimli bir arayüz(interface) oluşturup ilgili metotları tanımlayalım. Ardından BackgroundJobManager sınıfı oluşturup ilgili metotları sınıfa uygulayalım.

IBackgroundJobManager arayüzü:

public interface IBackgroundJobManager
{
    string Enqueue<T>(Expression<Func<T, Task>> methodCall) where T : class;
    string Schedule<T>(Expression<Func<T, Task>> methodCall, TimeSpan delay) where T : class;
    void AddOrUpdate<T>(string recurringJobId, Expression<Func<T, Task>> methodCall, string cronExpression) where T : class;
    void RemoveRecurring(string recurringJobId);
    void Delete(string jobId);
}

BackgroundJobManager sınıfına ilgili metotların uygulanması:

public class BackgroundJobManager : IBackgroundJobManager
{
    public void AddOrUpdate<T>(string recurringJobId, Expression<Func<T, Task>> methodCall, string cronExpression) where T : class
    {
        RecurringJob.AddOrUpdate(recurringJobId, methodCall, cronExpression);
    }

    public void Delete(string jobId)
    {
        BackgroundJob.Delete(jobId);
    }

    public string Enqueue<T>(Expression<Func<T, Task>> methodCall) where T : class
    {
        return BackgroundJob.Enqueue(methodCall);
    }

    public void RemoveRecurring(string recurringJobId)
    {
        RecurringJob.RemoveIfExists(recurringJobId);
    }

    public string Schedule<T>(Expression<Func<T, Task>> methodCall, TimeSpan delay) where T : class
    {
        return BackgroundJob.Schedule(methodCall, delay);
    }
}
  • Enqueue: İlgili işi hemen çalıştırır. (Fire-and-Forget) işlemleri için kullanılır.
  • Schedule:İligli işi belirlenen gecikme süresinde çalıştırır. Planlanmış(Scheduled) işler için kullanılır.
  • AddOrUpdate:Periyodik işler (Recurring) için kullanılır. Cron ifadesiyle zamanlaması ayarlanır.
  • Delete: Hangfire'ın BackgroundJob.Delete metodu kullanarak verilen jobId'ye sahip job silinir.
  • RemoveRecurring: Hangfire'ın RecurringJob.RemoveIfExists metodunu kullanarak tekrarlayan işleri recurringJobId üzerinden kontrol ederek siler.

Yeni eklenen bir kullanıcı için zamanlanmış mail gönderimi yapan küçük bir uygulama örneği yapalım. Öncelikle IMailService arayüzünü oluşturup ilgili arayüz içerisinde SendMailAsync() metodunu tanımlayalım. Daha sonra ilgili metodu MailService sınıfına uygulayalım.

IMailService arayüzü:

public interface IMailService
{
    Task SendMailAsync(string mailAddress, string mailSubject, string mailBody);
}

MailService sınıfı:

public class MailService(IOptions<SmtpSettings> options) : IMailService
{
    public async Task SendMailAsync(string mailAddress, string mailSubject, string mailBody)
    {
        using var client = new SmtpClient(options.Value.Host, options.Value.Port)
        {
            Credentials = new NetworkCredential(options.Value.Username, options.Value.Password),
            EnableSsl = options.Value.EnableSsl
        };

        var message = new MailMessage(
            from: options.Value.FromAddress,
            to: mailAddress,
            subject: mailSubject,
            body: mailBody
        )
        { IsBodyHtml = true };

        await client.SendMailAsync(message);
    }
}

Kullanıcı kaydının gerçekleştiği RegisterUserCommand sınıfı:

public class RegisterUserCommand : IRequest<bool>
{
    public string Email { get; set; }
    public string FullName { get; set; }   
}

RegisterUserCommandHandler sınıfı:

public class RegisterUserCommandHandler(IMailService mailService, IBackgroundJobManager backgroundJobManager) : IRequestHandler<RegisterUserCommand, bool>
{
    public async Task<bool> Handle(RegisterUserCommand request, CancellationToken cancellationToken)
    {
        // Kullanıcıyı kaydet (örnek kod, detaylar sistemine göre değişebilir)
        var userEmail = request.Email;
        var userName = request.FullName;

        // Kullanıcı başarıyla kaydedildiğinde, 1 dakika sonra mail gönder
        _backgroundJobManager.Schedule<IMailService>(
            mailService => mailService.SendMailAsync(
                userEmail,
                "Kayıt Başarılı!",
                $"Merhaba {userName}, sistemimize başarıyla kayıt oldunuz."),
            TimeSpan.FromMinutes(1)
        );

        return true;
    }
}

Yukarıdaki kod örneklerinde kullanıcı kaydından bir dakika sonra kullanıcıya gönderilmek üzere ayarlanan bir Zamanlanmış (Scheduled) Job tasarlamış olduk. İhtiyaca göre oluşturduğumuz arayüz üzerindeki metotlar yardımı ile Periyodik (Recurring) Job veya Anlık(Fire-and-Forget) Job tasarlayabiliriz.

Periyodik (Recurring) Job'lar içinCRON ifade yapısı * * * * * şeklindedir.

-> Birinci ifade dakikayı temsil eder. (0-59 arasında değer yazılabilir)

-> İkinci ifade saati temsil eder. (0-23 arasında değer yazılabilir)

-> Üçüncü ifade günü temsil ider. (1-31 arasında değer yazılabilir)

-> Dördüncü iface ayı temsil eder. (1-12 arasında değer yazılabilir)

-> Beşinci ifade haftanın gününü ifade eder. (0-6 arasında değer yazılabilir, 0=Pazar)

Yaygın kullanılan bazı CRON ifadeler için örnekler:
  • * * * * * Dakikada bir kez çalışır.
  • 0 * * * * Her saat başı çalışır.
  • 0 */6 * * * 6 saatte bir çaşır.
  • 0 6 * * * Her gün saat 6:00'da çalışır.
  • 0 9 * * 1 Her pazartesi sabah 09:00'da çalışır.

Recurring Job kullanım senaryosu için her gün saat 00:00'da sistemdeki kayıtlı kullanıcılar üzerinden sorgulama yapan ve doğum günü olan kullanıcılara kutlama maili göndern bir iş senaryosu tasarlayalım:

Doğum günü olan kullanıcı bilgilerini veritabanı üzerinden sorgulayan metot için IUserRepository arayüzü:

public interface IUserRepository
{
    Task<List<User>> GetUsersWithBirthdayAsync(DateTime date);
}

Doğum günü kutlama Job Sınıfı:

public class BirthdayJob(IUserRepository userRepository, IMailService mailService)
{
    public async Task SendBirthdayEmailsAsync()
    {
        var today = DateTime.Today;
        var birthdayUsers = await userRepository.GetUsersWithBirthdayAsync(today);

        foreach (var user in birthdayUsers)
        {
            var subject = "Doğum Gününüz Kutlu Olsun!";
            var body = $"Sevgili {user.FullName}, doğum gününüzü en içten dileklerimizle kutluyoruz!";

            await mailService.SendMailAsync(user.Email, subject, body);
        }
    }
}

Job'ların eklenmesi için extension metot oluşturalım:

public static class RecurringJobsExtensions
{
    public static void RegisterRecurringJobs()
    {
        RecurringJob.AddOrUpdate<BirthdayJob>(
            "send-birthday-mails",
            job => job.SendBirthdayEmailsAsync(),
            "0 0 * * *" // her gün 00:00'da çalışır.
        );

        // İleride başka recurring job'lar da buraya eklenebilir
    }
}

Program.cs dosyası içerisine extension metodu eklenerek kullanılabilir:

using Hangfire;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// Hangfire servislerini ve scheduler'ı ekle
builder.Services.AddHangfireScheduler(builder.Configuration);

// Mail servisini ve gerekli diğer servisleri ekle
builder.Services.AddScoped<IMailService, MailService>();
builder.Services.AddScoped<IUserRepository, UserRepository>(); // örnek: implement ettiğimiz repo

// BirthdayJob sınıfını DI container'a tanıt
builder.Services.AddScoped<BirthdayJob>();

var app = builder.Build();

// Hangfire dashboard
app.UseHangfireDashboard();

// Recurring Job'ların kayıt edildiği alan:
RecurringJobsExtensions.RegisterRecurringJobs();

app.Run();

 Sonuç olarak Hangfire kurulumu ve kullanımı oldukça pratik ve kolay olan bir kütüphanedir. Hangfire ile .NET uygulamalarında asenkron işlemleri zamanlayabilir ve ilgili işlemleri kolaylıkla yönetebilirsiniz.