Introduction
Modern applications contain multiple layers—controllers, business logic, data access, and domain models. Without proper structure, your project can become tightly coupled, difficult to maintain, and hard to test. This is where the Repository Pattern becomes extremely useful.
The Repository Pattern is a widely used design pattern in .NET applications to create a clean separation between the Data Access Layer (DAL) and the Business Logic Layer (BLL). It abstracts the underlying database interactions and provides a consistent, flexible, and testable way to manage data.
This guide explains:
What the Repository Pattern is
Why it is used
Benefits and limitations
Types of repositories
How to create repository interfaces
How to implement generic repositories
How to use repositories in ASP.NET Core with EF Core
Real-world examples
Best practices for production
Let's begin with a clear understanding.
What Is the Repository Pattern?
The Repository Pattern is a design pattern that acts as a mediator between the domain (business logic) and data source (database). It provides a clean abstraction layer for performing CRUD operations without exposing the underlying implementation details.
A repository is a class responsible for:
Retrieving data
Saving data
Updating data
Deleting data
But it hides how this process happens (SQL queries, EF Core, external APIs, MongoDB, etc.)
Why Do We Need the Repository Pattern?
Without a repository, controllers or services directly interact with Entity Framework or SQL, which causes problems:
Tight Coupling
Controllers depend directly on EF Core classes. Changing the database layer becomes complicated.
Difficult to Unit Test
You cannot mock or replace the database easily.
Duplicate Code Everywhere
Repeated Find(), Add(), SaveChanges(), etc. appear throughout the project.
Violates SOLID Principles
Especially SRP (Single Responsibility Principle) and DIP (Dependency Inversion Principle).
The Repository Pattern solves these issues by acting as an abstraction layer.
Benefits of the Repository Pattern
1. Loose Coupling
Controllers depend on an interface (abstraction) instead of the actual database.
2. Better Testability
You can mock the repository in unit tests without needing a real database.
3. Cleaner Code
Removes EF Core queries from controllers and services.
4. Separation of Concerns
Keeps business logic and data access logic separate.
5. Easier to Maintain & Extend
Switching from SQL Server to MongoDB requires only repository-level changes.
6. Supports SOLID Principles
DIP, SRP, and ISP are naturally implemented.
Key Concepts in Repository Pattern
IRepository Interface
Defines general CRUD operations.
Repository Class
Implements the interface and uses EF Core to interact with the database.
Generic Repository
Avoids repeating CRUD code for each entity.
Specific Repository
Contains custom queries for a specific entity.
Basic Repository Pattern Structure
A Repository Pattern typically contains:
Controllers → Services → Repository → DbContext → Database
This creates a clean flow of data with no direct controller-to-database calls.
Creating a Repository Pattern Step-by-Step
Let's implement a complete repository pattern using ASP.NET Core + EF Core + SQL Server.
Our example entity: Product
Step 1: Create Entity
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
}
Step 2: Create the DbContext
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<Product> Products { get; set; }
}
Step 3: Create the IRepository Interface
This defines common CRUD operations.
public interface IRepository<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);
Task SaveAsync();
}
Step 4: Implement Generic Repository
public class Repository<T> : IRepository<T> where T : class
{
private readonly ApplicationDbContext _context;
private readonly DbSet<T> _dbSet;
public Repository(ApplicationDbContext 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);
}
public async Task SaveAsync()
{
await _context.SaveChangesAsync();
}
}
Creating a Specific Repository (Optional)
Some entities need more custom logic.
For example, ProductRepository can contain special queries.
IProductRepository
public interface IProductRepository : IRepository<Product>
{
Task<IEnumerable<Product>> GetProductsAbovePrice(double amount);
}
ProductRepository
public class ProductRepository : Repository<Product>, IProductRepository
{
private readonly ApplicationDbContext _context;
public ProductRepository(ApplicationDbContext context) : base(context)
{
_context = context;
}
public async Task<IEnumerable<Product>> GetProductsAbovePrice(double amount)
{
return await _context.Products
.Where(p => p.Price > amount)
.ToListAsync();
}
}
Register Repositories in Dependency Injection
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
builder.Services.AddScoped<IProductRepository, ProductRepository>();
Using Repository in Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repo;
public ProductsController(IProductRepository repo)
{
_repo = repo;
}
[HttpGet]
public async Task<IActionResult> Get()
{
var products = await _repo.GetAllAsync();
return Ok(products);
}
[HttpPost]
public async Task<IActionResult> Post(Product product)
{
await _repo.AddAsync(product);
await _repo.SaveAsync();
return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
}
}
Repository Pattern Variations
1. Generic Repository + Specific Repository
Most popular approach in enterprise applications.
2. Only Specific Repositories
You write one repository per entity—more flexible but more code.
3. CQRS (Command Query Responsibility Segregation)
Commands use repositories, but queries use direct EF Core calls.
4. Unit of Work Pattern
Coordinates multiple repositories using a single SaveChanges().
Example
public interface IUnitOfWork
{
IProductRepository Products { get; }
Task SaveAsync();
}
When NOT to Use the Repository Pattern
In some scenarios, Repository Pattern may be unnecessary:
Small projects where EF Core already acts like a repository.
CQRS applications using Dapper or raw SQL.
Microservices with simple CRUD logic.
EF Core itself implements many repository-like features, so duplicating the pattern can sometimes lead to unnecessary complexity.
Best Practices for Repository Pattern
Keep repository logic focused on database operations only.
Do not mix business logic inside repositories.
Use async for all DB operations.
Add specific repositories only when needed.
Use Unit of Work when working with multiple entities.
Keep repositories clean, simple, and testable.