EF Core'da (Entity Framework Core) ExecuteUpdateAsync ve ExecuteDeleteAsync, LINQ ile seçtiğiniz satırları belleğe hiç yüklemeden tek bir SQL UPDATE ya da DELETE komutuyla değiştirir veya siler. Örneğin await context.Blogs.Where(b => b.Rating < 3).ExecuteDeleteAsync(); düşük puanlı tüm blogları tek komutla siler. Bu metotlar change tracker'ı kullanmaz, çağrıldıkları anda çalışır ve etkilenen satır sayısını döndürür.
Bu yazı iki metodun kullanımını, EF Core 10 ile gelen değişiklikleri (setter'lar için sıradan lambda ve JSON kolonlarında güncelleme), change tracker ve transaction ile ilişkilerini, eşzamanlılık denetimini ve sınırlamaları anlatır. İzlenen varlıklar üzerinden yapılan klasik kayıt EF Core SaveChanges yazısında anlatılıyor; iki yaklaşımın karşılaştırması da orada.
SaveChanges ile toplu silmenin maliyeti
Puanı belli bir eşiğin altındaki tüm blogları silmek istediğinizi düşünün. SaveChanges yaklaşımında önce bu blogları sorgular, her birini nesneye dönüştürüp izlemeye alır, sonra her biri için Remove çağırırsınız. SaveChanges da her blog için ayrı bir DELETE üretir. Belgeler bunu, eşleşen satır sayısı çok büyük olabileceği için oldukça verimsiz bir yol olarak tarif eder.
await foreach (var blog in context.Blogs.Where(b => b.Rating < 3).AsAsyncEnumerable())
{
context.Blogs.Remove(blog);
}
await context.SaveChangesAsync();İlişkisel veritabanları bunun çok daha verimli bir alternatifini sunar: hangi satırların silineceğini WHERE ile belirten tek bir DELETE. SaveChanges modeli böyle bir komut üretemez; ExecuteDelete ve ExecuteUpdate bu açığı kapatmak için vardır.
ExecuteDelete
Aynı işi ExecuteDeleteAsync ile yapmak tek satırdır. Etkilenecek satırları, sanki sorgu yazıyormuş gibi tanıdık LINQ operatörleriyle seçersiniz; EF Core bunu SQL'e çevirir ve veritabanında çalıştırır.
await context.Blogs.Where(b => b.Rating < 3).ExecuteDeleteAsync();Belgelerde SQL Server için gösterilen komut şöyledir:
-- Belgelerdeki örnek çıktı (SQL Server)
DELETE FROM [b]
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3Daha kısa olmasının yanında bu komut veritabanından hiç veri yüklemeden ve change tracker'ı hiç kullanmadan çalışır. Silinecek satırları seçmek için istediğiniz LINQ operatörlerini kullanabilirsiniz; bunlar normal bir sorguda olduğu gibi SQL'e çevrilir. Çevrilemeyen bir ifade kullanırsanız sorgu tarafında karşılaşılan çeviri sorunları burada da geçerlidir; bu konu client evaluation hatası yazısında anlatılıyor.
ExecuteUpdate ve SetProperty

Blogları silmek yerine gizlemek isterseniz ExecuteUpdateAsync kullanırsınız. Önce yine LINQ ile hangi satırların etkileneceğini belirtir, sonra SetProperty ile neyin değişeceğini söylersiniz. SetProperty iki argüman alır: değişecek özellik ve yeni değeri.
await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.IsVisible, false));-- Belgelerdeki örnek çıktı (SQL Server)
UPDATE [b]
SET [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3Birden çok özelliği güncellemek
Aynı çağrıda birden çok kolonu güncellemek için SetProperty çağrılarını zincirlersiniz. Belgelerdeki örnek hem IsVisible değerini false yapar hem de Rating değerini sıfırlar; SQL tarafında tek bir UPDATE iki kolonu birden ayarlar.
await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(setters => setters
.SetProperty(b => b.IsVisible, false)
.SetProperty(b => b.Rating, 0));Mevcut değere göre hesaplamak
Yeni değer sabit olmak zorunda değildir. SetProperty'nin ikinci argümanı bir lambda olabilir; lambdanın parametresi güncellenen satırı temsil eder ve onun içindeki b.Rating değişiklikten önceki değerdir. Aşağıdaki çağrı eşleşen blogların puanını bir artırır ve SQL'de [b].[Rating] + 1 olarak çalışır.
await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));Navigation'lar ve ilişkili varlıklar
Belgelere göre ExecuteUpdate şu anda SetProperty lambdası içinde navigation'lara başvurmayı desteklemez. Örneğin her blogun puanını yazılarının ortalama puanı yapmak için b.Posts.Average(...) ifadesini doğrudan SetProperty içine yazmak işe yaramaz. Bunun yerine önce Select ile ortalamayı hesaplayıp anonim bir tipe yansıtır, ExecuteUpdate'i bu yansıma üzerinde çalıştırırsınız:
await context.Blogs
.Select(b => new { Blog = b, NewRating = b.Posts.Average(p => p.Rating) })
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.Blog.Rating, b => b.NewRating));-- Belgelerdeki örnek çıktı (SQL Server)
UPDATE [b]
SET [b].[Rating] = CAST((
SELECT AVG(CAST([p].[Rating] AS float))
FROM [Post] AS [p]
WHERE [b].[Id] = [p].[BlogId]) AS int)
FROM [Blogs] AS [b]EF Core 10 yenilikleri

