Repository pattern

2 min read

The Repository pattern mediates between the domain logic and data mapping layers (for example an ORM like EF Core, or raw ADO.NET). It provides a collection-like interface for accessing domain objects, and abstracts the underlying data storage and retrieval mechanisms.

Generic repository

It’s often implemented with a generic interface like IRepository<TEntity> that defines common CRUD (Create, Read, Update, Delete) operations applicable to any entity type.

The where TEntity : class constraint is a generic constraintGenericsType parameters as placeholders, so one piece of code works for many types and stays type-safe.Opens in a new tab, and the five methods are the CRUD operations.

C#
public interface IRepository<TEntity> where TEntity : class // Assuming EntityBase or similar constraint{    Task<TEntity> GetByIdAsync(object id);    Task<IEnumerable<TEntity>> GetAllAsync();    Task AddAsync(TEntity entity);    Task UpdateAsync(TEntity entity);    Task DeleteAsync(TEntity entity);    // Potentially other methods like FindAsync(Expression<Func<TEntity, bool>> predicate)}

Specific implementations

Concrete repository classes, for example ProductRepository : IRepository<Product>, implement this interface. They typically use an ORM like EF Core to interact with the database, and they encapsulate the ORM-specific querying logic.

Benefits

  • Decoupling. Business logic becomes independent of the specific data access technology.
  • Testability. Repositories can be mocked or stubbed easily in unit tests, so business logic can be tested in isolation from the database.
  • Centralized data access logic. It consolidates data access concerns, which makes it easier to manage and optimize queries.

IDisposable in repositories

If a repository instance manages disposable resources, such as a DbContext that it creates and owns, the repository itself should implement IDisposableIDisposable and the Dispose patternHow to release the resources the garbage collector does not know about.Opens in a new tab so those resources are released properly. If the DbContext is injected and its lifetime is managed by a DI containerDependency injection and service lifetimesHow ASP.NET Core hands each class what it needs, and how long each instance lives.Opens in a new tab (for example scoped per request in ASP.NET Core), the repository typically doesn’t need to dispose of it.

Balancing abstraction

The Repository pattern has significant benefits, especially for abstracting data access and improving testability. But it’s important to consider it in the context of modern ORMs like EF Core. EF Core’s DbSet<T> already acts much like a repository and unit of work (see the DbContext noteORMs, DbContext and entitiesWhat EF Core is, and the four ideas its API is built on.Opens in a new tab). Adding a generic repository layer on top can be over-abstraction if it merely wraps DbSet methods one-to-one. It can hide useful EF Core features, or lead to a “leaky” abstraction.

The decision to use a full Repository pattern should weigh the complexity it adds against the benefits for a specific project. It’s most valuable when you need to support multiple data sources, enforce specific data access policies, or abstract the ORM away for a potential future replacement.


Sources