Yükleniyor

EF Core ile İş Akış Yönetimi: SaveChanges Yeterli mi?

Blog Kategorileri

.Net Core Mimariler
Tasarım Desenleri
ORM Araçları
API Geliştirme
Web Geliştirme
Veritabanları
20 Nisan 2026 Pazartesi
EF Core ile İş Akış Yönetimi: SaveChanges Yeterli mi?

Merhaba,

Entity Framework Core ile çalışırken şunu düşünebiliriz : Zaten SaveChanges() var. O halde veriler güvenli bir şekilde veritabanına yazılıyor demektir. Bu düşünce çoğu basit senaryoda doğrudur. Hatta tek bir kayıt ekleme, güncelleme ya da silme işlemi yapılıyosa, SaveChanges() çoğu zaman fazlasıyla yeterlidir. Fakat uygulama biraz büyüyüp birden fazla tabloyu, birden fazla adımı, harici servis çağrılarını, stok güncellemelerini veya ödeme süreçlerini aynı akışta birleştirmeye başladığında durum değişir.

Tam bu noktada kritik soru ortaya çıkar: Ben tek bir veri kaydı mı yapıyorum, yoksa bir iş akışını mı yönetiyorum? Bu ayrım oldukça önemlidir. Çünkü SaveChanges() veritabanına yazma işlemini güvenli hale getirebilir; ancak her zaman bir iş sürecinin tamamını koruyamaz.

SaveChanges() ne yapar?

EF Core’da DbContext, yapılan değişiklikleri doğrudan anında veritabanına göndermez. Önce bu değişiklikleri hafızada tutar ve takip eder. Bu mekanizma Change Tracker olarak adlandırılır. Örneğin bir entity üzerinde şu işlemler yapılır: yeni bir nesne eklenir, mevcut bir nesne güncellenir, bazı kayıtlar silinir. EF Core bu değişiklikleri hemen SQL’e dönüştürmez. Önce ilgili nesnelerin durumunu takip eder: Added, modified, deleted, unchanged

SaveChanges() çağrıldığında EF Core, bu takip edilen değişiklikleri alır ve veritabanına uygular. Bu şu anlama gelir: SaveChanges() bir veya birden fazla SQL komutu üretir. Bu komutları veritabanına gönderir. İlişkisel veritabanı kullanılan durumlarda işlemler genellikle transaction benzeri güvenli bir akışta yürütülür. İşlem başarılı olursa değişiklikler kalıcı hale gelir. Burada SaveChanges()’in garanti ettiği şey şudur: Bu çağrı kapsamında EF Core’un takip ettiği değişiklikler, veritabanına tutarlı bir biçimde yazılır. Ama Uygulamanın tamamındaki tüm iş akışını otomatik olarak güvence altına alınacağı garanti etmez.  Yani SaveChanges() veritabanına yazma kısmını çözer; iş sürecinin tamamını çözmez..

SaveChanges() hangi durumlarda yeterlidir?

Bazı senaryolarda ekstra transaction açmaya hiç gerek yoktur. Hatta gereksiz transaction kullanmak, kodu daha karmaşık ve bazen daha yavaş hale getirebilir.

Şu durumlarda çoğu zaman SaveChanges() yeterlidir:

  1. Tek bir entity insert/update/delete işlemi
  2. Aynı DbContext içinde birkaç değişiklik olsa bile tek bir atomik kayıt isteği
  3. Basit CRUD ekranları
  4. Kullanıcı profil güncellemesi
  5. Tek bir sipariş satırının eklenmesi gibi küçük işlemler

Örneğin basit bir müşteri ekleme işlemi şöyle olabilir:

 public async Task AddCustomerAsync()
 {
     var customer = new Customer
     {
         Name = "Sinan",
         Surname = "Tosun",
         CreatedAt = DateTime.UtcNow
     };

     db.Customers.Add(customer);

     // Tek bir basit veri yazma işlemi için çoğu zaman yeterlidir.
     await db.SaveChangesAsync();
 }

Bu senaryoda işlem nettir, tek bir kayıt ekleniyor ve ek bir iş akışı yönetilmiyor. Bu nedenle SaveChanges() burada yeterli olur.

SaveChanges() hangi durumlarda yetersiz kalır?

