Back to: ASP.NET Core Tutorials For Beginners and Professionals
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.
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.
About the Author: Pranaya Rout
Pranaya Rout has published more than 3,000 articles in his 11-year career. Pranaya Rout has very good experience with Microsoft Technologies, Including C#, VB, ASP.NET MVC, ASP.NET Web API, EF, EF Core, ADO.NET, LINQ, SQL Server, MYSQL, Oracle, ASP.NET Core, Cloud Computing, Microservices, Design Patterns and still learning new technologies.