Merhaba,
Kurumsal uygulamalarda veri erişim katmanının yönetilebilir, test edilebilir ve sürdürülebilir olması kritik öneme sahiptir. Bu noktada Unit of Work (UoW) Pattern, özellikle Entity Framework Core kullanılan projelerde sıkça tercih edilen bir tasarım desenidir. Bu yazıda, .NET Core 10 üzerinde Unit of Work kullanımını, basit ve gerçekçi bir örnek üzerinden ele alacağım.
Unit of Work Pattern Nedir?
Unit of Work, bir iş akışı boyunca yapılan tüm veritabanı işlemlerini tek bir işlem altında toplar ve bu işlemlerin tek noktadan commit veya rollback edilmesini sağlar.
Başlıca faydaları şunlardır:
-
Transaction yönetimini merkezileştirir
-
Repository katmanları arasında tutarlılık sağlar
-
DbContext.SaveChanges() çağrılarının dağınık kullanılmasını engeller
-
Test edilebilirliği artırır
-
Clean Architecture ve DDD yaklaşımlarıyla uyumludur
IUnitOfWork Arayüzü
Öncelikle, uygulama genelinde kullanılacak olan IUnitOfWork arayüzünü tanımlayalım
public interface IUnitOfWork
{
void SaveChanges();
}
Bu arayüz:
-
İşlemlerin veritabanına yansıtılmasını tek bir metod üzerinden yönetmemizi sağlayacak
UnitOfWork Implementasyonu
Ardından, bu arayüzü AppDbContext üzerinden implemente eden sınıfı inceleyelim:
public class UnitOfWork(AppDbContext _context) : IUnitOfWork
{
public void SaveChanges()
{
_context.SaveChanges();
}
}
Bu yapı:
-
SaveChanges() çağrısını tek bir noktada toplar
Bu yaklaşım, servis veya controller katmanında doğrudan DbContext kullanımını engeller.
Repository Pattern ile Birlikte Kullanım
Unit of Work genellikle Repository Pattern ile birlikte kullanılır. Örnek bir senaryoyu ele alalım.
public interface IProductRepository
{
void Add(Product product);
}
public class ProductRepository(AppDbContext context) : IProductRepository
{
public void Add(Product product)
{
context.Products.Add(product);
}
}
Bu noktada repository yalnızca veriyi işaretler, commit işlemi yapmaz. (veri tabanına yansımaz)
Service Katmanında IUnitOfWork Kullanımı
public class ProductService(IProductRepository productRepository, IUnitOfWork unitOfWork)
{
public void CreateProduct(Product product)
{
productRepository.Add(product);
unitOfWork.SaveChanges();
}
}
Bu yapı sayesinde:
-
Birden fazla repository işlemi tek transaction altında toplanabilir
-
İş kuralları servis katmanında kalır
-
Veri erişim detayları soyutlanmış olur
Dependency Injection Konfigürasyonu
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
Scoped yaşam döngüsü sayesinde:
-
Her HTTP request için tek bir DbContext
-
Dolayısıyla tek bir Unit of Work instance’ı kullanılır
Neden Bu Yaklaşımı Tercih Etmeliyiz?
IUnitOfWork kullanımı şu problemlerin önüne geçer:
-
Her repository’de SaveChanges() çağrılması
-
Transaction yönetiminin kontrolsüz olması
-
Test yazımında DbContext bağımlılığı
-
Kod tekrarları
Özellikle büyüyen projelerde, bu pattern uzun vadede ciddi bakım avantajı sağlar.
Gerçek Dünya Örneği
E-Ticaret Senaryosu
Bir sipariş; sipariş + sipariş kalemleri + ödeme birlikte oluşmuyorsa, hiçbiri oluşmamalıdır.
Senaryodaki Tablolar
-
Orders
-
OrderItems
-
Payments
public interface IOrderRepository
{
void Add(Order order);
}
public class OrderRepository(AppDbContext context) : IOrderRepository
{
public void Add(Order order)
{
context.Orders.Add(order);
}
}
public interface IPaymentRepository
{
void Add(Payment payment);
}
public class PaymentRepository(AppDbContext context) : IPaymentRepository
{
public void Add(Payment payment)
{
context.Payments.Add(payment);
}
}
public interface IOrderService
{
void PlaceOrder();
}
public class OrderService(IOrderRepository orderRepository, IPaymentRepository paymentRepository, IUnitOfWork unitOfWork) : IOrderService
{
public void PlaceOrder()
{
var order = new Order //sipariş bilgisi oluşturulur
{
CustomerId = Guid.NewGuid(),
CreatedAt = DateTime.UtcNow
};
order.Items.Add(new OrderItem //sipariş içeriği tutulur
{
ProductId = Guid.NewGuid(),
Quantity = 2,
UnitPrice = 100
});
orderRepository.Add(order);
var payment = new Payment // ödeme bilgileri tutulur
{
OrderId = order.Id,
Amount = 200,
PaidAt = DateTime.UtcNow
};
paymentRepository.Add(payment);
unitOfWork.SaveChanges(); // tüm işlemler tek bir yerden commit edilir
}
}
Böylece üç işleminde yönetimi sağlanır eğer birisinde bir hata olursa hiçbiri kaydolmaz Order kaydoldu OrderItem kaydoldu Payment kaydolmadı gibi bir işlem söz konusu olamaz, veri tutarlılığı sağlanır.
Olasi bir hata durumunda
-
Exception fırlar
-
SaveChanges() çalışmaz
-
Veritabanına tek kayıt bile yazılmaz
Yanlış Kullanım
orderRepository.Add(order);
unitOfWork.SaveChanges();
paymentRepository.Add(payment);
unitOfWork.SaveChanges();
Her bir işaretten sonra unitOfWork.SaveChanges(); cağırılırsa o işlem kendi içinde değerlendirilmiş olur yukarıdaki senaryoda;
Order Eklendi,
Ödeme bilgisi alındı ama veritabanına yansıtırken anlık bir hata oldu. Sipariş bilgisi eklendi ödeme bilgisi eklenemedi ve veri tutarsızlığı olmuş oldu.
Sonuç
.NET Core 10 ile birlikte gelen modern C# özellikleri ve EF Core altyapısı, Unit of Work Pattern’in uygulanmasını hem sade hem de güçlü hale getiriyor. Verdiğimiz IUnitOfWork örneği, küçük projeler için yeterli olduğu gibi, kurumsal mimarilere de kolayca evrilebilir.
Diğer bloglarımda görüşmek üzere 👋