Repository Pattern
The Repository Pattern is a design pattern that creates an abstraction layer between the data access logic and the business logic of an application. It acts as an in-memory collection of domain objects, providing a more object-oriented view of the persistence layer.
The Problem
Without the Repository Pattern, data access code tends to be scattered throughout the application, often directly in controllers or services:
[HttpGet("{id}")]
public async Task<ActionResult<Platform>> GetPlatform(int id)
{
// Direct database access in controller
var platform = await _dbContext.Platforms
.Include(p => p.Commands)
.FirstOrDefaultAsync(p => p.Id == id);
if (platform == null)
return NotFound();
return Ok(platform);
}
This approach creates several issues:
- Tight coupling: Business logic is coupled to a specific data access technology (EF Core)
- Code duplication: Similar queries repeated across multiple controllers
- Difficult testing: Hard to test without a real database
- Poor maintainability: Changes to data access require changes throughout the codebase
The Solution
The Repository Pattern encapsulates data access logic behind an interface:
Controller / Service
↓
IRepository (Interface)
↓
Repository Implementation
↓
Data Access Technology (EF Core, Dapper, etc.)
↓
Database
This creates a layer of abstraction that:
- Decouples business logic from data access
- Centralizes data access logic
- Enables easier testing through mocking
- Provides flexibility to change data access technologies
Basic Implementation
1. Define the Interface
public interface IPlatformRepository
{
Task<IEnumerable<Platform>> GetAllAsync();
Task<Platform> GetByIdAsync(int id);
Task<Platform> CreateAsync(Platform platform);
Task<bool> UpdateAsync(Platform platform);
Task<bool> DeleteAsync(int id);
Task<bool> SaveChangesAsync();
}
2. Implement the Repository
public class PlatformRepository : IPlatformRepository
{
private readonly AppDbContext _context;
public PlatformRepository(AppDbContext context)
{
_context = context;
}
public async Task<IEnumerable<Platform>> GetAllAsync()
{
return await _context.Platforms
.Include(p => p.Commands)
.ToListAsync();
}
public async Task<Platform> GetByIdAsync(int id)
{
return await _context.Platforms
.Include(p => p.Commands)
.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task<Platform> CreateAsync(Platform platform)
{
_context.Platforms.Add(platform);
await SaveChangesAsync();
return platform;
}
public async Task<bool> UpdateAsync(Platform platform)
{
_context.Entry(platform).State = EntityState.Modified;
return await SaveChangesAsync();
}
public async Task<bool> DeleteAsync(int id)
{
var platform = await GetByIdAsync(id);
if (platform == null)
return false;
_context.Platforms.Remove(platform);
return await SaveChangesAsync();
}
public async Task<bool> SaveChangesAsync()
{
return await _context.SaveChangesAsync() > 0;
}
}
3. Register with Dependency Injection
// In Program.cs
builder.Services.AddScoped<IPlatformRepository, PlatformRepository>();
4. Use in Controllers
[ApiController]
[Route("api/[controller]")]
public class PlatformsController : ControllerBase
{
private readonly IPlatformRepository _repository;
public PlatformsController(IPlatformRepository repository)
{
_repository = repository;
}
[HttpGet("{id}")]
public async Task<ActionResult<Platform>> GetPlatform(int id)
{
var platform = await _repository.GetByIdAsync(id);
if (platform == null)
return NotFound();
return Ok(platform);
}
[HttpPost]
public async Task<ActionResult<Platform>> CreatePlatform(Platform platform)
{
var created = await _repository.CreateAsync(platform);
return CreatedAtAction(
nameof(GetPlatform),
new { id = created.Id },
created);
}
}
Generic Repository Pattern
To avoid creating separate repository classes for each entity, you can implement a generic repository:
public interface IRepository<T> where T : class
{
Task<IEnumerable<T>> GetAllAsync();
Task<T> GetByIdAsync(int id);
Task<T> CreateAsync(T entity);
Task<bool> UpdateAsync(T entity);
Task<bool> DeleteAsync(int id);
Task<bool> SaveChangesAsync();
}
public class Repository<T> : IRepository<T> where T : class
{
private readonly AppDbContext _context;
private readonly DbSet<T> _dbSet;
public Repository(AppDbContext 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 async Task<T> CreateAsync(T entity)
{
await _dbSet.AddAsync(entity);
await SaveChangesAsync();
return entity;
}
public async Task<bool> UpdateAsync(T entity)
{
_context.Entry(entity).State = EntityState.Modified;
return await SaveChangesAsync();
}
public async Task<bool> DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity == null)
return false;
_dbSet.Remove(entity);
return await SaveChangesAsync();
}
public async Task<bool> SaveChangesAsync()
{
return await _context.SaveChangesAsync() > 0;
}
}
Usage:
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
// In controller
public class PlatformsController : ControllerBase
{
private readonly IRepository<Platform> _repository;
public PlatformsController(IRepository<Platform> repository)
{
_repository = repository;
}
}
Extending Repositories
Generic repositories often need entity-specific methods. Create specific interfaces that extend the generic one:
public interface IPlatformRepository : IRepository<Platform>
{
Task<IEnumerable<Platform>> GetPlatformsByPublisherAsync(string publisher);
Task<Platform> GetPlatformWithCommandsAsync(int id);
Task<bool> PlatformExistsAsync(string name);
}
public class PlatformRepository : Repository<Platform>, IPlatformRepository
{
private readonly AppDbContext _context;
public PlatformRepository(AppDbContext context) : base(context)
{
_context = context;
}
public async Task<IEnumerable<Platform>> GetPlatformsByPublisherAsync(string publisher)
{
return await _context.Platforms
.Where(p => p.Publisher == publisher)
.ToListAsync();
}
public async Task<Platform> GetPlatformWithCommandsAsync(int id)
{
return await _context.Platforms
.Include(p => p.Commands)
.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task<bool> PlatformExistsAsync(string name)
{
return await _context.Platforms
.AnyAsync(p => p.Name == name);
}
}
Unit of Work Pattern
Often used alongside the Repository Pattern, the Unit of Work Pattern maintains a list of objects affected by a business transaction and coordinates the writing out of changes:
public interface IUnitOfWork : IDisposable
{
IPlatformRepository Platforms { get; }
ICommandRepository Commands { get; }
Task<int> CompleteAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
public UnitOfWork(AppDbContext context)
{
_context = context;
Platforms = new PlatformRepository(context);
Commands = new CommandRepository(context);
}
public IPlatformRepository Platforms { get; }
public ICommandRepository Commands { get; }
public async Task<int> CompleteAsync()
{
return await _context.SaveChangesAsync();
}
public void Dispose()
{
_context.Dispose();
}
}
Usage:
public class PlatformService
{
private readonly IUnitOfWork _unitOfWork;
public PlatformService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task<Platform> CreatePlatformWithCommandsAsync(
Platform platform,
List<Command> commands)
{
// Create platform
var created = await _unitOfWork.Platforms.CreateAsync(platform);
// Add commands
foreach (var command in commands)
{
command.PlatformId = created.Id;
await _unitOfWork.Commands.CreateAsync(command);
}
// Save all changes in one transaction
await _unitOfWork.CompleteAsync();
return created;
}
}
Testing Benefits
The Repository Pattern makes testing significantly easier by allowing you to mock the repository:
public class PlatformsControllerTests
{
[Fact]
public async Task GetPlatform_ReturnsNotFound_WhenPlatformDoesNotExist()
{
// Arrange
var mockRepo = new Mock<IPlatformRepository>();
mockRepo.Setup(r => r.GetByIdAsync(999))
.ReturnsAsync((Platform)null);
var controller = new PlatformsController(mockRepo.Object);
// Act
var result = await controller.GetPlatform(999);
// Assert
Assert.IsType<NotFoundResult>(result.Result);
}
[Fact]
public async Task GetPlatform_ReturnsPlatform_WhenPlatformExists()
{
// Arrange
var expectedPlatform = new Platform
{
Id = 1,
Name = "Docker",
Publisher = "Docker Inc."
};
var mockRepo = new Mock<IPlatformRepository>();
mockRepo.Setup(r => r.GetByIdAsync(1))
.ReturnsAsync(expectedPlatform);
var controller = new PlatformsController(mockRepo.Object);
// Act
var result = await controller.GetPlatform(1);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var platform = Assert.IsType<Platform>(okResult.Value);
Assert.Equal("Docker", platform.Name);
}
}
The Controversy
The Repository Pattern is somewhat controversial in the .NET community, particularly when used with Entity Framework Core. Critics argue:
EF Core Already Implements Repository Pattern
DbContext and DbSet<T> already provide repository-like functionality:
DbSet<T>acts as a repository for an entity typeDbContextimplements Unit of Work- LINQ provides a powerful query interface
Adding another abstraction layer can be seen as unnecessary.
Over-Abstraction
Generic repositories can lead to:
- Leaky abstractions (exposing IQueryable)
- Loss of EF Core features (change tracking, lazy loading, query optimization)
- Increased complexity without clear benefits
Example of Over-Abstraction
// Leaky abstraction - exposing IQueryable
public interface IRepository<T>
{
IQueryable<T> GetAll(); // Leaks EF Core implementation details
}
// Better: Keep it simple
public interface IPlatformRepository
{
Task<IEnumerable<Platform>> GetAllAsync();
Task<Platform> GetByIdAsync(int id);
// Specific methods only
}
When to Use the Repository Pattern
Good Use Cases
Switching data access technologies: If you might replace EF Core with Dapper, ADO.NET, or another ORM.
// Can switch implementation without changing consumers
public class DapperPlatformRepository : IPlatformRepository
{
private readonly IDbConnection _connection;
public async Task<Platform> GetByIdAsync(int id)
{
return await _connection.QuerySingleOrDefaultAsync<Platform>(
"SELECT * FROM Platforms WHERE Id = @Id",
new { Id = id });
}
}
Complex domain logic: Encapsulating complex queries and data operations.
Testing: Making unit testing easier without database dependencies.
Team standards: When architectural consistency across projects is important.
When to Skip It
Simple CRUD applications: Where EF Core's DbContext is sufficient.
No testing requirements: If you're not writing unit tests (though you should).
Small team/project: Where the added complexity isn't justified.
Pragmatic Approach
A balanced approach often works best:
1. Start Simple
Use DbContext directly until you have a reason to abstract it:
public class PlatformService
{
private readonly AppDbContext _context;
public async Task<Platform> GetByIdAsync(int id)
{
return await _context.Platforms
.Include(p => p.Commands)
.FirstOrDefaultAsync(p => p.Id == id);
}
}
2. Add Repositories for Complex Queries
Create repositories when queries become complex or repeated:
public interface IPlatformRepository
{
Task<Platform> GetPlatformWithCommandsAsync(int id);
Task<IEnumerable<Platform>> GetPlatformsByPublisher(string publisher);
Task<PagedResult<Platform>> GetPagedPlatformsAsync(int page, int pageSize);
}
3. Avoid Over-Genericization
Don't create a generic repository just for the sake of it. Entity-specific repositories with focused methods are often better.
4. Keep It Testable
Whether you use repositories or not, ensure your code is testable:
// Inject DbContext via interface to allow mocking
public interface IAppDbContext
{
DbSet<Platform> Platforms { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
Best Practices
Use Async/Await
All repository methods should be asynchronous:
public async Task<Platform> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
Don't Return IQueryable
Returning IQueryable leaks implementation details and defeats the purpose of abstraction:
// ❌ Bad
public IQueryable<Platform> GetAll()
{
return _dbSet.AsQueryable();
}
// ✅ Good
public async Task<IEnumerable<Platform>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
Keep Repositories Focused
Each repository should focus on a single aggregate root:
// ✅ Good - focused on Platform
public interface IPlatformRepository
{
Task<Platform> GetByIdAsync(int id);
Task<IEnumerable<Platform>> GetAllAsync();
}
// ❌ Bad - mixing concerns
public interface IPlatformRepository
{
Task<Platform> GetByIdAsync(int id);
Task<Command> GetCommandByIdAsync(int id); // Wrong entity
}
Use Specification Pattern for Complex Queries
For complex query logic, consider the Specification Pattern:
public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
}
public class PlatformWithCommandsSpecification : ISpecification<Platform>
{
public PlatformWithCommandsSpecification(int platformId)
{
Criteria = p => p.Id == platformId;
Includes = new List<Expression<Func<Platform, object>>>
{
p => p.Commands
};
}
public Expression<Func<Platform, bool>> Criteria { get; }
public List<Expression<Func<Platform, object>>> Includes { get; }
}
public async Task<Platform> GetBySpecificationAsync(ISpecification<Platform> spec)
{
var query = _dbSet.AsQueryable();
if (spec.Criteria != null)
query = query.Where(spec.Criteria);
query = spec.Includes
.Aggregate(query, (current, include) => current.Include(include));
return await query.FirstOrDefaultAsync();
}
Conclusion
The Repository Pattern remains a valuable tool in certain scenarios, despite the debate around its necessity with modern ORMs like Entity Framework Core. The key is to apply it pragmatically:
- Use it when you need abstraction for testing or flexibility
- Avoid over-engineering with generic repositories unless truly needed
- Keep implementations simple and focused
- Remember that EF Core's DbContext already provides many repository-like features
Ultimately, the decision should be based on your project's specific requirements, team preferences, and architectural goals rather than blind adherence to patterns.