Previous Entity Framework Core Model First Unit of Work Patterns Next

📦 Repository Pattern in .NET Core

🧩 The Repository Pattern

The Repository pattern is a design pattern that creates an abstraction layer between an application's business logic and the data access logic. It decouples the application from the specific data persistence technology, such as Entity Framework (EF) Core.

In this pattern, a repository class is created for each aggregate root or entity, which encapsulates the logic required to store or retrieve data. This approach hides the implementation details of data access, allowing the application's business logic to focus on its core responsibilities without being tightly coupled to EF Core.

💡 Example: Implementing the Repository Pattern with EF Core

This example demonstrates a specific repository implementation for a Product entity within a .NET Core Web API, using the principles of Clean Architecture.

📁 Project Structure

  • ProductService.Domain: Defines the core business entities and repository interfaces.
  • ProductService.Infrastructure: Implements the repository interfaces using EF Core.
  • ProductService.Api: The entry point that uses dependency injection to link the application logic to the repository implementation.

📖 What is the Repository Pattern?

The Repository Pattern is a design pattern that provides an abstraction layer between the data access layer and the business logic layer. Instead of working directly with DbContext or SQL queries, you interact with repositories that encapsulate data access logic.

🛠 Example in .NET Core with EF Core

1. Define the Entity

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}
    

2. Define the Repository Interface

public interface IProductRepository
{
    Task<IEnumerable<Product>> GetAllAsync();
    Task<Product> GetByIdAsync(int id);
    Task AddAsync(Product product);
    Task UpdateAsync(Product product);
    Task DeleteAsync(int id);
}
    

3. Implement the Repository

using Microsoft.EntityFrameworkCore;

public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _context;

    public ProductRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<IEnumerable<Product>> GetAllAsync() =>
        await _context.Products.ToListAsync();

    public async Task<Product> GetByIdAsync(int id) =>
        await _context.Products.FindAsync(id);

    public async Task AddAsync(Product product)
    {
        _context.Products.Add(product);
        await _context.SaveChangesAsync();
    }

    public async Task UpdateAsync(Product product)
    {
        _context.Products.Update(product);
        await _context.SaveChangesAsync();
    }

    public async Task DeleteAsync(int id)
    {
        var product = await _context.Products.FindAsync(id);
        if (product != null)
        {
            _context.Products.Remove(product);
            await _context.SaveChangesAsync();
        }
    }
}
    

4. Register in Dependency Injection

services.AddScoped<IProductRepository, ProductRepository>();
    

5. Use in Business Logic / Controller

public class ProductsController : ControllerBase
{
    private readonly IProductRepository _repository;

    public ProductsController(IProductRepository repository)
    {
        _repository = repository;
    }

    [HttpGet]
    public async Task<IEnumerable<Product>> Get() =>
        await _repository.GetAllAsync();
}
    

Another example

1️⃣ Define the entity in ProductService.Domain

The Product entity is a plain class that represents a product in the business domain.

// ProductService.Domain/Entities/Product.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}
    

2️⃣ Define the repository interface in ProductService.Domain

The IProductRepository interface defines the contract for data operations on the Product entity. This interface belongs in the domain layer, making the application layer dependent on this contract, not on EF Core itself.

// ProductService.Domain/Interfaces/IProductRepository.cs
public interface IProductRepository
{
    Task<Product> GetByIdAsync(int id);
    Task<IEnumerable<Product>> GetAllAsync();
    Task AddAsync(Product product);
    Task UpdateAsync(Product product);
    Task DeleteAsync(Product product);
}
    

3️⃣ Implement the repository in ProductService.Infrastructure

This is where the EF Core-specific logic resides. The ProductRepository class implements the IProductRepository interface, using ApplicationDbContext to perform database operations.

// ProductService.Infrastructure/Repositories/ProductRepository.cs
using ProductService.Domain.Entities;
using ProductService.Domain.Interfaces;
using ProductService.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

public class ProductRepository : IProductRepository
{
    private readonly ApplicationDbContext _context;

