Skip to content

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.

Without Repository, business logic is coupled to the data access technology:

// Tightly coupled โ€” hard to test, hard to change ORM
public 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);
}
// Generic repository interface
public 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 implementation
public 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);
}
// Register
services.AddScoped<IOrderRepository, OrderRepository>();

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 transaction
public 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
}

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 required
var repo = new FakeOrderRepository();
var service = new OrderService(repo);
// Interface
interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<User>;
delete(id: string): Promise<void>;
}
// Prisma implementation
class 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(() => {}); }
}