7. Repository Pattern

  1. 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!