Caner Tosuner

Leave your code better than you found it

Asp.Net Core Hangfire Kullanarak Background Task İşlemleri

Projelerimizde olağan akışında ilerlerken veya bir business rule çalışırken mevcut akışı durdurmadan asenkron bir şekilde uygulamadan bağımsız çalışmasını istediğimiz process'ler olmuştur. Bu gibi ihtiyaçları karşılaması için Azure olsun Google-Cloud yada Amazon olsun kendi cloud çözümlerini üreterek kullanmamıza olanak sağlamaktadırlar.

Peki ya on-premise dediğimiz kurum içi yada domain içi çözüm olarak neler yapabiliriz ? Fire-and-forget (messaging queue) yapılarından birini kullanabilir veya mevcut uygulamada background job'lar üretebiliriz.

Bu yazımızda Asp.net Core uygulamalarında Hangfire kullanarak background-task'lar nasıl oluşturulur inceleyeceğiz.

Hangfire 

Özetle; open-source olarak geliştirilmiş schedule edilebilen process'lerin kolay bir şekilde yönetimini sağlayan bir kütüphanedir. Sahip olduğu dashboard ile job'larınızı historical olarak görüntüleyebilir, start-stop/restart gibi işlemler yapabilirsiniz.

An easy way to perform background processing in .NET and .NET Core applications. No Windows Service or separate process required.

Hangfire job'ları yönetirken storage alanı olarak hemen hemen bütün database türleri için destek sağlamaktadır. SQL Server, Redis, PostgreSQL, MongoDB etc.

İlk olarak uygulamamızda kullanmak üzere local Sql Server üzerinde Hangfire adında bir databse oluşturalım.

Sonrasında Vs.'da BackgroundTaskWithHangfire adında bir Api projesi oluşturalım ve nuget üzerinden bugün için en güncel olan Hangfire v1.6.20 paketini projemize ekleyelim. 

PM> Install-Package Hangfire

İlk başta oluşturduğumuz database'in conn string bilgilerini appSettings.json dosyasına ekleyelim.

  "HangfireDbConn": "Server=.;Initial Catalog=Hangfire;Persist Security Info=False;User ID=HangfireUser;Password=qwerty135*;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=True;Connection Timeout=30;"

Sırada uygulama üzerinde hangfire configuration'ı var. Bunun için Startup.cs de bulunan ConfigureServices metodu içerisinde Hangfire'ı service olarak ekleyelim ve sonrasında Configure metodu içerisinde bu service'i kullanacağımızı belirten kod bloklarını yazalım.

public void ConfigureServices(IServiceCollection services)
{
    services.AddHangfire(_ => _.UseSqlServerStorage(Configuration.GetValue<string>("HangfireDbConn")));
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseHangfireDashboard();
    app.UseHangfireServer();
}

Yukarıdaki kod bloğunda hangfire storage bilgisini vererek uygulamamız service'lerine register ettik ve devamındada hangfire'ın server ve dashboard service'lerini kullanacağımızı belirttik.

Hangfire dashboard default olarak uygulamanın çalıştığı portta' http://<application>/hangfire adresinde host edilir. Bizde localhostta çalıştığımızdan uygulamayı run edip browser üzerinden http://localhost/hangfire adresine gittiğimizde aşağıdaki gibi hangfire dashboard'u görüntüleyebiliriz.

Dashboard'la birlikte uygulama her start olduğunda db'de ilgili tablolar oluşmuşmu diye check ederek oluşmadıysa tabloları oluşturur. Hangfire tablolarını sql server management studio üzerinden görüntüleyebilirsiniz.

 

Hangfire konfigurasyonunu tamamladık şimdi sırasıyla Hangfire kütüphanesinde bulunan BackgroundJob sınıfını kullanarak oluşturabileceğimiz job türlerine bakacak olursak;

1- Fire-and-Forget Jobs

            Job create edildikten sonra çalışır ve process olur.

public class FireAndForgetJob
{
    public FireAndForgetJob()
    {
        //Fire and forget
        var jobId = BackgroundJob.Enqueue(() => ProcessFireAndForgetJob());
    }

    public void ProcessFireAndForgetJob()
    {
        Console.WriteLine("I am a Fire and Forget Job !!");
    }
}

2- Delayed Jobs

            Belli bir zaman bilgisi set edilerek sadece bir kez çalışmasını istediğimiz task'lar için kullanabileceğimiz job türü. Aşağıdaki gibi Job register olduktan 4 dkka sonra çalışacaktır.

