.Net Core’da DbContext ile Database’de Yapılan Değişiklikleri, ElasticSearch’de Saklama

Selamlar,

Sizden gelen istek ve sorular ışında, bu makalede güncel iş hayatında da bolca kullanılan, audit denilen Databasedeki değişiklikleri DbContext middleware katmanında yakalayıp, daha sonra sorgulama amaçlı ElasticSearch’e kaydedeceğiz. Bunun için Entity Framework 6 ve üstünde bir framework ile çalışılması gerekmektedir. Platform .Net Core. Bu projede sadece Update işleminin log’u tutulacaktır. Diğerleri size bırakılmıştır. Peki bunu neden yapıyoruz? Çünkü loglamak hayati bir önem taşımaktadır. Değişen bir kaydın önceki halini saklamak, bazen hata durumlarında tam bir can simididir. Ayrıca Bankalar, online alışveriş siteleri gibi işin içinde para geçen tüm projelerde audit, ayrı bir önem kazanmaktadır.

Şimdi gelin ilk sorulması gereken soruya cevap bulalım. Değişiklik olan datanın hangi alanlarını logluyacağız?

Models/ChangeLog.cs:

  • Hangi Tablo’da “EntityName
  • Hangi Kayıt “PrimaryKeyValue
  • Hangi Alan “PropertyName
  • Değişen Datanın Ilk Değeri “OldValue
  • Değişen Datanın Son Değeri “NewValue
  • Data Nasıl Değişti “State

Ne yapacağımız belli. Peki bu kaydı nerde yapmalıyız ? Her kaydetme işleminde mi yapmalıyız ? Ekstra kod yazmadan en az efor ile nasıl yapabiliriz ? Sürekli her yere kendini tekrarlayan koddan nasıl kaçınabiliriz?  Data Annotation mı kullansam, yoksa Action Fitler yazıp “OnActionExecuted“‘da mı yakalasam. Hiçbiri değil. Bunların hepsi bir şekilde olsa da, kod tekrarı gerektiren çözüm yollarıdır. Her yeni Action eklendiğinde, en azından üstüne bir Fitler konması gerekmektedir. Her kaydetme işleminden bahsetmiyorum bile. Bu projede, piyasada C# kullanan projelerin %80’inde tercih edilen, Entity Framework mimari yapısı kullanılmıştır. En üst katman olarak bizim de çözümümüz, ya DB katmanında yani Tabloya olan Trigger ile ki bu da sunucuya çok fazla yük bindireceğinden dolayı çok ihtiyaç olmadıkça tercih edilmemelidir. Ya da Data Acess Katmanında DBContext middleware’inde ilgili loglar tutularak yapılmalıdır..

Gelin test senaryosu için “auidtDB” adında Mvc Projemizi yaratalım.

Şimdi senaryo gereği, üzerinde işlem yapacağımız Product modelimizi oluşturalım.

Models/Product.cs: Aşağıda görüldüğü gibi “ID” Primary Index olarak tanımlanmıştır.

Gelelim DAL katmanı DBContext’e.

