repository-pattern
SKILL.md
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 repositoriesef-core-configuration- Configure entity mappingscqrs-command-generator- Use repositories in handlersdotnet-clean-architecture- Overall project structure
Weekly Installs
6
Repository
ronnythedev/dot…e-skillsGitHub Stars
42
First Seen
Mar 1, 2026
Security Audits
Installed on
opencode6
gemini-cli6
github-copilot6
codex6
amp6
cline6