EF Core change tracker, bir DbContext örneğinin izlediği her varlık için durum (EntityState), özelliklerin şimdiki ve orijinal değerleri, hangi özelliklerin değiştiği ve varlıklar arası ilişkiler gibi bilgileri tutan bileşendir. SaveChanges çağrıldığında veritabanına gönderilecek komutlar bu bilgilerden üretilir. Entity Framework Core'da bu bilgilere context.Entry(varlik), context.ChangeTracker.Entries(), Find ve DbSet.Local ile erişir, ChangeTracker.DebugView ile de okunabilir bir döküm alırsınız.

Bu yazı change tracker'ın iç işleyişini anlatır: varlıkların ne zaman izlenmeye başladığı, beş durumun anlamı, değişikliklerin nasıl ve ne zaman tespit edildiği, EntityEntry ile neler yapılabileceği ve hata ayıklama görünümünün nasıl okunacağı. Gündelik ekleme, güncelleme ve silme kodu EF Core SaveChanges yazısında, sorgu tarafındaki AsNoTracking seçimi AsNoTracking ve tracking sorguları yazısında, başka bir context'ten gelen varlıkların iliştirilmesi ise disconnected entity yazısında anlatılıyor.

Varlıklar ne zaman izlenir, ne zaman bırakılır?

Belgelere göre bir varlık örneği üç yoldan izlenmeye başlar: veritabanına çalıştırılan bir sorgudan döndüğünde, Add, Attach, Update veya benzeri bir metotla açıkça context'e verildiğinde ve zaten izlenen bir varlığa bağlı yeni bir varlık olarak keşfedildiğinde. İzleme ise context dispose edildiğinde, change tracker temizlendiğinde veya varlık açıkça ayrıldığında (detach) sona erer.

DbContext kısa ömürlü bir iş birimini temsil edecek şekilde tasarlanmıştır. Bu yüzden belgeler izlemeyi bitirmenin normal yolunun context'i dispose etmek olduğunu söyler. Önerilen yaşam döngüsü şudur: context'i oluşturun, bazı varlıkları izleyin, onlarda değişiklik yapın, SaveChanges ile veritabanını güncelleyin ve context'i dispose edin. Bu düzende change tracker'ı temizlemek ya da varlıkları tek tek ayırmak gerekmez. Yine de varlıkları ayırmanız gerekiyorsa ChangeTracker.Clear() çağırmak, varlıkları tek tek ayırmaktan daha verimlidir. Context'in uygulamada nasıl oluşturulduğu DbContext yapılandırma ve yaşam döngüsü yazısında anlatılıyor.

Beş EntityState değeri

Beş EntityState değeri: Detached, Added, Unchanged, Modified

İzlenen ya da izlenmeyen her varlığın bir EntityState değeri vardır. Detached izlenmeyen varlıktır. Added yeni olan ve henüz eklenmemiş varlıktır. Unchanged sorgulandığından beri değişmemiş varlıktır; sorgudan dönen tüm varlıklar bu durumda başlar. Modified sorgulandıktan sonra değiştirilmiş varlıktır. Deleted ise veritabanında var olan ama silinmek üzere işaretlenmiş varlıktır.

DurumContext izliyor muVeritabanında var mıÖzellik değişti miSaveChanges ne yapar
DetachedHayır---
AddedEvetHayır-INSERT
UnchangedEvetEvetHayır-
ModifiedEvetEvetEvetUPDATE
DeletedEvetEvet-DELETE

EF Core değişiklikleri özellik düzeyinde izler. Tek bir özellik değiştiyse güncelleme yalnızca o kolonu değiştirir. Belgeler önemli bir kuralı da vurgular: bir özellik ancak varlığın kendisi Modified durumundaysa değişmiş olarak işaretlenebilir. Başka bir açıdan bakınca Modified durumu, en az bir özelliğin değişmiş olarak işaretlendiği anlamına gelir.

