Repository Pattern in .NET Core
By FoxLearn 2/21/2025 7:51:55 AM 34
By implementing this pattern, we can neatly separate data persistence logic from business logic, creating a more structured and maintainable codebase. The Repository Pattern also makes database interactions easier to manage and test, leading to cleaner and more scalable applications.
This article will demonstrate how the Repository Pattern can be applied in a .NET Core Web API, starting with generic repository classes that cover basic CRUD operations. We will also explore how to create custom repositories when specialized functionality is necessary. The goal is to build a flexible data access layer that improves code maintenance, testing, and minimizes the role of controllers to merely handling HTTP requests.
What is the Repository Pattern?
The Repository Pattern is a widely used design pattern in software engineering that abstracts data access logic from business logic. Instead of embedding database access directly within business logic or controllers (as is the case with ORM tools like Entity Framework), the Repository Pattern introduces a dedicated layer that handles all database interactions. This approach improves code maintainability and testing by creating a clear separation between different concerns.
Benefits of Using the Repository Pattern
- Separation of Concerns: It isolates data access logic, enabling controllers to focus solely on handling HTTP requests and responses, resulting in a cleaner, more organized codebase.
- Testability: Repositories can be mocked in unit tests, which eliminates the need for direct database access during testing.
- Reusability: A single repository can be reused across multiple parts of the application, reducing redundancy.
- Flexibility: Changes to data access logic (e.g., switching to a different database) can be made within the repository layer without affecting other parts of the application.
- Consistency: The Repository Pattern ensures a consistent approach to data handling (CRUD operations) across the entire application, improving maintainability.
Step-by-Step Implementation
Step 1: Define the Repository Interface
Create a folder named Repositories
and define a generic repository interface (IRepository.cs
) that specifies the common operations for any entity.
// IRepository.cs using System.Linq.Expressions; namespace MyApp.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
Next, implement the IRepository<T>
interface in a concrete class Repository.cs
, utilizing ApplicationDbContext
for database operations.
// Repository.cs using MyApp.Data; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; namespace MyApp.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() => await _dbContext.Set<T>().ToListAsync(); public async Task<T> GetByIdAsync(Guid id) => 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) => await _dbContext.Set<T>().Where(predicate).ToListAsync(); } }
Step 3: Create a Specific Repository
For entity-specific operations, create a custom repository interface (e.g., IProductRepository
) and a corresponding concrete repository class (ProductRepository
).
// IProductRepository.cs namespace MyApp.Repositories { public interface IProductRepository : IRepository<Product> { // Add product-specific methods here } } // ProductRepository.cs using MyApp.Data; using MyApp.Models.Entities; namespace MyApp.Repositories { public class ProductRepository : Repository<Product>, IProductRepository { public ProductRepository(ApplicationDbContext dbContext) : base(dbContext) { } // Implement product-specific methods here } }
Step 4: Register Repositories with Dependency Injection
In your Program.cs
or Startup.cs
, register the repository services for dependency injection.
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); builder.Services.AddScoped<IProductRepository, ProductRepository>();
Step 5: Refactor the Controller
Refactor your controller to use the IProductRepository
instead of directly interacting with ApplicationDbContext
. This isolates the data access layer and improves maintainability.
// ProductsController.cs using MyApp.Models.Entities; using MyApp.Repositories; using Microsoft.AspNetCore.Mvc; namespace MyApp.Controllers { [Route("api/[controller]")] [ApiController] public class ProductsController : ControllerBase { private readonly IProductRepository _productRepository; public ProductsController(IProductRepository productRepository) { _productRepository = productRepository; } [HttpGet] public async Task<IActionResult> GetProducts() { var products = await _productRepository.GetAllAsync(); return Ok(products); } [HttpGet("{id:guid}")] public async Task<IActionResult> GetProductById(Guid id) { var product = await _productRepository.GetByIdAsync(id); if (product == null) { return NotFound(); } return Ok(product); } [HttpPost] public async Task<IActionResult> AddProduct(ProductDto productDto) { var product = new Product { Name = productDto.Name, Price = productDto.Price }; await _productRepository.AddAsync(product); return CreatedAtAction(nameof(GetProductById), new { id = product.Id }, product); } [HttpPut("{id:guid}")] public async Task<IActionResult> UpdateProduct(Guid id, UpdateProductDto productDto) { var product = await _productRepository.GetByIdAsync(id); if (product == null) { return NotFound(); } product.Name = productDto.Name; product.Price = productDto.Price; await _productRepository.UpdateAsync(product); return Ok(product); } [HttpDelete("{id:guid}")] public async Task<IActionResult> DeleteProduct(Guid id) { await _productRepository.DeleteAsync(id); return NoContent(); } } }
The Repository Pattern enables a clean separation between the application's business logic and data access layer. By using this pattern, we enhance code maintainability, flexibility, and testability, leading to a more scalable .NET Core application.
- Options Pattern In ASP.NET Core
- Implementing Rate Limiting in .NET
- IExceptionFilter in .NET Core
- CRUD with Dapper in ASP.NET Core
- How to Implement Mediator Pattern in .NET
- How to use AutoMapper in ASP.NET Core
- How to fix 'asp-controller and asp-action attributes not working in areas'
- Basic Authentication in ASP.NET Core