Introduction
Code organization is one of the most essential aspects when building modern applications, especially when their structure becomes complex. The Repository pattern serves as one of the most potent design solutions to the problem of separating the concerns of data persistence and business logic. By employing the repository pattern, we encapsulate how the data is accessed within the system and make our codes more structured and easy to test as database interactions become more seamless.
In this article, we’ll focus on how the Repository Pattern can be used to implement a .NET Core Web API. First, we will see how to create generic repository classes embodying basic CRUD functionality and then custom repositories when required. The result is a flexible data access layer that improves code maintenance and the utilization of the test layer while limiting the role of the controllers to only responding to HTTP requests.
This is a continuation of my previous article, where we discussed the basics of API creation in ASP.NET Core.
Prerequisites
- Visual Studio 2022
- .NET Core 8
- MS SQL Server
- Basic Understanding of C#
- Basic Understanding of Async, Await, and Task keywords
About Repository Pattern
In software engineering, the Repository Pattern is among those design patterns that many developers incorporate in their application architecture since it uses a clean way to encapsulate the data access logic. Instead of hardcoding the data access code into the business logic or relying on the ORM directly in the controllers like Entity Framework, the repository pattern comes in and even allows for a middle layer that does all the database operations.
Major Advantages of the Use of the Repository Pattern
- Separation of Concerns: Since the repository pattern provides a separate level of handling all the data access logic, the application's controllers can easily perform request and response operations with an enhanced focus, thereby improving the overall structure of the code.
- Testability: Repositories can be mocked out in unit tests because they adhere to the same interface, meaning testing can be done without having to rely on a database.
- Reusability: Using a single repository for all data-related tasks in different application sections will enhance the use of code and minimize repetitive tasks.
- Flexibility: In most cases, the changes with regard to data access logic, such as the shifting of physical databases, can be handled via the repository layer without the need to change other application components.
- Code Consistency: The repository pattern implements a similar repose for all the data-related tasks (creation, updating, reading, and deletion) of all the entities, making it easier to maintain all the codes for the different entities.
Step 1. Define the Repository Interface.
Create a new folder called Repositories and add a generic repository interface (IRepository.cs), which holds the common operations for any entities.
// IRepository.cs
using System.Linq.Expressions;
namespace EmployeePortal.Repositories
{
public interface IRepository<T> where T : class
{
Task<IEnumerable<T>> GetAllAsync();
Task<T> GetByIdAsync(Guid id);
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(Guid id);
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
}
}
Step 2. Implement the Generic Repository.
Now, create a concrete class Repository.cs that implements the IRepository<T> interface and uses ApplicationDbContext for data access.
// Repository.cs
using EmployeePortal.Data;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
namespace EmployeePortal.Repositories
{
public class Repository<T> : IRepository<T> where T : class
{
protected readonly ApplicationDbContext _dbContext;
public Repository(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbContext.Set<T>().ToListAsync();
}
public async Task<T> GetByIdAsync(Guid id)
{
return await _dbContext.Set<T>().FindAsync(id);
}
public async Task AddAsync(T entity)
{
await _dbContext.Set<T>().AddAsync(entity);
await _dbContext.SaveChangesAsync();
}
public async Task UpdateAsync(T entity)
{
_dbContext.Set<T>().Update(entity);
await _dbContext.SaveChangesAsync();
}
public async Task DeleteAsync(Guid id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_dbContext.Set<T>().Remove(entity);
await _dbContext.SaveChangesAsync();
}
}
public async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
{
return await _dbContext.Set<T>().Where(predicate).ToListAsync();
}
}
}
Step 3. Create an Entity-Specific Repository.
we need entity-specific operations and define a specific repository interface, such as IEmployeeRepository, and then create a concrete EmployeeRepository class.
// IEmployeeRepository.cs
using EmployeePortal.Models.Entities;
namespace EmployeePortal.Repositories
{
public interface IEmployeeRepository : IRepository<Employee>
{
// Add employee specific methods here
}
}
As of now, I don't have any entity-specific operations so I have left it blank.
// EmployeeRepository.cs
using EmployeePortal.Data;
using EmployeePortal.Models.Entities;
namespace EmployeePortal.Repositories
{
public class EmployeeRepository : Repository<Employee>, IEmployeeRepository
{
public EmployeeRepository(ApplicationDbContext dbContext) : base(dbContext)
{
}
// Implement employee specific methods here
}
}
Step 4. Register the Repository with Dependency Injection.
In your Program.cs or Startup.cs, register the repositories for dependency injection.
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
builder.Services.AddScoped<IEmployeeRepository, EmployeeRepository>();
Step 5. Update the Controller to Use the Repository
Finally, refactor your EmployeesController to use IEmployeeRepository instead of ApplicationDbContext. We need to remove the dbContext from the code and inject the repository into the methods as per the code.
using EmployeePortal.Data;
using EmployeePortal.DTO;
using EmployeePortal.Models.Entities;
using EmployeePortal.Repositories;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace EmployeePortal.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class EmployeesController : ControllerBase
{
private readonly IEmployeeRepository _employeeRepository;
public EmployeesController(IEmployeeRepository employeeRepository)
{
_employeeRepository = employeeRepository;
}
[HttpGet]
public async Task<IActionResult> GetEmployees()
{
var employees = await _employeeRepository.GetAllAsync();
return Ok(employees);
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetEmployeeById(Guid id)
{
var employee = await _employeeRepository.GetByIdAsync(id);
if (employee == null)
{
return NotFound();
}
return Ok(employee);
}
[HttpPost]
public async Task<IActionResult> AddEmployee(EmployeeDto employeeDto)
{
var employee = new Employee
{
Name = employeeDto.Name,
Email = employeeDto.Email,
PhoneNumber = employeeDto.PhoneNumber,
Salary = employeeDto.Salary
};
await _employeeRepository.AddAsync(employee);
return CreatedAtAction(nameof(GetEmployeeById), new { id = employee.Id }, employee);
}
[HttpPut("{id:guid}")]
public async Task<IActionResult> UpdateEmployee(Guid id, UpdateEmployeeDto employeeDto)
{
var employee = await _employeeRepository.GetByIdAsync(id);
if (employee == null)
{
return NotFound();
}
employee.Name = employeeDto.Name;
employee.Email = employeeDto.Email;
employee.PhoneNumber = employeeDto.PhoneNumber;
employee.Salary = employeeDto.Salary;
await _employeeRepository.UpdateAsync(employee);
return Ok(employee);
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> DeleteEmployee(Guid id)
{
await _employeeRepository.DeleteAsync(id);
return NoContent();
}
}
}
You can notice that the database access is not from the controller, but those can be accessed via the repositories. This makes the code more maintainable, testable & reusable by decoupling the data access from the controller. Now, you can build and run the application and the endpoints are working as expected.
Conclusion
The Repository Pattern provides us with a structured approach to managing data access, allowing for a clean separation between business logic and data operations. By implementing this pattern, you enhance code maintainability, testability, and flexibility within your .NET Core application. In our next article, we can implement the data validations for our data integrity.