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 to do a 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 defines common database operations for all the 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 defines the database operations related to a specific entity in a separate class. For example, if you have two entities, Employee and Department, 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

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 benefits both patterns. 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 Generic Repository. 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, we need 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.  Let us assume we have two entities, Employee and Product. Both entities have some common as well as specific operations. Here, we need to implement both Generic and Non-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 Implement 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: Create a specific repository for each entity that requires specific operations. These specific repositories can be inherited from the generic repository (if they require the common operations) and extended with additional methods to meet the entity’s specific needs. For example, a ProductRepository for the Product entity, with methods for complex queries related to products, or an 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 IEmployeeRepository.
  • 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 use the same example we have used so far. Let us first modify the IGenericRepository.cs file as shown below. This interface will define the common database operations for all entities in our application, such as employees, departments, salaries, projects, etc.

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 them 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 that it provides 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 need to register both 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: We can access all the operations using a Specific Repository, but while using a Generic Repository, we can access only the common operations defined in the generic repository.

When to Use Generic Repository?

The generic repository is best suited for applications with the following characteristics:

  • Standard CRUD Operations: If most of your entities require basic Create, Read, Update, and Delete operations without specific logic, a generic repository reduces repetition and keeps your data access layer clean and simple.
  • Rapid Development: They can significantly speed up development time because you can reuse the same code for multiple entities. This makes them ideal for projects with tight deadlines or in the early stages of development where you might need to prototype quickly.
  • Simple Business Rules: When business rules are straightforward or nearly identical across different entities, generic repositories provide an efficient and streamlined approach.
When to Use Non-Generic Repositories?

Non-generic repositories are more appropriate in scenarios like:

  • Complex Business Logic: If certain entities in your application involve complex operations, custom queries, or detailed logic that goes beyond simple CRUD, non-generic repositories allow you to create methods for these specific needs without overloading a generic repository.
  • Distinctive Transaction Management: When different data objects require unique ways of handling transactions, using non-generic repositories gives you the control needed to implement these specific requirements.
When to Use Both Generic and Non-Generic Repositories?

Many real-world applications benefit from using both generic and non-generic repositories. This approach uses the advantages of both methods:

  • Base and Extension Approach: Use generic repositories to handle standard operations across most entities. Extend this approach with non-generic repositories for complex entities or where special handling is necessary.
  • Clear Layer Separation: In applications where some parts are simple and others are complex, using both types of repositories helps organize your architecture neatly. Generic repositories can manage straightforward data interactions, while non-generic ones handle the complexities, ensuring each part of your application is optimally designed for its purpose.

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 article on Generic and Non-Generic Repository Patterns in ASP.NET Core MVC.

Leave a Reply

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