Setter için sıradan (expression olmayan) lambda
Önceki sürümlerde ExecuteUpdate'in setter argümanı bir ifade ağacıydı (Expression<Func<...>>). Bu, setter'ları koşula göre dinamik oluşturmayı çok zorlaştırıyordu; örneğin bir blogun Views değerini her zaman, Name değerini yalnızca değiştiyse güncellemek için Expression.Call ve Expression.Lambda ile elle ifade ağacı kurmanız gerekiyordu. EF Core 10 ile setter parametresi sıradan bir Func<...> oldu ve aynı iş artık basit bir if ile yazılabiliyor:
await context.Blogs.ExecuteUpdateAsync(s =>
{
s.SetProperty(b => b.Views, 8);
if (nameChanged)
{
s.SetProperty(b => b.Name, "foo");
}
});JSON kolonlarında ExecuteUpdate
EF Core JSON kolonlarını bir süredir SaveChanges ile güncelleyebiliyordu, ancak ExecuteUpdateAsync bunları desteklemiyordu. EF Core 10 ile JSON kolonlarına ve içlerindeki özelliklere ExecuteUpdateAsync içinde başvurmak mümkün. Bunun için tipin complex type olarak ve ToJson() ile eşlenmiş olması gerekir; owned entity olarak eşlenmiş tiplerde çalışmaz. Complex type ile owned entity farkı complex types, owned ve JSON kolon yazısında anlatılıyor.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().ComplexProperty(b => b.Details, bd => bd.ToJson());
}
await context.Blogs.ExecuteUpdateAsync(s =>
s.SetProperty(b => b.Details.Views, b => b.Details.Views + 1));Belgelere göre SQL Server 2025'te bu çağrı yeni JSON veri tipinin modify fonksiyonunu kullanan bir komut üretir. Verinin nvarchar(max) kolonda tutulduğu eski SQL Server sürümleri de desteklenir; diğer veritabanları için sağlayıcınızın belgelerine bakmanız gerekir.
-- Belgelerdeki örnek çıktı (SQL Server 2025)
UPDATE [b]
SET [Details].modify('$.Views', JSON_VALUE([b].[Details], '$.Views' RETURNING int) + 1)
FROM [Blogs] AS [b]Change tracker ile hiçbir etkileşim yok
SaveChanges alışkanlığıyla gelenler için en önemli fark budur. ExecuteUpdate ve ExecuteDelete çağrıldıkları anda çalışır; birden çok işlemi biriktirip sonra birlikte uygulamak mümkün değildir. Belgelerin ifadesiyle bu metotlar change tracker'dan tamamen habersizdir ve onunla hiç etkileşime girmez. Belgelerdeki örnek bunun nasıl sessiz bir veri kaybına yol açabileceğini gösterir:
// 1. Blog sorgulanır ve izlenmeye başlar (Rating = 5 varsayalım)
var blog = await context.Blogs.SingleAsync(b => b.Name == "SomeBlog");
// 2. Tüm blogların puanı veritabanında hemen bir artırılır (veritabanında 6)
await context.Blogs.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));
// 3. İzlenen nesnede puan iki artırılır (bellekte 7)
blog.Rating += 2;
// 4. SaveChanges orijinal 5 ile 7'yi karşılaştırır ve 7 yazar
await context.SaveChangesAsync();ExecuteUpdate veritabanını güncellediğinde izlenen .NET örneği bundan etkilenmez ve sorgulandığı andaki puanı taşımaya devam eder. Kayıt sırasında EF Core yeni değer 7'nin orijinal değer 5'ten farklı olduğunu görür ve 7'yi yazar; ExecuteUpdate'in yaptığı artış ezilir ve hesaba katılmaz. Belgeler bu yüzden izlenen SaveChanges değişiklikleriyle ExecuteUpdate/ExecuteDelete ile yapılan izlenmeyen değişiklikleri karıştırmaktan kaçınmanın genellikle iyi bir fikir olduğunu söyler.
İki yaklaşımı aynı iş biriminde kullanmanız gerçekten gerekiyorsa sırayı düşünerek kurun: toplu işlemi izlenen varlıkları yüklemeden önce yapmak, ya da toplu işlemden sonra etkilenen izlenen varlıkları context.Entry(blog).Reload() ile yenilemek bu tür ezilmeleri önler. Reload belgelere göre özellik değerlerini veritabanından okunan değerlerle ezer. En temiz çözüm ise toplu işlem için ayrı, kısa ömürlü bir context kullanmaktır; böylece bellekte eski değer taşıyan bir nesne kalmaz.
Aynı mantık silme için de geçerlidir. Change tracker devrede olmadığı için EF Core'un izlenen varlıklara uyguladığı cascade davranışı burada çalışmaz; bağımlı satırlar için veritabanındaki yabancı anahtar kısıtı neyi söylüyorsa o olur. Veritabanında ON DELETE CASCADE tanımlıysa bağımlılar da silinir, tanımlı değilse komut yabancı anahtar ihlaliyle başarısız olabilir. Silme davranışlarının veritabanı şemasına nasıl yansıdığı cascade delete yazısındaki tabloda gösteriliyor.
Transaction davranışı
ExecuteUpdate ve ExecuteDelete çağrıldıklarında kendiliğinden bir transaction başlatmaz. Her çağrı veritabanına tek bir komut gönderir. Belgelerdeki örnekte iki ExecuteUpdate, bir sorgu ve bir SaveChanges art arda çalışır; ikinci ExecuteUpdate başarısız olursa birincinin etkisi veritabanında kalır, çünkü dört işlemin her biri kendi transaction'ında çalışır. Birden çok işlemi tek bir transaction'a almak için context.Database üzerinden açıkça transaction başlatmanız gerekir:
await using (var transaction = await context.Database.BeginTransactionAsync())
{
await context.Blogs.ExecuteUpdateAsync(/* bir güncelleme */);
await context.Blogs.ExecuteUpdateAsync(/* başka bir güncelleme */);
await transaction.CommitAsync();
}Belgelerin ExecuteUpdate sayfasındaki örnek senkron BeginTransaction ile yazılmıştır; yukarıda transaction sayfasındaki asenkron kalıbı (BeginTransactionAsync ve CommitAsync) kullandık. Commit çağrılmadan dispose edilen transaction geri alınır. Belgeler ayrıca transaction'ı bu şekilde elle yönetmenin, otomatik yeniden deneme yapan execution strategy'lerle uyumsuz olduğunu not eder; bu konu connection resiliency yazısında. Commit, rollback ve savepoint ayrıntıları EF Core transaction yönetimi yazısında anlatılıyor.
Eşzamanlılık denetimi ve etkilenen satır sayısı
SaveChanges, concurrency token yapılandırdıysanız bir satırın siz yükledikten sonra değişip değişmediğini otomatik denetler. ExecuteUpdate ve ExecuteDelete change tracker'ı kullanmadığı için bunu otomatik yapamaz. Ancak iki metot da işlemden etkilenen satır sayısını döndürür ve bu, denetimi elle kurmak için kullanılabilir:
var numUpdated = await context.Blogs
.Where(b => b.Id == id && b.ConcurrencyToken == concurrencyToken)
.ExecuteUpdateAsync(/* ... */);
if (numUpdated == 0)
{
throw new Exception("Update failed!");
}Burada güncelleme yalnızca belirli bir blogda ve concurrency token daha önce okunan değere eşitse uygulanır. Sonuç sıfırsa hiçbir satır güncellenmemiştir ve büyük olasılıkla araya giren bir güncelleme token'ı değiştirmiştir. Concurrency token kavramı ve rowversion EF Core concurrency yazısında anlatılıyor.
Sınırlamalar

