Repository Pattern, veri erişim katmanını soyutlayan ve iş mantığını veri erişiminden ayıran önemli bir tasarım desenidir. GenericRepository yaklaşımı ile ortak CRUD operasyonlarını tekrar kullanılabilir hale getirebilir, kod tekrarını önleyebilir ve daha temiz bir mimari oluşturabilirsiniz.
Repository Pattern Nedir?
Veri Erişim Soyutlaması
Repository Pattern, veri erişim mantığını kapsülleyerek, iş mantığını veri tabanı detaylarından ayıran bir tasarım desenidir.
Neden Repository Pattern?
Katman Ayrımı
İş mantığı ile veri erişimi arasında net bir ayrım sağlar
Test Edilebilirlik
Mock nesnelerle kolay unit test yazılabilir
Sürdürülebilirlik
Veri erişim değişiklikleri merkezi olarak yapılır
Esneklik
Farklı veri kaynaklarına geçiş kolaylaşır
Klasik Repository vs GenericRepository
Klasik Repository
GelenekselHer entity için ayrı repository sınıfı oluşturulur
IProductRepository
ICategoryRepository
ICustomerRepository
// Her entity için ayrı...
Avantajlar
- Entity'ye özel metodlar
- Tip güvenliği
- Açık interface
Dezavantajlar
- Kod tekrarı
- Çok fazla sınıf
- Bakım zorluğu
GenericRepository
ModernTek generic sınıf ile tüm entity'ler için ortak operasyonlar
IGenericRepository<T>
// Tüm entity'ler için tek interface
GenericRepository<Product>
GenericRepository<Category>
Avantajlar
- Kod tekrarı yok
- Hızlı geliştirme
- Tek merkezi kod
Dezavantajlar
- Entity özel metodlar zor
- Over-abstraction riski
- Interface pollution
GenericRepository Implementasyonu
Interface Tanımı
Önce generic repository interface'imizi tanımlayalım
public interface IGenericRepository<T> where T : class
{
// Okuma operasyonları
Task<T?> GetByIdAsync(int id);
Task<T?> GetByIdAsync(Guid id);
Task<IEnumerable<T>> GetAllAsync();
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
Task<T?> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate);
// Sayım operasyonları
Task<int> CountAsync();
Task<int> CountAsync(Expression<Func<T, bool>> predicate);
Task<bool> ExistsAsync(Expression<Func<T, bool>> predicate);
// Yazma operasyonları
Task<T> AddAsync(T entity);
Task<IEnumerable<T>> AddRangeAsync(IEnumerable<T> entities);
void Update(T entity);
void UpdateRange(IEnumerable<T> entities);
void Delete(T entity);
void Delete(int id);
void Delete(Guid id);
void DeleteRange(IEnumerable<T> entities);
// Sayfalama
Task<IEnumerable<T>> GetPagedAsync(int page, int pageSize);
Task<IEnumerable<T>> GetPagedAsync(int page, int pageSize,
Expression<Func<T, bool>> predicate);
}
Entity Framework Implementation
Entity Framework kullanarak generic repository'yi implement edelim
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
protected readonly DbContext _context;
protected readonly DbSet<T> _dbSet;
public GenericRepository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public virtual async Task<T?> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public virtual async Task<T?> GetByIdAsync(Guid id)
{
return await _dbSet.FindAsync(id);
}
public virtual async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public virtual async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.Where(predicate).ToListAsync();
}
public virtual async Task<T?> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.FirstOrDefaultAsync(predicate);
}
public virtual async Task<int> CountAsync()
{
return await _dbSet.CountAsync();
}
public virtual async Task<int> CountAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.CountAsync(predicate);
}
public virtual async Task<bool> ExistsAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.AnyAsync(predicate);
}
public virtual async Task<T> AddAsync(T entity)
{
var result = await _dbSet.AddAsync(entity);
return result.Entity;
}
public virtual async Task<IEnumerable<T>> AddRangeAsync(IEnumerable<T> entities)
{
await _dbSet.AddRangeAsync(entities);
return entities;
}
public virtual void Update(T entity)
{
_dbSet.Update(entity);
}
public virtual void UpdateRange(IEnumerable<T> entities)
{
_dbSet.UpdateRange(entities);
}
public virtual void Delete(T entity)
{
_dbSet.Remove(entity);
}
public virtual void Delete(int id)
{
var entity = _dbSet.Find(id);
if (entity != null)
_dbSet.Remove(entity);
}
public virtual void Delete(Guid id)
{
var entity = _dbSet.Find(id);
if (entity != null)
_dbSet.Remove(entity);
}
public virtual void DeleteRange(IEnumerable<T> entities)
{
_dbSet.RemoveRange(entities);
}
public virtual async Task<IEnumerable<T>> GetPagedAsync(int page, int pageSize)
{
return await _dbSet
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
public virtual async Task<IEnumerable<T>> GetPagedAsync(int page, int pageSize,
Expression<Func<T, bool>> predicate)
{
return await _dbSet
.Where(predicate)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
}
Unit of Work Pattern
Repository'leri koordine etmek için Unit of Work pattern'ı ekleyelim
public interface IUnitOfWork : IDisposable
{
IGenericRepository<T> Repository<T>() where T : class;
Task<int> SaveChangesAsync();
int SaveChanges();
Task BeginTransactionAsync();
Task CommitTransactionAsync();
Task RollbackTransactionAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly DbContext _context;
private readonly Dictionary<Type, object> _repositories;
private IDbContextTransaction? _transaction;
public UnitOfWork(DbContext context)
{
_context = context;
_repositories = new Dictionary<Type, object>();
}
public IGenericRepository<T> Repository<T>() where T : class
{
var type = typeof(T);
if (!_repositories.ContainsKey(type))
{
var repositoryType = typeof(GenericRepository<>).MakeGenericType(type);
var repositoryInstance = Activator.CreateInstance(repositoryType, _context);
_repositories[type] = repositoryInstance!;
}
return (IGenericRepository<T>)_repositories[type];
}
public async Task<int> SaveChangesAsync()
{
return await _context.SaveChangesAsync();
}
public int SaveChanges()
{
return _context.SaveChanges();
}
public async Task BeginTransactionAsync()
{
_transaction = await _context.Database.BeginTransactionAsync();
}
public async Task CommitTransactionAsync()
{
if (_transaction != null)
{
await _transaction.CommitAsync();
await _transaction.DisposeAsync();
_transaction = null;
}
}
public async Task RollbackTransactionAsync()
{
if (_transaction != null)
{
await _transaction.RollbackAsync();
await _transaction.DisposeAsync();
_transaction = null;
}
}
public void Dispose()
{
_transaction?.Dispose();
_context?.Dispose();
}
}
Dependency Injection Konfigürasyonu
var builder = WebApplication.CreateBuilder(args);
// DbContext registration
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Generic Repository registration
builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
// Unit of Work registration
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
// Specific repositories (if needed)
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<ICategoryRepository, CategoryRepository>();
var app = builder.Build();
Kullanım Örnekleri
Controller'da Kullanım
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
private readonly IUnitOfWork _unitOfWork;
public ProductController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
var products = await _unitOfWork.Repository<Product>().GetAllAsync();
return Ok(products);
}
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(int id)
{
var product = await _unitOfWork.Repository<Product>().GetByIdAsync(id);
if (product == null)
return NotFound();
return Ok(product);
}
[HttpGet("search")]
public async Task<ActionResult<IEnumerable<Product>>> SearchProducts(string name)
{
var products = await _unitOfWork.Repository<Product>()
.FindAsync(p => p.Name.Contains(name));
return Ok(products);
}
[HttpPost]
public async Task<ActionResult<Product>> CreateProduct(Product product)
{
await _unitOfWork.Repository<Product>().AddAsync(product);
await _unitOfWork.SaveChangesAsync();
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProduct(int id, Product product)
{
if (id != product.Id)
return BadRequest();
var existingProduct = await _unitOfWork.Repository<Product>().GetByIdAsync(id);
if (existingProduct == null)
return NotFound();
_unitOfWork.Repository<Product>().Update(product);
await _unitOfWork.SaveChangesAsync();
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
var product = await _unitOfWork.Repository<Product>().GetByIdAsync(id);
if (product == null)
return NotFound();
_unitOfWork.Repository<Product>().Delete(product);
await _unitOfWork.SaveChangesAsync();
return NoContent();
}
}
Service Layer'da Kullanım
public interface IProductService
{
Task<ProductDto> CreateProductAsync(CreateProductRequest request);
Task<ProductDto?> GetProductByIdAsync(int id);
Task<IEnumerable<ProductDto>> GetActiveProductsAsync();
Task<bool> UpdateProductAsync(int id, UpdateProductRequest request);
Task<bool> DeleteProductAsync(int id);
}
public class ProductService : IProductService
{
private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
private readonly ILogger<ProductService> _logger;
public ProductService(
IUnitOfWork unitOfWork,
IMapper mapper,
ILogger<ProductService> logger)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_logger = logger;
}
public async Task<ProductDto> CreateProductAsync(CreateProductRequest request)
{
_logger.LogInformation("Creating product: {Name}", request.Name);
// Business logic validation
var existingProduct = await _unitOfWork.Repository<Product>()
.FirstOrDefaultAsync(p => p.Name == request.Name);
if (existingProduct != null)
throw new BusinessException("Product with this name already exists");
// Create entity
var product = _mapper.Map<Product>(request);
product.CreatedAt = DateTime.UtcNow;
product.IsActive = true;
// Save to database
await _unitOfWork.Repository<Product>().AddAsync(product);
await _unitOfWork.SaveChangesAsync();
_logger.LogInformation("Product created successfully: {Id}", product.Id);
return _mapper.Map<ProductDto>(product);
}
public async Task<ProductDto?> GetProductByIdAsync(int id)
{
var product = await _unitOfWork.Repository<Product>().GetByIdAsync(id);
return product != null ? _mapper.Map<ProductDto>(product) : null;
}
public async Task<IEnumerable<ProductDto>> GetActiveProductsAsync()
{
var products = await _unitOfWork.Repository<Product>()
.FindAsync(p => p.IsActive);
return _mapper.Map<IEnumerable<ProductDto>>(products);
}
public async Task<bool> UpdateProductAsync(int id, UpdateProductRequest request)
{
var product = await _unitOfWork.Repository<Product>().GetByIdAsync(id);
if (product == null)
return false;
_mapper.Map(request, product);
product.UpdatedAt = DateTime.UtcNow;
_unitOfWork.Repository<Product>().Update(product);
await _unitOfWork.SaveChangesAsync();
return true;
}
public async Task<bool> DeleteProductAsync(int id)
{
var product = await _unitOfWork.Repository<Product>().GetByIdAsync(id);
if (product == null)
return false;
_unitOfWork.Repository<Product>().Delete(product);
await _unitOfWork.SaveChangesAsync();
return true;
}
}
Gelişmiş Özellikler
Include Support
Related entity'leri yüklemek için Include desteği ekleyelim
public interface IGenericRepository<T> where T : class
{
// Existing methods...
Task<T?> GetByIdAsync(int id, params Expression<Func<T, object>>[] includes);
Task<IEnumerable<T>> GetAllAsync(params Expression<Func<T, object>>[] includes);
Task<IEnumerable<T>> FindAsync(
Expression<Func<T, bool>> predicate,
params Expression<Func<T, object>>[] includes);
}
// Implementation
public virtual async Task<T?> GetByIdAsync(int id, params Expression<Func<T, object>>[] includes)
{
IQueryable<T> query = _dbSet;
foreach (var include in includes)
{
query = query.Include(include);
}
return await query.FirstOrDefaultAsync(GetIdPredicate(id));
}
// Usage
var product = await _unitOfWork.Repository<Product>()
.GetByIdAsync(1, p => p.Category, p => p.Reviews);
Ordering Support
Sıralama desteği ekleyerek daha esnek sorgular yapabiliriz
public interface IGenericRepository<T> where T : class
{
Task<IEnumerable<T>> GetAllOrderedAsync<TKey>(
Expression<Func<T, TKey>> orderBy,
bool ascending = true);
Task<IEnumerable<T>> FindOrderedAsync<TKey>(
Expression<Func<T, bool>> predicate,
Expression<Func<T, TKey>> orderBy,
bool ascending = true);
}
// Implementation
public virtual async Task<IEnumerable<T>> GetAllOrderedAsync<TKey>(
Expression<Func<T, TKey>> orderBy,
bool ascending = true)
{
var query = ascending
? _dbSet.OrderBy(orderBy)
: _dbSet.OrderByDescending(orderBy);
return await query.ToListAsync();
}
// Usage
var products = await _unitOfWork.Repository<Product>()
.GetAllOrderedAsync(p => p.Name, ascending: true);
Specification Pattern
Karmaşık sorgu mantığını kapsüllemek için Specification pattern'ı entegre edelim
public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
Expression<Func<T, object>>? OrderBy { get; }
Expression<Func<T, object>>? OrderByDescending { get; }
int Take { get; }
int Skip { get; }
bool IsPagingEnabled { get; }
}
public class ProductSpecification : BaseSpecification<Product>
{
public ProductSpecification(string name, bool? isActive = null)
{
if (!string.IsNullOrEmpty(name))
Criteria = p => p.Name.Contains(name);
if (isActive.HasValue)
Criteria = p => p.IsActive == isActive.Value;
AddInclude(p => p.Category);
AddOrderBy(p => p.Name);
}
}
// Repository'de kullanım
public interface IGenericRepository<T> where T : class
{
Task<IEnumerable<T>> FindWithSpecAsync(ISpecification<T> spec);
Task<T?> FirstOrDefaultWithSpecAsync(ISpecification<T> spec);
Task<int> CountWithSpecAsync(ISpecification<T> spec);
}
// Usage
var spec = new ProductSpecification("laptop", isActive: true);
var products = await _unitOfWork.Repository<Product>()
.FindWithSpecAsync(spec);
Unit Testing
Repository Test Örneği
public class GenericRepositoryTests : IDisposable
{
private readonly DbContextOptions<TestDbContext> _options;
private readonly TestDbContext _context;
private readonly GenericRepository<Product> _repository;
public GenericRepositoryTests()
{
_options = new DbContextOptionsBuilder<TestDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_context = new TestDbContext(_options);
_repository = new GenericRepository<Product>(_context);
}
[Fact]
public async Task AddAsync_ShouldAddEntity_WhenEntityIsValid()
{
// Arrange
var product = new Product
{
Name = "Test Product",
Price = 100,
IsActive = true
};
// Act
var result = await _repository.AddAsync(product);
await _context.SaveChangesAsync();
// Assert
result.Should().NotBeNull();
result.Id.Should().BeGreaterThan(0);
var savedProduct = await _repository.GetByIdAsync(result.Id);
savedProduct.Should().NotBeNull();
savedProduct!.Name.Should().Be("Test Product");
}
[Fact]
public async Task GetByIdAsync_ShouldReturnEntity_WhenEntityExists()
{
// Arrange
var product = new Product
{
Name = "Test Product",
Price = 100,
IsActive = true
};
await _repository.AddAsync(product);
await _context.SaveChangesAsync();
// Act
var result = await _repository.GetByIdAsync(product.Id);
// Assert
result.Should().NotBeNull();
result!.Name.Should().Be("Test Product");
}
[Fact]
public async Task FindAsync_ShouldReturnMatchingEntities_WhenPredicateMatches()
{
// Arrange
var products = new[]
{
new Product { Name = "Active Product", IsActive = true },
new Product { Name = "Inactive Product", IsActive = false },
new Product { Name = "Another Active", IsActive = true }
};
await _repository.AddRangeAsync(products);
await _context.SaveChangesAsync();
// Act
var result = await _repository.FindAsync(p => p.IsActive);
// Assert
result.Should().HaveCount(2);
result.All(p => p.IsActive).Should().BeTrue();
}
[Fact]
public async Task CountAsync_ShouldReturnCorrectCount_WhenEntitiesExist()
{
// Arrange
var products = new[]
{
new Product { Name = "Product 1", IsActive = true },
new Product { Name = "Product 2", IsActive = false },
new Product { Name = "Product 3", IsActive = true }
};
await _repository.AddRangeAsync(products);
await _context.SaveChangesAsync();
// Act
var totalCount = await _repository.CountAsync();
var activeCount = await _repository.CountAsync(p => p.IsActive);
// Assert
totalCount.Should().Be(3);
activeCount.Should().Be(2);
}
public void Dispose()
{
_context.Dispose();
}
}
Service Test ile Mock Kullanımı
public class ProductServiceTests
{
private readonly Mock<IUnitOfWork> _mockUnitOfWork;
private readonly Mock<IGenericRepository<Product>> _mockProductRepository;
private readonly Mock<IMapper> _mockMapper;
private readonly Mock<ILogger<ProductService>> _mockLogger;
private readonly ProductService _productService;
public ProductServiceTests()
{
_mockUnitOfWork = new Mock<IUnitOfWork>();
_mockProductRepository = new Mock<IGenericRepository<Product>>();
_mockMapper = new Mock<IMapper>();
_mockLogger = new Mock<ILogger<ProductService>>();
_mockUnitOfWork.Setup(u => u.Repository<Product>())
.Returns(_mockProductRepository.Object);
_productService = new ProductService(
_mockUnitOfWork.Object,
_mockMapper.Object,
_mockLogger.Object);
}
[Fact]
public async Task CreateProductAsync_ShouldCreateProduct_WhenRequestIsValid()
{
// Arrange
var request = new CreateProductRequest { Name = "Test Product", Price = 100 };
var product = new Product { Id = 1, Name = "Test Product", Price = 100 };
var productDto = new ProductDto { Id = 1, Name = "Test Product", Price = 100 };
_mockProductRepository.Setup(r => r.FirstOrDefaultAsync(It.IsAny<Expression<Func<Product, bool>>>()))
.ReturnsAsync((Product?)null);
_mockMapper.Setup(m => m.Map<Product>(request))
.Returns(product);
_mockProductRepository.Setup(r => r.AddAsync(It.IsAny<Product>()))
.ReturnsAsync(product);
_mockMapper.Setup(m => m.Map<ProductDto>(product))
.Returns(productDto);
// Act
var result = await _productService.CreateProductAsync(request);
// Assert
result.Should().NotBeNull();
result.Name.Should().Be("Test Product");
_mockProductRepository.Verify(r => r.AddAsync(It.IsAny<Product>()), Times.Once);
_mockUnitOfWork.Verify(u => u.SaveChangesAsync(), Times.Once);
}
[Fact]
public async Task CreateProductAsync_ShouldThrowException_WhenProductNameExists()
{
// Arrange
var request = new CreateProductRequest { Name = "Existing Product", Price = 100 };
var existingProduct = new Product { Id = 1, Name = "Existing Product" };
_mockProductRepository.Setup(r => r.FirstOrDefaultAsync(It.IsAny<Expression<Func<Product, bool>>>()))
.ReturnsAsync(existingProduct);
// Act & Assert
var action = async () => await _productService.CreateProductAsync(request);
await action.Should().ThrowAsync<BusinessException>()
.WithMessage("Product with this name already exists");
_mockProductRepository.Verify(r => r.AddAsync(It.IsAny<Product>()), Times.Never);
_mockUnitOfWork.Verify(u => u.SaveChangesAsync(), Times.Never);
}
}
Avantajlar vs Dezavantajlar
Avantajlar
Kod Tekrarını Önler
CRUD operasyonları tek yerde tanımlanır
Hızlı Geliştirme
Yeni entity'ler için hemen kullanılabilir
Tutarlı API
Tüm repository'ler aynı interface'i kullanır
Test Edilebilirlik
Mock nesneler kolayca oluşturulabilir
Merkezi Bakım
Değişiklikler tek yerden yapılır
Dezavantajlar
Over-Abstraction
Gereksiz soyutlama katmanı oluşturabilir
Entity Özel Metodlar
Özel business logic implementasyonu zor
Interface Pollution
Kullanılmayan metodlar interface'i kirletir
Performance Sorunları
Generic yapı bazen optimize olmayabilir
EF Core Abstraction
EF Core zaten repository pattern implement eder
En İyi Pratikler
Yapılması Gerekenler
Yapılmaması Gerekenler
Alternatif Yaklaşımlar
MediatR + CQRS
ModernRepository pattern yerine MediatR ile Command/Query ayrımı
public class GetProductQuery : IRequest<ProductDto>
public class CreateProductCommand : IRequest<int>
// Handler'lar direkt DbContext kullanır
Direct EF Core
BasitRepository olmadan doğrudan EF Core kullanımı
// Service'te direkt DbContext
var products = await _context.Products
.Where(p => p.IsActive)
.ToListAsync();
Specification Only
OdaklanmışSadece Specification pattern ile query abstraction
var spec = new ActiveProductsSpec();
var products = await _context.Products
.Where(spec.Criteria)
.ToListAsync();
Sonuç
GenericRepository Pattern, orta ölçekli projeler için kod tekrarını önleyen ve geliştirme hızını artıran faydalı bir yaklaşımdır. Ancak her projeye uygun olmayabilir. Proje gereksinimlerinizi, ekip deneyimini ve karmaşıklık seviyesini değerlendirerek karar vermeniz önemlidir.
Karar Verme Rehberi
GenericRepository Kullanın
- Orta ölçekli projelerde
- Hızlı prototype geliştirmede
- Çok entity'li CRUD uygulamalarda
- Team standardizasyonu gerektiğinde
GenericRepository Kullanmayın
- Karmaşık business logic'li projelerde
- Performance kritik uygulamalarda
- Çok özel sorgu gereksinimlerinde
- Microservice mimarilerinde