Belgeler aynı context'in hem sorgu hem kayıt için kullanılmasının, varlıkları elle iliştirmeye göre neden daha iyi çalıştığını da sıralar: kod basittir ve durumları elle yönetmek nadiren gerekir; güncellemeler yalnızca gerçekten değişen değerlerle sınırlı kalır; shadow property değerleri, özellikle shadow olarak tutulan yabancı anahtarlar korunur; orijinal değerler otomatik saklanır ve verimli güncellemeler için kullanılır.

Değişiklikler nasıl tespit edilir: snapshot change tracking

Değişiklikler nasıl tespit edilir: snapshot change tracking: SaveChanges ve SaveChangesAsync, ChangeTracker.Entries() ve Entries<TEntity>(), ChangeTracker.HasChanges()...

EF Core varsayılan olarak bir varlık ilk kez izlendiğinde tüm özellik değerlerinin bir anlık görüntüsünü alır. Daha sonra varlığın şimdiki değerlerini bu görüntüyle karşılaştırarak nelerin değiştiğini bulur. Bu karşılaştırmayı yapan metot ChangeTracker.DetectChanges()'tir. Değişikliği EF Core üzerinden değil, doğrudan nesnenin özelliğine atama yaparak gerçekleştirdiğinizde, EF Core bunu ancak bir sonraki tespitte öğrenir.

Belgelerdeki örnek bunu açıkça gösterir. Bir blog yüklenir, adı değiştirilir ve koleksiyonuna yeni bir yazı eklenir; ardından DetectChanges çağrılmadan önce ve sonra hata ayıklama görünümü yazdırılır.

csharp
using var context = new BlogsContext();
var blog = await context.Blogs.Include(e => e.Posts).FirstAsync(e => e.Name == ".NET Blog");

blog.Name = ".NET Blog (Updated!)";
blog.Posts.Add(new Post { Title = "What's next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..." });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Belgelerde gösterilen ilk çıktıda blog hâlâ Unchanged durumundadır ve yeni yazı izlenen varlıklar arasında yoktur; koleksiyonda <not found> olarak görünür. Ad özelliğinin yeni değeri görünse bile bu, görünümün değeri doğrudan nesneden okumasından kaynaklanır; değişiklik henüz tespit edilmemiştir. DetectChanges sonrasındaki çıktıda ise blog Modified olur ve yeni yazı geçici bir anahtarla Added olarak izlenmeye başlar. Belgelerdeki örnek çıktının ilgili satırları:

bash
# Belgelerdeki örnek çıktı, DetectChanges öncesi (kısaltıldı)
Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, <not found>]

# DetectChanges sonrası (kısaltıldı)
Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482643}]
Post {Id: -2147482643} Added
  Id: -2147482643 PK Temporary
  BlogId: 1 FK

Aynı değişikliği context.Entry(blog).Property(e => e.Name).CurrentValue = ... ve context.Add(...) gibi EF Core metotlarıyla yaparsanız, EF Core değişikliği anında bilir ve tespite gerek kalmaz. Belgeler buna rağmen tespitten kaçınmak için her değişikliği EF Core metotlarıyla yapmaya çalışmamanızı açıkça söyler: bu yaklaşım hem daha zahmetli hem de çoğu zaman daha yavaştır. Amaç, tespitin ne zaman gerektiğini bilmektir.

DetectChanges otomatik olarak ne zaman çalışır?

DetectChanges, sonucu etkileyebileceği metotlarda otomatik çağrılır. Belgelerin listesi şöyledir:

  • SaveChanges ve SaveChangesAsync: veritabanı güncellenmeden önce tüm değişikliklerin bulunması için.
  • ChangeTracker.Entries() ve Entries<TEntity>(): durumların ve değişen özelliklerin güncel olması için.
  • ChangeTracker.HasChanges(): sonucun doğru olması için.
  • ChangeTracker.CascadeChanges(): cascade işleminden önce ana varlıkların durumunun doğru olması için.
  • DbSet<TEntity>.Local: izlenen grafiğin güncel olması için.

