Using Both Generic and Non-Generic Repository Pattern in ASP.NET Core MVC

Using Both Generic and Non-Generic Repository Patterns in ASP.NET Core MVC

In this article, I will discuss Using Generic and Non-Generic Repository Patterns in ASP.NET Core MVC applications using Entity Framework Core. The Generic Repository contains the methods that are common for all entities. But if you want some specific operation for some specific entities, you need to create a Specific Repository with the required operations. We will also work with the same example we have worked on so far.

Generic Repository Design Pattern in C#

The Generic Repository Design Pattern in ASP.NET Core MVC defines common database operations for all the database entities in a single class, such as Create, Retrieve, Update, Delete, etc. That means the common operations for all the database entities will be defined inside the Generic Repository.

Non-Generic Repository Pattern (Specific Repository)

The Non-Generic or Basic Repository Design Pattern in ASP.NET Core MVC defines the database operations related to a specific entity in a separate class. For example, if you have two entities, Employee and Customer, each entity will have its own implementation Repository. That means the common operations will be defined inside the Generic Repository, and the operations specific to an entity will be defined in a separate repository.

Using Both Generic and Non-Generic Repository Patterns in ASP.NET Core MVC

So, using both Generic and Non-Generic Repository Patterns together in ASP.NET Core MVC with Entity Framework Core (EF Core) can offer a balanced approach that provides both patterns’ benefits. So, before implementing both generic and specific repositories, let us first understand the implementation guidelines. 

Repository Pattern Implementation Guidelines

We cannot use specific operations for an entity with the Generic Repository. We can only define the common operations which are common for all entities. In the case of a Non-Generic Repository, we have to create a separate repository for each entity, which will have both common and specific operations of the entity. It means the specific repository will be inherited from the Generic Repository.

So, a better way is to create a Generic Repository for commonly used CRUD operations, and for specific operations, create a Non-Generic Repository for each entity and then inherit the Non-Generic Repository from the Generic Repository. For a better understanding, please have a look at the following image.

Using Generic and Non-Generic Repository Patterns in ASP.NET Core MVC applications using Entity Framework Core

As you can see in the above image, the Generic Repository contains common operations such as GetAll, GetById, Insert, Update, and Delete, which will be common for all Entities. The specific or Non-Generic Repository, i.e., EmployeeRepository, contains GetEmployeesByGender and GetEmployeesByDepartment operations, which are going to be used only by Employee Entity, and again, this Non-Generic Repository inherits from the Generic Repository. Similarly, the Non-Generic Repository, i.e., ProductRepository contains GetActiveProducts and GetProductsByCategory operations, which are going to be used only by Product Entity, and again, this ProductRepository inherits from the Generic Repository.

Example to Understand Generic and Non-Generic Repository in ASP.NET Core MVC:

The following are the steps to implement Generic and Non-Generic Repository in ASP.NET Core MVC:

  • Generic Repository: Create a generic repository that handles common CRUD operations. This will serve as a base for all entities. Example: GenericRepository<T> for basic add, delete, get, and update operations.
  • Specific Repositories: For each entity that requires specialized data handling or complex queries, create a specific repository. These specific repositories can be inherited from the generic repository and extended with additional methods to the entity’s specific needs. Example: ProductRepository for the Product entity, with methods for complex queries related to products. EmployeeRepository for the Employee entity, with methods for complex queries related to Employees.
  • Repository Interfaces: Define interfaces for both generic and specific repositories. This promotes the Dependency Inversion Principle and makes your code more testable. Example: IGenericRepository<T> and IProductRepository.
  • Dependency Injection: Register Generic and Specific Repositories in the DI (Dependency Injection) container.
  • Usage in Controllers or Services: Inject the appropriate repository type (generic or specific) depending on the needs of the controller or service.
Modifying IGenericRepository

We will work with the same example we have worked on so far. Let us first modify the IGenericRepository.cs file as shown below. This interface will define the common database operations for all entities, such as Employees, Customers, Products, Payments, Departments, etc., of our application.

