Repository Pattern in .NET Core

By FoxLearn 2/21/2025 7:51:55 AM   34
Effective code organization is crucial when developing modern software, especially as the application structure becomes more intricate. One of the most effective design patterns to manage this complexity is the Repository Pattern.

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.