Asıl önemli nokta burada başlıyor: SaveChanges() ilk bakışta çoğu senaryo için yeterli bir çözüm gibi görünse de, uygulama mimarisi karmaşıklaştıkça ve iş süreçleri birbirine bağımlı adımlardan oluşmaya başladıkça bu yaklaşım yetersiz kalmaya başlar; dolayısıyla, veri bütünlüğünü ve sistem kararlılığını korumak adına, aşağıdaki senaryolarda SaveChanges()’in sunduğu standart yaklaşımın ötesine geçmek zorunlu hale gelir.

1.Birden fazla SaveChanges() çağrısı varsa

En yaygın problemlerin başında bu gelir.  Örneğin önce siparişi kaydederiz, sonra ödeme kaydını ekleriz, ardından stok düşeriz. Her adımda ayrı SaveChanges() cağrılırsa, arada herhangi bir hata oluşabilir. Bu durumda ilk adımlar veritabanına yazılmış olur, sonraki adımda ise hata alınır. Sonuç olarak yarım kalmış bir iş akışı oluşur.

2.Birden fazla tabloya yazma işlemi bir bütünse

Diyelim ki: Order tablosuna kayıt atılıyor, OrderItem tablosuna satır ekleniyor, Payment tablosuna ödeme kaydı düşülüyor. Bu işlemler iş mantığı açısından birbirine bağlıysa, hepsinin birlikte başarılı olması gerekir. Bir tanesi başarısız olursa diğerlerinin de geri alınması gerekir. Aksi halde sistemin veri bütünlüğü bozulur.

3.Veritabanı dışı işlemler varsa

Aynı akış içinde: e-posta gönderiliyor, mesaj kuyruğuna event basılıyor, dosya sistemi üzerinde işlem yapılıyor, harici bir API’ye çağrı atılıyor. Bunlar SaveChanges()’in kapsamına girmez. Çünkü SaveChanges() yalnızca EF Core’un izlediği veritabanı değişikliklerini kapsar.

4.Harici servis çağrısı varsa

Ödeme servisi, kargo servisi, stok doğrulama servisi gibi dış sistemler işin içindeyse, tek başına SaveChanges() korumaz.

Bir örnek düşünelim: siparişi veritabanına yazılıyor, ödeme servisinden onay bekleniyor, ödeme başarısız oluyor. Bu durumda sipariş veritabanına yazılmış, fakat ödeme tamamlanmamış olabilir. Böyle bir durumda iş süreci tutarsız hale gelir.

5.Aynı iş akışında hem veri yazıp hem kritik bir adım yapılıyorsa

Örneğin: müşteri oluştur, stok düş, ödeme kaydı oluştur, fatura üret. Bunların hepsi tek bir iş akışının parçalarıdır. Bu tür bir senaryoda biri oldu, biri olmadı durumu iş açısından kabul edilemez olabilir.

6.Farklı DbContext örnekleri kullanılıyorsa

Her DbContext kendi takip alanına sahiptir. Bir context ile yapılan değişiklik, başka bir context tarafından otomatik olarak korunmaz. Farklı DbContext örnekleri arasında atomiklik gerekiyorsa, iş daha da büyür. Bu noktada manuel transaction ya da daha gelişmiş transaction koordinasyonu gerekebilir.

7.Birden fazla repository veya unit of work akışında tutarlılık gerekiyorsa

Repository pattern kullanılıyorsa, bazen farklı repository’ler aynı iş akışı içinde ayrı ayrı çalışır. Eğer bunların tamamı aynı transaction içinde değilse, sistem parçalı bir durumda kalabilir. Bu da veri tutarsızlığına ve bakım zorluğuna yol açar.

Gerçek hayat örneği: Sipariş oluşturma senaryosu

Bir e-ticaret uygulaması düşünelim. Kullanıcı bir sipariş veriyor.

Bu akışta şunlar olsun:

  1. Order tablosuna sipariş kaydı
  2. OrderItems tablosuna ürün satırları
  3. Inventory tablosunda stok düşme
  4. Payment tablosunda ödeme kaydı

İlk bakışta şöyle bir yaklaşım mantıklı görünebilir: Siparişi kaydet, sipariş kalemlerini kaydet stok düş, ödeme kaydet fakat burada kritik bir sorun vardır: Bu adımların her biri ayrı SaveChanges() ile kaydediliyorsa, son adımda hata oluştuğunda önceki adımlar çoktan veritabanına yazılmış olabilir.

Örneğin:

  1. Order kaydedildi
  2. OrderItems kaydedildi
  3. stok düşüldü
  4. ödeme kaydı sırasında exception oluştu