namespace CRUDinCoreMVC.GenericRepository
{
    //Here, we are creating the IGenericRepository interface as a Generic Interface
    //Here, we are applying the Generic Constraint 
    //The constraint is, T is going to be a class
    public interface IGenericRepository<T> where T : class
    {
        Task<IEnumerable<T>> GetAllAsync();
        Task<T?> GetByIdAsync(object Id);
        Task InsertAsync(T Entity);
        Task UpdateAsync(T Entity);
        Task DeleteAsync(object Id);
        Task SaveAsync();
    }
}
Modifying GenericRepository

Next, modify the GenericRepository.cs file as shown below. This class implements the IGenericRepository<T> interface, where T will be a class. The following code implements the Generic Repository, where we implement the code for common CRUD operations for each entity.

using CRUDinCoreMVC.Models;
using Microsoft.EntityFrameworkCore;
namespace CRUDinCoreMVC.GenericRepository
{
    //The following GenericRepository class Implement the IGenericRepository Interface
    //And Here T is going to be a class
    //While Creating an Instance of the GenericRepository type, we need to specify the Class Name
    //That is we need to specify the actual Entity Nae for the type T
    public class GenericRepository<T> : IGenericRepository<T> where T : class
    {
        //The following variable is going to hold the EFCoreDbContext instance
        protected readonly EFCoreDbContext _context;

        //The following Variable is going to hold the DbSet Entity
        protected readonly DbSet<T> _dbSet;

        //we are initializing the context object and DbSet variable
        public GenericRepository(EFCoreDbContext context)
        {
            _context = context;

            //Whatever Entity name we specify while creating the instance of GenericRepository
            //That Entity name  will be stored in the DbSet<T> variable
            _dbSet = context.Set<T>();
        }

        //This method will return all the Records from the table
        public async Task<IEnumerable<T>> GetAllAsync()
        {
            return await _dbSet.ToListAsync();
        }

        //This method will return the specified record from the table
        //based on the Id which it received as an argument
        public async Task<T?> GetByIdAsync(object Id)
        {
            return await _dbSet.FindAsync(Id);
        }

        //This method will Insert one object into the table
        //It will receive the object as an argument which needs to be inserted into the database
        public async Task InsertAsync(T Entity)
        {
            //It will mark the Entity state as Added
            await _dbSet.AddAsync(Entity);
        }

        //This method is going to update the record in the table
        //It will receive the object as an argument
        public async Task UpdateAsync(T Entity)
        {
            //It will mark the Entity state as Modified
            _dbSet.Update(Entity);
        }

        //This method is going to remove the record from the table
        //It will receive the primary key value as an argument whose information needs to be removed from the table
        public async Task DeleteAsync(object Id)
        {
            //First, fetch the record from the table
            var entity = await _dbSet.FindAsync(Id);
            if (entity != null)
            {
                //This will mark the Entity State as Deleted
                _dbSet.Remove(entity);
            }
        }

        //This method will make the changes permanent in the database
        //That means once we call InsertAsync, UpdateAsync, and DeleteAsync Methods, 
        //Then we need to call the SaveAsync method to make the changes permanent in the database
        public async Task SaveAsync()
        {
            await _context.SaveChangesAsync();
        }
    }
}
Creating Specific or Non-Generic Repositories:

We need to provide the Specific or Non-Generic implementation for each entity. Let’s say we want a few extra operations for the Employee entity, such as getting employees by department, fetching all employees, including department data, and fetching an employee by id with the corresponding department data. As these operations are specific to the Employee entity, there is no point in adding these operations to the Generic Repository.

So, we need to create a Non-Generic or Specific Repository for the Employee Entity. Let us say EmployeeRepository, which will contain these specific operations, and this Non-Generic Repository is also inherited from the IGenericRepository<T> interface, and here we are specifying T as Employee. So, modify the IEmployeeRepository.cs class file as follows:

using CRUDinCoreMVC.GenericRepository;
using CRUDinCoreMVC.Models;

namespace CRUDinCoreMVC.Repository
{
    public interface IEmployeeRepository : IGenericRepository<Employee>
    {
        //Here, you need to define the operations which are specific to Employee Entity

        //This method returns all the Employee entities along with Department data
        Task<IEnumerable<Employee>> GetAllEmployeesAsync();