- Yalnızca güncelleme ve silme desteklenir; ekleme
DbSet<TEntity>.AddveSaveChangesile yapılmalıdır. - SQL
UPDATEveDELETEetkilenen satırların özgün kolon değerlerini döndürebilse de bu şu anda desteklenmez. - Bu metotların birden çok çağrısı gruplanamaz (batching yok); her çağrı veritabanına ayrı bir gidiş dönüş yapar.
- Veritabanları genellikle bir
UPDATEya daDELETEile yalnızca tek bir tablonun değiştirilmesine izin verir. - Bu metotlar şu anda yalnızca ilişkisel veritabanı sağlayıcılarıyla çalışır.
Hangi durumda hangisi?
Belgelerin özet önerileri şöyledir. Hangi değişikliklerin olacağını önceden bilmiyorsanız, örneğin kullanıcıya düzenlenebilir bir form gösteriyorsanız, SaveChanges kullanın; o değişiklikleri kendisi bulur. Birbirine bağlı nesnelerden oluşan bir grafiği değiştiriyorsanız, örneğin bir blogu güncelleyip yazılarının bazılarını değiştirip bazılarını siliyorsanız, yine SaveChanges kullanın; değişikliklerin doğru sırasını ve bağlantıları o çözer.
Bir ölçüte göre çok sayıda satırı değiştirecekseniz, örneğin tüm çalışanlara zam yapacak ya da adı belirli bir harfle başlayan tüm blogları silecekseniz, ExecuteUpdate ve ExecuteDelete kullanın. Tam olarak hangi satırı nasıl değiştireceğinizi zaten biliyorsanız, örneğin Id değeri 5 olan blogun adını değiştirecekseniz, yine bu metotlar daha basittir. Belgeler, değişikliği change tracking ile yapmanın bu durumda gereğinden karmaşık olabileceğini belirtir: bir varlık örneği oluşturmak, Attach ile izlemek, değişikliği yapmak ve sonunda SaveChanges çağırmak gerekir.
Performans belgesindeki maaş örneği farkı somutlaştırır. Çalışanları foreach ile gezip maaşı artırıp SaveChanges çağırmak, önce tüm çalışan satırlarını istemciye getirir, change tracking anlık görüntüleri oluşturup karşılaştırır ve her çalışan için ayrı bir UPDATE gönderir. ExecuteUpdateAsync ile aynı iş, hiçbir veri taşımadan tek bir komutla yapılır:
await context.Employees.ExecuteUpdateAsync(s => s.SetProperty(e => e.Salary, e => e.Salary + 1000));-- Belgelerdeki örnek çıktı
UPDATE [Employees] SET [Salary] = [Salary] + 1000;Bu metotların kapsamadığı karmaşık bir komut gerekiyorsa ham SQL çalıştırmak da bir seçenektir; parametrelerin güvenli kullanımı FromSql ve ham SQL sorguları yazısında anlatılıyor. Genel performans önerileri için EF Core performans optimizasyonu yazısına bakabilirsiniz.
Sık yapılan hatalar
- Aynı iş biriminde ExecuteUpdate ve izlenen değişiklikleri karıştırmak: izlenen nesne eski değeri taşır ve
SaveChangestoplu güncellemeyi ezebilir. - Otomatik transaction beklemek: art arda çağrılar ayrı ayrı commit edilir; bütünlük gerekiyorsa transaction'ı siz açın.
- Concurrency token'ın otomatik kontrol edileceğini sanmak:
Wherekoşuluna token'ı ekleyin ve dönen satır sayısını kontrol edin. - SetProperty içinde navigation kullanmak: desteklenmez; değeri önce
Selectile yansıtın. - EF Core 10'a geçerken elle kurulan ifade ağaçları: setter parametresi artık
Funcolduğu için bu kod derlenmez; koşulluSetPropertyçağrılarına dönüştürün. - Owned entity içindeki JSON özelliğini güncellemeye çalışmak: JSON desteği yalnızca complex type eşlemesinde vardır.
Sık Sorulan Sorular
ExecuteUpdate ile toplu ekleme yapılabilir mi?
Hayır. Yalnızca güncelleme ve silme desteklenir; ekleme için Add/AddRange ve SaveChanges kullanılır.
ExecuteDelete sonrası izlenen nesneler ne olur?
Hiçbir şey; change tracker değişiklikten haberdar olmaz. Silinen satırlara karşılık gelen nesneler hâlâ izlenen durumlarında kalır. Bu yüzden iki yaklaşımı aynı context'te karıştırmamak en güvenlisidir.
Kaç satırın etkilendiğini nasıl öğrenirim?
İki metot da etkilenen satır sayısını döndürür. Bu değeri eşzamanlılık denetimi ya da iş kuralları için kontrol edebilirsiniz.
SQLite veya PostgreSQL ile çalışır mı?
Belgeler bu metotların ilişkisel sağlayıcılarla çalıştığını söyler; üretilen SQL sağlayıcıya göre değişir. JSON kolonlarıyla ilgili desteğin ayrıntıları için kullandığınız sağlayıcının belgelerine bakı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 · SaveChanges ile ekleme, güncelleme, silme · Transaction yönetimi · Concurrency ve rowversion · EF Core 10 yenilikleri