Generic Repository Pattern in ASP.NET Core MVC

Generic Repository Pattern in ASP.NET Core MVC with EF Core

In this article, I will discuss How to Implement the Generic Repository Design Pattern in ASP.NET Core MVC with Entity Framework Core. This is a continuation of our previous article, where we discussed How to Implement the Basic or Non-Generic Repository Pattern in ASP.NET Core MVC with Entity Framework Core. We will work with the same application we worked on in our previous two articles.

Why do we need a Generic Repository Design Pattern in ASP.NET Core MVC?

As we already discussed, in a Basic or Non-Generic Repository, we need to create separate repositories for every entity in our application. For example, if we have three entities, Employee, Product, and Customer, we must create three repositories: EmployeeRepository, ProductRepository, and CustomerRepository.

This is boring and repetitive work, especially if all the repositories will do the same kind of work (i.e., typically database CRUD operations). This is against the DRY (Don’t Repeat Yourself) principle, as we are repeating the same logic again and again in each repository. To solve the above problem, the Generic Repository Design Pattern comes into the picture. Please have a look at the below diagram for a better understanding.

Why do we need a Generic Repository Design Pattern in ASP.NET Core MVC?

What is a Generic Repository Design Pattern?

The Generic Repository Pattern in ASP.NET Core MVC using Entity Framework Core (EF Core) is a way to abstract data access logic into a generic class, making your code cleaner, more modular, and easier to maintain. This pattern is useful in large applications with many entities where we perform similar database operations on multiple entities. It implements the common data operations in a single, generic repository rather than having separate repositories for each entity type.

How to Implement Generic Repository Design Pattern in ASP.NET Core?
  • Generic Interface: A generic repository typically starts with a generic interface defining common operations like Add, Delete, FindById, FindAll, and Update. These operations are defined in a generic way, applicable to any entity type.
  • Implementation: The generic interface is then implemented in a concrete class. This class handles database interactions, such as querying a database using an ORM (like Entity Framework Core).
  • Entity Framework Core DBontext: The Concrete Implementation class will use the Entity Framework Core DbContext object to interact with the database.

A Generic Repository in ASP.NET Core typically does at least five operations as follows.

  • Selecting all records from a table
  • Selecting a single record based on its primary key
  • Insert a new record into a table
  • Update an existing record in a table
  • Delete an existing record from a table

However, the above list is not fixed. You may have more or fewer methods in your generic repository as per your business requirements. For the simplicity of this demo, let’s assume our Generic Repository will perform the above Five Operations.

To implement a Generic Repository Design Pattern in ASP.NET Core MVC, first, we need to create an interface, let’s say IGenericRepository, with the above five methods, and then we need to create a class, let’s say GenericRepository, which will implement the IGenericRepository interface, and provide the generic implementations for the IGenericRepository interface methods. Let us proceed and implement this step by step in our existing ASP.NET Core MVC Application that we have worked on so far in our previous two articles.

Adding GenericRepository Folder

First, let us add a folder in the project’s root directory named GenericRepository. To do so, right-click on the Project => Add => New Folder and then Rename the folder name as GenericRepository.

Creating Generic Repository Interface:

Next, add an Interface within the GenericRepository folder named IGenericRepository.cs and copy and paste the following code. Here, you can see that instead of Employee, Department, or any type, the interface works with the T type, where T will be a class. That T can be Employee, Product, Customer, Department, 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();
    }
}

The IGenericRepository interface is a generic interface that defines the same set of five methods we created in the IEmployeeRepository interface in our previous article. Notice that instead of the Employee entity, we use T everywhere. Also, notice that the GetByIdAsync() and DeleteAsync() methods now accept object parameters instead of integer parameters. This is necessary because different tables may have different types of primary keys (The Customers table has a string primary key, whereas the Employees table has an integer primary key, etc.).

Implementing IGenericRepository Interface

Now, we need to implement the IGenericRepository interface. Add a class file named GenericRepository.cs within the GenericRepository Folder and copy and paste the following code. The following GenericRepository<T> class is a generic class and implements the IGenericRepository<T> interface.

As the above GenericRepository class uses the generic type, T, we can’t access the DbSet as a property on the DbContext object. Because we don’t know in advance what DbSet type we need to use, i.e., it may be Employee, Product, Customer, Department, etc. That is why a generic DbSet variable is declared at the top that points to an appropriate DbSet based on the type of T. Then, using that DbSet variable, we are doing the operations.

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
        private readonly EFCoreDbContext _context;

        //The following Variable is going to hold the DbSet Entity
        private 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();
        }
    }
}
Register the Generic Repository in the DI Container