ProductContext.cs: İşte tüm bu loglama işinin yapılacağı esas kısm burasıdır. Bu projede, loglamaya ait kod işlemleri, buranın dışında herhangi bir yerde yapılmamaktadır.

  • DBContext olarak “ProductContext” ve üzerinde işlem yapılacak entity de “Products” olarak tanımlanmıştır.
  • OnConfiguring(): Database Azure üzerinde bir SqlServer’da bulunmaktadır. Connection string burada tanımlanır.
  • SaveChanges() : DBContext’in save işleminin yapıldığı yer burasıdır. Önce değişen tablolar bulunur ve değiştiği alanların önceki ve sonraki halleri kaydedilir.
    • “ChangeTracker.Entries()” :Tüm entry’ler alınır.
    • “.Where(p => p.State == EntityState.Modified).ToList()” : Update olanlar bir listeye atılır.
    • “foreach (var change in modifiedEntities”) : Tam değişen Entry’ler tek tek gezilir.
    • “var entityName = change.Entity.GetType().Name” : Değişen tablonun ismi alınır. Bu projede sadece “Product” bulunmaktadır.
    • “var PrimaryKey=change.OriginalValues.Properties.FirstOrDefault(prop=>prop.IsPrimaryKey()==true).Name” : Bu entry’e ait [Key] yani PrimaryKey alanı adı bulunur. Bu örnek de “ID” alanına denk gelmektedir.
    • “foreach (IProperty prop in change.OriginalValues.Properties)” : İlgili entry’e ait tüm propertyler yani kolonlar gezilir.
    • “var originalValue = change.OriginalValues[prop.Name].ToString();var currentValue = change.CurrentValues[prop.Name].ToString();” : İlgili field’ın önceki ve sonraki değerleri alınır.
    • “if (originalValue != currentValue)” : Değişiklik var mı diye bakılır.
    • “ChangeLog log = new ChangeLog()” : Eğer değişiklik var ise loglanma amaçlı ChangeLog modeli oluşturulur.
    • “EntityName = entityName” : Tablo adı alınır.
    • “PrimaryKeyValue = int.Parse(change.OriginalValues[PrimaryKey].ToString())” : Product tablosuna ait PrimaryKey yani, “ID” değeri kaydedilir.
    • “OldValue = originalValue, NewValue = currentValue,”: Değişen field’ın eski ve yeni değerleri kaydedilir.
    • “DateChanged” : Değişim zamanı kaydedilir.
    • “State = EnumState.Update” :  Bu projede sadece Update işleminin log’u tutulacaktır. Burada yapılan işlemin tipi kaydedilmektedir.
    • ElasticSearch.CheckExistsAndInsert(log)” : Tüm çekilen Log bilgisi ElasticSearch’e kaydedilir. Bir sonraki aşamada bu method derinlemesine incelenecektir.
    • “return base.SaveChanges()” Tüm loglama işleminden sonra, base’e ait Save() işlemine ait sonuç geri dönülür.

ElasticSearch/ElasticSearch.cs: .Net Core üzerinde ElasticSearch hakkında daha detaylı bilgiye buradan erişebilirsiniz. Öncelikle gelin neden elastik search’ü tercih ettik, onu tartışalım. Eğer binlerce kayıt üzerinde anlık değişimin yoğun olduğu bir ortamda, ve bizden belli bir ID’ye ait ürünün son 1 yıldaki fiyat değişim grafiği istense, Relational DB yerine NoSql çözümlerine gidilmesi ve bunlardan biri olarak Elastic Search’ün kullanılması, hiç de yanlış olmaz.

Eğer makinanızda ElasticSearch kurulu ise, terminal açılarak alttaki komutun çalıştırılması ile, 9200 portundan ElasticSearch kolayca ayağı kaldırılır.

