Repository Pattern for modular code

𝗪𝗵𝗮t is the 𝗿𝗲𝗽𝗼𝘀𝗶𝘁𝗼𝗿𝘆 𝗽𝗮𝘁𝘁𝗲𝗿𝗻?

The repository pattern is used to create an abstraction layer between the application and the data storage, allowing the application to interact with the data storage in a standardized way without having to know the details of how the data is stored. This makes the code more modular, testable, and maintainable.

By using the repository pattern, it’s also possible to switch the data storage technology without affecting the application’s code, which can be useful when you need to replace or scale your data storage.
However, it’s important to note that the repository pattern should not be overused or misused.

It’s best to use it in situations where you have a complex data access layer or need to have multiple data sources or data storage technologies in your application. For simpler applications, a simpler approach to data access may be sufficient.

Customer CRUD repository in C#

Let’s begin with a simple CRUD (Create Read Update Delete) example.

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

// Customer.cs
public class Customer
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

// CustomerRepository.cs
public class CustomerRepository : IRepository<Customer>
{
    private readonly DbContext _context;

    public CustomerRepository(DbContext context)
    {
        _context = context;
    }

    public Customer GetById(int id)
    {
        return _context.Customers.Find(id);
    }

    public void Add(Customer entity)
    {
        _context.Customers.Add(entity);
        _context.SaveChanges();
    }

    public void Update(Customer entity)
    {
        _context.Entry(entity).State = EntityState.Modified;
        _context.SaveChanges();
    }

    public void Delete(Customer entity)
    {
        _context.Customers.Remove(entity);
        _context.SaveChanges();
    }
}

// Usage example
var context = new MyDbContext();
var customerRepository = new CustomerRepository(context);
var customer = customerRepository.GetById(1);
customer.FirstName = "John";
customerRepository.Update(customer);

In this example, we have an IRepository interface with methods for CRUD operations. We also have a Customer class that represents a domain entity.

The CustomerRepository class implements the IRepository<Customer> interface, providing an implementation for each method. The DbContext class is used for data access and is injected into the repository’s constructor.

Finally, we have an example usage of the CustomerRepository, where we get a customer by ID, update its first name, and then persist the changes to the database.

The repository encapsulates the data access logic and presents a clean, domain-specific interface to the business layer.

In large projects, it may be necessary to manage transactions across multiple repositories. The easiest approach to achieve this is to use a repository wrapper.

Simplify managing transactions across multiple repositories with Repository Wrapper

A repository wrapper is a design pattern that provides a simplified interface to access one or more repositories consistently. The main concern of a repository wrapper is to abstract away the details of data access and provide a high-level interface that is easier to work with.

The repository wrapper pattern is particularly useful in large projects where there may be multiple repositories for different entities, each with its own DbContext, and where there is a need to manage transactions across multiple repositories.

By creating a repository wrapper, we can consolidate all the repository logic into a single place, which can help reduce duplication and make the code easier to maintain. The wrapper can also provide additional functionality such as transaction management, caching, and logging.

Here’s an example of a repository wrapper in C#:

// IRepositoryWrapper.cs
public interface IRepositoryWrapper
{
    ICustomerRepository Customers { get; }
    IOrderRepository Orders { get; }
    void Save();
}

// RepositoryWrapper.cs
public class RepositoryWrapper : IRepositoryWrapper
{
    private readonly CustomerDbContext _customerContext;
    private readonly OrderDbContext _orderContext;
    private ICustomerRepository _customerRepository;
    private IOrderRepository _orderRepository;

    public RepositoryWrapper(CustomerDbContext customerContext, OrderDbContext orderContext)
    {
        _customerContext = customerContext;
        _orderContext = orderContext;
    }

    public ICustomerRepository Customers
    {
        get
        {
            if (_customerRepository == null)
            {
                _customerRepository = new CustomerRepository(_customerContext);
            }

            return _customerRepository;
        }
    }

    public IOrderRepository Orders
    {
        get
        {
            if (_orderRepository == null)
            {
                _orderRepository = new OrderRepository(_orderContext);
            }

            return _orderRepository;
        }
    }

    public void Save()
    {
        _customerContext.SaveChanges();
        _orderContext.SaveChanges();
    }
}

// Usage example
var customerDbContext = new CustomerDbContext();
var orderDbContext = new OrderDbContext();
var repositoryWrapper = new RepositoryWrapper(customerDbContext, orderDbContext);

var customer = repositoryWrapper.Customers.GetById(1);
var order = repositoryWrapper.Orders.GetById(1);
order.Customer = customer;

repositoryWrapper.Save();

In this example, we have an IRepositoryWrapper interface that defines properties for each repository, and a RepositoryWrapper class that implements the interface and provides implementations for each property.

