Entity Framework Core: Repository Pattern & Unit of Work

The Repository Pattern is an abstraction of the data layer. In essence, a repository acts as a black box that can receive and retrieve data from your database. The beauty of the repository pattern lies in its ability to abstract the implementation details, such as the specific database type or implementation.

Imagine you have an application with a MySQL database. Your application needs to retrieve and store data in this database. Instead of scattering SQL queries throughout your code, you can encapsulate all these operations within a repository. This way, if you ever decide to switch to a different database, the process becomes much simpler.

Benefits of the Repository Pattern

The Repository Pattern offers several advantages, including:

  1. Cleaner and Maintainable Code: By abstracting the data layer, the repository pattern makes your code cleaner and easier to maintain. It separates the data access logic from the rest of your code, improving code organization and readability.

  2. Flexibility for Database Switching: If you ever need to switch to a different database system, you only need to update the repository implementation, rather than modifying your entire codebase. This flexibility saves time and effort in future migrations.

  3. Improved Testability: The repository pattern makes your code more testable. You can use mock repositories in unit tests to simulate database behavior, allowing you to test your business logic without worrying about setting up a real database.

DbContext and DbSet: Repository Pattern Implementation?

Technically, when using Entity Framework's DbContext, it resembles the Unit of Work pattern, and DbSet can be seen as an implementation of the repository pattern.

However, some developers, including myself, prefer having an additional layer of abstraction above the database, making it easier to test and decoupled from Entity Framework.

Ultimately, the decision depends on the design needs and preferences of your team.

When to Use the Repository Pattern?

Like all design patterns, the Repository Pattern is not mandatory in every implementation.

In some situations, it may introduce unnecessary overhead, especially if your application is simple and only requires basic CRUD operations.

Additionally, there may be cases where you want to leverage specific database features that cannot be easily encapsulated within a repository.

Consider the complexity and future scalability of your application when deciding whether to adopt the repository pattern.

Implementing the Repository Pattern in C#

Before we dive into the code, please note that the complete code is available on our GitHub repository. For this example, we will continue with the code we worked on in the Entity Framework Core course.

Let's start by implementing the UserRepository interface and its corresponding class:

public interface IUserRepository
{
    Task<User> Insert(User user);
    Task<User?> GetById(int id);
}

public class UserRepository : IUserRepository
{
    private readonly ejemploEFContext _context;

    public UserRepository(ejemploEFContext context)
    {
        _context = context;
    }

    public async Task<User> Insert(User user)
    {
        EntityEntry<User> insertedUser = await _context.Users.AddAsync(user);
        await _context.SaveChangesAsync();
        return insertedUser.Entity;
    }

    public async Task<User?> GetById(int id)
        => await _context.Users
            .Include(a => a.jobexperiencies)
            .FirstOrDefaultAsync(x => x.Id == id);
}

In the above code, we define the UserRepository interface with methods for inserting a user and retrieving a user by ID. The UserRepository class implements these methods using the provided DbContext.

Next, let's create the generic repository interface and its implementation:

public interface IGenericRepository<T> where T : class
{
    Task<T> Insert(T entity);
    Task<T?> GetById(int id);
}

public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    private readonly ejemploEFContext _context;

    public GenericRepository(ejemploEFContext context)
    {
        _context = context;
    }

    public async Task<T> Insert(T entity)
    {
        EntityEntry<T> insertedEntity = await _context.Set<T>().AddAsync(entity);
        await _context.SaveChangesAsync();
        return insertedEntity.Entity;
    }

    public async Task<T?> GetById(int id)
        => await _context.Set<T>()
            .FindAsync(id);
}

In this code, we define the IGenericRepository interface with generic methods for inserting an entity and retrieving an entity by ID. The GenericRepository class implements these methods using the provided DbContext and DbSet.

To utilize these repositories, we need to add them to the dependency injection container:

builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IGenericRepository<Workingexperience>, GenericRepository<Workingexperience>>();

Now, we can refactor our existing code to use the repository pattern. Here's an example:

public class RelationsController : Controller
{
    private readonly IUserRepository _userRepository;
    private readonly IGenericRepository<Workingexperience> _workingExperienceRepository;

    public RelationsController(IUserRepository userRepository, IGenericRepository<Workingexperience> workingExperienceRepository)
    {
        _userRepository = userRepository;
        _workingExperienceRepository = workingExperienceRepository;
    }

    [HttpPost("InsertDataExample1")]
    public async Task InsertDataExample1()
    {
        User user1 = new User()
        {
            Email = $"{Guid.NewGuid()}@mail.com",
            UserName = "id1"
        };

        List<Workingexperience> workingExperiences1 = new List<Workingexperience>()
        {
            new Workingexperience()
            {
                User = user1,
                Name = "experience 1",
                Details = "details1",
                Environment = "environment"
            },
            new Workingexperience()
            {
                User = user1,
                Name = "experience 2",
                Details = "details2",
                Environment = "environment"
            }
        };

        _ = await _userRepository.Insert(user1);
        await _workingExperienceRepository.Insert(workingExperiences1);
    }
}

In the updated code, we inject the UserRepository and WorkingExperienceRepository into the RelationsController. We use these repositories to insert the user and working experiences.

By implementing the repository pattern, our code becomes more maintainable, testable, and decoupled from specific database implementations.