Unit of Work ile Transaction Yönetimi
Unit of Work, aynı iş birimindeki değişikliklerin takibini ve kaydetme sınırını düzenler. EF Core’da DbContext bu rolü zaten üstlenir; ayrı IUnitOfWork eklemek uygulamanın sözleşme ihtiyacına bağlıdır. Atomicity ise sınıfın adından değil, provider ve transaction kapsamından gelir.
- Atomicity: Destekleyen provider’da aynı transaction içindeki değişiklikler birlikte commit veya rollback edilir.
- Commit sınırı: Kaydetme noktalarını business iş birimine göre belirleyin. Birden fazla save gerekiyorsa atomiklik kapsamını ayrıca yönetin.
- Temiz Kod: Servis katmanında transaction detayları yerine iş mantığına odaklanırsınız.
Tanım ve Prensipler
Unit of Work deseni, bir iş birimindeki değişiklikleri toplar ve kaydetmeyi koordine eder. EF Core’da tek SaveChanges çağrısının kapsadığı tracked değişiklikler, transaction destekleyen provider’da birlikte kaydedilir. Birden fazla SaveChanges aynı atomik işleme aitse açık transaction gerekebilir.
- Değişiklikleri takip etmek
- Tüm işlemleri tek seferde commit etmek
- Hata durumunda rollback sağlamak
Avantajlar & Dezavantajlar
Avantajlar
- Transaction Tutarlılığı: Birden fazla repository çağrısı tek transaction’da toplanır.
- Performans: Sadece bir
SaveChanges()çağrısı, gereksiz tekrar commit’leri önler. - SRP Uyumu: Repository’ler sadece CRUD odaklı kalır; transaction yönetimi UoW’da toplanır.
Dezavantajlar
- Ekstra Katman: Küçük projelerde basit CRUD işlemleri için gereksiz soyutlama olabilir.
- Complexity: Transaction kapsamını geniş tutmak bazen performansı olumsuz etkileyebilir.
Basit bir CRUD işleminde doğrudan DbContext kullanmak ek katmanı azaltabilir. Daha hızlı çalışacağını ölçmeden söyleyemeyiz. Repository’nin kendi içinde hemen save yapması, daha büyük bir business işleminin commit sınırını bozabilir.
ASP.NET Core Örnek Uygulama
1. IUnitOfWork Arayüzü
public interface IUnitOfWork
{
IRepository<Product> Products { get; }
IRepository<Order> Orders { get; }
Task<int> CommitAsync(
CancellationToken cancellationToken = default
);
}
2. UnitOfWork Sınıfı
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
public UnitOfWork(AppDbContext context)
{
_context = context;
Products = new EfRepository<Product>(_context);
Orders = new EfRepository<Order>(_context);
}
public IRepository<Product> Products { get; }
public IRepository<Order> Orders { get; }
public async Task<int> CommitAsync(
CancellationToken cancellationToken = default
)
{
// Aynı context üzerinde izlenen değişiklikleri kaydet
return await _context.SaveChangesAsync(cancellationToken);
}
}
3. Servis Katmanında Kullanımı
public class OrderService(IUnitOfWork unitOfWork)
{
public async Task PlaceOrderAsync(
Order order,
CancellationToken cancellationToken = default
)
{
ArgumentNullException.ThrowIfNull(order);
if (order.Quantity <= 0)
throw new ArgumentOutOfRangeException(
nameof(order.Quantity)
);
var product =
await unitOfWork.Products.GetByIdAsync(
order.ProductId,
cancellationToken
)
?? throw new InvalidOperationException("Ürün bulunamadı.");
if (product.Stock < order.Quantity)
throw new InvalidOperationException("Stok yetersiz.");
product.Stock -= order.Quantity;
product.Version = Guid.NewGuid(); // Application-managed concurrency token
unitOfWork.Products.Update(product);
await unitOfWork.Orders.AddAsync(order, cancellationToken);
await unitOfWork.CommitAsync(cancellationToken);
}
}
Bu örnek aynı scoped AppDbContext içindeki sipariş ve stok değişikliğini tek SaveChanges ile kaydeder. Concurrency token olmadan iki istek aynı eski stoğu okuyup lost update üretebilir; yalnız tek commit bu durumu çözmez.
Gerçek Projelerde Dikkat Edilmesi Gerekenler
- Transaction süresi: Lock süresini ve batch boyutunu ölçün. Chunk’ları ayrı ayrı commit etmek işlem atomikliğini değiştirir; bunu business kuralıyla birlikte değerlendirin.
- Concurrency Kontrolü:
RowVersionveya optimistik kilitleme ile eş zamanlı güncellemelerde çakışmayı yönetin. - Logging: Hangi transaction’da hangi repository çağrılarının yapıldığını kaydedin; debug sürecini kolaylaştırır.
- Hata sonrası state: Transaction rollback, bellekteki Change Tracker değerlerini eski haline getirmez. Concurrency veya beklenmeyen DB hatasından sonra aynı değişiklikleri körlemesine yeniden save etmeyin; yeni context ve güncel verilerle iş kuralını tekrar değerlendirin.
İleri Okuma ve Kaynaklar
Unit of Work için kapsam ve sınır
Unit of Work için önce iş birimini ve atomicity ihtiyacını belirleyin. Ayrı wrapper, dış servis çağrısını, başka bir context’i veya farklı database’i kendiliğinden transaction’a katmaz. Küçük işlemlerde DbContext yeterli olabilir; daha fazla katman eklemek veri tutarlılığı garantisi değildir.
Stok örneğinde concurrency token
Örnek .NET 10/EF Core 10 ve aynı context üzerinde çalışan repository sözleşmesini esas alır. Guid Version, uygulamanın stok güncellemesinde yenilediği concurrency token’dır:
using System.ComponentModel.DataAnnotations;
public class Product
{
public int Id { get; set; }
public int Stock { get; set; }
[ConcurrencyCheck]
public Guid Version { get; set; } = Guid.NewGuid();
}
public class Order
{
public int Id { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
}
AppDbContext Product ve Order entity’lerini map etmelidir. Version alanı için uygun migration’ı uygulayın; stoğu değiştiren bütün yazma yolları aynı token kuralını izlemelidir. SQL Server’ın otomatik rowversion alanı başka bir seçenektir; SQLite’da bu otomatik özellik yoktur.
İki istek aynı Version değerini okursa yalnız ilk güncelleme eşleşir; ikinci save DbUpdateConcurrencyException üretir. Üst katman bunu uygun conflict yanıtına veya sınırlı retry akışına çevirmelidir. Retry’de yeni context ile stok ve business kurallarını tekrar okuyun; eski stok hesabına yalnız yeni token takıp kaydetmeyin. Ayrıntılar EF Core concurrency dokümantasyonunda yer alıyor.
Buradaki UnitOfWork, DI’ın verdiği DbContext’i dispose etmez. AddDbContext ve AddScoped<IUnitOfWork, UnitOfWork> ile aynı scope’u kullanın; container context’i scope kapanınca temizler. Nesneyi DI dışında kendiniz oluşturursanız context’in ömrünü siz yönetirsiniz. DI ve dispose sorumluluğunu birbirine karıştırmayın.
Bu akış idempotent bir sipariş API’si değildir. Response kaybından sonra request’i tekrar gönderme ihtiyacı varsa request kimliği ve duplicate işleme davranışını ayrıca tasarlayın. Dış ödeme/email/message işlemlerini local DB transaction’ın parçası sanmayın; gerektiğinde outbox veya compensation sınırı kurun.