        //This method returns one the Employee along with Department data based on the Employee Id
        Task<Employee?> GetEmployeeByIdAsync(int EmployeeID);

        //This method will return Employees by Departmentid
        Task<IEnumerable<Employee>> GetEmployeesByDepartmentAsync(int Departmentid);
    }
}
Modifying EmployeeRepository.cs file

Next, modify the EmployeeRepository.cs file as shown below. Here, you can see it is providing implementations for the IEmployeeRepository interface methods. Further, this class is inherited from the GenericRepository<Employee> class. Here, we are specifying the Generic Type as Employee. Here, we use the Parent class, i.e., GenericRepository class _context object, to access the Employee entity.

using CRUDinCoreMVC.GenericRepository;
using CRUDinCoreMVC.Models;
using Microsoft.EntityFrameworkCore;

namespace CRUDinCoreMVC.Repository
{
    public class EmployeeRepository : GenericRepository<Employee>, IEmployeeRepository
    {
        public EmployeeRepository(EFCoreDbContext context) : base(context) { }

        //Returns all employees from the database.
        public async Task<IEnumerable<Employee>> GetAllEmployeesAsync()
        {
            return await _context.Employees.Include(e => e.Department).ToListAsync();
        }

        //Retrieves a single employee by their ID along with Department data.
        public async Task<Employee?> GetEmployeeByIdAsync(int EmployeeID)
        {
            var employee = await _context.Employees
               .Include(e => e.Department)
               .FirstOrDefaultAsync(m => m.EmployeeId == EmployeeID);

            return employee;
        }

        //Retrieves Employees by Departmentid
        public async Task<IEnumerable<Employee>> GetEmployeesByDepartmentAsync(int DepartmentId)
        {
            return await _context.Employees
                .Where(emp => emp.DepartmentId == DepartmentId)
                .Include(e => e.Department).ToListAsync();
        }
    }
}
Register both the generic and specific repositories.

Next, we must register generic and specific repositories into the dependency injection container. So, modify the Program class as follows:

using CRUDinCoreMVC.GenericRepository;
using CRUDinCoreMVC.Models;
using CRUDinCoreMVC.Repository;
using Microsoft.EntityFrameworkCore;

namespace CRUDinCoreMVC
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            //Configure the ConnectionString and DbContext Class
            builder.Services.AddDbContext<EFCoreDbContext>(options =>
            {
                options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection"));
            });

            // Add services to the container.
            builder.Services.AddControllersWithViews();

            //Registering the Specific Repository
            builder.Services.AddScoped<IEmployeeRepository, EmployeeRepository>();

            //Registering the GenericRepository
            builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            //Setting the Employees and Index action method as the default Route
            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Employees}/{action=Index}/{id?}");

            app.Run();
        }
    }
}

Modifying Employees Controller.

We need to use Generic and Non-Generic repositories inside the Employees Controller. So, modify the Employee Controller as shown below.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using CRUDinCoreMVC.Models;
using CRUDinCoreMVC.GenericRepository;
using System.Net;
using CRUDinCoreMVC.Repository;

namespace CRUDinCoreMVC.Controllers
{
    public class EmployeesController : Controller
    {
        //Other Than Employee Entity
        private readonly EFCoreDbContext _context;

        //Generic Reposiory, specify the Generic type T as Employee
        private readonly IGenericRepository<Employee> _repository;

        private readonly IEmployeeRepository _employeeRepository;

        public EmployeesController(IGenericRepository<Employee> repository, IEmployeeRepository employeeRepository, EFCoreDbContext context)
        {
            _repository = repository;
            _employeeRepository = employeeRepository;
            _context = context;
        }

        // GET: Employees
        public async Task<IActionResult> Index()
        {
            //Use Employee Repository to Fetch all the employees along with the Department Data
            var employees = await _employeeRepository.GetAllEmployeesAsync();

            return View(employees);
        }

        // GET: Employees/Details/5
        public async Task<IActionResult> Details(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            //Use Employee Repository to Fetch Employees along with the Department Data by Employee Id
            var employee = await _employeeRepository.GetEmployeeByIdAsync(Convert.ToInt32(id));
            if (employee == null)
            {
                return NotFound();
            }
           
            return View(employee);
        }

