👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
Enterprise Design Pattern - Repository

Enterprise Design Pattern - Repository

Author - Abdul Rahman (Bhai)

Design Pattern

13 Articles

Improve

Table of Contents

  1. What we gonna do?
  2. Why we gonna do?
  3. How we gonna do?
  4. Summary

What we gonna do?

In this article, let's learn about Repository Design Pattern in .NET.

The Repository pattern is a commonly used enterprise design pattern, especially in enterprise applications. It helps to abstract and encapsulate data access logic, promoting a clean separation of concerns between business logic and data access logic.

Why we gonna do?

The Repository pattern provides a way to manage data access with minimal code duplication and improved testability. It abstracts the data layer, making the business logic unaware of the underlying data source. This allows developers to switch persistence technologies, if needed, without impacting the business logic. Repository Pattern in other words allows developer to have a specific persistence for each repository based on need.

Instead of directly interacting with the database using tools like ADO.NET or Entity Framework Core, the business logic communicates with repositories. These repositories provide a set of methods to perform CRUD (Create, Read, Update, Delete) operations on data entities.

How we gonna do?

Implementing Repository Pattern

A typical repository starts with a contract, often defined as an interface. This interface defines common data operations that can be reused across different entities, reducing redundancy in your codebase. For example:


public interface IRepository<T> where T : class
{
    Task<T> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task AddAsync(T entity);
    void Update(T entity);
    void Delete(T entity);
    Task SaveChangesAsync();
}
            

Generic Repository

A generic repository allows you to define common CRUD operations once and reuse them across multiple entities, reducing code duplication. In this example, lets take EF Core as the ORM and define a generic repository that implements the IRepository interface.


public class GenericRepository<T> : IRepository<T> where T : class
{
    private readonly DbContext _context;
    private readonly DbSet<T> _dbSet;

    public GenericRepository(DbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    public async Task<T> GetByIdAsync(int id) => await _dbSet.FindAsync(id);
    public async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync();
    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 SaveChangesAsync() => await _context.SaveChangesAsync();
}
            
Advantages
  • Reusability: Common data operations are defined once and reused across multiple entities.
  • Consistency: All entities have a consistent API for CRUD operations.
Disadvantages
  • Lack of Specificity: May not provide methods tailored to specific entities, requiring additional custom methods.
  • Over-generalization: Can become too abstract, making it difficult to implement entity-specific logic.

Non Generic Repository

Non-generic repositories are tailored to specific entities. They define custom methods that are unique to the entity they represent. But also have the same methods defined in the generic repository. For example:


public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(int id);
    Task<IEnumerable<Order>> GetAllAsync();
    Task<IEnumerable<Order>> GetOrdersByCustomerIdAsync(int customerId);
    Task AddAsync(Order entity);
    void Update(Order entity);
    void Delete(Order entity);
    Task SaveChangesAsync();
}
            
Advantages
  • Specificity: Tailored to the needs of specific entities, making them more flexible for complex queries and operations.
  • Clear Intent: The repository's purpose is clear, as it deals with a specific entity.
Disadvantages
  • Potential Code Duplication: Common CRUD operations may be repeated across different repositories.
  • Maintenance Overhead: Managing multiple repositories can become cumbersome in large projects.

Combining Generic and Non Generic Repository

In practice, you can mix both approaches. Use a generic repository for common operations and a non-generic repository for entity-specific methods. All we need to do is to create a non-generic repository that inherits from the generic repository and implements the entity-specific methods. This way, we can reuse the common CRUD operations defined in the generic repository and add custom methods as needed reducing massive code duplication.


public interface IOrderRepository : IRepository<Order>
{
    Task<IEnumerable<Order>> GetOrdersByCustomerIdAsync(int customerId);
}
            

public class OrderRepository(DbContext dbContext) : GenericRepository<Order>(dbContext), IOrderRepository
{
    private readonly DbContext _dbContext = dbContext;
    
    public async Task<IEnumerable<Order>> GetOrdersByCustomerIdAsync(int customerId)
    {
        return await _dbContext.Orders.Where(x => x.CustomerId == customerId).ToListAsync();
    }
}
            

FAQs

Entity Framework Core and the Repository Pattern

Entity Framework Core (EF Core) can be used with the Repository pattern to simplify data access. While EF Core provides a built-in repository pattern, you can choose whether to use it directly or implement your own.

Should You Use EF Core's Built-In Repository?

Entity Framework Core provides a repository implementation via the DbSet. But it is considered a bit leaky, so it's often encapsulated in another repository just as we did in the demos.

Multiple SaveChangesAsync()?

The problem with multiple SaveChangesAsync() calls in each respository is that they can lead to inconsistent data states. To avoid this, consider using a Unit of Work pattern to manage transactions across repositories which we will cover in next article.

Summary

The Repository pattern, whether generic, non-generic, or a combination, is a powerful tool for managing data access in a clean, maintainable way. It enhances testability, flexibility, and maintainability, allowing your application to adapt as requirements change. But, we've currently got those SaveChanges called in our repositories, does such a method really belong there? And what if we need to work with transactions across repositories? That is where the Unit of Work pattern comes in. Let's have a look at that one next.

👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • Design Pattern
  • Enterprise
  • Repository