7. Repository Pattern
- Repository pattern
About this chapter
Abstracting data access with the repository pattern:
- Repository pattern: Why and when to use it
- Interfaces: Defining contracts for data operations
- Implementations: Writing concrete repository classes
- Async/await: True asynchronous operations with Task
- Dependency injection: Registering and injecting repositories
- Testing: Mocking repositories for unit tests
Learning outcomes:
- Understand the repository pattern and its trade-offs
- Create repository interfaces with async methods
- Implement repositories with proper async patterns
- Register repositories in the DI container
- Mock repositories in tests
5.1 Why Use the Repository Pattern
- Pros:
- Abstraction over data access
- Centralized data logic
- Easier to test (mock repositories)
- Swap data sources without changing controllers
- Cons (honest discussion):
- Extra layer of abstraction
- EF Core already is a repository (Unit of Work pattern)
- Can be overkill for simple CRUD
When to Use: Complex queries, multiple data sources, testing requirements
5.2 Creating Repository Interfaces
namespace CommandAPI.Data;
public interface ICommandRepository
{
// Platform methods
Task<PaginatedList<Platform>> GetPlatformsAsync(int pageIndex, int pageSize);
Task<Platform?> GetPlatformByIdAsync(int id);
Task CreatePlatformAsync(Platform platform);
Task UpdatePlatformAsync(Platform platform);
void DeletePlatform(Platform platform);
// Command methods
Task<PaginatedList<Command>> GetCommandsAsync(int pageIndex, int pageSize);
Task<Command?> GetCommandByIdAsync(int id);
Task<PaginatedList<Command>> GetPlatformCommandsAsync(int platformId, int pageIndex, int pageSize);
Task CreateCommandAsync(Command command);
Task UpdateCommandAsync(Command command);
void DeleteCommand(Command command);
// Save changes
Task<bool> SaveChangesAsync();}
- Design Decision: Single repository for related entities vs separate repositories
- Current Code Issue: ICommandRepository handles both Commands AND Platforms
- Better Approach: Consider IPlatformRepository and ICommandRepository separately
5.3 Implementing Repositories with Async/Await
public class PgSqlCommandRepository : ICommandRepository
{
private readonly AppDbContext _context;
public PgSqlCommandRepository(AppDbContext context)
{
_context = context;
}
public async Task<Command?> GetCommandByIdAsync(int id)
{
return await _context.Commands
.Include(c => c.Platform) // Eager loading
.FirstOrDefaultAsync(c => c.Id == id);
}
public async Task CreateCommandAsync(Command command)
{
ArgumentNullException.ThrowIfNull(command);
await _context.Commands.AddAsync(command);
// Note: SaveChanges called separately
}
public async Task<bool> SaveChangesAsync()
{
return await _context.SaveChangesAsync() >= 0;
}
}
- Async Best Practices:
- Use async/await for I/O operations
- Append Async to method names
- Return Task or Task
- Use EF Core async methods: ToListAsync(), FirstOrDefaultAsync(), SaveChangesAsync()
5.4 CORRECTION: Fixing Async Methods That Aren’t Truly Async
// ❌ WRONG (current code has this)
public Task UpdatePlatformAsync(Platform platform)
{
// EF Core tracks changes automatically
return Task.CompletedTask; // Fake async
}
// ✅ CORRECT Option 1: Make it synchronous
public void UpdatePlatform(Platform platform)
{
// No-op - EF tracks changes
// // Or explicitly: _context.Entry(platform).State = EntityState.Modified;
}
// ✅ CORRECT Option 2: Keep async for consistency but document it
public Task UpdatePlatformAsync(Platform platform)
{
// EF Core change tracking handles this automatically
// Keeping async signature for interface consistency
return Task.CompletedTask;
}
- Teaching Point: Explain EF Core change tracking
- When to Use Task.CompletedTask: Interface consistency, but note it’s not truly async
5.5 Scoped vs Singleton vs Transient Lifetimes
// Program.cs service registration
// Scoped: One instance per HTTP
requestbuilder.Services.AddScoped<ICommandRepository, PgSqlCommandRepository>();
builder.Services.AddDbContext<AppDbContext>(...); // Scoped by default
// Singleton: One instance for entire application lifetime
builder.Services.AddSingleton<IConnectionMultiplexer>(
ConnectionMultiplexer.Connect(redisConnection));
builder.Services.AddSingleton<JobStatusService>();
// Transient: New instance every time it's requested
builder.Services.AddTransient<IEmailService, EmailService>();
- Lifetime Guidelines:
- Scoped: Repositories, DbContext, per-request services
- Singleton: Configuration, caches, expensive-to-create objects
- Transient: Lightweight, stateless services
- Captive Dependency Problem: Don’t inject scoped into singleton!