        // GET: Employees/Create
        public IActionResult Create()
        {
            ViewData["DepartmentId"] = new SelectList(_context.Departments, "DepartmentId", "Name");
            return View();
        }

        // POST: Employees/Create
        // To protect from overposting attacks, enable the specific properties you want to bind to.
        // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("EmployeeId,Name,Email,Position,DepartmentId")] Employee employee)
        {
            if (ModelState.IsValid)
            {
                //Use Generic Reposiory to Insert a new employee
                await _repository.InsertAsync(employee);

                //Call SaveAsync to Insert the data into the database
                await _repository.SaveAsync();
                return RedirectToAction(nameof(Index));
            }
            ViewData["DepartmentId"] = new SelectList(_context.Departments, "DepartmentId", "Name", employee.DepartmentId);
            return View(employee);
        }

        // GET: Employees/Edit/5
        public async Task<IActionResult> Edit(int? id)
        {
            if (id == null )
            {
                return NotFound();
            }

            var employee = await _repository.GetByIdAsync(id);
            if (employee == null)
            {
                return NotFound();
            }
            ViewData["DepartmentId"] = new SelectList(_context.Departments, "DepartmentId", "Name", employee.DepartmentId);
            return View(employee);
        }

        // POST: Employees/Edit/5
        // To protect from overposting attacks, enable the specific properties you want to bind to.
        // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id, [Bind("EmployeeId,Name,Email,Position,DepartmentId")] Employee employee)
        {
            if (id != employee.EmployeeId)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                try
                {
                    //Use Generic Reposiory to Insert a new employee
                    await _repository.UpdateAsync(employee);
                    await _repository.SaveAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    var emp = await _repository.GetByIdAsync(employee.EmployeeId);
                    if (emp == null)
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
                return RedirectToAction(nameof(Index));
            }
            ViewData["DepartmentId"] = new SelectList(_context.Departments, "DepartmentId", "Name", employee.DepartmentId);
            return View(employee);
        }

        // GET: Employees/Delete/5
        public async Task<IActionResult> Delete(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            //Use Employee Repository to Fetch Employees along with the Department Data by Employee Id
            var employee = await _employeeRepository.GetEmployeeByIdAsync(Convert.ToInt32(id));
           
            if (employee == null)
            {
                return NotFound();
            }
            
            return View(employee);
        }

        // POST: Employees/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            var employee = await _repository.GetByIdAsync(id);
            if (employee != null)
            {
                await _repository.DeleteAsync(id);
                await _repository.SaveAsync();
            }
            
            return RedirectToAction(nameof(Index));
        }
    }
}

Now, run the application and perform the operation, and everything should work as expected.

Note: Using a Specific Repository, we can access all the operations. But using the generic repository, we can access only the operations defined in the generic repository.

When to Use Which Repository?
  • Use Generic Repositories: For standard CRUD operations that are common across different entities. This reduces code duplication and simplifies maintenance.
  • Use Specific Repositories: For complex queries, operations that involve multiple entities, or when performance optimization is necessary. This allows you to bypass the limitations of a generic approach and tailor the data access logic to specific requirements.
Benefits of a Hybrid Approach
  • Flexibility: Provides the flexibility to optimize data access where needed while maintaining simplicity and reusability for standard operations.
  • Maintainability: Simplifies the maintenance of common CRUD operations and allows for focused optimization and customization in specific repositories.
  • Scalability: Facilitates scalability by providing a structured way to handle simple and complex data access scenarios.
  • Testability: This makes unit testing easier as you can mock specific or generic repositories based on your testing needs.

In the next article, I will discuss using the Unit of Work Pattern in ASP.NET Core MVC Applications using Entity Framework Core. In this article, I explain How to Implement Generic and Non-Generic Repository Patterns in ASP.NET Core MVC applications using Entity Framework Core. I hope you enjoy this Generic and Non-Generic Repository Patterns in ASP.NET Core MVC article.

Leave a Reply

Your email address will not be published. Required fields are marked *