Bazı yerlerde tespit yalnızca tek bir varlık için yapılır: DbContext.Entry kullanıldığında, EntityEntry üzerindeki Property, Collection, Reference, Member gibi metotlar çağrıldığında ve zorunlu bir ilişki koptuğu için bir bağımlı varlık silinmek üzereyken (varlığın yeni bir ana varlığa bağlanıp bağlanmadığını anlamak için). Tek varlık için tespiti EntityEntry.DetectChanges() ile siz de tetikleyebilirsiniz. Belgeler, yerel tespitin başka varlıklardaki tespit edilmemiş değişikliklerin yol açtığı cascade etkilerini kaçırabileceği konusunda uyarır; böyle durumlarda tam tarama için ChangeTracker.DetectChanges() çağırmanız gerekir.

Otomatik tespiti kapatmak

Belgelere göre değişiklik tespiti çoğu uygulama için darboğaz değildir, ancak binlerce varlık izleyen bazı uygulamalarda performans sorunu hâline gelebilir. Bu durumda ChangeTracker.AutoDetectChangesEnabled kapatılabilir. Belgelerdeki örnek, SaveChangesAsync metodunu ezerek yeni eklenen birleştirme varlıklarına damga vurur; Entries<PostTag>() zaten tespit yaptığı ve sonrasında durum değiştiren bir işlem olmadığı için temel SaveChangesAsync çağrısı sırasında tekrar tespit yapılmasını engeller.

csharp
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>()) // burada tespit otomatik yapılır
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
            entityEntry.Entity.TaggedOn = DateTime.Now;
        }
    }

    try
    {
        ChangeTracker.AutoDetectChangesEnabled = false;
        return await base.SaveChangesAsync(cancellationToken); // ikinci tespitten kaçınılır
    }
    finally
    {
        ChangeTracker.AutoDetectChangesEnabled = true;
    }
}

try/finally bloğu, kayıt hata verse bile varsayılan ayarın geri yüklenmesini sağlar. Belgeler bu ayarı ancak profil ölçümü değişiklik tespitinin gerçekten sorun olduğunu gösterdiğinde kapatmanızı önerir. Value converter kullanan özelliklerde ise anlık görüntü alma, karşılaştırma ve hash üretme işlerini bir value comparer üstlenir; bu konu EF Core value conversion yazısında anlatılıyor.

Entry() ve EntityEntry ile tek bir varlığa erişmek

context.Entry(blog) size o varlık için bir EntityEntry<Blog> verir. En sık kullanım varlığın durumunu okumak ve değiştirmektir. Entry metodu henüz izlenmeyen bir varlıkla da çağrılabilir; bu çağrı izlemeyi başlatmaz, durum Detached kalır. Dönen nesne üzerinden durumu atadığınızda varlık o durumda izlenmeye başlar.

csharp
var newBlog = new Blog();
Debug.Assert(context.Entry(newBlog).State == EntityState.Detached);

context.Entry(newBlog).State = EntityState.Added;
Debug.Assert(context.Entry(newBlog).State == EntityState.Added);

EF6'dan farklı olarak tek bir varlığın durumunu bu şekilde ayarlamak, ona bağlı varlıkları izlemeye almaz. Bu yüzden durum ataması, grafiğin tamamı üzerinde çalışan Add, Attach ve Update'e göre daha düşük seviyeli bir işlemdir.

EntityEntry üyesiİşlevi
StateVarlığın EntityState değerini okur ve ayarlar
EntityVarlık örneğinin kendisini verir
ContextVarlığı izleyen DbContext
MetadataVarlık tipinin IEntityType meta verisi
IsKeySetAnahtar değerinin atanıp atanmadığı
ReloadÖzellik değerlerini veritabanından okunanlarla ezer
DetectChangesYalnızca bu varlık için değişiklik tespiti yapar

Tek bir özellikle çalışmak: PropertyEntry

context.Entry(blog).Property(e => e.Name) tipli bir PropertyEntry<Blog, string> döndürür; özellik adı metin olarak da verilebilir. Tipli sürüm değer tiplerini kutulamadan (boxing) eriştiği için tercih edilir; tip derleme anında bilinmiyorsa tipsiz PropertyEntry kullanılabilir. Bu nesnenin CurrentValue, OriginalValue, IsModified ve IsTemporary üyeleri vardır.

