| name | dotnet-repository-pattern |
| description | Generates Repository interfaces and implementations following the Repository pattern. Provides data access abstraction for aggregate roots with EF Core implementations. |
| version | 1.0.0 |
| language | C# |
| framework | .NET 8+ |
| dependencies | Entity Framework Core |
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)
namespace {name}.domain.{aggregate};
public interface I{Entity}Repository
{
Task<{Entity}?> GetByIdAsync(
Guid id,
CancellationToken cancellationToken = default);
Task<{Entity}?> GetByIdWithDetailsAsync(
Guid id,
CancellationToken cancellationToken = default);
Task<{Entity}?> GetByNameAsync(
string name,
CancellationToken cancellationToken = default);
Task<IReadOnlyList<{Entity}>> GetByOrganizationIdAsync(
Guid organizationId,
CancellationToken cancellationToken = default);
Task<IReadOnlyList<{Entity}>> GetAllActiveAsync(
CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(
Guid id,
CancellationToken cancellationToken = default);
Task<bool> ExistsByNameAsync(
string name,
CancellationToken cancellationToken = default);
void Add({Entity} {entity});
void AddRange(IEnumerable<{Entity}> {entities});
void Update({Entity} {entity});
void Remove({Entity} {entity});
void RemoveRange(IEnumerable<{Entity}> {entities});
}
Template: Repository Implementation (Infrastructure Layer)
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;
}
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);
}
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
namespace {name}.domain.{aggregate};
public interface I{Entity}Repository
{
Task<{ChildEntity}?> Get{ChildEntity}ByIdAsync(
Guid {entity}Id,
Guid {childEntity}Id,
CancellationToken cancellationToken = default);
Task<IReadOnlyList<{ChildEntity}>> Get{ChildEntities}By{Entity}IdAsync(
Guid {entity}Id,
CancellationToken cancellationToken = default);
}
internal sealed class {Entity}Repository : I{Entity}Repository
{
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
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; }
}
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;
}
}
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);
}
}
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)
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);
}
}
internal sealed class {Entity}Repository : Repository<{Entity}>, I{Entity}Repository
{
public {Entity}Repository(ApplicationDbContext dbContext) : base(dbContext)
{
}
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
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();
});
services.AddScoped<IUnitOfWork>(sp =>
sp.GetRequiredService<ApplicationDbContext>());
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IOrganizationRepository, OrganizationRepository>();
services.AddScoped<IDepartmentRepository, DepartmentRepository>();
services.AddScoped<ISurveyRepository, SurveyRepository>();
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()
.Where(e => e.IsActive)
.OrderBy(e => e.Name)
.ToListAsync(cancellationToken);
}
Selective Includes (Avoid Over-fetching)
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)
.FirstOrDefaultAsync(e => e.Id == id, ct);
}
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()
.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
public void Add({Entity} {entity})
{
_dbContext.Set<{Entity}>().Add({entity});
_dbContext.SaveChanges();
}
public void Add({Entity} {entity})
{
_dbContext.Set<{Entity}>().Add({entity});
}
await _unitOfWork.SaveChangesAsync(ct);
public interface IOrderItemRepository { ... }
public interface IOrderRepository
{
Task<OrderItem?> GetOrderItemAsync(Guid orderId, Guid itemId, ...);
}
public IQueryable<{Entity}> GetAll() => _dbContext.Set<{Entity}>();
public async Task<IReadOnlyList<{Entity}>> GetAllAsync(CancellationToken ct)
{
return await _dbContext.Set<{Entity}>().ToListAsync(ct);
}
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");
return entity;
}
public async Task<{Entity}?> GetByIdAsync(Guid id, CancellationToken ct)
{
return await _dbContext.Set<{Entity}>()
.FirstOrDefaultAsync(e => e.Id == id, ct);
}
Related Skills
dotnet-domain-entity-generator - Generate entities for repositories
dotnet-ef-core-configuration - Configure entity mappings
dotnet-cqrs-command-generator - Use repositories in handlers
dotnet-clean-architecture - Overall project structure