Building scalable and maintainable APIs in ASP.NET Core often requires a structured data access layer. While EF Core provides a powerful ORM, directly using DbContext across multiple services can lead to tight coupling, duplicated logic, and difficulty in maintaining transactions.
That’s where Repository and Unit of Work (UoW) patterns come into play. They help encapsulate data access logic, ensure a clean separation of concerns, and provide a consistent transactional boundary across repositories.
This article explains how to implement Repository and Unit of Work patterns in ASP.NET Core with EF Core, with clear, production-grade code examples.
1. Why Use Repository and Unit of Work Patterns?
Repository Pattern
The Repository Pattern acts as an abstraction layer between your application and data source. It centralizes common database operations (CRUD) and hides EF Core details from business logic.
Unit of Work Pattern
The Unit of Work Pattern ensures all database changes occur within a single transaction. If one operation fails, everything rolls back — maintaining data integrity.
Example Use Case
When you need to:
Save a
PurchaseOrderand relatedPurchaseOrderPartstogether.Update multiple entities in one transaction.
Keep your service layer clean and testable.
2. Architecture Overview
Here’s how these layers interact in a clean architecture setup:
Controller → Service → UnitOfWork → Repository → EF Core DbContext → Database
Each repository handles one entity, while the Unit of Work coordinates multiple repositories and manages the EF Core DbContext lifecycle.
3. Step-by-Step Implementation
We’ll build a simple implementation using an example Product entity.
Step 1: Define the Entity
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public DateTime CreatedDate { get; set; }
}
Step 2: Create the DbContext
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products { get; set; }
}
Step 3: Define a Generic Repository Interface
A generic repository ensures you don’t repeat CRUD logic for every entity.
public interface IGenericRepository<T> where T : class
{
Task<IEnumerable<T>> GetAllAsync();
Task<T> GetByIdAsync(int id);
Task AddAsync(T entity);
void Update(T entity);
void Delete(T entity);
}
Step 4: Implement the Generic Repository
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
protected readonly AppDbContext _context;
private readonly DbSet<T> _dbSet;
public GenericRepository(AppDbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public async Task<T> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
}
public void Update(T entity)
{
_dbSet.Update(entity);
}
public void Delete(T entity)
{
_dbSet.Remove(entity);
}
}
Step 5: Create a Specific Repository (Optional)
If your entity needs custom queries, you can extend the generic repository.
public interface IProductRepository : IGenericRepository<Product>
{
Task<IEnumerable<Product>> GetExpensiveProductsAsync(decimal minPrice);
}
public class ProductRepository : GenericRepository<Product>, IProductRepository
{
public ProductRepository(AppDbContext context) : base(context) { }
public async Task<IEnumerable<Product>> GetExpensiveProductsAsync(decimal minPrice)
{
return await _context.Products
.Where(p => p.Price > minPrice)
.ToListAsync();
}
}

Ganesh JangamPosted Sep 1, 2026, 5:51 AM
Thank you for sharing this article. Great job