Back to: ASP.NET Core Tutorials For Beginners and Professionals
Repository Design Pattern in ASP.NET Core MVC with Entity Framework Core
In this article, I will discuss How to Implement the Repository Design Pattern in ASP.NET Core MVC with Entity Framework Core. Please read our previous article discussing How to Perform Database CRUD Operations in ASP.NET Core MVC Web Applications using Entity Framework Core. We will work with the same application we created in our previous article.
Implementing the Repository Design Pattern in an ASP.NET Core MVC application using Entity Framework Core is a great way to abstract data access and make your code more maintainable and testable. Before Implementing the Repository Design Pattern, let us first understand why we need the Repository Design Pattern.
How Does a Modern Data-driven Application Access Data from a Database?
Nowadays, most data-driven applications need to access the data residing in one or more other data sources, i.e., in the databases. The easiest or simplest approach is to write all the data access-related code in the main application.
For example, if you have an ASP.NET Core MVC Controller, let’s say EmployeesController. Then, the EmployeesController class may have many action methods that can perform the typical CRUD (Create, Read, Update, and Delete) operations against the underlying database. Let’s assume we use Entity Framework Core for all these database-related operations. In that case, your application would do something as shown in the below diagram.
As you can see in the above diagram, the action methods of the Employees Controller directly interact with the Entity Framework Core DbContext class and execute the queries to retrieve the data from the database. They also perform the INSERT, UPDATE, and DELETE operations using the DbContext and DbSet properties. The Entity Framework Core, in turn, talks with the underlying SQL Server database. Our previous article used this approach to perform the database CRUD operations using EF Core in ASP.NET Core MVC Application.
Drawbacks of the Above Implementation:
The above implementation works as expected, which we have seen in our previous article. However, the drawback is that the database access code (i.e., creating the DbContext object, writing the queries, manipulating the data, persisting the changes to the database, etc.) is embedded directly inside the controller action methods. This design or implementation can cause Code Duplication, and further, we need to change the controller even if we make a small change in the data access logic.
For example, if the application modifies employee information from two controllers, each controller will repeat the same data access code. Future modifications must also be done at two places, i.e., the two controllers where we write the same data access code.
What is the Repository Design Pattern?
The Repository Design Pattern is a widely used design pattern that abstracts the data layer, making your application code more maintainable, scalable, and testable. The Repository pattern allows you to manage the data from a data source (like a database) in a more object-oriented way.
The Repository Design Pattern acts as a middleman or middle layer between the rest of the application and the data access logic. That means a repository pattern isolates all the data access codes from the rest of the application.
The advantage of doing so is that if you need to make any changes, you must do it in one place. Another benefit is that testing your controllers becomes easy because the testing framework must not run against the database access code. The previous diagram will change to the following diagram with the Repository Design Pattern.
As shown in the above diagram, the Employees Controller won’t directly talk with the Entity Framework Core DbContext class. That means, now, there are no queries or any other data access code written in the action methods of the Employees Controller.
The Employee Repository wraps all these operations (i.e., basic CRUD operations). The Employee Repository uses the Entity Framework Core DbContext and DbSet properties to perform the CRUD operations. As you can see, now the Employee repository has methods such as GetAll(), GetByID(), Insert(), Update(), and Delete(). These methods will perform the Typical CRUD operations against the underlying database. The Employees Controller uses those methods of the Employee Repository to perform the required database operations.
What is a Repository?
A repository is a class defined for an entity with all the possible database operations. For example, a repository for an employee entity will have the basic CRUD operations and any other possible operations related to the Employee entity.
How to Implement Repository Design Pattern in ASP.NET Core MVC using EF Core?
We are going to work with the same application that we created in our previous article, where we created the following Employee and Department Entities:
Employee.cs
using System.ComponentModel.DataAnnotations; namespace CRUDinCoreMVC.Models { public class Employee { public int EmployeeId { get; set; } public string Name { get; set; } public string Email { get; set; } public string Position { get; set; } [Display(Name ="Department Name")] public int DepartmentId { get; set; } public Department? Department { get; set; } } }
Department.cs
namespace CRUDinCoreMVC.Models { public class Department { public int DepartmentId { get; set; } public string Name { get; set; } public List<Employee> Employees { get; set; } } }
We have also created the following DbContext class:
using Microsoft.EntityFrameworkCore; namespace CRUDinCoreMVC.Models { public class EFCoreDbContext : DbContext { //Constructor calling the Base DbContext Class Constructor public EFCoreDbContext(DbContextOptions<EFCoreDbContext> options) : base(options) { } //OnConfiguring() method is used to select and configure the data source protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { //We will store the connection string in AppSettings.json file instead of hard coding here } protected override void OnModelCreating(ModelBuilder modelBuilder) { } //Adding Domain Classes as DbSet Properties public DbSet<Employee> Employees { get; set; } public DbSet<Department> Departments { get; set; } } }
We have then added the database connection string in the appsettings.json file as follows:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "EFCoreDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=EFCoreMVCDB;Trusted_Connection=True;TrustServerCertificate=True;" } }
Then we registered the connection string and DbContext class to use the connection string in the program class and we have set the Employees Controller Index action method as the default route as follows:
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(); 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(); app.MapControllerRoute( name: "default", pattern: "{controller=Employees}/{action=Index}/{id?}"); app.Run(); } } }
Then we created the Employees and Departments controller using MVC Controller with views, using Entity Framework template. Then we modified some of the actions and views of the Employees Controller.
Create Repository Interfaces
Define interfaces for your repositories. These interfaces declare the operations you can perform on the entities. A repository 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 the database
- Update an existing record in the database
- Delete an existing record in the database
However, this list is not fixed. You may have more or fewer operations in the repository as per your business requirement. For our example, these five operations are needed from the Employee repository.
To implement this, first, we will create an Interface (i.e., IEmployeeRepository) with these five methods, and then we will implement this interface in a class (i.e., EmployeeRepositpry).
First, add a folder with the name Repository to your project. To do so, right-click on the Project => Add => New Folder and then rename the folder name as Repository. Add an Interface within this Repository folder named IEmployeeRepository.cs, and copy and paste the code below.
using CRUDinCoreMVC.Models; namespace CRUDinCoreMVC.Repository { public interface IEmployeeRepository { //This method returns all the Employee entities as an enumerable collection Task<IEnumerable<Employee>> GetAllAsync(); //This method accepts an integer parameter representing an Employee ID and //returns a single Employee entity matching that Employee ID. Task<Employee?> GetByIdAsync(int EmployeeID); //This method accepts an Employee object as the parameter and //adds that Employee object to the Employees DbSet. //mark the entity state as Added Task InsertAsync(Employee employee); //This method accepts an Employee object as a parameter and //marks that Employee object as a modified Employee in the DbSet. Task UpdateAsync(Employee employee); //This method accepts an EmployeeID as a parameter and //removes that Employee entity from the Employees DbSet. //Mark the Entity state as Deleted Task DeleteAsync(int employeeId); //This method Saves changes to the EFCoreDb database. Task SaveAsync(); } }
Adding EmployeeRepository Class
Now, we need to add the EmployeeRepository class by implementing the IEmployeeRepository interface and providing implementations for the interface methods. To do so, add a class file within the Repository folder named EmployeeRepository.cs and copy and paste the code below. The following example code is self-explained.
using CRUDinCoreMVC.Models; using Microsoft.EntityFrameworkCore; namespace CRUDinCoreMVC.Repository { public class EmployeeRepository : IEmployeeRepository { //The following variable is going to hold the EmployeeDBContext instance private readonly EFCoreDbContext _context; //Initializing the EmployeeDBContext instance which it received as an argument //MVC Framework DI Container will provide the EFCoreDbContext instance public EmployeeRepository(EFCoreDbContext context) { _context = context; } //Returns all employees from the database. public async Task<IEnumerable<Employee>> GetAllAsync() { return await _context.Employees.Include(e => e.Department).ToListAsync(); } //Retrieves a single employee by their ID. public async Task<Employee?> GetByIdAsync(int EmployeeID) { var employee = await _context.Employees .Include(e => e.Department) .FirstOrDefaultAsync(m => m.EmployeeId == EmployeeID); return employee; } //Adds a new employee to the database. public async Task InsertAsync(Employee employee) { await _context.Employees.AddAsync(employee); } //Updates an existing employee's details. public async Task UpdateAsync(Employee employee) { _context.Employees.Update(employee); } //Deletes an employee from the database public async Task DeleteAsync(int employeeId) { var employee = await _context.Employees.FindAsync(employeeId); if (employee != null) { _context.Employees.Remove(employee); } } //InsertAsync, UpdateAsync, and DeleteAsync methods, //remember to call SaveAsync to commit the changes to the database. public async Task SaveAsync() { await _context.SaveChangesAsync(); } } }
Using Employee Repository inside Employees Controller:
Next, modify the Employees Controller to Use Employee Repository instead of DbContext and DbSet Properties to perform the CRUD operations on the Employee entity as follows:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; using CRUDinCoreMVC.Models; using CRUDinCoreMVC.Repository; namespace CRUDinCoreMVC.Controllers { public class EmployeesController : Controller { //Other Than Employee Entity private readonly EFCoreDbContext _context; //For Employee Entity private readonly IEmployeeRepository _employeeRepository; public EmployeesController(IEmployeeRepository employeeRepository, EFCoreDbContext context) { _employeeRepository = employeeRepository; _context = context; } // GET: Employees public async Task<IActionResult> Index() { var employees = await _employeeRepository.GetAllAsync(); return View(employees); } // GET: Employees/Details/5 public async Task<IActionResult> Details(int? id) { if (id == null) { return NotFound(); } var employee = await _employeeRepository.GetByIdAsync(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) { await _employeeRepository.InsertAsync(employee); //Call SaveAsync to Insert the data into the database await _employeeRepository.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 _employeeRepository.GetByIdAsync(Convert.ToInt32(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 _employeeRepository.UpdateAsync(employee); await _employeeRepository.SaveAsync(); } catch (DbUpdateConcurrencyException) { var emp = await _employeeRepository.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 _employeeRepository.GetByIdAsync(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 _employeeRepository.GetByIdAsync(id); if (employee != null) { await _employeeRepository.DeleteAsync(id); await _employeeRepository.SaveAsync(); } return RedirectToAction(nameof(Index)); } } }
Registering the EmployeeRepository:
Next, we must register the IEmployeeRepositiry and EmployeeRepositiry in the program class. So, add the following line to your Program class:
builder.Services.AddScoped<IEmployeeRepository, EmployeeRepository>();
Now, run the application and perform the database CRUD Operations using Employee Controller action methods and Views, and it should work as expected.
Note: The way we have implemented the Repository Pattern for Employee Entity, in the same way, you need to implement the Repository for Department Entity.
Tips for Performance Improvements:
- Asynchronous Operations: Use the async/await pattern for database operations to improve performance.
- AsNoTracking: For methods that only read data (like GetAllAsync and GetByIdAsync), consider using AsNoTracking for performance optimization, especially if the result set is large.
When to Use Repository Design Pattern in ASP.NET Core MVC?
The Repository design pattern is a popular approach in ASP.NET Core MVC for abstracting data access. It’s useful in certain scenarios, but it’s not always necessary. Here’s when you might want to use the Repository pattern:
Complex Data Access Logic
- Multiple Data Sources: If your application interacts with multiple data sources (e.g., databases, web services, file systems), a repository can provide a unified interface for data access, making it easier to manage.
- Complex Queries: For applications with complex queries or where you need to implement custom data retrieval logic, repositories can encapsulate this complexity.
Decoupling Application and Data Access Layers
- Decoupling: Repositories help decouple the data access logic from the rest of your application, which leads to cleaner, more maintainable code.
- Testing: By abstracting data access into repositories, you can easily mock data access in your unit tests, making them more reliable and faster.
Code Reusability and Maintenance
- Reusability: If you have common data access patterns across multiple parts of your application, a repository can avoid code duplication.
- Maintainability: Changes in the data access layer (like switching ORMs or databases) can be managed in repositories without affecting the rest of your application.
When Using ORMs like Entity Framework
- ORMs: When using Object-Relational Mappers (ORMs) like Entity Framework, repositories can add an extra layer that might seem redundant. However, they are useful if you need to abstract away the ORM-specific details from your application.
Large or Growing Applications
- Scalability: In large applications or applications expected to grow, repositories can provide a scalable way to manage data access.
When Not to Use Repository Design Pattern?
- Simple CRUD Operations: For simple applications with basic CRUD (Create, Read, Update, Delete) operations, adding repositories might be overkill.
- Tight Deadlines: If you’re working under tight deadlines, introducing an additional layer might slow down development.
- Small Team or Solo Projects: In small or solo projects where you have full control over the codebase, the extra layer of abstraction might not provide significant benefits.
In the next article, I will discuss How to Implement the Generic Repository Pattern in ASP.NET Core MVC with Entity Framework Core. In this article, I explain the Repository Design Pattern in ASP.NET Core MVC with EF Core. I hope you enjoy this Repository Design Pattern in ASP.NET Core MVC with Entity Framework Core 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.