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

İ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.
| Durum | Context izliyor mu | Veritabanında var mı | Özellik değişti mi | SaveChanges ne yapar |
|---|---|---|---|---|
| Detached | Hayır | - | - | - |
| Added | Evet | Hayır | - | INSERT |
| Unchanged | Evet | Evet | Hayır | - |
| Modified | Evet | Evet | Evet | UPDATE |
| Deleted | Evet | Evet | - | 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

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.
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ı:
# 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 FKAynı 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:
SaveChangesveSaveChangesAsync: veritabanı güncellenmeden önce tüm değişikliklerin bulunması için.ChangeTracker.Entries()veEntries<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.
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.
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 |
|---|---|
| State | Varlığın EntityState değerini okur ve ayarlar |
| Entity | Varlık örneğinin kendisini verir |
| Context | Varlığı izleyen DbContext |
| Metadata | Varlık tipinin IEntityType meta verisi |
| IsKeySet | Anahtar değerinin atanıp atanmadığı |
| Reload | Özellik değerlerini veritabanından okunanlarla ezer |
| DetectChanges | Yalnı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.
var entry = context.Entry(blog);
entry.Property(e => e.Name).CurrentValue = "1unicorn2";
entry.Property(e => e.Name).IsModified = false; // bu kolon güncellenmezTü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.
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.
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.
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?

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:
# 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.
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 -2147482643Change 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.
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,Updatekullanı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;
CreateProxykullanı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