Repository Pattern Generator
Overview
This skill generates Repositories that provide an abstraction over data access:
- Interface in Domain layer - Defines data access contract
- Implementation in Infrastructure - Uses EF Core
- Per Aggregate Root - Not per entity
- Unit of Work integration - SaveChanges via IUnitOfWork
Quick Reference
| Repository Method | Purpose | Returns |
|---|---|---|
GetByIdAsync | Retrieve by primary key | Entity? |
GetByXxxAsync | Retrieve by business key | Entity? |
GetAllAsync | Retrieve all (use sparingly) | IReadOnlyList<Entity> |
Add | Track new entity | void |
Update | Track modified entity | void |
Remove | Track deleted entity | void |
ExistsAsync | Check existence | bool |
Repository Structure
/Domain/{Aggregate}/
└── I{Entity}Repository.cs # Interface (Domain layer)
/Infrastructure/Repositories/
└── {Entity}Repository.cs # Implementation (Infrastructure layer)
Template: Repository Interface (Domain Layer)
// src/{name}.domain/{Aggregate}/I{Entity}Repository.cs
namespace {name}.domain.{aggregate};
public interface I{Entity}Repository
{
// ═══════════════════════════════════════════════════════════════
// QUERY METHODS
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Gets an entity by its unique identifier
/// </summary>
Task<{Entity}?> GetByIdAsync(
Guid id,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets an entity by its unique identifier with related entities
/// </summary>
Task<{Entity}?> GetByIdWithDetailsAsync(
Guid id,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets an entity by a unique business key
/// </summary>
Task<{Entity}?> GetByNameAsync(
string name,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets all entities for a parent organization
/// </summary>
Task<IReadOnlyList<{Entity}>> GetByOrganizationIdAsync(
Guid organizationId,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets all active entities
/// </summary>
Task<IReadOnlyList<{Entity}>> GetAllActiveAsync(
CancellationToken cancellationToken = default);
/// <summary>
/// Checks if an entity exists
/// </summary>
Task<bool> ExistsAsync(
Guid id,
CancellationToken cancellationToken = default);
/// <summary>
/// Checks if an entity with the given name exists
/// </summary>
Task<bool> ExistsByNameAsync(
string name,
CancellationToken cancellationToken = default);
// ═══════════════════════════════════════════════════════════════
// COMMAND METHODS (tracking only, no SaveChanges)
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Adds a new entity to the context
/// </summary>
void Add({Entity} {entity});
/// <summary>
/// Adds multiple entities to the context
/// </summary>
void AddRange(IEnumerable<{Entity}> {entities});
/// <summary>
/// Updates an existing entity in the context
/// </summary>
void Update({Entity} {entity});
/// <summary>
/// Removes an entity from the context
/// </summary>
void Remove({Entity} {entity});
/// <summary>
/// Removes multiple entities from the context
/// </summary>
void RemoveRange(IEnumerable<{Entity}> {entities});
}
Template: Repository Implementation (Infrastructure Layer)
// src/{name}.infrastructure/Repositories/{Entity}Repository.cs
using Microsoft.EntityFrameworkCore;
using {name}.domain.{aggregate};
namespace {name}.infrastructure.repositories;
internal sealed class {Entity}Repository : I{Entity}Repository
{
private readonly ApplicationDbContext _dbContext;
public {Entity}Repository(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
// ═══════════════════════════════════════════════════════════════
// QUERY METHODS
// ═══════════════════════════════════════════════════════════════
public async Task<{Entity}?> GetByIdAsync(
Guid id,
CancellationToken cancellationToken = default)
{
return await _dbContext
.Set<{Entity}>()
.FirstOrDefaultAsync(e => e.Id == id, cancellationToken);
}
public async Task<{Entity}?> GetByIdWithDetailsAsync(
Guid id,
CancellationToken cancellationToken = default)
{
return await _dbContext
.Set<{Entity}>()
.Include(e => e.{ChildEntities})
.Include(e => e.{OtherRelation})
.FirstOrDefaultAsync(e => e.Id == id, cancellationToken);
}
public async Task<{Entity}?> GetByNameAsync(
string name,
CancellationToken cancellationToken = default)
{
return await _dbContext
.Set<{Entity}>()
.FirstOrDefaultAsync(
e => e.Name.ToLower() == name.ToLower(),
cancellationToken);
}
public async Task<IReadOnlyList<{Entity}>> GetByOrganizationIdAsync(
Guid organizationId,
CancellationToken cancellationToken = default)
{
return await _dbContext
.Set<{Entity}>()
.Where(e => e.OrganizationId == organizationId)
.OrderBy(e => e.Name)
.ToListAsync(cancellationToken);
}
public async Task<IReadOnlyList<{Entity}>> GetAllActiveAsync(
CancellationToken cancellationToken = default)
{
return await _dbContext
.Set<{Entity}>()
.Where(e => e.IsActive)
.OrderBy(e => e.Name)
.ToListAsync(cancellationToken);
}
public async Task<bool> ExistsAsync(
Guid id,
CancellationToken cancellationToken = default)
{
return await _dbContext
.Set<{Entity}>()
.AnyAsync(e => e.Id == id, cancellationToken);
}
public async Task<bool> ExistsByNameAsync(
string name,
CancellationToken cancellationToken = default)
{
return await _dbContext
.Set<{Entity}>()
.AnyAsync(
e => e.Name.ToLower() == name.ToLower(),
cancellationToken);
}
// ═══════════════════════════════════════════════════════════════
// COMMAND METHODS
// ═══════════════════════════════════════════════════════════════
public void Add({Entity} {entity})
{
_dbContext.Set<{Entity}>().Add({entity});
}
public void AddRange(IEnumerable<{Entity}> {entities})
{
_dbContext.Set<{Entity}>().AddRange({entities});
}
public void Update({Entity} {entity})
{
_dbContext.Set<{Entity}>().Update({entity});
}
public void Remove({Entity} {entity})
{
_dbContext.Set<{Entity}>().Remove({entity});
}
public void RemoveRange(IEnumerable<{Entity}> {entities})
{
_dbContext.Set<{Entity}>().RemoveRange({entities});
}
}
Template: Repository with Child Entity Access
// src/{name}.domain/{Aggregate}/I{Entity}Repository.cs
namespace {name}.domain.{aggregate};
public interface I{Entity}Repository
{
// Standard methods...
// ═══════════════════════════════════════════════════════════════
// CHILD ENTITY QUERIES (accessed through aggregate root)
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Gets a child entity through its aggregate root
/// </summary>
Task<{ChildEntity}?> Get{ChildEntity}ByIdAsync(
Guid {entity}Id,
Guid {childEntity}Id,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets all child entities