How to Simplify Data Access Using EF

Written by ssukhpinder | Published 2023/03/10
Tech Story Tags: software-development | design-patterns | repository-pattern | dotnet | programming | entity-framework | csharp | database-design | web-monetization

TLDRThe repository architectural pattern is frequently employed in software development to segregate an application's business logic from the data access layer. The Object-Relational Mapping (ORM) tool Entity Framework for .NET applications offers a simple method for interacting with databases. The article demonstrates how to use the Entity Framework to construct a repository pattern.via the TL;DR App

Implementing a Repository Pattern With Entity Framework

The repository architectural pattern is frequently employed in software development to segregate an application's business logic from the data access layer. It facilitates easier unit testing of the application's business logic and results in a cleaner, more manageable codebase.

Prerequisites

  • Basic knowledge of OOPS concepts.

  • Any programming language knowledge.

So, to begin with, C# and Entity Framework:

Introduction to C#

Get Started with EF Core Migrations: A Step-by-Step Guide

Learning Objectives

  • How to implement a Repository Pattern with Entity Framework

  • Learn more about the separation of concerns

  • How it provides code testability

  • How it provides code reusability

Getting Started

The Object-Relational Mapping (ORM) tool Entity Framework for .NET applications offers a simple method for interacting with databases. The article demonstrates how to use Entity Framework to construct a repository pattern.

Create the Repository Interface

Creating an interface specifying the methods to interact with the data is the first step in implementing the repository design. Please find below an example of the interface.

public interface IRepository<TEntity> where TEntity : class
{
    TEntity Get(int id);
    IEnumerable<TEntity> GetAll();
    IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate);
    void Add(TEntity entity);
    void AddRange(IEnumerable<TEntity> entities);
    void Remove(TEntity entity);
    void RemoveRange(IEnumerable<TEntity> entities);
}

The above interface defines the standard CRUD (Create, Read, Update, Delete) operations typically used when working with data.

Create the Repository Implementation

The IRepository interface's practical implementation is the next step. Please find below the illustration of the implementation.

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
    private readonly DbContext _context;
    private readonly DbSet<TEntity> _dbSet;

    public Repository(DbContext context)
    {
        _context = context;
        _dbSet = context.Set<TEntity>();
    }

    public TEntity Get(int id)
    {
        return _dbSet.Find(id);
    }

    public IEnumerable<TEntity> GetAll()
    {
        return _dbSet.ToList();
    }

    public IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate)
    {
        return _dbSet.Where(predicate);
    }

    public void Add(TEntity entity)
    {
        _dbSet.Add(entity);
    }

    public void AddRange(IEnumerable<TEntity> entities)
    {
        _dbSet.AddRange(entities);
    }

    public void Remove(TEntity entity)
    {
        _dbSet.Remove(entity);
    }

    public void RemoveRange(IEnumerable<TEntity> entities)
    {
        _dbSet.RemoveRange(entities);
    }
}

The above-mentioned Repository class implements the IRepository interface and provides the actual implementation of each method.

Use the Repository in Your Application

Construct an instance of the Repository, and pass it to the DbContext that intends to use it in the application. Please find below an illustration of how the Repository can be used in a console application:

static void Main(string[] args)
{
    using (var context = new MyDbContext())
    {
        var repository = new Repository<Customer>(context);

        var customer = new Customer { Name = "John Doe", Email = "[email protected]" };
        repository.Add(customer);

        var customers = repository.GetAll();
        foreach (var c in customers)
        {
            Console.WriteLine(c.Name);
        }
    }
}

In the above-mentioned illustration, we have constructed a Repository instance for the Customer object and supplied the desired DbContext.

Why Consider a Repository Pattern Along With EF?

Implementing a repository pattern with Entity Framework has several benefits:

Separation of Concerns

The repository pattern helps your code become more modular and easier to maintain by separating the business logic from the data access logic.

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

public class Repository<T> : IRepository<T> where T : class
{
    private readonly DbContext _context;
    private readonly DbSet<T> _entities;

    public Repository(DbContext context)
    {
        _context = context;
        _entities = context.Set<T>();
    }

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

    public T GetById(int id)
    {
        return _entities.Find(id);
    }

    public void Add(T entity)
    {
        _entities.Add(entity);
        _context.SaveChanges();
    }

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

