Ana içeriğe geç

Repository Pattern

Repository deseni, veri erişim mantığını soyutlayarak iş mantığını veritabanı detaylarından ayırır; yanlış uygulamalar ise gereksiz soyutlama katmanlarına ve performans sorunlarına yol açar.


1. Generic Repository Anti-Pattern

Yanlış Kullanım: Her entity için aynı CRUD metodlarını sunan generic repository.

public interface IRepository<T>
{
    Task<T> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(T entity);
}

// Her entity aynı interface'i kullanır, özel sorgular yapılamaz
var orders = await _orderRepository.GetAllAsync(); // Tüm siparişleri çeker!

İdeal Kullanım: Entity’ye özel repository interface’leri tanımlayın.

public interface IOrderRepository
{
    Task<Order> GetByIdAsync(int id);
    Task<IEnumerable<Order>> GetPendingOrdersAsync();
    Task<IEnumerable<Order>> GetByCustomerAsync(int customerId);
    Task AddAsync(Order order);
}

public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;

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

    public async Task<IEnumerable<Order>> GetPendingOrdersAsync()
    {
        return await _context.Orders
            .Where(o => o.Status == OrderStatus.Pending)
            .ToListAsync();
    }
}

2. DbContext’i Sızdırmak

Yanlış Kullanım: Repository’den IQueryable döndürerek DbContext’i dışarı sızdırmak.

public interface IProductRepository
{
    IQueryable<Product> GetAll(); // Çağıran taraf istedigi sorguyu yazabilir
}

// Controller'da
var products = await _repo.GetAll()
    .Include(p => p.Category)
    .Where(p => p.Price > 100)
    .ToListAsync(); // Repository soyutlaması anlamsızlaşıyor

İdeal Kullanım: Repository’den somut sonuçlar döndürün.

public interface IProductRepository
{
    Task<IEnumerable<Product>> GetExpensiveProductsAsync(decimal minPrice);
    Task<Product?> GetWithCategoryAsync(int id);
}

public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _context;

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

    public async Task<Product?> GetWithCategoryAsync(int id)
    {
        return await _context.Products
            .Include(p => p.Category)
            .FirstOrDefaultAsync(p => p.Id == id);
    }
}

3. Repository İçinde İş Mantığı

Yanlış Kullanım: İş mantığını repository katmanına koymak.

public class OrderRepository
{
    public async Task CreateOrderAsync(Order order)
    {
        if (order.Items.Count == 0)
            throw new BusinessException("Sipariş boş olamaz");

        order.TotalPrice = order.Items.Sum(i => i.Price * i.Quantity);
        order.Status = OrderStatus.Pending;

        await _context.Orders.AddAsync(order);
        await _context.SaveChangesAsync();
    }
}

İdeal Kullanım: İş mantığını servis katmanında tutun, repository sadece veri erişimi yapsın.

public class OrderService
{
    private readonly IOrderRepository _repository;

    public async Task CreateOrderAsync(Order order)
    {
        if (order.Items.Count == 0)
            throw new BusinessException("Sipariş boş olamaz");

        order.CalculateTotal();
        order.SetStatus(OrderStatus.Pending);

        await _repository.AddAsync(order);
    }
}

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

4. Her Repository İçin Ayrı SaveChanges

Yanlış Kullanım: Her repository kendi SaveChangesAsync çağrısını yapmak.

public async Task PlaceOrderAsync(Order order, Payment payment)
{
    await _orderRepository.AddAsync(order);     // SaveChangesAsync çağrılır
    await _paymentRepository.AddAsync(payment);  // Ayrı SaveChangesAsync çağrılır
    // İlk başarılı ikinci başarısız olursa tutarsızlık oluşur
}

İdeal Kullanım: Unit of Work ile tek bir transaction kullanın.

public interface IUnitOfWork
{
    IOrderRepository Orders { get; }
    IPaymentRepository Payments { get; }
    Task<int> SaveChangesAsync();
}

public async Task PlaceOrderAsync(Order order, Payment payment)
{
    _unitOfWork.Orders.Add(order);
    _unitOfWork.Payments.Add(payment);
    await _unitOfWork.SaveChangesAsync(); // Tek transaction
}

5. Specification Pattern Kullanmamak

Yanlış Kullanım: Filtreleme kombinasyonları için çok sayıda repository metodu yazmak.

public interface IProductRepository
{
    Task<IEnumerable<Product>> GetByCategoryAsync(int categoryId);
    Task<IEnumerable<Product>> GetByPriceRangeAsync(decimal min, decimal max);
    Task<IEnumerable<Product>> GetByCategoryAndPriceAsync(int categoryId, decimal min, decimal max);
    Task<IEnumerable<Product>> GetActiveByCategoryAsync(int categoryId);
    // Kombinasyonlar çoğaldıkça metod sayısı patlar
}

İdeal Kullanım: Specification Pattern ile esnek filtreleme yapın.

public abstract class Specification<T>
{
    public abstract Expression<Func<T, bool>> ToExpression();
}

public class ActiveProductSpec : Specification<Product>
{
    public override Expression<Func<Product, bool>> ToExpression()
        => p => p.IsActive;
}

public class PriceRangeSpec : Specification<Product>
{
    private readonly decimal _min, _max;
    public PriceRangeSpec(decimal min, decimal max) { _min = min; _max = max; }

    public override Expression<Func<Product, bool>> ToExpression()
        => p => p.Price >= _min && p.Price <= _max;
}

public interface IProductRepository
{
    Task<IEnumerable<Product>> GetAsync(Specification<Product> spec);
}