If you inject the DbContext directly into your Controllers to execute LINQ queries, your application has tightly coupled its Web API HTTP logic directly to the Database engine. To achieve Clean Architecture, we decouple these concerns using the Repository Pattern.
A Repository provides an abstraction over database access. Instead of writing EF Core LINQ inside a controller, you call a method like _repo.GetActiveUsers(). It offers two massive advantages:
// 1. The Interface (The Contract)
public interface IUserRepository
{
Task<User> GetByIdAsync(int id);
Task AddAsync(User user);
Task SaveChangesAsync();
}
// 2. The Implementation (EF Core Specific)
public class UserRepository : IUserRepository
{
// The DbContext is injected into the Repo, NOT the Controller
private readonly ApplicationDbContext _context;
public UserRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<User> GetByIdAsync(int id)
{
return await _context.Users.FindAsync(id);
}
public async Task AddAsync(User user)
{
await _context.Users.AddAsync(user);
}
public async Task SaveChangesAsync()
{
await _context.SaveChangesAsync();
}
}
What if your API request needs to create a User, add a Billing Record, and send an Audit Log? If you use three different repositories (UserRepository, BillingRepository, AuditRepository) and call SaveChanges() on each of them sequentially, you violate transaction safety. If the Audit fails, the User was already saved!
The Unit of Work Pattern centralizes the SaveChanges() call to a single parent class containing all the repositories. All DB changes happen in one massive transaction.
public interface IUnitOfWork
{
IUserRepository Users { get; }
IBillingRepository Billing { get; }
Task<bool> CompleteAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationDbContext _context;
public IUserRepository Users { get; private set; }
public IBillingRepository Billing { get; private set; }
public UnitOfWork(ApplicationDbContext context)
{
_context = context;
Users = new UserRepository(_context);
Billing = new BillingRepository(_context);
}
// Call this exactly once at the end of the Controller Action
public async Task<bool> CompleteAsync()
{
return await _context.SaveChangesAsync() > 0;
}
}
Q: "Some senior .NET architects argue that the Repository Pattern is useless when using EF Core. Why do they say that, and when is it actually useful?"
Architect Answer: "They argue that EF Core's `DbSet` IS already a Repository, and `DbContext` IS already a Unit of Work. Creating a wrapper around them is technically 'wrapping a wrapper', resulting in redundant boilerplate. However, the Repository Pattern becomes highly beneficial in Enterprise environments where we use complex Domain-Driven Design (DDD). We use reporitories to enforce aggregate roots—ensuring developers cannot bypass domain rules (like accidentally adding an OrderItem directly to the database without updating the parent Order's TotalPrice). We don't wrap EF Core just for the sake of it; we wrap it to hide `IQueryable` and enforce strict domain business logic."