skills/data/repository-patterns/SKILL.md
Use when implementing the repository pattern with Unit of Work or specification pattern.
npx skillsauth add faysilalshareef/dotnet-ai-kit repository-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
public interface IRepository<T> where T : class, IAggregateRoot
{
Task<T?> FindAsync(Guid id, CancellationToken ct = default);
Task<List<T>> ListAsync(CancellationToken ct = default);
void Add(T entity);
void Update(T entity);
void Remove(T entity);
}
public interface IReadRepository<T> where T : class
{
Task<T?> FindAsync(Guid id, CancellationToken ct = default);
Task<List<T>> ListAsync(CancellationToken ct = default);
Task<bool> ExistsAsync(Guid id, CancellationToken ct = default);
}
public interface IUnitOfWork
{
Task<int> SaveChangesAsync(CancellationToken ct = default);
}
// DbContext IS the Unit of Work
internal sealed class UnitOfWork(AppDbContext db) : IUnitOfWork
{
public Task<int> SaveChangesAsync(CancellationToken ct)
=> db.SaveChangesAsync(ct);
}
internal class EfRepository<T>(AppDbContext db)
: IRepository<T> where T : class, IAggregateRoot
{
protected readonly AppDbContext Db = db;
private readonly DbSet<T> _set = db.Set<T>();
public virtual async Task<T?> FindAsync(
Guid id, CancellationToken ct)
=> await _set.FindAsync([id], ct);
public virtual async Task<List<T>> ListAsync(CancellationToken ct)
=> await _set.ToListAsync(ct);
public void Add(T entity) => _set.Add(entity);
public void Update(T entity) => _set.Update(entity);
public void Remove(T entity) => _set.Remove(entity);
}
public interface IOrderRepository : IRepository<Order>
{
Task<Order?> FindWithItemsAsync(
Guid id, CancellationToken ct = default);
Task<List<Order>> FindByCustomerAsync(
Guid customerId, CancellationToken ct = default);
Task<List<Order>> FindPendingAsync(CancellationToken ct = default);
}
internal sealed class OrderRepository(AppDbContext db)
: EfRepository<Order>(db), IOrderRepository
{
public async Task<Order?> FindWithItemsAsync(
Guid id, CancellationToken ct)
=> await Db.Orders
.Include(o => o.Items)
.AsSplitQuery()
.FirstOrDefaultAsync(o => o.Id == id, ct);
public async Task<List<Order>> FindByCustomerAsync(
Guid customerId, CancellationToken ct)
=> await Db.Orders
.Where(o => o.CustomerId == customerId)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(ct);
public async Task<List<Order>> FindPendingAsync(CancellationToken ct)
=> await Db.Orders
.Where(o => o.Status == OrderStatus.Pending)
.ToListAsync(ct);
}
// Using Ardalis.Specification
public sealed class OrdersByCustomerSpec : Specification<Order>
{
public OrdersByCustomerSpec(
Guid customerId, int page, int pageSize)
{
Query
.Where(o => o.CustomerId == customerId)
.Include(o => o.Items)
.OrderByDescending(o => o.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize);
}
}
public sealed class OrderByIdSpec : SingleResultSpecification<Order>
{
public OrderByIdSpec(Guid orderId)
{
Query
.Where(o => o.Id == orderId)
.Include(o => o.Items)
.AsSplitQuery();
}
}
// Projection specification
public sealed class OrderSummarySpec
: Specification<Order, OrderSummaryDto>
{
public OrderSummarySpec(DateTime fromDate)
{
Query
.Where(o => o.CreatedAt >= fromDate)
.OrderByDescending(o => o.Total)
.Select(o => new OrderSummaryDto(
o.Id, o.CustomerName, o.Total));
}
}
public interface ISpecRepository<T> where T : class
{
Task<T?> FirstOrDefaultAsync(
ISpecification<T> spec, CancellationToken ct = default);
Task<List<T>> ListAsync(
ISpecification<T> spec, CancellationToken ct = default);
Task<int> CountAsync(
ISpecification<T> spec, CancellationToken ct = default);
Task<List<TResult>> ListAsync<TResult>(
ISpecification<T, TResult> spec, CancellationToken ct = default);
}
public static class DependencyInjection
{
public static IServiceCollection AddRepositories(
this IServiceCollection services)
{
// Generic repository
services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
// Specialized repositories
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<ICustomerRepository, CustomerRepository>();
// Unit of Work
services.AddScoped<IUnitOfWork, UnitOfWork>();
return services;
}
}
internal sealed class CreateOrderHandler(
IOrderRepository orderRepository,
IUnitOfWork unitOfWork,
ILogger<CreateOrderHandler> logger)
: IRequestHandler<CreateOrderCommand, Result<Guid>>
{
public async Task<Result<Guid>> Handle(
CreateOrderCommand request, CancellationToken ct)
{
var order = Order.Create(request.CustomerName);
foreach (var item in request.Items)
order.AddItem(item.ProductId, item.Quantity, item.Price);
orderRepository.Add(order);
await unitOfWork.SaveChangesAsync(ct);
logger.LogInformation("Order {OrderId} created", order.Id);
return Result<Guid>.Success(order.Id);
}
}
IQueryable<T> from repository (leaky abstraction)IRepository< interface definitionsIUnitOfWork interfaceRepositories/ folder in Infrastructure projectArdalis.Specification package referenceSpecifications/ folderIRepository<T>, IUnitOfWork| Scenario | Approach |
|----------|----------|
| Simple CRUD | Generic IRepository<T> |
| Complex aggregate queries | Specialized repository interface |
| Reusable query criteria | Specification pattern |
| Read-only queries | IReadRepository<T> or direct DbContext |
| VSA style | Skip repository, use DbContext directly |
data-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.