Generic Repository with EF Core in .NET Core 8

Introduction

Observe the Entity Framework Core Generic Repository. The subject that will make some people uncomfortable. They are unwilling to discuss it at all. Others, on the other hand, adore it and become giddy at the mere mention of the generic repository pattern.

The generic repository pattern has advantages and disadvantages like anything else. Whether or not it works well for your project is up to you. You don't have to commit to using the generic repository approach entirely—you can always use it for just a portion of your application.

Having a generic CRUD repository has the benefit of allowing you to pass its entity type, inherit from it, and have a CRUD repository for any type of entity with little to no code.

Note: We won't construct something that will forever satisfy all of your demands. Instead, we will work to establish the framework for a generic repository that you can use to quickly and simply create CRUD operations, and then modify it to suit your needs. Thus, this will only address CRUD-capable generic repositories. You might extend the Repository class and inherit it for anything more. This is merely a concept proof. You don't want your Web layer to be aware of your Database layer in larger real-world applications. As a result, your controllers won't have any repository injections done to them.

It's only applicable to certain aspects of your application, once again. It's not required that you use it as the only option for all of your database requirements.

As an illustration

I will create a new empty Asp.Net Core Web API project.

Prerequisites

We need to install the below-mentioned packages to the application.

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer

Set up the DbContext

Here is the code for the Emp.cs class.

public class Emp
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public EmployeeType Type { get; set; }
    public string Mno { get; set; }
    public decimal Salary { get; set; }
}

Here is the code for the EmpDBContext.cs class.

public class EmpDBContext : DbContext
{
    public EmpDBContext(DbContextOptions<EmpDBContext> dbContextOptions)
        : base(dbContextOptions)
    {
    }
    public DbSet<Emp> Emps { get; set; }
    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
    }
}

As you can see, we have a matching table in the database represented by the Emps DbSet.

We are developing a constructor that takes a parameter called DbContextOptions. This will allow us to pass options to the Startup class.

builder.Services.AddDbContext<EmpDBContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServer")));

We're going to add the following connection string to appsettings.json so that I can communicate with the database.

"ConnectionStrings": {
  "SqlServer": "Data Source=server_name;Initial Catalog=EmpDB;Integrated Security=True;TrustServerCertificate=True"
}

Constructing a generic repository

Similar to Entity Framework 6, DbContext is used in EF Core to query a database and aggregate changes that are to be written back to the store collectively.

The fantastic thing about the DbContext class is that it supports generics on methods that we'll use to communicate with the database because it is generic.

Here is the code for the FindOptions.cs class.

public class FindOptions
{
    public bool IsIgnoreAutoIncludes { get; set; }
    public bool IsAsNoTracking { get; set; }
}

Here is the code for the IRepository.cs class.

public interface IRepository<TEntity> where TEntity : class
{
    IQueryable<TEntity> GetAll(FindOptions? findOptions = null);
    TEntity FindOne(Expression<Func<TEntity, bool>> predicate, FindOptions? findOptions = null);
    IQueryable<TEntity> Find(Expression<Func<TEntity, bool>> predicate, FindOptions? findOptions = null);
    void Add(TEntity entity);
    void AddMany(IEnumerable<TEntity> entities);
    void Update(TEntity entity);
    void Delete(TEntity entity);
    void DeleteMany(Expression<Func<TEntity, bool>> predicate);
    bool Any(Expression<Func<TEntity, bool>> predicate);
    int Count(Expression<Func<TEntity, bool>> predicate);
}

The generic TEntity type—which corresponds to our entity type in the database—will be the first thing you notice (Emp, Department, User, Role, etc.).

Here is the code for the implementation of the IRepository interface.

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
    private readonly EmpDBContext _empDBContext;
    public Repository(EmpDBContext empDBContext)
    {
        _empDBContext = empDBContext;
    }
    public void Add(TEntity entity)
    {
        _empDBContext.Set<TEntity>().Add(entity);
        _empDBContext.SaveChanges();
    }
    public void AddMany(IEnumerable<TEntity> entities)
    {
        _empDBContext.Set<TEntity>().AddRange(entities);
        _empDBContext.SaveChanges();
    }
    public void Delete(TEntity entity)
    {
        _empDBContext.Set<TEntity>().Remove(entity);
        _empDBContext.SaveChanges();
    }
    public void DeleteMany(Expression<Func<TEntity, bool>> predicate)
    {
        var entities = Find(predicate);
        _empDBContext.Set<TEntity>().RemoveRange(entities);
        _empDBContext.SaveChanges();
    }
    public TEntity FindOne(Expression<Func<TEntity, bool>> predicate, FindOptions? findOptions = null)
    {
        return Get(findOptions).FirstOrDefault(predicate)!;
    }
    public IQueryable<TEntity> Find(Expression<Func<TEntity, bool>> predicate, FindOptions? findOptions = null)
    {
        return Get(findOptions).Where(predicate);
    }
    public IQueryable<TEntity> GetAll(FindOptions? findOptions = null)
    {
        return Get(findOptions);
    }
    public void Update(TEntity entity)
    {
        _empDBContext.Set<TEntity>().Update(entity);
        _empDBContext.SaveChanges();
    }
    public bool Any(Expression<Func<TEntity, bool>> predicate)
    {
        return _empDBContext.Set<TEntity>().Any(predicate);
    }
    public int Count(Expression<Func<TEntity, bool>> predicate)
    {
        return _empDBContext.Set<TEntity>().Count(predicate);
    }
    private DbSet<TEntity> Get(FindOptions? findOptions = null)
    {
        findOptions ??= new FindOptions();
        var entity = _empDBContext.Set<TEntity>();
        if (findOptions.IsAsNoTracking && findOptions.IsIgnoreAutoIncludes)
        {
            entity.IgnoreAutoIncludes().AsNoTracking();
        }
        else if (findOptions.IsIgnoreAutoIncludes)
        {
            entity.IgnoreAutoIncludes();
        }
        else if (findOptions.IsAsNoTracking)
        {
            entity.AsNoTracking();
        }
        return entity;
    }
}