Bu durumda sipariş vardır, kalemler vardır, stok düşmüştür; fakat ödeme kaydı yoktur. İş açısından bakınca bu oldukça problemli bir durumdur. Müşteri “Siparişim oluştu mu?” diye sorar, sistem bir yandan siparişi göstermeye başlar ama ödeme tamamlanmamıştır. Destek ekibi, muhasebe ve stok yönetimi tarafı karışır. İşte manuel transaction tam olarak burada devreye girer.

Manuel transaction neden gerekir?

Manuel transaction’ın amacı nettir: Bir iş akışındaki tüm adımlar ya birlikte başarılı olsun ya da birlikte geri alınsın. Bu yaklaşım veritabanı tarafında atomicity sağlar. Yani ya hepsi olur ya da hiçbiri olmaz. Transaction kullanmadığında şu riskler ortaya çıkar: yarım kalmış kayıtlar, tutarsız stok miktarı, ödemesi olmayan sipariş, loglarda başarılı görünen ama iş olarak başarısız olan akışlar, kullanıcıya yanlış geri bildirim. Özetle transaction, veri yazma ile iş süreci yönetimi arasındaki farkı kapatır.

BeginTransaction() ile kullanım

Şimdi aynı sipariş senaryosunu doğru şekilde kuralım.

Aşağıdaki örnekte EF Core’un BeginTransactionAsync() mekanizmasını kullanıyoruz. Bu sayede tüm adımlar tek transaction içinde çalışır. Bir yerde hata olursa rollback yapılır.

 public async Task CreateOrderAsync(CreateOrderRequest request)
 {
     await using var transaction = await db.Database.BeginTransactionAsync();

     try
     {
         // 1) Sipariş kaydı
         var order = new Order
         {
             CustomerId = request.CustomerId,
             OrderDate = DateTime.UtcNow,
             Status = "Pending"
         };

         db.Orders.Add(order);
         await db.SaveChangesAsync();

         // 2) Sipariş kalemleri
         foreach (var item in request.Items)
         {
             var orderItem = new OrderItem
             {
                 OrderId = order.Id,
                 ProductId = item.ProductId,
                 Quantity = item.Quantity,
                 UnitPrice = item.UnitPrice
             };

             db.OrderItems.Add(orderItem);
         }

         await db.SaveChangesAsync();

         // 3) Stok düşme
         foreach (var item in request.Items)
         {
             var inventory = await db.Inventories
             .FirstAsync(x => x.ProductId == item.ProductId);

             if (inventory.Quantity < item.Quantity)
             {
                 throw new InvalidOperationException(
                 $"Yeterli stok yok. ProductId: {item.ProductId}");
             }

             inventory.Quantity -= item.Quantity;
         }

         await db.SaveChangesAsync();

         // 4) Ödeme kaydı
         var payment = new Payment
         {
             OrderId = order.Id,
             Amount = request.TotalAmount,
             PaymentDate = DateTime.UtcNow,
             Status = "Completed"
         };

         db.Payments.Add(payment);
         await db.SaveChangesAsync();

         // Her şey başarılıysa transaction'ı commit et
         await transaction.CommitAsync();
     }
     catch
     {
         // Hata varsa tüm işlemleri geri al
         await transaction.RollbackAsync();
         throw;
     }
 }

Bu kod neden doğru?

Çünkü burada tek bir veri kaydı değil, tek bir iş akışı yönetiliyor. Sipariş oluşuyo, kalemler ekleniyor, stok düşüyor, ödeme kaydı alınıyor bu adımlardan biri bozulursa tüm süreç geri alınıyor. Böylece veritabanı tutarlı kalıyor. Burada dikkat edilmesi gereken önemli bir nokta da şudur: Transaction içine mümkün olduğunca yalnızca veritabanı işlemleri konulmalıdır. Uzun süren network çağrıları transaction süresini gereksiz uzatır.

Kod örnekleri

Basit SaveChanges() örneği

  public async Task AddOrderAsync()
  {
      var order = new Order
      {
          CustomerId = 10,
          OrderDate = DateTime.UtcNow,
          Status = "Pending"
      };

      db.Orders.Add(order);

      // Tek bir basit veri yazma işlemi için yeterlidir.
      await db.SaveChangesAsync();
  }

Bu örnek oldukça temizdir. Tek iş vardır, tek kayıt yazılmaktadır. Bu nedenle ekstra transaction açmaya gerek yoktur.

