Repository Pattern

The repository is intended to create an abstraction layer between the data access layer and the business logic layer of an application. Repositories are part of the Domain-Driven Design approach, so first let's quickly cover what is DDD.

Domain-Driven Design

In the context of domain-driven design, a domain could be defined as a sphere of knowledge and activity around which the application logic revolves.

Domain-Driven Design was introduced by Eric Evans in his 2004 book Domain-Driven Design: Tackling Complexity in the Heart of Software.

DDD focuses on three core principles:

  • Focus on the core domain and domain logic.

  • Base complex designs on models of the domain.

  • Collaboration with domain experts to improve the application model and resolve any emerging domain-related issues.

When talking about repositories in terms of DDD, it is important to understand some key concepts:

  • Entity: a domain model element that represents some domain object. It has attributes and methods. It has an identity that never changes through the life cycle of an entity.

  • Value Objects: they have attributes and methods as entities. Attributes of value objects are immutable though which implies that methods of value objects can only be queries, never commands that change the internal state of an object.

  • Aggregate: they group entities and value objects into a cohesive unit. The main entity of the aggregate is called an aggregate root. Clients are not allowed to access the other elements of the aggregate directly but only through the root entity. This way, external objects no longer have direct access to every individual entity or value object within the aggregate but instead, only have access to the single aggregate root item.

  • Service: an operation or form of business logic. It is a functionality that can't be related to some specific entity.

  • Factories: they encapsulate the logic of creating complex objects and aggregates, ensuring that the client does not know the inner workings of object manipulation.

  • Repository: a service that uses a global interface to provide access to all entities and value objects within a particular aggregate collection. Methods should be defined to allow for the creation, modification, and deletion of objects within the aggregate.

Using Repository Pattern with Entity Framework

Per DDD patterns, you should encapsulate domain behavior and rules within the entity class itself, so it can control invariants, validations, and rules when accessing any collection. This is possible by using POCO code-first entities.

public class Order : Entity
{
    private readonly List<OrderItem> _orderItems;
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;

    protected Order() { }

    public Order(int buyerId, int paymentMethodId, Address address)
    {
        // Initializations ...
    }

    public void AddOrderItem(int productId, string productName,
                             decimal unitPrice, decimal discount,
                             string pictureUrl, int units = 1)
    {
        // Validation logic...

        var orderItem = new OrderItem(productId, productName,
                                      unitPrice, discount,
                                      pictureUrl, units);
        _orderItems.Add(orderItem);
    }
}

At the implementation level, a repository is simply a class with data persistence code coordinated by a unit of work (DBContext in EF Core) when performing transactions.

public class BuyerRepository : IBuyerRepository
    {
        private readonly OrderingContext _context;
        public IUnitOfWork UnitOfWork
        {
            get
            {
                return _context;
            }
        }

        public BuyerRepository(OrderingContext context)
        {
            _context = context ?? throw new ArgumentNullException(nameof(context));
        }

        public Buyer Add(Buyer buyer)
        {
            return _context.Buyers.Add(buyer).Entity;
        }

        public async Task<Buyer> FindAsync(string buyerIdentityGuid)
        {
            var buyer = await _context.Buyers
                .Include(b => b.Payments)
                .Where(b => b.FullName == buyerIdentityGuid)
                .SingleOrDefaultAsync();

            return buyer;
        }
    }

Each repository should contain only the persistence methods that update the state of entities contained by the specific aggregate that is related to the repository.

A repository operates upon an Aggregate Root. In other words, the root is the API of the aggregate.

Using DbContext directly

The Entity Framework DbContext class is based on the Unit of Work and Repository patterns and can be used directly from your code, such as from services or controllers. This results in a simpler code but has some drawbacks.

Repository Pattern is intended to encapsulate the persistence layer so it is decoupled from the application and domain-model layers. This means that when using Repository Pattern and custom repositories it is easy to mock repositories simulating access to the database. But, when using DbContext directly, you would have to mock DbContext or use an in-memory database which is a lot harder. Also, another option is to use a real database during tests. But in this case, we are talking about integration tests, not unit tests, which are a lot slower.

Another issue is caused by LINQ queries that are scattered over methods in services and often have some complex logic or are hidden behind if statements. This makes it harder to write integration tests for them. Also, inline LINQ queries inside services are not reusable and they tend to duplicate themselves over the codebase.

Takeaways

For simple applications, it makes sense to use DbContext directly, but in most cases, it is better to use the repository pattern. It is easier to test the persistence layer and it improves the readability of the queries.

Repositories are frequently used in CQRS pattern or other lightweight variants of the CQRS pattern.

Repository pattern does have some drawbacks, the most important one is that you accumulate dozens of similar methods used for querying. There are ways to overcome this drawback, one is to use a query object pattern.

References: