Caner Tosuner

Leave your code better than you found it

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.