Jump to content

Repository

From Knowledge Base

Repository Pattern

The repository pattern is a software design pattern used to separate the data access layer from the business logic layer of an application. It provides an abstraction that allows to work with data without directly interacting with the underlying data storage system.

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.

Concepts

  1. Separation of Concerns: The repository pattern promotes the separation of concerns within an application. It isolates the data access logic from the rest of the application, making it easier to maintain and test.
  2. Abstraction: A repository acts as an abstraction layer for data access, providing a set of well-defined methods for performing common database operations, such as querying, creating, updating, and deleting records.
  3. Testing: By using the repository pattern, it becomes easier to write unit tests for the application since you can mock or substitute the repository with test data. This ensures that the business logic is independent of the specific data storage implementation.
  4. Flexibility: The repository pattern allows you to change the underlying data storage system (e.g., switching from a SQL database to a NoSQL database) without affecting the rest of the application. This flexibility is particularly useful when dealing with changing data storage requirements.

General Implementation

1. Create Interfaces: Define interfaces that specify the contract for data access operations. These interfaces will typically include methods for querying, creating, updating, and deleting data.

// IRepository interface
public interface IRepository<T> {
    T GetById(int id);
    IEnumerable<T> GetAll();
    void Add(T entity);
    void Update(T entity);
    void Delete(int id);
}

2. Create Repository Classes: Implement concrete repository classes that implements the defined interfaces. These classes will contain the actual data access logic, interacting with the data storage system.

// Concrete repository class
public class CustomeRepository : IRepository<Data> {
    // Implementation of IRepository methods
}

3. Dependency Injection: Use dependency injection to inject the repository implementations into your application's services or controllers. This allows you to switch between different data storage implementations easily.

// Usage in a service
public class DataService {
    private readonly IRepository<Data> _repository;

    public DataService(IRepository<Data> repository) {
        _repository = repository;
    }

    public IEnumerable<Data> GetData() {
        return _repository.GetAll();
    }

    // Other business logic
}

Implementation in EF Core

To implement the repository pattern with Entity Framework Core project:

1. Create a DbContext: Define a class that inherits from DbContext and represents your application's database context. This class will contain DbSet properties for each entity you want to work with. The DbContext class in EF Core is a crucial part of the Entity Framework and it represents a session with the database. It manages database connections, tracks changes to entities, and provides a way to query and interact with the database.

// DbContext class
public class AppDbContext : DbContext {
    public DbSet<Data> Data { get; set; }
    // Other DbSet properties for additional entities
}

// IRepository interface
public interface IRepository<T> {
    T GetById(int id);
    IEnumerable<T> GetAll();
    void Add(T entity);
    void Update(T entity);
    void Delete(int id);
}

2. Create Repository Classes: Implement concrete repository classes that encapsulate data access operations using EF Core. These repository classes will interact with the DbContext and provide a clean API for data access.

// Repository class
public class Repository<T> : IRepository<T> {
    private readonly AppDbContext context;  // Represents the database context

    public Repository(AppDbContext context) {
        this.context = context;  // Inject the database context
    }

    public T GetById(int id) {
        return context.Set<T>().Find(id);
    }

    public IEnumerable<T> GetAll() {
        return context.Set<T>().ToList();
    }

    public void Add(T entity) {
        context.Set<T>().Add(entity);
        context.SaveChanges();
    }

    public void Update(T entity) {
        context.Set<T>().Update(entity);
        context.SaveChanges();
    }

    public void Delete(int id) {
        var entity = context.Set<T>().Find(id);
        if (entity != null) {
            context.Set<T>().Remove(entity);
            context.SaveChanges();
        }
    }
}

3. Dependency Injection: Use dependency injection to inject your repository implementations and the DbContext into your application's services or controllers.

// Usage in a service
public class DataService {
    private readonly IRepository<Data> _repository;  // Repository for the 'Data' entity

    public DataService(IRepository<Data> repository) {
        _repository = repository;  // Inject the repository
    }

    public IEnumerable<Data> GetData() {
        return _repository.GetAll();  // Retrieve data using the repository
    }

    // Other business logic
}

4. Usage: The application business logic or services interact with the repositories rather than directly accessing the DbContext. This ensures that your code remains decoupled from the underlying data access technology.

We can provide different DbContext classes to our Repository:

Providing SQLite
Install-Package System.Data.SQLite
Install-Package Microsoft.EntityFrameworkCore.Sqlite

// SqliteDbContext class
public class SqliteDbContext : DbContext {
    public DbSet<Customer> Customers { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
        optionsBuilder.UseSqlite("Data Source=mydatabase.db");
    }
}
services.AddScoped<IRepository<Data>>(provider => {
    var dbContext = provider.GetRequiredService<SqliteDbContext>();
    return new Repository<Data>(dbContext);
});

Providing MongoDB
Install-Package MongoDB.Driver
Install-Package MongoFramework

// MongoDbContext class
public class MongoDbContext : MongoDbContext {
    public MongoDbSet<Customer> Customers { get; set; }

    protected override void OnConfiguring(IMongoDbConnectionConfigurationBuilder mongoDbConnectionConfigurationBuilder) {
        mongoDbConnectionConfigurationBuilder.UseConnectionString("mongodb://localhost:27017/mydatabase");
    }
}

services.AddScoped<IRepository<Data>>(provider => {
    var dbContext = provider.GetRequiredService<SqliteDbContext>();
    return new Repository<Data>(dbContext);
});

// For MongoDB
services.AddScoped<IRepository<Data>>(provider => {
    var dbContext = provider.GetRequiredService<MongoDbContext>();
    return new Repository<Data>(dbContext);
});


Other 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

  1. Overusing Generic Repositories:
  * Leads to leaky abstractions.
  * May inadvertently couple domain and persistence layers.
  1. Exposing IQueryable:
  * Allows higher layers to define queries, violating encapsulation.
  1. 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.


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.