Belgelerin bu üyelerle ilgili üç notu pratikte çok işe yarar. Birincisi, orijinal değer varlığın veritabanından sorgulandığı andaki değerdir; ancak varlık bağlantısız hâle gelip Attach veya Update ile başka bir context'e iliştirildiyse orijinal değer bilinmez ve şimdiki değerle aynı döner. İkincisi, SaveChanges yalnızca değişmiş olarak işaretlenen özellikleri günceller; IsModified değerini true yaparak bir özelliğin güncellenmesini zorlayabilir, false yaparak engelleyebilirsiniz. Üçüncüsü, geçici değerler genellikle EF Core değer üreticilerinden gelir; şimdiki değeri elle atamak geçici değeri değiştirir ve özelliği geçici olmaktan çıkarır.

csharp
var entry = context.Entry(blog);
entry.Property(e => e.Name).CurrentValue = "1unicorn2";
entry.Property(e => e.Name).IsModified = false;   // bu kolon güncellenmez

Tüm özellik değerleri: CurrentValues, OriginalValues, GetDatabaseValuesAsync

EntityEntry tüm özellik değerlerini bir arada temsil eden PropertyValues nesneleri de sunar: şimdiki değerler, orijinal değerler ve GetDatabaseValuesAsync ile veritabanında şu an duran değerler. SetValues ile bu değerleri bir DTO'dan, aynı adlı özelliklere sahip herhangi bir nesneden ya da ad/değer sözlüğünden kopyalayabilirsiniz. Belgeler, yalnızca değeri gerçekten farklı olan özelliklerin değişmiş olarak işaretlendiğini vurgular. ToObject() ise bu değerlerden izlenmeyen ve ilişkileri kurulmamış yeni bir kopya üretir; eşzamanlılık çakışmalarını çözerken kullanışlıdır.

csharp
var databaseValues = await context.Entry(blog).GetDatabaseValuesAsync();
context.Entry(blog).CurrentValues.SetValues(databaseValues);
context.Entry(blog).OriginalValues.SetValues(databaseValues);

Navigation'larla çalışmak

Reference tekil navigation'lara (bire çok ilişkinin bir tarafı ve bire bir ilişkinin iki tarafı), Collection koleksiyon navigation'larına, Navigation ise ikisine de tipsiz erişim sağlar. Bu nesnelerin CurrentValue, IsLoaded, Load ve Query üyeleri vardır. Load ve Query ile ilişkili verinin açıkça yüklenmesi EF Core Include ve eager loading yazısındaki explicit loading bölümünde anlatılıyor.

Tüm izlenen varlıklar: ChangeTracker.Entries ve DbSet.Local

context.ChangeTracker.Entries() izlenen her varlık için bir EntityEntry döndürür; Entries<Post>() ise sonucu bir tiple sınırlar ve tipli giriş verir. Bu genel tip modelde eşlenmiş bir varlık tipi olmak zorunda değildir; tüm varlıklarınızın uyguladığı bir arayüz de olabilir. Denetim alanlarını doldurmak gibi işler için bu çok kullanışlıdır.

csharp
public interface IEntityWithKey
{
    int Id { get; set; }
}

foreach (var entityEntry in context.ChangeTracker.Entries<IEntityWithKey>())
{
    Console.WriteLine($"Found {entityEntry.Metadata.Name} entity with ID {entityEntry.Property(e => e.Id).CurrentValue}");
}

DbSet<TEntity>.Local ise EntityEntry yerine varlık örneklerinin kendisini verir ve change tracker'ın o anki kavramsal durumunu yansıtır: Added varlıklar listede vardır, Deleted varlıklar yoktur. Normal sorgularda tam tersi geçerlidir; eklenmiş ama kaydedilmemiş bir varlık sorgu sonucunda görünmez, silinmek üzere işaretlenmiş bir varlık ise hâlâ görünür. Local görünümüne nesne eklemek onu izlemeye alır, çıkarmak Deleted olarak işaretler.

