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 Pattern, we need to create separate repositories for every entity in our application. For example, if we have four entities, Employee, Department, Project, and Salary, we must create four repositories: EmployeeRepository, DepartmentRepository, ProjectRepository, and SalaryRepository.

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?

As you can see in the above image, we have a single Generic Repository, which will take responsibility for performing the typical database CRUD Operations on multiple entities. That means now, using the Generic Repository, we can perform database CRUD Operations on Employees, Departments, Projects, Salaries, and other entities. We don’t have to repeat the same logic repeatedly at multiple repositories.

What is a Generic Repository Design Pattern?

A generic repository can be used with any data type, reducing the need to write specific repositories for each entity. This can lead to more streamlined code and easier maintenance.

The Generic Repository Pattern abstracts data access logic into a generic class, making your code cleaner, 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, and 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 interact with the database using the Entity Framework Core DbContext object.

A Generic Repository in ASP.NET Core typically performs 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, we first need to create an interface, let’s say IGenericRepository, with the above five methods. 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, which we have worked on so far in our previous two articles.

Adding GenericRepository Folder

First, let’s add a GenericRepository folder to the project’s root directory. Right-click on Project => Add => New Folder and then Rename the folder to 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 the interface works with the T type instead of Employee, Department, or any type. The where T : class constraint specifies that T must be a reference type, not a value type. That T can be Employee, Product, Customer, Department, etc.

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 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 Projects table has a string primary key, whereas the Employees table has an integer primary key, 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 interface defines several asynchronous methods to perform typical data operations; supporting asynchronous programming is essential for highly scalable web applications to handle more requests.

  • GetAllAsync: This method returns all instances of type T. It uses IEnumerable<T> to allow the lazy loading of items if needed. The Task indicates that the operation is asynchronous.
  • GetByIdAsync: Retrieves a single instance of type T based on the provided ID. It returns null if no entity is found, which is indicated by T?.
  • InsertAsync: Adds a new entity of type T to the database.
  • UpdateAsync: This operation updates an existing entity of type T in the database. It could involve changing the entity’s state based on the changes to the entity instance.
  • DeleteAsync: Removes an entity of type T from the database by its ID.
  • SaveAsync: This method commits any changes made in the context into the database. It is important to ensure that all modified, added, or deleted entities are correctly persisted in the database.
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 following GenericRepository class uses the generic type T, we can’t access the DbSet as a property on the DbContext object. We don’t know in advance what DbSet type we need to use, i.e., it may be Employee, Department, Project, Salary, 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 perform the operations. The following code is self-explained, so please go through the comment lines for a better understanding.

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 EmployeesController 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 Employees 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 they are 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.

Advantages of Generic Repository Pattern in ASP.NET Core MVC:
  • Simplification of Data Access: If you have multiple entities and want a uniform way to handle CRUD operations, the Generic Repository Pattern can simplify your code by centralizing common data operations.
  • Unit Testing: By using interfaces that abstract the data layer, the Generic Repository Pattern makes it easier to unit test the business logic of your application without relying on the database. This is because you can mock the repository interface in your tests.
  • Decoupling: It helps in decoupling the application from the data access technologies. If you ever need to switch out your ORM or database, having a repository layer can make that transition smoother.
  • Maintainability: When changes are required in the data access logic, they can be made in one place rather than throughout your application, leading to better maintainability.
Disadvantages of Generic Repository Pattern in ASP.NET Core MVC:
  • Over-abstraction: Sometimes, it abstracts too much, hiding useful features of EF Core like tracking changes and lazy loading.
  • Generic issues: Not all operations can be generic; sometimes, specific operations are required that the generic repository cannot handle.

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 *