Ayrca ElasticSearch’ün monütor edilebilmesi için, eğer kurulu ise aşağıdaki gibi “Kibana” yazılarak, 5601 portundan ayağı kaldırılması sağlanır.

  • “private static readonly ConnectionSettings connSettings” : İle Elastic Search’ün connection yolu, performans için static olarak tanımlanmıştır.
  • “ChangeLog” modeline göre ilgili index oluşturulur.
  • “private static readonly ElasticClient elasticClient = new ElasticClient(connSettings);” : Performas amaçlı singleton ElasticClient nesnesi, tüm clientlar için ortak olarak oluşturulmuştur.
  • “CheckExistsAndInsert()” : Eğer Elastic Search’e atılmak istenen “ChangeLog” modeli, daha önceden Elastic Search’e ait “change_log” Index’i oluşturulmamış ise önce ilgili index oluşturulur sonra istenen model ElasticClient’a atanır.
  • “var indexSettings = new IndexSettings();”,” indexSettings.NumberOfReplicas = 1;”,”indexSettings.NumberOfShards = 3;” : Yeni index oluşturulup Replica ve Shardingleri tanımlanır.
  • “var createIndexDescriptor = new CreateIndexDescriptor(“change_history”)” : “change_history” adında index yaratılır.
  • “.Mappings(ms => ms .Map<ChangeLog>(m => m.AutoMap()) )” : ChangeLog modeline göre Mapleme işi yapılır.
  • “.InitializeUsing(new IndexState() { Settings = indexSettings })” : Yukarıda tanımlı index ayarları, yeni oluşturulan bu “change_history” Index’ine atanır.
  • “.Aliases(a => a.Alias(“change_log”));” : “change_histoy” adından farklı olarak, ilgili index’a “change_log” adında bir alias atanmıştır. Böylece ilgili index güncellenmek istendiğinde alias adına göre yeni bir index yaratılarak eski index silinebilir. Bu şekilde, var olan index’e ait kayıtların silinmesi engellenir.
  • “elasticClient.Index<ChangeLog>(log, idx => idx.Index(“change_history”));” : Eğer “change_log” index’i yok ise oluşturulup, yeni gönderilen “ChangeLog” datasi, Elastic Search’ün “change_history” index’i altına kaydedilir.

Buraya kadar olan kısımda, DBContext middleware katmanında “SaveChanges()” methodu override edilerek, ilgili güncelleme yapılan datanın değişen kolonları tek tek gezilerek bulunmuş ve değişen her bir kolona ait “ChangeLog” modeli oluşturulmuştur. Ayrıca belirli bir connection’a göre bağlanılan Elastic Search’de yoksa “change_log” Index’i oluşturulup, değişen datalar bu Index altına kaydedilmişlerdir.

Sıra elimizdeki Product ürünleri Listeleyip, düzenleyeceğimiz CRUD sayfalarını oluşturmaya geldi.

HomeController.cs:

  • Product ürünlerin tamanın çekilip dönüldüğü yer Index() Action’ıdır.
  • Detail(int? ID) Action’ında ilgili ID’ye göre product bulunup, üzerinde işlem yapılması amacı ile geri dönülmektedir.
  • Update(Product product) methodunda, 2 işlem yapılmaktadır. Eğer işlem yapılması istenen Product’ın ID’si yok ise bu yeni bir ürün demektir. Bu durum da “var newProduct = new Product();” ile yeni yaratılan Product’a ait tüm propertyler atanıp, “await dbContext.Products.AddAsync(newProduct);” ile dbContext’e asenkron olarak eklenir. Bu durumda tüm data yeni girildiği için, yukarıda override ettiğimiz SaveChanges() methodunda “ChangeLog” modeline herhangi bir kayıt atılmayacaktır. Eğer Product’a ait bir “ID” değer var ise, bu durumda ilgili product bulunup, yeni değerleri üzerine güncellenir. İşte bu durumda, en sonda çalışan “dbContext.SaveChanges()” methodu ile Product’a ait değişen kolonların eski ve yeni değerleri o anki tarih değeri ile “ChangeLog” modeline atanıp, Elastic Search’e kaydedilir.  Böylece Update() methodunun sonunda, ürün ya güncellenir ya da yenisi kaydedilir.
  • Önemli Not: Burada dikkat edilmesi gereken konu, loglama işinin var olan projenin dışında, yani o anki geliştirmeden bağımsız bir şekilde ekstra efor gerektirmeden DB katmanında yapılmasıdır. Bu da bize farklı projelerde ve farklı guruplarda, istenen tablolar bazında, efor sarf etmeden kolaylıkla loglamayı sağlar :)

Index.cshtml: Aşağıda görüldüğü gibi Razor ile gelen “List<Product>” model, içinde gezilerek sayfaya basılmaktadır. Bir de sayfa üzerinde “Yeni Kayıt Gir” ve herbir ürüne ait “Detay” adında buttonlar bulunmaktadır. Her ikisi de “Detail()” action’ına gitmektedir. Yalnız yeni kayıt’da ID parametresi yok iken, product’a ait detay sayfası için parametre olarak, ilgili secilen ürüne ait “ID” değeri gönderilmektedir.