SaveChanges() ile yanlış / eksik senaryo

 public async Task CreateOrderWithoutTransactionAsync()
 {
     var order = new Order
     {
         CustomerId = 10,
         OrderDate = DateTime.UtcNow,
         Status = "Pending"
     };

     db.Orders.Add(order);
     await db.SaveChangesAsync(); // Sipariş kaydedildi

     var payment = new Payment
     {
         OrderId = order.Id,
         Amount = 1500,
         PaymentDate = DateTime.UtcNow,
         Status = "Completed"
     };

     db.Payments.Add(payment);

     // Burada bir hata olursa sipariş zaten kaydedilmiş olur,
     // ama ödeme kaydı oluşmamış olabilir.
     await db.SaveChangesAsync();
 }

Bu örnekte problem şudur: İlk SaveChangesAsync() başarılı olur, ikinci adımda hata oluşursa sistemde yarım kalmış bir iş akışı meydana gelir. Bu tür akışlarda tek SaveChanges() yeterlidir demek doğru olmaz. Çünkü sorun SaveChanges()’in kendisi değil, iş akışının parçalı yazılmasıdır.

BeginTransaction() ile doğru çözüm

 public async Task CreateOrderWithTransactionAsync()
 {
     await using var transaction = await db.Database.BeginTransactionAsync();

     try
     {
         var order = new Order
         {
             CustomerId = 10,
             OrderDate = DateTime.UtcNow,
             Status = "Pending"
         };

         db.Orders.Add(order);
         await db.SaveChangesAsync();

         var payment = new Payment
         {
             OrderId = order.Id,
             Amount = 1500,
             PaymentDate = DateTime.UtcNow,
             Status = "Completed"
         };

         db.Payments.Add(payment);
         await db.SaveChangesAsync();

         await transaction.CommitAsync();
     }
     catch
     {
         await transaction.RollbackAsync();
         throw;
     }
 }
 

Bu yaklaşımda iki adım da aynı transaction içinde çalışır. Arada hata oluşursa rollback uygulanır ve yarım kayıt kalmaz.

En sık yapılan hatalar

1.Her işlemde gereksiz transaction açmak

Basit bir profil güncellemesinde transaction açmak çoğu zaman gereksizdir. Bu durum kodu şişirir, karmaşıklığı artırır ve okunabilirliği düşürür.

2.Uzun süren transaction tutmak

Transaction açık kaldığı sürece, veritabanı üzerinde tuttuğu kilitleri de bırakmaz. Bu şu anlama gelir: Bir transaction uzun süre açık kalırsa, kilitlediği veriler diğer işlemler tarafından kullanılamaz. Çünkü veritabanı tutarlılığı bozulmasın diye o veriler bekletilir.

3.Transaction içinde network call yapmak

Ödeme servisini çağırmak, mail göndermek veya dış API’den cevap beklemek transaction içinde yapılmamalıdır. Çünkü transaction süresi gereksiz şekilde uzar ve sistem bloklanabilir.

4.Hatalı rollback yönetimi

Rollback’i yapmamak, exception sonrası bağlantıyı kapatmamak veya hatayı gizlemek; veri tutarsızlığına, kaynak sızıntısına ve zor tespit edilen ciddi sistem hatalarına neden olur.

5.Gereksiz yere SaveChanges()’i bölmek

Aynı iş akışında her küçük adım için ayrı SaveChanges() çağırmak, yarım kalmış veri riskini artırır ve yönetimi zorlaştırır.

Sonuç

SaveChanges() güçlüdür. Ama gücü, sınırları içinde değerlidir.  Akılda tutmak gerekir ki:

  1. Tek bir veri güncellemesi yapıyorsak SaveChanges() çoğu zaman yeterlidir.
  2. Bir iş akışı yönetiyorsak manuel transaction gerekebilir.
  3. Birden fazla kayıt, birden fazla tablo, birden fazla adım ve hata durumunda geri alma ihtiyacı varsa transaction neredeyse zorunlu hale gelir.

En pratik düşünme yöntemi: Ben tek bir atomic veri güncellemesi mi yapıyorum, yoksa bir iş akışını mı yönetiyorum? sorusunun cevabıdır ve çoğu zaman hangi yaklaşımı kullanmamız gerektiğini zaten söyler.

Diğer bloglarımda görüşmek üzere. 👋