If you observed that the GetAll, Find, and FindOne methods used the find options class, basically based on the provided configuration, the system should fetch data.

Let's develop the Emp repository interface.

public interface IEmpRepository : IRepository<Emp>
{
    //TODO: Write here custom methods that are required for specific requirements.
}

Since this inherits from the IRepository interface, all of these methods must be implemented.

All those methods from IRepository will be covered, though, if we build an EmpRepository that derives from Repository. Furthermore, we must implement IEmpRepository.

Here's how it would appear.

public class EmpRepository : Repository<Emp>, IEmpRepository
{
    public EmpRepository(EmpDBContext empDBContext)
        : base(empDBContext)
    {
    }
    //TODO: Write here custom methods that are required for specific requirements.
}

We have to register the Repository and EmpRepository in the DI container for usage in our application; otherwise, it will not resolve those dependencies and give an error in the runtime.

Here is the code to register the dependencies in the DI container.

builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
builder.Services.AddScoped<IEmpRepository, EmpRepository>();

Here is the code for the EmpController.cs class, inside that, inject the IEmpRepository interface, resolve the dependency on the constructor side, and use it in our application.

[Route("api/[controller]")]
[ApiController]
public class EmpController : ControllerBase
{
    private readonly IEmpRepository _empRepository;
    public EmpController(IEmpRepository empRepository)
    {
        _empRepository = empRepository;
    }
    [HttpGet]
    public IActionResult Get()
    {
        IEnumerable<Emp> employees = _empRepository.GetAll();
        return Ok(employees);
    }
    [HttpGet("{id}", Name = "Get")]
    public IActionResult Get(Guid id)
    {
        Emp employee = _empRepository.FindOne(x => x.Id == id);
        if (employee == null)
        {
            return NotFound("The Employee record couldn't be found.");
        }
        return Ok(employee);
    }
    [HttpPost]
    public IActionResult Post([FromBody] Emp employee)
    {
        if (employee == null)
        {
            return BadRequest("Employee is null.");
        }

        employee.Id = Guid.NewGuid();
        _empRepository.Add(employee);
        return CreatedAtRoute(
              "Get",
              new { Id = employee.Id },
              employee);
    }

    [HttpPut("{id}")]
    public IActionResult Put(Guid id, [FromBody] Emp employee)
    {
        if (employee == null)
        {
            return BadRequest("Employee is null.");
        }
        Emp employeeToUpdate = _empRepository.FindOne(x => x.Id == id);
        if (employeeToUpdate == null)
        {
            return NotFound("The Employee record couldn't be found.");
        }

        employeeToUpdate.Mno = employee.Mno;
        employeeToUpdate.Salary = employee.Salary;
        employeeToUpdate.Name = employee.Name;
        employeeToUpdate.Type = employee.Type;

        _empRepository.Update(employeeToUpdate);
        return NoContent();
    }

    [HttpDelete("{id}")]
    public IActionResult Delete(Guid id)
    {
        Emp employee = _empRepository.FindOne(x => x.Id == id);
        if (employee == null)
        {
            return NotFound("The Employee record couldn't be found.");
        }
        _empRepository.Delete(employee);
        return NoContent();
    }
}

Let's run the application, and we will perform the get-all-employees operation.

Emp

Request URL

You need to perform all the operations (POST, PUT, DELETE, GET), then you need to download the sample code, unzip it, and open it in Visual Studio.

Change the database connection string, run the application, and play around in this application as well.

The only requirement for this sample code is that it be written using the .NET 8 version, as the sample application was. If you haven't already, I would prefer that you install the .NET 8 version on your computer before attempting to use it.

We learned the new technique and evolved together.

Happy coding!