Detail.cshtml: Aşağıda görüldüğü gibi Detay sayfası hem bir yeni ürün giriş hem de seçilen bir ürünün güncelleme sayfasıdır.  Sayfada “Form.Post” kullanılmaktadır.

  • @using(Html.BeginForm(“Update”,”Home”,FormMethod.Post)) : Güncelleme veya Kaydetme işlemi Home Controller’ın “Update()” Action’ında yapılmaktadır.
  • btnText=Model.ID > 0 ? “Güncelle” : “Kaydet” : Kaydetme button’unun üzerine gelen Product model’in ID’si var ise “Güncelle” yok ise “Kaydet” yazısı yazılır.
  • “<input type=”hidden” id=”ID” name=”ID” value=”@Model.ID”>” : İlgili Form içerisinde, hidden olarak var ise, product’ın ID’si tutulmalıdır. Aksi takdirde güncelleme durumunda “ID”, ilgili Action’a null olarak gönderilir.

Productlarda değişiklik yapıldığı zaman, “Elastic Search“‘deki Log kayıtları “Kibana” ile aşağıdaki gibi görülmektedir .

Bu makalede, iş hayatında da bolca kullanılan bir konuya hep beraber parmak bastık. Kullanılan teknolojiler değişse de, mantığı değişimeyen yegane şey Log tutmaktır. Eğer sizin de log dosyanız çok büyük ve yüksek trafikli bir projede ilgili dosyalardan anlık sorgulama yapmanız gerekiyor ise, Elastic Search’e bir şans vermenizi şiddet ile tavsiye ediyorum. Ayrıca eğer DB katmanında EntityFramework kullanıyorsanız, loglama işlemlerini DbContext’de “SaveChanges()” methodu üzerinde yapmanız, sizi birçok zahmet ve kod kalabalığından kurtaracaktır.

Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere hoşçakalın.

Source Code: https://github.com/borakasmer/AuditDB

Kaynaklar: https://stackoverflow.com/questions/37210914/changetracker-entries-currentvalue-equals-originalvalue-in-ef7-ef-core

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