    public ProductRepository(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<Product> GetByIdAsync(int id)
    {
        return await _context.Products.FindAsync(id);
    }

    public async Task<IEnumerable<Product>> GetAllAsync()
    {
        return await _context.Products.ToListAsync();
    }

    public async Task AddAsync(Product product)
    {
        await _context.Products.AddAsync(product);
        await _context.SaveChangesAsync();
    }

    public async Task UpdateAsync(Product product)
    {
        _context.Entry(product).State = EntityState.Modified;
        await _context.SaveChangesAsync();
    }

    public async Task DeleteAsync(Product product)
    {
        _context.Products.Remove(product);
        await _context.SaveChangesAsync();
    }
}
    

4️⃣ Register the dependencies in Program.cs

In the API's Program.cs, you register the DbContext and tell the dependency injection (DI) container to provide an instance of ProductRepository whenever IProductRepository is requested.

// ProductService.Api/Program.cs
using ProductService.Domain.Interfaces;
using ProductService.Infrastructure.Data;
using ProductService.Infrastructure.Repositories;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddScoped<IProductRepository, ProductRepository>();

// ... other services and middleware
    

5️⃣ Use the repository in a controller

The controller, which represents the business logic layer, depends only on the IProductRepository interface. It has no knowledge of EF Core, making it highly decoupled.

// ProductService.Api/Controllers/ProductsController.cs
[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductRepository _repository;

    public ProductsController(IProductRepository repository)
    {
        _repository = repository;
    }

    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        var products = await _repository.GetAllAsync();
        return Ok(products);
    }

    [HttpPost]
    public async Task<IActionResult> AddProduct([FromBody] Product product)
    {
        await _repository.AddAsync(product);
        return CreatedAtAction(nameof(GetAll), null, product);
    }
}
    

✅ Advantages

  • Decoupling from EF Core: The application's core logic is not tied to a specific data access technology.
  • Improved testability: Easily mock repositories for unit testing without a real database.
  • Centralized data access: Improves maintainability and readability.
  • Enforced domain rules: Ensures consistency by handling rules within the repository.
  • Decouples business logic from data access logic.
  • Makes unit testing easier (mock repositories).
  • Centralizes data access logic in one place.
  • Improves maintainability and readability.

⚠️ Disadvantages

  • Can be redundant: EF Core already implements repository/unit of work patterns.
  • Increased complexity: Adds classes, interfaces, and DI configuration.
  • Limited query flexibility: May cause inefficiencies.
  • Can cause N+1 problem: If lazy loading isn’t managed carefully.
  • Extra layer of abstraction adds boilerplate code.
  • May duplicate EF Core’s built-in abstractions (DbSet already acts like a repository).
  • Over-engineering for small/simple projects.

🧭 Best Practices

  • Use Generic Repository for common CRUD operations.
  • Combine with Unit of Work for transaction management.
  • Keep repositories focused on persistence logic only.
  • Use Specification Pattern for complex queries.

🕐 When to Use

  • Complex or long-lived applications.
  • High testability requirements.
  • Domain-driven design (DDD) alignment.
  • Potential for data layer changes.

🚫 When Not to Use

  • Simple CRUD applications.
  • Rapid prototyping.
  • Small teams with limited experience.

🔒 Precautions

  • Don’t hide EF Core features unnecessarily (e.g., IQueryable).
  • Ensure async methods are used for scalability.
  • Secure repository methods if dealing with sensitive data.
  • Log repository operations for debugging and auditing.
  • Avoid "useless wrappers".
  • Be mindful of query performance.
  • Enforce the pattern.
  • Decide on generic vs. specific approach.

🎯 Summary

The Repository Pattern is a powerful way to structure data access in .NET Core. While EF Core already provides repository-like behavior, using repositories makes your code more testable, maintainable, and aligned with clean architecture principles. For large projects, combine it with Unit of Work and Specification Pattern for maximum flexibility.

Back to Index
Previous Entity Framework Core Model First Unit of Work Patterns Next
*