EF Core'da (Entity Framework Core) bir varlık bir context ile sorgulanıp başka bir context ile kaydediliyorsa buna bağlantısız (disconnected) varlık denir. Bu varlıkları yeniden izlemeye almak için Attach (değişmemiş olarak), Update (değişmiş olarak) veya Add (yeni olarak) kullanırsınız. Sık görülen "The instance of entity type 'Blog' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked" hatası ise aynı anahtara sahip ikinci bir nesneyi aynı context'e vermeye çalıştığınızda oluşur; çözümü, her anahtar için tek bir örnek izlenmesini sağlamaktır.
Bu yazı belgelerdeki bağlantısız senaryoları sırayla ele alır: varlığın yeni mi mevcut mu olduğunu anlamak, tek varlık ve grafik kaydetmek, silmeleri işlemek, TrackGraph ile özel izleme ve identity resolution kuralının neden bu hatayı ürettiği. Varlık durumlarının ve Entry() API'sinin kendisi EF Core change tracker yazısında, aynı context içindeki gündelik kayıt işlemleri EF Core SaveChanges yazısında anlatılıyor. Web tarafındaki controller ve endpoint kodu ASP.NET Core'un ayrı bir konusu; burada yalnızca EF Core'un davranışına odaklanıyoruz.
Bağlantısız varlık nedir ve neden ayrı ele alınır?
Normal akışta bir context sorgudan dönen varlıkları izler, siz değişiklik yaparsınız ve SaveChanges değişiklikleri kendisi tespit eder. Web uygulamalarında ise akış genellikle şöyledir: veri bir istekte sorgulanır, istemciye gönderilir, istemcide değiştirilir ve başka bir istekte sunucuya geri döner. İkinci istekte yeni bir context vardır ve bu context nesneyi hiç görmemiştir. Bu yüzden nesnenin eklenmesi mi güncellenmesi mi gerektiğini, hangi özelliklerinin değiştiğini kendiliğinden bilemez.
Belgeler bu noktada iki temel ilkeyi öne çıkarır. Birincisi, EF Core belirli bir birincil anahtar değeri için yalnızca tek bir varlık örneği izleyebilir. İkincisi, bunun sorun olmasını önlemenin en iyi yolu her iş birimi için kısa ömürlü bir context kullanmaktır: context boş başlar, varlıklar ona iliştirilir, kaydedilir ve context atılır. Aynı context'ten sorgulanmış varlıkları aynı context'e yeniden iliştirmek normalde gerekmez; belgeler izlemesiz sorgu yapıp sonuçları aynı context'e iliştirmenin hem daha yavaş olduğunu hem de shadow property değerlerinin kaybolması gibi sorunlara yol açabileceğini söyler.
Varlık yeni mi, mevcut mu?