19 Cevaplar

  1. Selçuk İTMİŞ dedi ki:

    Elinize sağlık hocam, güzel bir konu olmuş.
    Yalnız şöyle bir sorum olacak; Modelimizde Relational bir alanımız olursa ve bu alan değişirse bunu nasıl yakalayabiliriz?

    Şimdiden teşekkürler

    • Berdan dedi ki:

      Merhaba,

      ChangeTracker.Entries sana ilgili modelde ki bütün değişiklikleri verir. Yanın Product sınfıında ProductBarcode isimli ayrı bir list oduğunu düşünelim. Alt tabloda bir değişiklik olduğunda ChangeTracker.Entries().Where(e => e.State == Modified) sorgusunda bu değişiklikte gelecektir.

  2. Berdan dedi ki:

    Merhaba Hocam,

    Transactional bir işlemi elastic search ile nasıl yönetebiliriz? Logları veritabanına kaydettiğimiz senaryoda bütün işlem aynı transaction içinde olduğundan herhangi bir hatada rollback olacağı için tutarlılığı sağlamış oluyoruz. Ama farklı bir kaynağa gönderdiğimizde tutarlılığı sağlayabilmemiz için neler yapmamız gerekiyor?

    Teşekkürler.

    • borsoft dedi ki:

      Selam Berdan,

      Öncelikle sorun nefis :) Mesela Redisdeki kayıtları düşünelim. Redis’de transection var da, o daha çok transection cık :) Ne yapıyoruz peki redise atılan datalarda bir sorun olabileceği için, manuel yapıyoruz :) Memento Design Pattern mantığındaki aynı Undo Redo’da olduğu gibi bir önceki hallerini tablo:columnName:id keyword’ü ile NoSql tipi bir db’de tutup, hata olursa o sistem buradan eski hallerini geri gönderiyor.

      Kolay gele…

  3. Mahmut dedi ki:

    Merhaba,
    benim her seferinde old data & new data aynı geliyor. Sebebi sanırım _dbSet.AsNoTracking() kullandığımız generic repository de böyle cağırıp atach edip kullanıyor. Bu durum için bir öneriniz var mı?

    • borsoft dedi ki:

      Selamlar,
      NoTracking readonly datada kullanılan bir yapıdır. Doğal olarak eski yeni değerler alınamaz. NoTracking kullanmanız durumunda, yeni tarih bilgisini elle manuel çekmeniz ve eskisini de yine elle bu esnada güncellemeniz sorunu çözebilir.

      Bu eski değeri verir: var originalEntity = context.MyEntities.AsNoTracking().FirstOrDefault(me => me.MyEntityID == myEntity.MyEntityID);
      Yeni değeri de elle set edebilirsiniz.

  4. Nurullah dedi ki:

    Merhabalar Hocam,

    Anlatışınız sunumunuz çok iyi geldi bana. Her bir videonuzu izlediğimde bir okadar sarılıyorum kod yazmaya. Her defasında aa bu böylemiymiş diyorum ve yeni yeni şeyleri sayenizde öğreniyorum. Emeğinize sağlık. Her paylaşımınız için ayrı ayrı çok çok teşekkür ediyorum hocam.

    • borsoft dedi ki:

      Teşekkür ederim Nurullah.

      Bu güzel yorumlarınız bana moral oluyor :)
      Para parayı, bilgi bilgiyi çeker:)

      İyi çalışmalar.

  5. Umut dedi ki:

    Hocam merhaba biraz konu dışı olabilir ama bir sorum var :)

    .NET Core ile .Net arasında ki fark nedir ve .NET Core ile MVC arasında ki farklar nelerdir? .Net Core u neden kullanmalıyız vs.. bu konu da çok yabancıyım ve anlaşılır bir makale bulamadım, bu konuda yardımcı olursanız sevinirim.

  6. Yunus dedi ki:

    Merhaba. Hocam VsCode ‘da .cshtml tarafında intellisense çalışmıyor .. Yardım edebilirmisiniz

  7. Yunus dedi ki:

    Dönün artık amerikadan hocam sorularımız var yaaa :D

  8. Necip dedi ki:

    Hocam merhaba.dbcontext sınıfının dışında başka bir sınıfta savechanges metodunu nasıl override edebilirim.
    Database code first model olusturyorum ve her seferinde dbcontext sinifim tekrar oluşuyor.tesekkur ederim

    • borsoft dedi ki:

      Selamlar,
      DBContext sınıfını her seferinde oluşturmana gerek yok. Autofac veya ninject gibi yapılarla bunu aşabilirsin.

      İyi çalışmalar.

  9. İsmail TÜRKMENOĞLU dedi ki:

    Merhaba hocam,

    Emeğinize sağlık. Çok yararlı bir post idi.

  10. Furkan dedi ki:

    Selam Bora abi bir sorum olacak changetracker ile değişikliği buluyorum bana şöyle bir senaryo gerekiyor id değişiyor ama elimde yeni id nin verisi var bir propertynin bağlı olduğu (foreignkey) modeli bulup o modeldeki entityi çekip json vb şekilde almam gerekiyor bu nasıl mümkün olabilir

  11. mustafa sinan dedi ki:

    Bora hocam selam :)
    Bu gerekliydi gerçi gene ben mysql e kaydediyorum ama olsun :)
    burada originalValue ve currentValue hep aynı geliyordu kaynaklarınızda birinde bir düzeltme olmuş onu buraya not bırakayım :)
    Elinize sağlık.

    Replace: var originalValue = change.Property(prop.Name).OriginalValue;
    to: var originalValue = change.GetDatabaseValues().GetValue(prop.Name);

Yunus için bir cevap yazın Cevabı iptal et

E-posta hesabınız yayımlanmayacak.