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 for a parent
/// </summary>
Task<IReadOnlyList<{ChildEntity}>> Get{ChildEntities}By{Entity}IdAsync(
Guid {entity}Id,
CancellationToken cancellationToken = default);
}
// src/{name}.infrastructure/Repositories/{Entity}Repository.cs internal sealed class {Entity}Repository : I{Entity}Repository { // ... other methods
public async Task<{ChildEntity}?> Get{ChildEntity}ByIdAsync(
Guid {entity}Id,
Guid {childEntity}Id,
CancellationToken cancellationToken = default)
{
var {entity} = await _dbContext
.Set<{Entity}>()
.Include(e => e.{ChildEntities})
.FirstOrDefaultAsync(e => e.Id == {entity}Id, cancellationToken);
return {entity}?.{ChildEntities}
.FirstOrDefault(c => c.Id == {childEntity}Id);
}
public async Task<IReadOnlyList<{ChildEntity}>> Get{ChildEntities}By{Entity}IdAsync(
Guid {entity}Id,
CancellationToken cancellationToken = default)
{
var {entity} = await _dbContext
.Set<{Entity}>()
.Include(e => e.{ChildEntities})
.FirstOrDefaultAsync(e => e.Id == {entity}Id, cancellationToken);
return {entity}?.{ChildEntities}.ToList()
?? new List<{ChildEntity}>();
}
}
Template: Repository with Specification Pattern
// src/{name}.domain/Abstractions/ISpecification.cs using System.Linq.Expressions;
namespace {name}.domain.abstractions;
public interface ISpecification<T> { Expression<Func<T, bool>> Criteria { get; } List<Expression<Func<T, object>>> Includes { get; } List<string> IncludeStrings { get; } Expression<Func<T, object>>? OrderBy { get; } Expression<Func<T, object>>? OrderByDescending { get; } int? Take { get; } int? Skip { get; } bool IsPagingEnabled { get; } }
// src/{name}.domain/Abstractions/BaseSpecification.cs using System.Linq.Expressions;
namespace {name}.domain.abstractions;
public abstract class BaseSpecification<T> : ISpecification<T> { public Expression<Func<T, bool>> Criteria { get; private set; } = _ => true; public List<Expression<Func<T, object>>> Includes { get; } = new(); public List<string> IncludeStrings { get; } = new(); public Expression<Func<T, object>>? OrderBy { get; private set; } public Expression<Func<T, object>>? OrderByDescending { get; private set; } public int? Take { get; private set; } public int? Skip { get; private set; } public bool IsPagingEnabled { get; private set; }
protected void AddCriteria(Expression<Func<T, bool>> criteria)
{
Criteria = criteria;
}
protected void AddInclude(Expression<Func<T, object>> includeExpression)
{
Includes.Add(includeExpression);
}
protected void AddInclude(string includeString)
{
IncludeStrings.Add(includeString);
}
protected void ApplyOrderBy(Expression<Func<T, object>> orderByExpression)
{
OrderBy = orderByExpression;
}
protected void ApplyOrderByDescending(Expression<Func<T, object>> orderByDescExpression)
{
OrderByDescending = orderByDescExpression;
}
protected void ApplyPaging(int skip, int take)
{
Skip = skip;
Take = take;
IsPagingEnabled = true;
}
}
// src/{name}.domain/{Aggregate}/Specifications/Active{Entities}Specification.cs using {name}.domain.abstractions;
namespace {name}.domain.{aggregate}.specifications;
public sealed class Active{Entities}Specification : BaseSpecification<{Entity}> { public Active{Entities}Specification() { AddCriteria(e => e.IsActive); ApplyOrderBy(e => e.Name); } }
public sealed class {Entities}ByOrganizationSpecification : BaseSpecification<{Entity}> { public {Entities}ByOrganizationSpecification(Guid organizationId) { AddCriteria(e => e.OrganizationId == organizationId && e.IsActive); AddInclude(e => e.{ChildEntities}); ApplyOrderBy(e => e.Name); } }
// Repository with specification support public interface I{Entity}Repository { Task<IReadOnlyList<{Entity}>> GetAsync( ISpecification<{Entity}> specification, CancellationToken cancellationToken = default);
Task<{Entity}?> GetFirstOrDefaultAsync(
ISpecification<{Entity}> specification,
CancellationToken cancellationToken = default);
Task<int> CountAsync(
ISpecification<{Entity}> specification,
CancellationToken cancellationToken = default);
}
Template: Generic Repository Base (Optional)
// src/{name}.infrastructure/Repositories/Repository.cs using Microsoft.EntityFrameworkCore; using {name}.domain.abstractions;
namespace {name}.infrastructure.repositories;
internal abstract class Repository<T> where T : Entity { protected readonly ApplicationDbContext DbContext;
protected Repository(ApplicationDbContext dbContext)
{
DbContext = dbContext;
}
public async Task<T?> GetByIdAsync(
Guid id,
CancellationToken cancellationToken = default)
{
return await DbContext
.Set<T>()
.FirstOrDefaultAsync(e => e.Id == id, cancellationToken);
}
public void Add(T entity)
{
DbContext.Set<T>().Add(entity);
}
public void Update(T entity)
{
DbContext.Set<T>().Update(entity);
}
public void Remove(T entity)
{
DbContext.Set<T>().Remove(entity);
}
}
// Using the base repository internal sealed class {Entity}Repository : Repository<{Entity}>, I{Entity}Repository { public {Entity}Repository(ApplicationDbContext dbContext) : base(dbContext) { }
// Add entity-specific methods
public async Task<{Entity}?> GetByNameAsync(
string name,
CancellationToken cancellationToken = default)
{
return await DbContext
.Set<{Entity}>()
.FirstOrDefaultAsync(
e => e.Name.ToLower() == name.ToLower(),
cancellationToken);
}
}
Registering Repositories
// src/{name}.infrastructure/DependencyInjection.cs private static void AddPersistence(IServiceCollection services, IConfiguration configuration) { var connectionString = configuration.GetConnectionString("Database") ?? throw new ArgumentNullException(nameof(configuration));
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseNpgsql(connectionString)
.UseSnakeCaseNamingConvention();
});
// Register Unit of Work
services.AddScoped<IUnitOfWork>(sp =>
sp.GetRequiredService<ApplicationDbContext>());
// Register Repositories
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IOrganizationRepository, OrganizationRepository>();
services.AddScoped<IDepartmentRepository, DepartmentRepository>();
services.AddScoped<ISurveyRepository, SurveyRepository>();
// Add more repositories here...
// Register SQL Connection Factory for Dapper queries
services.AddSingleton<ISqlConnectionFactory>(_ =>
new SqlConnectionFactory(connectionString));
}
Query Optimization Patterns
AsNoTracking for Read-Only Queries
public async Task<IReadOnlyList<{Entity}>> GetAllForDisplayAsync( CancellationToken cancellationToken = default) { return await _dbContext .Set<{Entity}>() .AsNoTracking() // Performance: no change tracking .Where(e => e.IsActive) .OrderBy(e => e.Name) .ToListAsync(cancellationToken); }
Selective Includes (Avoid Over-fetching)
// ❌ WRONG: Loading everything public async Task<{Entity}?> GetByIdAsync(Guid id, CancellationToken ct) { return await _dbContext .Set<{Entity}>() .Include(e => e.Children) .Include(e => e.Parent) .Include(e => e.Logs) // Potentially thousands of records! .FirstOrDefaultAsync(e => e.Id == id, ct); }
// ✅ CORRECT: Separate methods for different needs public async Task<{Entity}?> GetByIdAsync(Guid id, CancellationToken ct) { return await _dbContext .Set<{Entity}>() .FirstOrDefaultAsync(e => e.Id == id, ct); }
public async Task<{Entity}?> GetByIdWithChildrenAsync(Guid id, CancellationToken ct) { return await _dbContext .Set<{Entity}>() .Include(e => e.Children) .FirstOrDefaultAsync(e => e.Id == id, ct); }
Split Queries for Large Collections
public async Task<{Entity}?> GetByIdWithAllRelationsAsync( Guid id, CancellationToken cancellationToken = default) { return await _dbContext .Set<{Entity}>() .Include(e => e.Children) .Include(e => e.OtherRelation) .AsSplitQuery() // Splits into multiple SQL queries .FirstOrDefaultAsync(e => e.Id == id, cancellationToken); }
Critical Rules
-
Repository per aggregate root - Not per entity
-
No SaveChanges in repository - That's IUnitOfWork's job
-
Interface in Domain - Implementation in Infrastructure
-
Use CancellationToken - All async methods
-
Return null for not found - Let handler decide what to do
-
AsNoTracking for reads - When not modifying
-
Selective Includes - Don't over-fetch
-
Avoid GetAll without filters - Can be dangerous at scale
-
Child entities through aggregate - Don't expose child repositories
-
Internal class for implementation - Hide implementation details
Anti-Patterns to Avoid
// ❌ WRONG: SaveChanges in repository public void Add({Entity} {entity}) { _dbContext.Set<{Entity}>().Add({entity}); _dbContext.SaveChanges(); // Don't do this! }
// ✅ CORRECT: Only track, save via UnitOfWork public void Add({Entity} {entity}) { _dbContext.Set<{Entity}>().Add({entity}); } // In handler: await _unitOfWork.SaveChangesAsync(ct);
// ❌ WRONG: Repository for child entities public interface IOrderItemRepository { ... }
// ✅ CORRECT: Access through aggregate root public interface IOrderRepository { Task<OrderItem?> GetOrderItemAsync(Guid orderId, Guid itemId, ...); }
// ❌ WRONG: Exposing IQueryable public IQueryable<{Entity}> GetAll() => _dbContext.Set<{Entity}>();
// ✅ CORRECT: Return materialized lists public async Task<IReadOnlyList<{Entity}>> GetAllAsync(CancellationToken ct) { return await _dbContext.Set<{Entity}>().ToListAsync(ct); }
// ❌ WRONG: Business logic in repository public async Task<{Entity}?> GetActiveByIdAsync(Guid id, CancellationToken ct) { var entity = await GetByIdAsync(id, ct); if (entity?.IsActive == false) throw new BusinessException("Entity is inactive"); // Wrong! return entity; }
// ✅ CORRECT: Let handler handle business logic public async Task<{Entity}?> GetByIdAsync(Guid id, CancellationToken ct) { return await _dbContext.Set<{Entity}>() .FirstOrDefaultAsync(e => e.Id == id, ct); }
Related Skills
-
domain-entity-generator
-
Generate entities for repositories
-
ef-core-configuration
-
Configure entity mappings
-
cqrs-command-generator
-
Use repositories in handlers
-
dotnet-clean-architecture
-
Overall project structure