İkinci context'in ilk sorusu budur. En basit durum, istemcinin bunu kendisinin bildirmesidir; örneğin ekleme ve güncelleme farklı isteklerle yapılır. Bu mümkün değilse anahtar değerine bakılır.
Otomatik üretilen anahtarlarla
Tamsayı ve GUID anahtarlar varsayılan olarak otomatik üretilir. Bu durumda kural basittir: anahtar CLR varsayılan değerindeyse (0, null, boş GUID) varlık yenidir ve eklenmelidir; anahtarın bir değeri varsa daha önce kaydedilmiş, istemciye gidip geri gelmiştir ve güncellenmelidir. Varlık tipini biliyorsanız bunu doğrudan kontrol edebilir, bilmiyorsanız EF Core'un genel yöntemini kullanabilirsiniz:
public static bool IsItNew(Blog blog)
=> blog.BlogId == 0;
public static bool IsItNew(DbContext context, object entity)
=> !context.Entry(entity).IsKeySet;Otomatik üretilmeyen anahtarlarla
Anahtarları uygulama atıyorsa iki yol vardır: varlığı veritabanında aramak ya da istemciden bir işaret almak. Arama için FindAsync yeterlidir; null dönüyorsa kayıt yoktur. İstemciden işaret almanın ayrıntısı uygulamanıza bağlıdır; web uygulamasında genellikle farklı işlemler için farklı istekler kullanmak ya da istekte bir durum bilgisi taşımak anlamına gelir.
public static async Task<bool> IsItNew(BloggingContext context, Blog blog)
=> (await context.Blogs.FindAsync(blog.BlogId)) == null;Attach, Update ve Add ne yapar?
Üç metot da kendisine verilen nesneyle birlikte navigation'lar üzerinden ulaşılabilen tüm grafiği gezer. Fark, varlıkları hangi duruma koydukları ve anahtar değeri atanmamış varlıklara nasıl davrandıklarıdır.
| Metot | Anahtarı atanmış varlık | Anahtarı atanmamış varlık (üretilen anahtar) | SaveChanges sonucu |
|---|---|---|---|
| Add | Added | Added | INSERT |
| Attach | Unchanged | Added | Değişmeyenler için komut yok, yeniler için INSERT |
| Update | Modified (tüm özellikler) | Added | Mevcutlar için tüm kolonlarla UPDATE, yeniler için INSERT |
| Remove | Deleted | - | DELETE |
Belgelerdeki örnekte Id = 1 olan bir blog ve anahtarları atanmış iki yazı Attach ile iliştirildiğinde hepsi Unchanged olur ve SaveChanges hiçbir şey yapmaz. Grafiğe anahtarı atanmamış üçüncü bir yazı eklendiğinde EF Core bu yazının anahtarını 0 olarak görür, onu Added olarak işaretler ve kayıtta yalnızca onun için INSERT gönderir. Belgelerin özetiyle: üretilen anahtarlar kullanıldığında EF Core anahtarı atanmamış bir varlığı her zaman ekler, böylece bağlantısız bir grafikte yeni ve mevcut varlıkları kendiliğinden ayırt eder.
context.Attach(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post { Id = 1, Title = "Announcing the Release of EF Core 5.0", Content = "..." },
new Post { Id = 2, Title = "Announcing F# 5", Content = "..." },
new Post { Title = "Announcing .NET 5.0", Content = "..." } // anahtar yok: Added
}
});Update aynı şekilde çalışır, tek farkı anahtarı atanmış varlıkları Modified olarak işaretlemesidir. Belgeler bunun bağlantısız bir grafikten güncelleme ve ekleme üretmenin çok kolay bir yolu olduğunu, ancak değeri değişmemiş olsa bile her izlenen varlığın her özelliği için güncelleme gönderdiğini belirtir ve ekler: küçük grafiklerle çalışan birçok uygulama için bu pratik bir yaklaşımdır, gereğinden fazla endişelenmeye gerek yoktur.
Silme tarafında ise Remove izlenmeyen bir varlıkla çağrılırsa EF Core onu önce iliştirir, sonra Deleted olarak işaretler. Belgeler, gerçek uygulamalarda daha yaygın olan akışın önce grafiği Attach veya Update ile izlemek, sonra silinecek varlıklarda Remove çağırmak olduğunu söyler. Ana varlık silindiğinde bağımlılara ne olduğu cascade delete yazısında anlatılıyor.
Tek bir varlığı kaydetmek
Ekleme mi güncelleme mi gerektiği biliniyorsa Add ya da Update doğrudan kullanılır. Varlık üretilen anahtar kullanıyorsa Update iki durumu birden karşılar: anahtar atanmamışsa varlığı ekleme için, atanmışsa güncelleme için işaretler.
public static async Task InsertOrUpdate(DbContext context, object entity)
{
context.Update(entity);
await context.SaveChangesAsync();
}Anahtar üretilmiyorsa karar uygulamaya kalır. Belgelerdeki kalıp önce FindAsync ile kaydı arar; bulunamazsa Add çağırır, bulunursa izlenen mevcut örneğe SetValues ile istemciden gelen değerleri kopyalar. SetValues yalnızca değeri gerçekten farklı olan özellikleri değişmiş olarak işaretler; hiçbir şey değişmediyse hiç UPDATE gönderilmez.
public static async Task InsertOrUpdate(BloggingContext context, Blog blog)
{
var existingBlog = await context.Blogs.FindAsync(blog.BlogId);
if (existingBlog == null)
{
context.Add(blog);
}
else
{
context.Entry(existingBlog).CurrentValues.SetValues(blog);
}
await context.SaveChangesAsync();
}Bu kalıpta dikkat edilmesi gereken nokta, istemciden gelen blog nesnesinin kendisinin hiç izlenmemesidir. İzlenen tek örnek, sorgudan dönen existingBlog'dur. Bu yüzden "already being tracked" hatası oluşmaz.
Bir varlığı güncellemenin üç yolu

