Ana içeriğe geç

Unit of Work Pattern

Unit of Work deseni, birden fazla repository işlemini tek bir transaction altında yöneterek veri tutarlılığını sağlar; yanlış kullanımlar veri bütünlüğü sorunlarına neden olur.


1. Her Repository’de Ayrı SaveChanges

Yanlış Kullanım: Her repository kendi kaydetme işlemini yapmak.

public class OrderRepository
{
    public async Task AddAsync(Order order)
    {
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
    }
}

public class InventoryRepository
{
    public async Task UpdateStockAsync(int productId, int quantity)
    {
        var product = await _context.Products.FindAsync(productId);
        product.Stock -= quantity;
        await _context.SaveChangesAsync();
    }
}

// Sipariş eklenir ama stok güncellemesi başarısız olursa tutarsızlık oluşur

İdeal Kullanım: Unit of Work ile tüm işlemleri tek transaction’da yönetin.

public interface IUnitOfWork : IDisposable
{
    IOrderRepository Orders { get; }
    IInventoryRepository Inventory { get; }
    Task<int> SaveChangesAsync(CancellationToken ct = default);
}

public class UnitOfWork : IUnitOfWork
{
    private readonly AppDbContext _context;

    public UnitOfWork(AppDbContext context)
    {
        _context = context;
        Orders = new OrderRepository(context);
        Inventory = new InventoryRepository(context);
    }

    public IOrderRepository Orders { get; }
    public IInventoryRepository Inventory { get; }

    public Task<int> SaveChangesAsync(CancellationToken ct) => _context.SaveChangesAsync(ct);
    public void Dispose() => _context.Dispose();
}

2. DbContext’in Zaten Unit of Work Olduğunu Görmezden Gelmek

Yanlış Kullanım: EF Core üzerine gereksiz Unit of Work soyutlaması yazmak.

public class UnitOfWork : IUnitOfWork
{
    private readonly AppDbContext _context;
    private IOrderRepository _orders;
    private IProductRepository _products;
    private ICustomerRepository _customers;
    // Her yeni entity için property eklenmeli

    public IOrderRepository Orders => _orders ??= new OrderRepository(_context);
    public IProductRepository Products => _products ??= new ProductRepository(_context);
    // ...
}

İdeal Kullanım: Basit senaryolarda DbContext’i doğrudan Unit of Work olarak kullanın.

public class OrderService
{
    private readonly AppDbContext _context;

    public OrderService(AppDbContext context) => _context = context;

    public async Task PlaceOrderAsync(Order order)
    {
        _context.Orders.Add(order);

        var product = await _context.Products.FindAsync(order.ProductId);
        product.Stock -= order.Quantity;

        await _context.SaveChangesAsync(); // Tek transaction, EF Core bunu zaten yönetir
    }
}

3. Transaction Scope’unu Yanlış Yönetmek

Yanlış Kullanım: Transaction scope’unu çok geniş tutmak.

public async Task ProcessAsync()
{
    using var transaction = await _context.Database.BeginTransactionAsync();

    var orders = await _context.Orders.ToListAsync();
    foreach (var order in orders)
    {
        await _emailService.SendAsync(order.Email); // Harici servis çağrısı transaction içinde
        order.IsNotified = true;
    }

    await _context.SaveChangesAsync();
    await transaction.CommitAsync(); // E-posta servisi yavaşlarsa transaction uzar
}

İdeal Kullanım: Transaction scope’unu minimum tutun, harici çağrıları dışarıda bırakın.

public async Task ProcessAsync()
{
    var orders = await _context.Orders
        .Where(o => !o.IsNotified)
        .ToListAsync();

    foreach (var order in orders)
    {
        order.IsNotified = true;
    }
    await _context.SaveChangesAsync(); // Sadece DB işlemi transaction içinde

    foreach (var order in orders)
    {
        await _emailService.SendAsync(order.Email); // Transaction dışında
    }
}

4. Concurrent SaveChanges Çağrıları

Yanlış Kullanım: Aynı DbContext üzerinde paralel SaveChanges çağrısı yapmak.

public async Task UpdateAllAsync(List<Order> orders)
{
    var tasks = orders.Select(async order =>
    {
        order.Status = OrderStatus.Processed;
        await _context.SaveChangesAsync(); // DbContext thread-safe değil!
    });

    await Task.WhenAll(tasks);
}

İdeal Kullanım: Tüm değişiklikleri toplu yapıp tek SaveChanges çağırın.

public async Task UpdateAllAsync(List<Order> orders)
{
    foreach (var order in orders)
    {
        order.Status = OrderStatus.Processed;
    }

    await _context.SaveChangesAsync(); // Tek çağrı, tüm değişiklikler
}

5. Unit of Work ile Concurrency Kontrolü

Yanlış Kullanım: Concurrency conflict’leri görmezden gelmek.

public async Task UpdatePriceAsync(int productId, decimal newPrice)
{
    var product = await _context.Products.FindAsync(productId);
    product.Price = newPrice;
    await _context.SaveChangesAsync(); // Başka biri aynı anda güncellerse veri kaybı
}

İdeal Kullanım: Optimistic concurrency ile çakışmaları yönetin.

public class Product
{
    public int Id { get; set; }
    public decimal Price { get; set; }

    [Timestamp]
    public byte[] RowVersion { get; set; }
}

public async Task UpdatePriceAsync(int productId, decimal newPrice)
{
    var product = await _context.Products.FindAsync(productId);
    product.Price = newPrice;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        // Çakışma durumunda yeniden oku ve kullanıcıyı bilgilendir
        throw new ConflictException("Ürün başka bir kullanıcı tarafından güncellenmiş.");
    }
}