Entity Framework Core - Repository Pattern
The Repository Pattern serves as an abstraction layer between the business logic (Service Layer) and the data access logic (Data Access Layer). It hides the details of how data is fetched or saved, allowing the business code to focus on logic rather than database implementation details.
Core Objectives
Decoupling: Remove dependency on the specific ORM (EF Core) from the business logic.
Testability: Allow business logic to be unit tested by "mocking" the database.
Centralization: Keep complex database queries in one place (DRY Principle).
1. Standard Implementation (Strict Approach)
In the strict implementation, the Repository returns fully materialized data (IEnumerable or List). The Service layer does not know that SQL or EF Core exists.
Step 1: The Generic Interface
Defines the contract for data access operations.
public interface IRepository<T> where T : class
{
Task<IEnumerable<T>> GetAllAsync();
Task<T?> GetByIdAsync(int id);
void Add(T entity);
void Remove(T entity);
// Note: No SaveChanges() here. That is handled by Unit of Work.
}
Step 2: The Generic Implementation
Wraps the EF Core DbSet.
public class Repository<T> : IRepository<T> where T : class
{
protected readonly DbContext _context;
protected readonly DbSet<T> _dbSet;
public Repository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public async Task<T?> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public void Add(T entity) => _dbSet.Add(entity);
public void Remove(T entity) => _dbSet.Remove(entity);
}
2. The "IQueryable" Debate (Flexible vs. Leaky)
A common modification is returning IQueryable<T> instead of IEnumerable<T>. This allows the Service layer to add more LINQ filters.
The Approach
// Repository method returns a query builder, not data
public IQueryable<T> GetQueryable()
{
return _dbSet.AsQueryable();
}
Trade-off Analysis
| Aspect | Returning IEnumerable (Strict) | Returning IQueryable (Flexible) |
| Philosophy | Clean Architecture. Database logic stays in the Repo. | Pragmatic. Blends Business and Data logic. |
| Performance | Risk. Must implement custom methods for every filter to avoid fetching all data into memory. | Efficient. SQL is generated at the last moment, fetching only what is needed. |
| Abstraction | Strong. Service layer doesn't know about EF. | Leaky. Service layer can write code that fails SQL translation. |
| Testing | Easy. Can return a simple List in tests. | Difficult. Mocking Async IQueryable is complex. |
Verdict: Use IQueryable for rapid prototyping or CRUD apps. Avoid it for complex Enterprise applications where strict testing is required.
4. The Solution: The Specification Pattern
To solve the problem of "I need flexibility but I don't want to expose IQueryable," we use the Specification Pattern. This encapsulates query logic into objects.
Concept
Instead of writing .Where(x => x.IsActive) in the Service, you pass a "Specification" object to the Repository.
Implementation Snippet
1. The Specification Interface
public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
}
2. A Concrete Specification (e.g., Active Users)
public class ActiveUsersSpecification : ISpecification<User>
{
public Expression<Func<User, bool>> Criteria => u => u.IsActive;
public List<Expression<Func<User, object>>> Includes => new() { u => u.Profile };
}
3. The Repository Method
public IEnumerable<T> Find(ISpecification<T> spec)
{
// Apply criteria and includes dynamically
var query = _dbSet.Where(spec.Criteria);
foreach (var include in spec.Includes)
{
query = query.Include(include);
}
return query.ToList();
}
5. The Unit of Work (Handling Transactions)
The Repository handles collections, but the Unit of Work handles the transaction (saving changes). This ensures that if you modify a User and an Order, they are saved together or not at all.
public interface IUnitOfWork : IDisposable
{
IRepository<User> Users { get; }
IRepository<Order> Orders { get; }
Task<int> CompleteAsync(); // Calls _context.SaveChanges()
}
Usage in Service:
public async Task RegisterUserAndOrder(User user, Order order)
{
_unitOfWork.Users.Add(user);
_unitOfWork.Orders.Add(order);
// Both are saved here in a single transaction
await _unitOfWork.CompleteAsync();
}
6. Decision Matrix: Which approach should you use?
| Scenario | Recommendation | Why? |
| Simple CRUD App / MVP | No Repository (Use DbContext directly) | EF Core is already a repository. Adding layers adds unnecessary complexity. |
| Mid-Size App | Generic Repository returning IQueryable | Balances development speed with a slight separation of concerns. |
| Enterprise / DDD | Strict Repository + Specification Pattern | Maximum testability, clear boundaries, and database agnostic. |
Final Summary
Repository = Collection of objects (Abstraction).
Unit of Work = Transaction Manager (Atomic saves).
Strict Mode = Returns
List/IEnumerable(Clean but requires more code).Flexible Mode = Returns
IQueryable(Fast but leaky abstraction).Best of Both = Specification Pattern (Flexible querying without leaking EF Core).