Identity resolution belgesi aynı güncelleme işini üç farklı yolla yapar ve her birinin bedelini açıkça yazar. Hepsinde ortak kural şudur: ya sorgu yapılır ya Update/Attach çağrılır, ama ikisi birden yapılmaz.
// 1. Update çağırmak
context.Update(blog);
await context.SaveChangesAsync();
// 2. Önce sorgulamak, sonra değişiklikleri uygulamak
var trackedBlog = await context.Blogs.FindAsync(blog.Id);
trackedBlog.Name = blog.Name;
trackedBlog.Summary = blog.Summary;
await context.SaveChangesAsync();
// 3. Orijinal değerleri istemciden almak
context.Attach(blog);
context.Entry(blog).OriginalValues.SetValues(originalValues);
await context.SaveChangesAsync();| Yaklaşım | Sorgu yapılır mı | Güncellenen kolonlar | Veritabanı gidiş dönüşü |
|---|---|---|---|
| Update çağırmak | Hayır | Değişsin değişmesin tüm kolonlar | 1 |
| Sorgula, sonra uygula (SetValues) | Evet | Yalnızca değişenler | 2 |
| Attach ve orijinal değerleri uygulamak | Hayır | Yalnızca değişenler | 1 |
Üçüncü yolda varlık önce Attach ile Unchanged olarak izlenir, ardından istemcinin gönderdiği orijinal değerler uygulanır; şimdiki değeri orijinalinden farklı olan özellikler değişmiş olarak işaretlenir. Belgeler bunun çekici bir yöntem olduğunu ama orijinal değerlerin istemciye gidip gelmesini gerektirdiğini hatırlatır ve bu ek karmaşıklığın gerçekten değip değmeyeceğini tartmanızı önerir; birçok uygulama için daha basit yollardan biri daha pratiktir.
Grafiklerle çalışmak
Bir blogu yazılarıyla birlikte kaydederken grafikteki tüm varlıklar eklenecekse Add, tümü güncellenecekse Update yeterlidir. Üretilen anahtarlarla karışık bir grafikte de Update kullanılabilir: anahtarı olmayan her varlık eklenir, diğerleri güncellenir. Anahtarlar üretilmiyorsa belgeler mevcut grafiği Include ile yükleyip istemciden gelen grafikle karşılaştırmayı önerir:
public static async Task InsertUpdateOrDeleteGraph(BloggingContext context, Blog blog)
{
var existingBlog = await context.Blogs
.Include(b => b.Posts)
.FirstOrDefaultAsync(b => b.BlogId == blog.BlogId);
if (existingBlog == null)
{
context.Add(blog);
}
else
{
context.Entry(existingBlog).CurrentValues.SetValues(blog);
foreach (var post in blog.Posts)
{
var existingPost = existingBlog.Posts.FirstOrDefault(p => p.PostId == post.PostId);
if (existingPost == null)
{
existingBlog.Posts.Add(post);
}
else
{
context.Entry(existingPost).CurrentValues.SetValues(post);
}
}
foreach (var post in existingBlog.Posts)
{
if (!blog.Posts.Any(p => p.PostId == post.PostId))
{
context.Remove(post);
}
}
}
await context.SaveChangesAsync();
}Son döngü silmeleri işler. Bağlantısız senaryoda silme zordur, çünkü çoğu zaman silinmesi gereken varlık istemciden gelen grafikte yoktur; yokluğu silinmesi gerektiğini gösterir. Bu yüzden mevcut kayıtlarla gelen kayıtlar arasında bir fark çıkarmak gerekir. Belgelerin önerdiği alternatif soft delete'tir: varlık gerçekten silinmez, silindi olarak işaretlenir; böylece silme bir güncellemeye dönüşür. Soft delete'in sorgu filtresiyle nasıl kurulduğu global query filter ve soft delete yazısında anlatılıyor.
TrackGraph ile özel kurallar
Add, Attach ve Update içeride grafiği gezip her varlık için bir durum seçer. ChangeTracker.TrackGraph bu mekanizmayı dışarı açar ve izlemeden önce her varlık için bir geri çağırma (callback) çalıştırır. Belgelerdeki örnekte istemci her varlıkta nasıl işleneceğini gösteren bir işaret gönderir ve geri çağırma bu işarete göre durumu atar:
context.ChangeTracker.TrackGraph(
rootEntity,
n =>
{
var entity = (EntityBase)n.Entry.Entity;
n.Entry.State = entity.IsNew
? EntityState.Added
: entity.IsChanged
? EntityState.Modified
: entity.IsDeleted
? EntityState.Deleted
: EntityState.Unchanged;
});Belgeler işaretlerin burada sadelik için varlığın üzerinde tutulduğunu, gerçekte genellikle bir DTO'da ya da istekle gelen başka bir durumda bulunacağını not eder. TrackGraph'ın basit aşırı yüklemesinde gezinti, varlık zaten izleniyorsa ya da geri çağırma varlığı izlemeye almadıysa o varlıktan öteye geçmez. Geri çağırmanın bool döndürdüğü gelişmiş aşırı yüklemede ise false döndürmek gezintiyi durdurur; burada sonsuz döngülere dikkat etmek gerekir.
"Already being tracked" hatası: identity resolution

