Jump to content

Repository

From Knowledge Base
Revision as of 06:22, 22 January 2025 by Chr1ss (talk | contribs) (Created page with "= Repository Pattern = The Repository Pattern mediates between the domain and data mapping layers, using a collection-like interface for accessing domain objects. It provides an abstraction of data persistence, enabling applications to work with domain models without being concerned about database connections or queries. == Purpose == The primary purpose of the Repository Pattern is to achieve a clean separation and one-way dependency between the domain and data mappin...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Repository Pattern

The Repository Pattern mediates between the domain and data mapping layers, using a collection-like interface for accessing domain objects. It provides an abstraction of data persistence, enabling applications to work with domain models without being concerned about database connections or queries.

Purpose

The primary purpose of the Repository Pattern is to achieve a clean separation and one-way dependency between the domain and data mapping layers. It acts as a conceptual in-memory collection, encapsulating:

  • The set of objects persisted in a data store.
  • Operations performed over them.

Repositories offer a more object-oriented view of the persistence layer, helping achieve loose coupling and keeping domain objects persistence-ignorant.

Advantages

  1. Minimizes Duplicate Query Logic: Encapsulates query construction in one place.
  2. Loose Coupling: Separates the domain logic from the database access code.
  3. Persistence Ignorance: Domain objects remain unaware of database concerns.
  4. Encapsulation: Avoids exposing database-specific details to the application layer.

Implementation Approaches

Repository per Entity or Business Object

This approach creates a dedicated repository implementation for each business object. Only required methods are implemented, adhering to the YAGNI ("You Aren't Gonna Need It") principle.

Guidelines:

  • Implement repositories only for aggregate root objects.
  • Avoid unnecessary methods for entities that do not need them.

Pros:

  • Simplifies interactions for specific aggregates.
  • Keeps repositories focused on business needs.

Cons:

  • May lead to duplicate code if the same operations are repeated across repositories.

Generic Repository Interface

A generic repository interface provides a common structure for all repositories. It typically includes CRUD methods and supports async operations.

Example:

interface IRepository<T> where T : EntityBase {
    Task<T> GetByIdAsync(int id);
    Task<List<T>> ListAsync();
    Task<List<T>> ListAsync(Expression<Func<T, bool>> predicate);
    Task AddAsync(T entity);
    Task DeleteAsync(T entity);
    Task EditAsync(T entity);
}

public abstract class EntityBase {
    public int Id { get; protected set; }
}

Advantages:

  • Consistent interface across entities.
  • Reusable implementation for common operations.

Generic Repository Implementation

A generic repository implementation can be paired with a generic interface to streamline repository creation. For example, using Entity Framework Core:

public class Repository<T> : IRepository<T> where T : EntityBase {
    private readonly ApplicationDbContext _dbContext;

    public Repository(ApplicationDbContext dbContext) {
        _dbContext = dbContext;
    }

    public virtual async Task<T> GetByIdAsync(int id) {
        return await _dbContext.Set<T>().FindAsync(new object[] { id });
    }

    public virtual async Task<List<T>> ListAsync() {
        return await _dbContext.Set<T>().ToListAsync();
    }

    public async Task AddAsync(T entity) {
        _dbContext.Set<T>().Add(entity);
        await _dbContext.SaveChangesAsync();
    }

    public async Task UpdateAsync(T entity) {
        _dbContext.Set<T>().Update(entity);
        await _dbContext.SaveChangesAsync();
    }

    public async Task DeleteAsync(T entity) {
        _dbContext.Set<T>().Remove(entity);
        await _dbContext.SaveChangesAsync();
    }
}

Considerations:

  • Add Unit of Work if SaveChanges() should be deferred.
  • Avoid exposing IQueryable to prevent business logic leakage into higher layers.

Advanced Concepts

Specification Pattern

Repositories that expose multiple query methods can become bloated. The Specification Pattern separates queries into their own types, encapsulating:

  • Query filters (e.g., expressions).
  • Parameters.
  • Data loading requirements (e.g., .Include() for EF Core).

Combining the Repository and Specification patterns ensures adherence to the Single Responsibility and Open Closed principles.

Ardalis.Specification

The Ardalis.Specification NuGet package provides a ready-made implementation of the Repository and Specification patterns, especially for .NET applications using Entity Framework Core. This library helps:

  • Avoid custom query method bloat.
  • Centralize query logic.
  • Maintain clean, testable repository code.

Common Pitfalls

  • Overusing Generic Repositories:
    • Leads to leaky abstractions.
    • May inadvertently couple domain and persistence layers.
  • Exposing IQueryable:
    • Allows higher layers to define queries, violating encapsulation.
  • Misinterpreting as ORM Types:
    • Directly exposing ORM entities (e.g., EF Core types) is not the same as implementing a repository.


The Repository Pattern provides a robust way to manage data persistence while maintaining clean code and loose coupling. However, its implementation must be tailored to the application’s needs, balancing flexibility and encapsulation.