Modify the Program.cs class file as follows:

using CRUDinCoreMVC.GenericRepository;
using CRUDinCoreMVC.Models;
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();

            //You can comment the following
            //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();
        }
    }
}
Using the Generic Repository in Controllers:

Once the GenericRepository is ready, we need to use that generic repository in our Employees Controller. So, modify the Employees Controller as shown below. The following Controller uses the GenericRepository to perform the CRUD Operations. Further, if you notice, while creating the instance of GenericRepository, we have specified the type T as Employee. So, in this case, DbSet<T> will be replaced as DbSet<Employee> in the GenericRepository, and the operations will be performed on the Employee table.

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

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;

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

        // GET: Employees
        public async Task<IActionResult> Index()
        {
            var employees = from emp in await _repository.GetAllAsync() //Left Data Source
                              join dept in _context.Departments.ToList() //Right Data Source
                              on emp.DepartmentId equals dept.DepartmentId //Inner Join Condition
                              into EmployeeDepartmentGroup //Performing LINQ Group Join
                              from departments in EmployeeDepartmentGroup.DefaultIfEmpty() //Performing Left Outer Join
                              select new Employee
                              { 
                                  EmployeeId = emp.EmployeeId,
                                  DepartmentId = emp.DepartmentId,
                                  Name = emp.Name,
                                  Email = emp.Email,
                                  Position = emp.Position,
                                  Department = departments,
                              };

            return View(employees);
        }

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

            var employee = await _repository.GetByIdAsync(id);
            if (employee == null)
            {
                return NotFound();
            }
            employee.Department = await _context.Departments.FindAsync(employee.DepartmentId);
            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)
            {
                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
                {
                    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();
            }

            var employee = await _repository.GetByIdAsync(Convert.ToInt32(id));
            if (employee == null)
            {
                return NotFound();
            }
            employee.Department = await _context.Departments.FindAsync(employee.DepartmentId);
            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));
        }
    }
}

We are done with our implementation. Run the application and perform the CRUD operations, which should work as expected. Here, you can observe one thing while fetching the data, either all employees or specific employees by ID: it is only fetching the data from the Employees database table, not fetching the corresponding Departments table data. Here, we cannot change the Generic Repository implementations as it is common for all Entities. In situations like this, we need to use both Generic and Non-Generic Repositories in our application, which we will discuss in our next article.

When to Use and Not Use Generic Repository Pattern in ASP.NET Core MVC:

The decision to use the Generic Repository Pattern in ASP.NET Core MVC with Entity Framework Core depends on several factors related to the complexity and requirements of your application. While this pattern offers benefits like abstraction, maintainability, and testability, it’s not always the best choice for every project. Here are some considerations to help you decide when to use it:

When to Use the Generic Repository Design Pattern in ASP.NET Core MVC?
  • Complex Applications with Large Data Models: If your application has a complex domain model with many entities, the Generic Repository Pattern can help manage this complexity by abstracting data access logic.
  • Need for Abstraction: If you want to abstract away data access details to make your application more maintainable, the Generic Repository Pattern can be beneficial. It allows you to centralize data access rules and logic.
  • Unit Testing: The pattern is particularly useful for unit testing because it allows you to mock repositories easily, testing business logic without relying on a database.
  • Code Reusability: In scenarios where multiple projects have similar data access requirements, using a generic repository can promote code reusability.
  • Decoupling: This pattern can be quite effective if you want to decouple the application layers (especially separating the data layer from the business logic).
When Not to the Generic Repository Design Pattern in ASP.NET Core MVC?
  • Simple Applications: For simple applications with few entities or straightforward CRUD operations, the Generic Repository Pattern might be overkill, adding unnecessary complexity.
  • Performance-Sensitive Queries: In cases where you need highly optimized queries or complex data manipulations, the abstraction provided by the generic repository might hinder performance. EF Core itself is capable of handling complex queries efficiently.
  • Direct Dependency on EF Core Features: If you need to use advanced features of Entity Framework Core (like tracking, lazy loading, etc.), directly working with EF Core might be more beneficial.
  • Tight Coupling with EF Core: Sometimes, using a generic repository can lead to a design that is still tightly coupled with EF Core, negating some of the benefits of the pattern.

We created Entity-Specific Repositories in our previous article and Generic Repository in this article. In the next article, I will discuss Using Both Generic and Non-Generic Repository Patterns in ASP.NET Core MVC with Entity Framework Core. In this article, I explain the Generic Repository Design Pattern in ASP.NET Core MVC with EF Core. I hope you enjoy this Generic Repository Design Pattern in ASP.NET Core MVC with Entity Framework Core article.

Leave a Reply

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