Generic Repository Pattern
The Repository pattern abstracts data access behind an interface, making services testable with mocks and isolating them from EF Core details. A generic IRepository<T> provides CRUD for all entities, while specialized repositories add domain-specific queries.
IRepository<T> & EF Core Implementation
// Domain/Interfaces/IRepository.cs
// Note: interfaces are in Domain — no EF Core dependency here
public interface IRepository<T> where T : BaseEntity
{
Task<T?> FindByIdAsync(Guid id, CancellationToken ct = default);
Task<IReadOnlyList<T>> GetAllAsync(CancellationToken ct = default);
Task<bool> ExistsAsync(Guid id, CancellationToken ct = default);
void Add(T entity);
void Update(T entity);
void Remove(T entity);
}
// Domain-specific interface (Domain/Interfaces/IProductRepository.cs)
public interface IProductRepository : IRepository<Product>
{
Task<Product?> FindBySlugAsync(string slug, CancellationToken ct = default);
Task<IReadOnlyList<Product>> GetByCategoryAsync(Guid categoryId, CancellationToken ct = default);
Task<bool> SlugExistsAsync(string slug, Guid? excludeId = null, CancellationToken ct = default);
Task<CursorPage<Product>> GetPagedAsync(ProductPageQuery query, CancellationToken ct = default);
}
// Infrastructure/Repositories/BaseRepository.cs
public abstract class BaseRepository<T>(AppDbContext db) : IRepository<T>
where T : BaseEntity
{
protected readonly AppDbContext Db = db;
protected readonly DbSet<T> Set = db.Set<T>();
public virtual async Task<T?> FindByIdAsync(Guid id, CancellationToken ct = default) =>
await Set.FindAsync([id], ct);
public virtual async Task<IReadOnlyList<T>> GetAllAsync(CancellationToken ct = default) =>
await Set.AsNoTracking().ToListAsync(ct);
public virtual async Task<bool> ExistsAsync(Guid id, CancellationToken ct = default) =>
await Set.AnyAsync(e => e.Id == id, ct);
public virtual void Add(T entity) => Set.Add(entity);
public virtual void Update(T entity) => Set.Update(entity);
public virtual void Remove(T entity) => Set.Remove(entity);
}
// Infrastructure/Repositories/ProductRepository.cs
public class ProductRepository(AppDbContext db) : BaseRepository<Product>(db), IProductRepository
{
public async Task<Product?> FindBySlugAsync(string slug, CancellationToken ct = default) =>
await Set
.Include(p => p.Category)
.AsNoTracking()
.FirstOrDefaultAsync(p => p.Slug == slug, ct);
public async Task<IReadOnlyList<Product>> GetByCategoryAsync(Guid categoryId, CancellationToken ct = default) =>
await Set
.Where(p => p.CategoryId == categoryId)
.AsNoTracking()
.OrderBy(p => p.Name)
.ToListAsync(ct);
public async Task<bool> SlugExistsAsync(string slug, Guid? excludeId = null, CancellationToken ct = default) =>
await Set.AnyAsync(p => p.Slug == slug && p.Id != excludeId, ct);
public async Task<CursorPage<Product>> GetPagedAsync(ProductPageQuery query, CancellationToken ct = default)
{
var q = Set.Where(p => query.CategoryId == null || p.CategoryId == query.CategoryId);
if (query.AfterDate.HasValue)
q = q.Where(p => p.CreatedAt < query.AfterDate.Value);
var items = await q.OrderByDescending(p => p.CreatedAt).Take(query.Limit + 1).ToListAsync(ct);
var hasNext = items.Count > query.Limit;
if (hasNext) items.RemoveAt(items.Count - 1);
return new CursorPage<Product>([.. items], null, hasNext, query.Limit);
}
}
record ProductPageQuery(Guid? CategoryId, DateTime? AfterDate, int Limit = 20);Tip
Tip
Practice Generic Repository Pattern in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
.NET provides a unified platform for building various application types
Practice Task
Note
Practice Task — (1) Write a working example of Generic Repository Pattern from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with Generic Repository Pattern is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready dotnet code.