Each repository property is implemented using lazy loading, which means that the repository is only created the first time it is accessed. This helps to reduce unnecessary instantiation of repositories.

Finally, we have an example usage of the RepositoryWrapper, where we get a customer by ID using the Customers property, and then update an order with that customer using the Orders property. We then save the changes to both DbContexts using the Save method on the RepositoryWrapper.

When to use repository wrapper ?

Repository wrapper can be used in situations where you have multiple repositories and want to provide a simplified and consistent interface to access them. Here are some scenarios where you might want to use a repository wrapper:

  1. Multiple repositories for different entities: If you have multiple repositories for different entities (such as customers, orders, products, etc.), a repository wrapper can help consolidate the repository logic into a single place.
  2. Transactions across multiple repositories: If you need to perform a transaction that involves multiple repositories (such as updating an order and deducting the order amount from a customer’s balance), a repository wrapper can help manage the transaction across multiple DbContexts.
  3. Separation of concerns: A repository wrapper can help separate the concerns of data access and business logic. By providing a high-level interface to access the repositories, the business logic can be kept separate from the details of data access.
  4. Testability: A repository wrapper can make it easier to write unit tests for the business logic, as the repositories can be mocked or replaced with test implementations.

Overall, a repository wrapper can help simplify the code and make it easier to maintain, especially in large projects where there may be multiple repositories and complex business logic. However, it is important to note that a repository wrapper may not be necessary or appropriate for every project, and should be evaluated on a case-by-case basis.

While the repository wrapper can simplify managing transactions across multiple repositories, it’s important to consider performance when retrieving and persisting entities. One approach to improving performance is using non-hydrated entities, which can reduce the amount of data that needs to be transferred and processed. By combining the repository pattern with non-hydrated entities, we can optimize data retrieval and persistence while reducing the number of database queries executed and the amount of data transferred, leading to better overall performance.

Improving performance is using non-hydrated entities (aka DTOs)

Suppose you have a database table called « Customers » with the following columns: « CustomerId », « FirstName », « LastName », « Email », and « Phone ». You want to display a list of all customers in your application, but you only need to display their first name, last name, and email address.

First, you can define a non-hydrated entity called « CustomerDTO » to represent the subset of data you want to display:

public class CustomerDTO
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}

Next, you can define a repository interface for managing customer entities:

public interface ICustomerRepository
{
    IEnumerable<CustomerDTO> GetAllCustomers();
}

Then, you can implement the repository using Entity Framework and LINQ to retrieve only the required data and map it to the « CustomerDTO » entity:

public class CustomerRepository : ICustomerRepository
{
    private readonly MyDbContext _context;

    public CustomerRepository(MyDbContext context)
    {
        _context = context;
    }

    public IEnumerable<CustomerDTO> GetAllCustomers()
    {
        return _context.Customers.Select(c => new CustomerDTO
        {
            FirstName = c.FirstName,
            LastName = c.LastName,
            Email = c.Email
        });
    }
}

Finally, you can use the repository in your application code to retrieve and display the list of customers:

public class CustomerController : Controller
{
    private readonly ICustomerRepository _customerRepository;

    public CustomerController(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository;
    }

    public IActionResult Index()
    {
        var customers = _customerRepository.GetAllCustomers();
        return View(customers);
    }
}

In this example, we are only retrieving the data that we need by selecting only the required fields from the database using Entity Framework and mapping them to the « CustomerDTO » entity. This can help to reduce the amount of data that needs to be transferred and improve the performance of the application. Additionally, the repository pattern provides a layer of abstraction over the data access operations, which can help to reduce the number of database queries that are executed and improve performance.

Conclusion

Repository is like the glue between Business logic and DAL.

It acts as a mediator between the business layer and the data access layer (DAL), providing a way for the business layer to interact with the DAL without having to know the details of how the data is stored or retrieved.

The repository encapsulates the data access logic and presents a clean, domain-specific interface to the business layer. This allows the business layer to focus on its core responsibilities and delegate data access tasks to the repository.

However, it’s important to note that the repository should not be considered a part of the DAL itself. While it may use DAL components internally, the repository should be treated as a separate layer that is responsible for translating between the domain objects and the underlying data storage.

As with any software design pattern or technique, the repository pattern should be used thoughtfully and with consideration of the specific needs and requirements of the project. While it can bring many benefits, such as improved testability, maintainability, and scalability, it is not a silver bullet solution for all situations.

It is important to carefully consider the trade-offs of using the repository pattern, such as increased complexity or decreased performance, and to determine if it is the best fit for the project. Additionally, it is important to consider other factors such as the size and scope of the project, the skill level and experience of the development team, and the expected usage patterns and growth potential of the application.

By taking a thoughtful and balanced approach to using the repository pattern, developers can harness its benefits while avoiding potential pitfalls and ensuring that it is used effectively and appropriately in their projects.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *