Repository Pattern
Repository Pattern
Section titled โRepository PatternโThe Repository pattern provides an abstraction layer between the business logic and the data access layer. Business code works against an interface; the implementation deals with SQL, ORMs, or external APIs.
Why Repository
Section titled โWhy RepositoryโWithout Repository, business logic is coupled to the data access technology:
// Tightly coupled โ hard to test, hard to change ORMpublic class OrderService{ private readonly AppDbContext _db;
public async Task<Order> GetOrderAsync(int id) { return await _db.Orders .Include(o => o.Items) .FirstOrDefaultAsync(o => o.Id == id); }}With Repository, the business layer knows nothing about Entity Framework:
public class OrderService{ private readonly IOrderRepository _orders;
public async Task<Order?> GetOrderAsync(int id) => await _orders.GetByIdAsync(id);}C# Implementation
Section titled โC# Implementationโ// Generic repository interfacepublic interface IRepository<T> where T : class{ Task<T?> GetByIdAsync(int id); Task<IEnumerable<T>> GetAllAsync(); Task AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entity);}
// Specific repository interface (extends generic with domain-specific queries)public interface IOrderRepository : IRepository<Order>{ Task<IEnumerable<Order>> GetByCustomerAsync(int customerId); Task<IEnumerable<Order>> GetPendingOrdersAsync(); Task<Order?> GetWithItemsAsync(int orderId);}
// EF Core implementationpublic class OrderRepository : IOrderRepository{ private readonly AppDbContext _db;
public OrderRepository(AppDbContext db) => _db = db;
public async Task<Order?> GetByIdAsync(int id) => await _db.Orders.FindAsync(id);
public async Task<IEnumerable<Order>> GetAllAsync() => await _db.Orders.ToListAsync();
public async Task AddAsync(Order order) { await _db.Orders.AddAsync(order); await _db.SaveChangesAsync(); }
public async Task UpdateAsync(Order order) { _db.Orders.Update(order); await _db.SaveChangesAsync(); }
public async Task DeleteAsync(Order order) { _db.Orders.Remove(order); await _db.SaveChangesAsync(); }
public async Task<IEnumerable<Order>> GetByCustomerAsync(int customerId) => await _db.Orders .Where(o => o.CustomerId == customerId) .OrderByDescending(o => o.CreatedAt) .ToListAsync();
public async Task<IEnumerable<Order>> GetPendingOrdersAsync() => await _db.Orders .Where(o => o.Status == OrderStatus.Pending) .ToListAsync();
public async Task<Order?> GetWithItemsAsync(int orderId) => await _db.Orders .Include(o => o.Items) .ThenInclude(i => i.Product) .FirstOrDefaultAsync(o => o.Id == orderId);}
// Registerservices.AddScoped<IOrderRepository, OrderRepository>();Unit of Work Pattern (Companion)
Section titled โUnit of Work Pattern (Companion)โWhen multiple repositories must participate in a single transaction:
public interface IUnitOfWork : IDisposable{ IOrderRepository Orders { get; } IProductRepository Products { get; } ICustomerRepository Customers { get; } Task<int> CommitAsync();}
public class UnitOfWork : IUnitOfWork{ private readonly AppDbContext _db;
public UnitOfWork(AppDbContext db) { _db = db; Orders = new OrderRepository(db); Products = new ProductRepository(db); Customers = new CustomerRepository(db); }
public IOrderRepository Orders { get; } public IProductRepository Products { get; } public ICustomerRepository Customers { get; } public Task<int> CommitAsync() => _db.SaveChangesAsync(); public void Dispose() => _db.Dispose();}
// Usage โ single transactionpublic async Task PlaceOrderAsync(CreateOrderRequest request){ using var uow = _unitOfWorkFactory.Create();
var product = await uow.Products.GetByIdAsync(request.ProductId); product.ReduceStock(request.Quantity);
var order = new Order(request.CustomerId, product, request.Quantity); await uow.Orders.AddAsync(order);
await uow.CommitAsync(); // one transaction for both changes}Testability
Section titled โTestabilityโThe key benefit โ swap in an in-memory fake for tests:
public class FakeOrderRepository : IOrderRepository{ private readonly List<Order> _orders = new();
public Task<Order?> GetByIdAsync(int id) => Task.FromResult(_orders.FirstOrDefault(o => o.Id == id));
public Task AddAsync(Order order) { _orders.Add(order); return Task.CompletedTask; }
public Task<IEnumerable<Order>> GetByCustomerAsync(int customerId) => Task.FromResult(_orders.Where(o => o.CustomerId == customerId));
// ... other methods}
// In tests โ no database requiredvar repo = new FakeOrderRepository();var service = new OrderService(repo);TypeScript / Prisma Example
Section titled โTypeScript / Prisma Exampleโ// Interfaceinterface UserRepository { findById(id: string): Promise<User | null>; findByEmail(email: string): Promise<User | null>; save(user: User): Promise<User>; delete(id: string): Promise<void>;}
// Prisma implementationclass PrismaUserRepository implements UserRepository { constructor(private prisma: PrismaClient) {}
findById(id: string) { return this.prisma.user.findUnique({ where: { id } }); } findByEmail(email: string) { return this.prisma.user.findUnique({ where: { email } }); } save(user: User) { return this.prisma.user.upsert({ where: { id: user.id }, update: user, create: user }); } delete(id: string) { return this.prisma.user.delete({ where: { id } }).then(() => {}); }}