public class DelayedJob
{
    public DelayedJob()
    {
        //Delayed job
        var jobId = BackgroundJob.Schedule(() => ProcessDelayedJob(), TimeSpan.FromMinutes(4));
    }

    public void ProcessDelayedJob()
    {
        Console.WriteLine("I am a Delayed Job !!");
    }
}

3- Recurring Jobs

            Recurring yani tekrar eden task'lar için kullanılan job türü. Örneğin; her saat başı çalışmasını istediğiniz bir job'a ihtiyacınız olduğunda aşağıdaki gibi tanımlayabiliriz.

public class Recurring_Job
{
    public Recurring_Job()
    {
        //Recurring job
        RecurringJob.AddOrUpdate(() => ProcessRecurringJob(), Cron.Hourly);
    }

    public void ProcessRecurringJob()
    {
        Console.WriteLine("I am a Recurring Job !!");
    }
}

4- Continuations Jobs

            Parent-child ilişkisinin olduğu yani bir job'ın çalışması için başka bir job'ın tamamlanmasını bekleyip o Cmplete olduktan sonra çalışmasını istediğimiz işler için kullanabileceğimiz job türü.

public class ContinuationsJob
{
    public ContinuationsJob()
    {
        //Delayed job
        var parentJobId = BackgroundJob.Schedule(() => Console.WriteLine("I am a Delayed Job !!"), TimeSpan.FromMinutes(4));


        //Continuations job
        BackgroundJob.ContinueWith(parentJobId, () => ProcessContinuationsJob());
    }

    public void ProcessContinuationsJob()
    {
        Console.WriteLine("I am a Recurring Job !!");
    }
}

Projeyi run edip tekrardan  http://localhost/hangfire adresine gittiğimizde ilgili job türlerine ait bilgileri dashboard'da görüntüleyebiliriz.

Özetlemek gerekirse; uygulamanızda çalıştırmanız gereken background-task'ları için Hangfire implementasyonunu hızlı bir şekilde yapıp dashboard'u ile birlikte kolayca kullanabilirsiniz. Eğer .net core'un kendi background task sınıfını kullanarak ilerlemek isterseniz hangfire'a göre daha zorlu bir süreç sizi bekliyor olacaktır. Hem yönetilebilirlik açısından hemde visualization olarak hangfire kesinlikle sizin için daha sorunsuz ve kullanışlı bir çözüm olacaktır. Hangfire'ın muadili olan Quartz.net veya bir queue çözümü de kullanarak işlemlerinizi yapabilirsiniz.

Source Code

Entity Framework Core Data Concurrency Optimistic Lock

Data Concurrency yönetimi server-side bir projede oldukça büyük önem arzetmektedir. Kullanıcıya hem doğru hemde fresh dediğimiz bayat olmayan veriyi Concurrency conflicts'lerine yol açmadan ulaştırıyobilmek gerekir. Concurrency conflict'leri dediğimiz durum Optimistic Lock Nedir ? Pessimistic Lock Nedir ?

Data concurrency yazımızdada bahsettiğimiz üzre;

Bir internet sitesinde kayıtlı bulunan adres bilginizi güncellemek istiyorsunuz. Aynı anda 2 farklı bilgisayardan bilgileri güncelle sayfasını açtınız ve adresiniz o an "Samsun" olarak kayıtlı yani 2 ekranda da "Samsun" yazıyor. İlk bilgisayarda bulunan kişi adres bilgisini "Ankara" olarak değiştirdi ve güncelle butonuna basıp bilgiyi güncelledi.

İkinci ekranda bulunan kişi ise ekranda halen "Samsun" yazılı iken adres bilgisini "İstanbul" olarak değiştirdi ve güncelle butonuna basıp bilgiyi güncelledi. Ekranda yazan "Samsun" kaydı artık bizim için bayat bir kayıttır ve birinci kullanıcı değişikliği "Samsun" => "Ankara" yaptığını düşünürken ikinci kişi bu değişikliği "Samsun" => "İstanbul" yaptığını düşünüyor. Halbuki gerçekte olan ikinci kişi adres bilgisi ekranda "Ankara" iken => "İstanbul" olarak değiştirmiş oldu.

Kısaca Last In Wins yani son gelen kazanır. Bu gibi durumlara yol açmamak adına kullanmış olduğunuz ORM çeşidine göre farklı çözümler sunulmakta. Data Concurrency'yi sağlayabilmek adına genellikle üzerinde işlem yapılan data'ya lock işlemi uygulanır. Locking işlemi için 2 farklı yaklaşım vardır. Pessimistik Lock ve Optimistic Lock.

