Repository Pattern
What:
- The Repository pattern is a design pattern that acts as an abstraction layer between your application's business logic and the data access layer (DAL).
- It encapsulates the logic needed to access data sources (such as databases) and provides a collection-like interface for managing domain entities.
- It helps to isolate and decouple data access code from business logic.
Why use it:
- Separation of concerns: Keeps your business logic independent from data access details.
- Testability: Makes it easier to mock or stub data access in unit tests.
- Maintainability: Centralizes CRUD operations and common queries for entities.
- Flexibility: Allows switching data sources or ORMs without affecting business logic.
How it works:
- Typically, you define repository interfaces with methods like
Add
,Update
,Delete
,GetById
,GetAll
etc.
- Concrete repository classes implement these interfaces using the specific data access technology (e.g., Entity Framework).
Example:
public interface IProductRepository { Task<Product> GetByIdAsync(int id); Task<IEnumerable<Product>> GetAllAsync(); void Add(Product product); void Update(Product product); void Delete(Product product); } public class ProductRepository : IProductRepository { private readonly DbContext _context; public ProductRepository(DbContext context) => _context = context; // Implement methods using EF DbSets }
Unit of Work Pattern
What:
- The Unit of Work (UoW) pattern maintains a list of objects affected by a business transaction and coordinates the writing out of changes as a single operation (transaction).
- It ensures that multiple operations across one or more repositories are committed atomically, so either all succeed or all fail (rollback on error).
- In .NET's Entity Framework, the
DbContext
itself implements UoW internally.
Why use it:
- Consistency: Guarantees that related changes in multiple repositories are saved together.
- Transaction management: Helps manage database transactions at a higher level than individual repositories.
- Simplification: Aggregates multiple repository operations into a single commit point.
- Reduced coupling: Instead of directly saving changes per repository, the UoW handles the commit lifecycle.
How it works:
- UoW exposes access to repositories as properties.
- It provides a
SaveChanges()
orCommit()
method to persist changes.
- All repositories share the same database context or transaction scope.
Example:
public interface IUnitOfWork : IDisposable { IProductRepository Products { get; } ICategoryRepository Categories { get; } int SaveChanges(); } public class UnitOfWork : IUnitOfWork { private readonly DbContext _context; public UnitOfWork(DbContext context) => _context = context; // Lazy-loaded repository instances public IProductRepository Products { get; } public ICategoryRepository Categories { get; } public int SaveChanges() => _context.SaveChanges(); public void Dispose() => _context.Dispose(); }
Can Repository and Unit of Work be used together or are they different?
- Yes, they are complementary and often used together.
- Repository abstracts data access for an entity or aggregate providing CRUD and query methods.
- Unit of Work coordinates multiple repositories and manages transactions, committing all changes in a single atomic operation.
- In Entity Framework, the
DbContext
acts as both repository (viaDbSet
) and unit of work (transaction context), but creating your own repositories and UoW adds abstraction and testability.
- Using both together provides clean separation, easier testing, and transaction management across multiple repositories.
Other Common Database-Related Patterns to Know
- Generic Repository Pattern
- A generic implementation of repository methods (
Add
,Update
,Delete
,Get
) for most entities to avoid repetition. - Can be combined with specific repositories for entity-specific queries.
- Specification Pattern
- Encapsulates query criteria in reusable specification objects.
- Helps build complex queries easily and keeps repository methods simple.
- Data Mapper Pattern
- Separates in-memory objects from database schema mapping.
- ORM libraries like Entity Framework implement this pattern.
- Identity Map
- Ensures each object is loaded only once per transaction to avoid duplicate loading and maintain consistency.
- Lazy Loading / Eager Loading Patterns
- Related to how related entities are fetched (on demand or upfront).
- Transaction Script Pattern
- Organizes business logic into scripts or services; simpler alternative to domain-driven approaches combining with repositories.
Summary Table
Pattern | Purpose | Relationship |
Repository | Provides abstraction to CRUD and queries for entities | Focuses on entity-level data access |
Unit of Work | Manages transaction and coordination of multiple repositories | Coordinates repositories and saves work |
Generic Repository | Reusable generic data access logic | Simplifies repository implementations |
Specification | Encapsulates query criteria in objects | Used alongside Repository |
Data Mapper | Maps between database and in-memory objects | Underlies Entity Framework and ORMs |
Identity Map | Ensures single object instance per transaction | Often implemented inside ORM |
Summary
- The Repository pattern abstracts data access per entity or aggregate.
- The Unit of Work pattern manages a set of changes across repositories and commits them transactionally.
- They are often used together but serve distinct roles.
- Additional patterns like Generic Repository, Specification, and Data Mapper improve the flexibility and maintainability of data access.
- Entity Framework internally implements Repository and Unit of Work, but creating your own abstractions is a best practice for larger or testable applications.
References
- Microsoft Docs on Repository and Unit of Work Patterns
- Dev.to article on Unit of Work and Repository Pattern in .NET Core
- LinkedIn article on Repository and Unit of Work Patterns in .NET Core