Bir context, belirli bir birincil anahtar değeri için yalnızca tek bir varlık örneği izleyebilir. Aynı anahtara sahip birden çok örneğin tek örneğe indirgenmesine identity resolution denir. EF Core'un tek örnek istemesinin iki nedeni vardır: örneklerin özellik değerleri farklı olabilir ve veritabanını güncellerken hangisinin kullanılacağı belli olmaz; ilişkileri de farklı olabilir, örneğin iki blog örneği farklı yazı koleksiyonlarına bağlı olabilir. Belgelerdeki en kısa örnek, sorgulanmış bir blog izlenirken aynı anahtarlı yeni bir nesneyi Update ile vermeye çalışır:
var blogA = await context.Blogs.SingleAsync(e => e.Id == 1);
var blogB = new Blog { Id = 1, Name = ".NET Blog (All new!)" };
context.Update(blogB); // Bu satır hata verir
// Belgelerdeki hata mesajı:
// System.InvalidOperationException: The instance of entity type 'Blog' cannot be tracked because another
// instance with the key value '{Id: 1}' is already being tracked. When attaching existing entities,
// ensure that only one entity instance with a given key value is attached.Belgeler bu hatanın tipik olarak dört durumda ortaya çıktığını söyler. Her birinin çözümü farklıdır:
- Bir varlığı güncellerken: aynı context'te önce sorgu yapıp sonra istemciden gelen nesneyle
Updateçağırmak. Çözüm, yukarıdaki üç yoldan birini seçmek: ya yalnızcaUpdate, ya sorgu veSetValues; ikisi birden değil. - Serileştirilmiş bir grafiği izlerken: JSON'dan gelen grafikte aynı blog ya da yazı birden çok kez bulunabilir.
- Otomatik üretilmeyen bir anahtarı atamayı unutunca: iki yeni nesnenin ikisi de anahtar 0 ile gelir ve çakışır.
- Tek bir context'i birden çok iş birimi için kullanınca: önceki işten kalan izlenen örnek, yeni işin nesnesiyle çakışır.
Serileştirilmiş grafiklerde yinelenen örnekler
Her blogu yazılarıyla birlikte serileştirmek genelde yineleme üretmez ve Update sorunsuz çalışır. Ancak her yazıyı bloguyla birlikte serileştirirseniz aynı blog JSON'da birden çok kez yer alır ve grafiği izlemeye çalışmak belgelerdeki örnekte 'Post' tipi ve '{Id: 2}' anahtarı için aynı hatayı verir. Belgeler iki çözüm sunar. İlki, serileştiricinin referansları koruma seçeneğidir: Json.NET'te PreserveReferencesHandling, System.Text.Json'da ReferenceHandler.Preserve. Bu seçenek yinelenen nesneleri "$ref" ile önceki örneğe yapılan referanslara çevirir.
var serialized = JsonSerializer.Serialize(
posts, new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, WriteIndented = true });Serileştirmede yinelemeyi önlemek mümkün değilse ikinci çözüm TrackGraph ile izleme sırasında yinelemeleri ayıklamaktır: her düğümde varlığın tipi ve anahtarı alınır, change tracker'da aynı tip ve anahtarla bir giriş varsa bu düğüm atlanır, yoksa Modified olarak izlenir. Belgeler bu kodun tüm yinelemelerin aynı olduğunu varsaydığını özellikle vurgular; yinelemeler farklı değerler taşıyabiliyorsa hangisinin kullanılacağına ve değerlerin nasıl birleştirileceğine sizin karar vermeniz gerekir. Belgelerin genel önerisi de yinelemeleri uygulama akışında mümkün olduğunca erken birleştirmektir.
Anahtarı atanmamış yeni varlıklar
Varlık tipi otomatik üretilen anahtar kullanmıyorsa, izlemeden önce anahtara açıkça değer atanmalıdır. Atamazsanız iki yeni nesnenin ikisi de 0 anahtarıyla izlenmeye çalışılır ve belgelerdeki örnekte 'Pet' tipi için '{Id: 0}' anahtarıyla aynı hata alınır. Çözüm ya anahtarları açıkça atamak ya da anahtar özelliğini üretilen değer kullanacak şekilde yapılandırmaktır; bu yapılandırma EF Core değer üretimi yazısında anlatılıyor.
Tek context'i aşırı kullanmak
Belgeler iki yaygın örnek verir. Birincisi testlerde aynı context'i hem test verisini hazırlamak hem de testi çalıştırmak için kullanmaktır; hazırlıktan kalan izlenen örnek testteki yeni örnekle çakışır. Çözüm, hazırlık ve test için ayrı context örnekleri kullanmaktır; test stratejileri EF Core test yazma yazısında. İkincisi bir repository içinde paylaşılan tek bir context kullanmaktır; repository'nin her iş birimi için tek bir context örneği kullandığından emin olmalısınız.
Identity resolution ve sorgular
İzlemeli sorgularda identity resolution otomatik yapılır: dönen satırın anahtarına sahip bir örnek zaten izleniyorsa yeni örnek oluşturulmaz, izlenen örnek kullanılır. Bunun önemli bir sonucu vardır: veritabanındaki veri değişmiş olsa bile sonuç bunu yansıtmaz. Belgeler, EF Core'un sorguyu her zaman veritabanında çalıştırdığını ama izlenen örnekler varsa onları döndürdüğünü vurgular. Güncel değer gerekiyorsa Reload veya GetDatabaseValues kullanılabilir; daha iyisi her iş birimi için yeni bir context açmaktır.
İzlemesiz sorgular ise identity resolution yapmaz, çünkü büyük sonuçları akıtırken her örneği hatırlamak performansı etkiler. Bu yüzden izlemesiz bir sorgu, JSON örneğindeki gibi aynı varlığın yinelenen örneklerini döndürebilir. Sonuçlar yalnızca serileştirilip istemciye gönderilecekse bu genelde sorun değildir. Yineleme istemiyorsanız AsNoTrackingWithIdentityResolution kullanılabilir. Bu seçenekler AsNoTracking ve tracking sorguları yazısında karşılaştırılıyor.
Son bir ayrıntı: EF Core varlık örneklerini karşılaştırırken, varlık tipi Equals metodunu ezmiş olsa bile referans eşitliği kullanır. Ancak koleksiyon navigation'ları ezilmiş eşitliği kullanırsa birden çok örneği aynı sanabilir. Belgeler bu yüzden varlıklarda eşitliği ezmekten kaçınmayı, ezilecekse koleksiyonları ReferenceEqualityComparer gibi referans eşitliği zorlayan bir karşılaştırıcıyla oluşturmayı önerir. Ayrıca anahtar ve benzersiz index olarak kullanılan tüm tiplerin IComparable<T> ve IEquatable<T> uygulaması gerekir; int, Guid, string gibi tipler bunu zaten yapar.
Sık yapılan hatalar
- Aynı context'te hem sorgu hem Update: "already being tracked" hatasının en yaygın nedeni budur.
- İzlemesiz sorgu yapıp sonucu aynı context'e iliştirmek: belgeler bunu düzenli bir alışkanlık hâline getirmemeyi önerir; izlemeli sorgu hem daha hızlı hem daha doğrudur.
- Anahtar kontrolünü izlemeden sonra yapmak: izlenen varlığın anahtarı geçici değerle doldurulur,
IsKeySetartık yeni varlığı ayırt edemez. - Update ile her şeyi güncellemenin bedelini bilmemek: tüm kolonlar yazılır; bu çoğu zaman kabul edilebilir ama eşzamanlı değişiklikleri ezebilir. Bunu önlemek için concurrency token kullanın.
- Grafikte yinelenen nesneler: serileştiricinin referans koruma seçeneğini kullanın ya da
TrackGraphile ayıklayın.
Sık Sorulan Sorular
Attach ile Update arasındaki fark nedir?
İkisi de grafiği izlemeye alır ve anahtarı atanmamış varlıkları ekler. Attach anahtarı atanmış varlıkları Unchanged yapar, yani siz bir şey değiştirmedikçe komut gitmez. Update ise bu varlıkları Modified yapar ve tüm kolonları günceller.
Yalnızca değişen kolonları güncellemek için ne yapmalıyım?
Ya kaydı sorgulayıp SetValues ile istemci değerlerini uygulayın (iki gidiş dönüş), ya da varlığı Attach ile iliştirip istemcinin gönderdiği orijinal değerleri OriginalValues.SetValues ile verin (tek gidiş dönüş).
Hatayı ChangeTracker.Clear() ile geçiştirmek doğru mu?
Belgelerin önerdiği yol her iş birimi için kısa ömürlü bir context kullanmaktır. Bu düzen kurulduğunda context boş başlar ve önceki işlerden kalan örneklerle çakışma yaşanmaz.
Silinen kayıtları bağlantısız senaryoda nasıl yakalarım?
Mevcut grafiği yükleyip istemciden gelenle karşılaştırın ve gelende olmayanlar için Remove çağırın; ya da soft delete ile silmeyi bir güncellemeye dönüştürün.
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 · Change tracker ve EntityState · SaveChanges · AsNoTracking · Concurrency ve rowversion