Bu yazımızda ise Entity Framework Core kullandığımız bir projede Concurrency sağlamak adına neler yapabiliriz değineceğiz.

Entity Framework Core Concurrency Conflict'lerini engel olmak adına 2 seçenek sunmakta.

  1. Mevcut Property'i bir Concurrency Token attribute'ü ile konfigüre etmek,
  2. RowVersion oluşturarak tıpkı bir concurency token gibi davranmasını sağlamak. 

Property Based Configuration (ConcurrencyCheck Attribute)

Property'ler ConcurrencyCheck attribute'ü kullanılarak o property için bir concurrency token oluşturmasını sağlar ve conflict'lere engel olmamıza olanak sağlar.

public class Customer
{
    [ConcurrencyCheck]
    public string FullName { get; set; }
    public string CityCode { get; set; }
    public DateTime BirthDate { get; set; }
}

Concurrency Token tanımlamanın bir diğer yoluda mapping oluştururken property için bu bilgiyi IsConcurrencyToken() metodunu kullanarak set etmek.

public class CustomerDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
         modelBuilder.Entity<Customer>()
            .Property(a => a.FullName).IsConcurrencyToken();
    } 
}

public class Customer
{
    public string FullName { get; set; }
    public string CityCode { get; set; }
    public DateTime BirthDate { get; set; }
}

Yukarıda yaptığımız konfigurasyonlar sonucunda model'i retrieve ederken almış olduğumuz concurrency token bilgisi her bir Update ve Delete query'si için where koşuluna eklenir. Execution sırasında Entity Framework where koşuluna eklediği bu token bilgisi konfigüre edilmiş kolonlardan birisi data'nın retrieve edildiği ve update işlemi gönderildiği zaman diliminde değiştirilmişse DbUpdateConcurrencyException throw eder.

Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.

Test ettiğimizde update işlemi gönderdiğimiz sırada data retrieve anında db de ilgli alanı manuel olarak update ettikten sonra devam ettiğimizde aşağıdaki gibi hata alırız. 

Concurrency token kullanarak modelinizde bulunan property'ler için tanımlamalar yaparak data-concurrency'i sağlayabilirsiniz ancak unutmamalıyız ki aynı modelde bulunan çok fazla property için(more than 20) kullanmak where koşulunda belirtilen condition'ları arttırmak demektir ve buda execution süresinin artmasına sebep olacaktır. 

RowVersion Column

Data Concurrency sağlamak adına kullanılabilecek ikinci yöntem ise Rowversion kullanmak. İlgili tabloya RowVersion adında yeni bir column ekleyerek o row için geçerli bir concurrency token yada version bilgisi store ederek. Concerrency Conflict oluşmasına engel olabiliriz. RowVersion incremental olması gerekmekte bu sebeple numeric bir değeri olmalı. Örnek olarak ; userA ve userB aynı model'i rowVersion 1 olarak retrieve ettiler ancak userA bir update işlemi yaparak rowVersion'ı 2 yaptı. userB update işlemi gönderdiğinde Entity Framework Core DbUpdateConcurrencyException throw ederek conflict hatası verecektir ve update gerçekleşmeyecektir. 

RowVersion property'sinin tipi byte array olması gerekmekte ve TimeStamp data annotations attribute'ü kullanarak konfigüre edilebilmekte.

public class Customer
{
    public string FullName { get; set; }
    public string CityCode { get; set; }
    public DateTime BirthDate { get; set; }
    [TimeStamp]
    public byte[] RowVersion { get; set; }
}

Fluent Api kullanarak yapmak istersekte aşağıdaki gibi mapping sırasında bu bilgiyi set edebiliriz.

public class CustomerDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
         modelBuilder.Entity<Customer>()
            .Property(a => a.RowVersion).IsRowVersion();
    } 
}

public class Customer
{
    public string FullName { get; set; }
    public string CityCode { get; set; }
    public DateTime BirthDate { get; set; }
    public byte[] RowVersion { get; set; }
}

Yine yukarıdaki gibi projeyi çalıştırıp 2 farklı session'da data get işleminden ilgili row'Da bulunan column'lar dan herhangi birini başarılı bir şekilde update edip sonra diğerinde update işlemi gönderdiğimizde entity franework tarafından data-conflict olduğunu söyleyen DbUpdateConcurrencyException throw edildiğini göreceksinizdir.

Bu exception'ı bir try-catch bloğu kullanarak handle edebilir ve ikinci kullanıcıya anlamlı bir error-message göstererek tekrardan en güncel olan kaydı gösterip ardından yeniden update işlemi yapmasını sağlayabilirsiniz.