    public void Delete(T entity)
    {
        _entities.Remove(entity);
        _context.SaveChanges();
    }
}

In the above example, we have established an interface IRepository and a practical implementation Repository pattern to distinguish the data access logic from the business logic.

Now, the business logic of our application may communicate with the data access layer via the IRepository interface abstracting how the data is being saved or retrieved.

Testability

Repository patterns simplify developing unit tests for your application's business logic without worrying about the database.

Consider the following example of a service class that depends on the Repository mentioned above:

public class ProductService
{
    private readonly IRepository<Product> _repository;

    public ProductService(IRepository<Product> repository)
    {
        _repository = repository;
    }

    public IEnumerable<Product> GetAllProducts()
    {
        return _repository.GetAll();
    }

    public Product GetProductById(int id)
    {
        return _repository.GetById(id);
    }

    public void AddProduct(Product product)
    {
        _repository.Add(product);
    }

    public void UpdateProduct(Product product)
    {
        _repository.Update(product);
    }

    public void DeleteProduct(Product product)
    {
        _repository.Delete(product);
    }
}

To test the ProductService class, we need to implement a mock repository object that implements the IRepository<Product> interface. Please find below the code example for the same.

[TestClass]
public class ProductServiceTests
{
    [TestMethod]
    public void GetAllProducts_Returns_All_Products()
    {
        // Arrange
        var products = new List<Product>
        {
            new Product { Id = 1, Name = "Product 1", Price = 10.0m },
            new Product { Id = 2, Name = "Product 2", Price = 20.0m },
            new Product { Id = 3, Name = "Product 3", Price = 30.0m }
        };

        var mockRepository = new Mock<IRepository<Product>>();
        mockRepository.Setup(r => r.GetAll()).Returns(products.AsQueryable());
        var productService = new ProductService(mockRepository.Object);

        // Act
        var result = productService.GetAllProducts();

        // Assert
        CollectionAssert.AreEqual(products, result.ToList());
    }

    // Other unit tests for the ProductService class...
}

Code Reuse

The repository design encourages code reuse since it allows you to reuse the same data access logic throughout multiple components of your application.

Let's imagine that our application contains two entities, Customer and Order. Along with retrieving a customer by ID and all of a client's orders, we also want to recover all customers and all orders.

To manage these typical CRUD operations, we can develop the generic repository interface IRepository<TEntity> and the generic repository implementation Repository<TEntity>.

Next, develop the customized repository interfaces and implementations for our Customer and Order entities.

Now, let's create specific repository interfaces and implementations for both Customer and Order entities:

public interface ICustomerRepository : IRepository<Customer>
{
    Customer GetCustomerById(int id);
}

public class CustomerRepository : Repository<Customer>, ICustomerRepository
{
    public CustomerRepository(DbContext dbContext) : base(dbContext)
    {
    }

    public Customer GetCustomerById(int id)
    {
        return GetById(id);
    }
}

public interface IOrderRepository : IRepository<Order>
{
    IQueryable<Order> GetOrdersByCustomer(int customerId);
}

public class OrderRepository : Repository<Order>, IOrderRepository
{
    public OrderRepository(DbContext dbContext) : base(dbContext)
    {
    }

    public IQueryable<Order> GetOrdersByCustomer(int customerId)
    {
        return GetAll().Where(o => o.CustomerId == customerId);
    }
}

By reusing the Repository<TEntity> class for both the CustomerRepository and OrderRepository classes, we are inheriting from the IRepository<TEntity> Interface to provide common CRUD operations.

Doing so can avoid duplicating code and reduce the boilerplate code required for each repository implementation.

Flexibility

The repository design alters the underlying data storage system without impacting your application's business logic. You might switch from a SQL server to a NoSQL database without limiting the business logic.

Conclusion

A repository pattern can create a cleaner, more maintainable codebase and simplify business logic unit testing. It concentrates on developing a more scalable and flexible application by isolating the data access logic from the business logic.

More About Design Patterns

Unit of Work in C#: A Practical Guide for Developers

Follow me on

C# Publication, LinkedIn, Instagram, Twitter, Dev.to


Also published here


Written by ssukhpinder | Programmer by heart | C# | Python | .Net Core | Xamarin | Angular | AWS
Published by HackerNoon on 2023/03/10