Find ve FindAsync önce yerelde arar

Find ve FindAsync birincil anahtarı bilinen tek bir varlığı aramak için tasarlanmıştır. Önce varlığın zaten izlenip izlenmediğine bakar ve izleniyorsa veritabanına hiç gitmeden aynı örneği döndürür. Belgelerdeki örnekte ilk çağrı bir SELECT çalıştırır, ikinci çağrı ise sorgu yapmadan aynı nesneyi verir. Varlık yerelde de veritabanında da yoksa null döner. Bileşik anahtarlarda değerleri HasKey ile tanımlanan sırayla vermeniz gerekir.

csharp
var blog1 = await context.Blogs.FindAsync(1);   // veritabanına sorgu gider
var blog2 = await context.Blogs.FindAsync(1);   // izlenen örnek döner, sorgu gitmez
Debug.Assert(blog1 == blog2);

DebugView nasıl okunur?

DebugView nasıl okunur? Geçici anahtar değerleri, Change tracker logları

ChangeTracker.DebugView iki biçim sunar. ShortView izlenen her varlığı, durumunu ve anahtar değerlerini tek satırda gösterir. LongView buna ek olarak tüm özellik ve navigation değerlerini listeler. Belgelerdeki kısa görünümden birkaç satır şöyledir:

bash
# Belgelerdeki örnek çıktı (ShortView, kısaltıldı)
Blog {Id: 1} Modified AK {AssetsId: ed727978-1ffe-4709-baee-73913e8e44a0}
Post {Id: -2147482643} Added FK {BlogId: 1}
Post {Id: 4} Deleted FK {BlogId: 2}
PostTag (Dictionary<string, object>) {PostsId: 4, TagsId: 2} Deleted FK {PostsId: 4} FK {TagsId: 2}

Her satır varlık tipi ve birincil anahtarla başlar. Paylaşımlı tipli varlıklarda CLR tipi de parantez içinde görünür. Ardından durum gelir, sonra varsa alternatif anahtar (AK) ve yabancı anahtar (FK) değerleri. Uzun görünümde her özelliğin yanında PK, AK ya da FK etiketi bulunur; değişen özellikler Modified Originally '...' ile eski değeriyle birlikte gösterilir; geçici anahtarlar PK Temporary olarak işaretlenir. Navigation'lar ilgili varlıkların birincil anahtarlarıyla yazılır: Blog: {Id: 2} tek bir referansı, Posts: [{Id: 1}, {Id: 2}] bir koleksiyonu ifade eder.

Geçici anahtar değerleri

Anahtarı veritabanında üretilecek yeni bir varlık izlendiğinde EF Core change tracker içinde geçici bir değer oluşturur. Bu değer varlığın özelliğine yazılmaz. Belgelerdeki örnekte yeni bir blog eklendikten sonra nesnedeki Id değeri 0 görünürken change tracker'ın tuttuğu değer negatif bir sayıdır. Değerin geçici olup olmadığını IsTemporary ile kontrol edebilirsiniz.

csharp
context.Add(blog);
Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");
// Belgelerdeki örnek çıktı:
// Blog.Id set on entity is 0
// Blog.Id tracked by EF is -2147482643

Change tracker logları

Change tracker, değişiklik tespit ettiğinde ve ilişkileri düzelttiğinde Debug seviyesinde log mesajları üretir. Belgeler CoreEventId.DetectChangesStarting, PropertyChangeDetected, ForeignKeyChangeDetected, CollectionChangeDetected, ReferenceChangeDetected, StartedTracking, StateChanged, ValueGenerated ve SkipCollectionChangeDetected gibi olay kimliklerini listeler. Bu loglar bir varlığın neden Modified ya da Deleted olduğunu anlamak için idealdir. Debug loglamanın nasıl açılacağı EF Core SQL loglama yazısında anlatılıyor.

Olaylar, notification entity ve proxy

Change tracker iki .NET olayı yayınlar: bir varlık ilk kez izlendiğinde ChangeTracker.Tracked, izlenen bir varlığın durumu değiştiğinde ChangeTracker.StateChanged. Belgeler önemli bir ayrıntıyı not eder: varlık ilk izlendiğinde durumu Detached'dan başka bir duruma geçse de StateChanged tetiklenmez; tüm bildirimleri almak için iki olayı birlikte dinlemelisiniz. Belgelerdeki örnek, aynı işleyiciyi iki olaya bağlayarak eklenen, güncellenen ve silinen varlıklara zaman damgası vurur.

csharp
public BlogsContext()
{
    ChangeTracker.StateChanged += UpdateTimestamps;
    ChangeTracker.Tracked += UpdateTimestamps;
}

Anlık görüntüye dayalı izleme çoğu uygulama için önerilen yoldur. Çok sayıda varlık izleyen ve bunlarda çok değişiklik yapan uygulamalar ise INotifyPropertyChanging ve INotifyPropertyChanged arayüzlerini uygulayan notification entity'lerden yararlanabilir; bu varlıklar değişikliği EF Core'a kendileri bildirir. Bu arayüzleri elle yazmak yerine Microsoft.EntityFrameworkCore.Proxies paketi ve UseChangeTrackingProxies() ile dinamik proxy tipleri ürettirmek de mümkündür. Bu durumda varlık özelliklerinin virtual olması gerekir ve yeni nesneleri new ile değil context.CreateProxy<Post>() ile oluşturmanız gerekir, çünkü proxy olmayan örnekler bildirim üretmez.

Sık yapılan hatalar

  • DebugView öncesi DetectChanges çağırmamak: görünüm otomatik tespit yapmadığı için durumlar eski görünür.
  • Context'i uzun süre yaşatmak: izlenen varlıklar birikir, aynı anahtarlı ikinci örnek izlenemez ve sorgular izlenen eski örneği döndürür. Context'i iş birimi başına oluşturun.
  • Entry().State ile grafiği izlemeye çalışmak: durum ataması yalnızca o varlığı etkiler; bağlı varlıklar için Add, Attach, Update kullanın.
  • Attach sonrası OriginalValue'a güvenmek: iliştirilen bağlantısız varlıkta orijinal değerler şimdiki değerlerle aynıdır.
  • Performans için AutoDetectChangesEnabled'ı peşinen kapatmak: belgeler bunu yalnızca ölçüm gerektirdiğinde önerir; kapattıktan sonra gerekli tespitleri sizin yapmanız gerekir.
  • Proxy kullanırken new ile nesne oluşturmak: bu örneklerdeki değişiklikler kaçırılır; CreateProxy kullanın.

Sık Sorulan Sorular

Bir varlığın hangi durumda olduğunu nasıl görürüm?

context.Entry(varlik).State ile okuyabilirsiniz. Tüm context için context.ChangeTracker.DebugView.LongView yazdırmak, önce DetectChanges çağırmak kaydıyla, en hızlı yoldur.

ChangeTracker.Clear() ne zaman kullanılır?

Normalde gerekmez; context'i dispose etmek izlemeyi bitirmenin olağan yoludur. Aynı context'te izlenen varlıkları bırakmanız gerekiyorsa Clear(), varlıkları tek tek ayırmaktan daha verimlidir.

Sorgu her zaman veritabanından güncel veri getirir mi?

Sorgu her zaman veritabanında çalışır, ancak izlemeli bir sorguda dönen varlık zaten izleniyorsa EF Core veritabanındaki yeni değerler yerine izlenen örneği kullanır. Güncel değerleri almak için Reload veya GetDatabaseValuesAsync kullanabilir ya da yeni bir context açabilirsiniz.

Bir özelliğin güncellenmesini nasıl engellerim?

context.Entry(varlik).Property(e => e.Ozellik).IsModified = false atamasıyla o özellik SaveChanges sırasında güncellenmez.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: EF Core öğrenme rehberi · SaveChanges ile ekleme, güncelleme, silme · AsNoTracking · Disconnected entity · SQL loglama ve interceptor