ASP.NET Core  

Repository Pattern in ASP.NET Core: A Simple Explanation

Introduction

When building web applications with ASP.NET Core, developers often need to interact with a database. While Entity Framework Core allows direct database operations, using raw database queries in controllers or services can make your code messy, tightly coupled, and hard to maintain.

This is where the Repository Pattern comes into play. It provides a clean, organized way to manage data access, separating the database logic from the business logic.

This article explains the repository pattern in a simple, step-by-step way, including why it is useful, how it works, and how to implement it in ASP.NET Core.

What is the Repository Pattern?

The repository pattern is a design pattern that abstracts the data access logic and provides a centralized way to interact with the database.

Key Principles

  1. Separation of Concerns: Controllers or services do not directly access the database.

  2. Abstraction: The rest of the application only interacts with repository interfaces.

  3. Testability: Makes unit testing easier because repositories can be mocked.

Think of a repository as a middleman between your application and the database.

Why Use the Repository Pattern?

Using the repository pattern has several advantages:

  1. Clean Code: Keeps controllers and services simple.

  2. Reusability: Common database operations can be reused across multiple controllers.

  3. Maintainability: Changes in the database layer do not affect business logic.

  4. Unit Testing: Allows mocking repositories without connecting to the actual database.

  5. Consistency: Ensures consistent data access patterns across the application.

Basic Structure of Repository Pattern

A typical implementation includes:

  1. Repository Interface: Defines the methods for data access.

  2. Repository Implementation: Contains actual database logic using Entity Framework Core or other ORMs.

  3. Service or Controller: Uses the repository to perform operations.

Step 1: Create the Model

Suppose we have a User model:

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

Step 2: Create the Repository Interface

Create an interface IUserRepository:

using System.Collections.Generic;
using System.Threading.Tasks;

public interface IUserRepository
{
    Task<IEnumerable<User>> GetAllUsersAsync();
    Task<User> GetUserByIdAsync(int id);
    Task AddUserAsync(User user);
    Task UpdateUserAsync(User user);
    Task DeleteUserAsync(int id);
}
  • This defines all the operations we want to perform on users.

  • Notice that the interface does not include database-specific code, just method signatures.

Step 3: Implement the Repository

Create a class UserRepository:

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;

public class UserRepository : IUserRepository
{
    private readonly AppDbContext _context;

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

    public async Task<IEnumerable<User>> GetAllUsersAsync()
    {
        return await _context.Users.ToListAsync();
    }

    public async Task<User> GetUserByIdAsync(int id)
    {
        return await _context.Users.FindAsync(id);
    }

    public async Task AddUserAsync(User user)
    {
        await _context.Users.AddAsync(user);
        await _context.SaveChangesAsync();
    }

    public async Task UpdateUserAsync(User user)
    {
        _context.Users.Update(user);
        await _context.SaveChangesAsync();
    }

    public async Task DeleteUserAsync(int id)
    {
        var user = await _context.Users.FindAsync(id);
        if (user != null)
        {
            _context.Users.Remove(user);
            await _context.SaveChangesAsync();
        }
    }
}
  • AppDbContext is the Entity Framework Core database context.

  • All database operations are encapsulated in the repository.

Step 4: Register Repository in Dependency Injection

In Program.cs:

builder.Services.AddScoped<IUserRepository, UserRepository>();
  • This registers the repository so it can be injected into controllers or services.

Step 5: Use Repository in Controller

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IUserRepository _userRepository;

    public UsersController(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    [HttpGet]
    public async Task<IEnumerable<User>> GetUsers()
    {
        return await _userRepository.GetAllUsersAsync();
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<User>> GetUser(int id)
    {
        var user = await _userRepository.GetUserByIdAsync(id);
        if (user == null) return NotFound();
        return user;
    }

    [HttpPost]
    public async Task<ActionResult> AddUser(User user)
    {
        await _userRepository.AddUserAsync(user);
        return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
    }

    [HttpPut("{id}")]
    public async Task<ActionResult> UpdateUser(int id, User user)
    {
        if (id != user.Id) return BadRequest();
        await _userRepository.UpdateUserAsync(user);
        return NoContent();
    }

    [HttpDelete("{id}")]
    public async Task<ActionResult> DeleteUser(int id)
    {
        await _userRepository.DeleteUserAsync(id);
        return NoContent();
    }
}
  • The controller no longer contains database code.

  • It simply calls repository methods, keeping the code clean and maintainable.

When to Use the Repository Pattern

  • Medium to large projects where multiple controllers use the same data models.

  • When you need testable code for unit tests.

  • When database logic changes frequently, keeping it separate prevents breaking the application.

For small projects, the repository pattern may be optional because it can add extra layers.

Benefits of Using Repository Pattern

  1. Clean Architecture: Separates data access from business logic.

  2. Maintainability: Database changes do not affect controllers.

  3. Reusability: Common methods are reused across the application.

  4. Testability: Interfaces allow mocking repositories for testing.

  5. Flexibility: Can switch database providers without affecting the rest of the code.

Common Mistakes to Avoid

  1. Mixing business logic with repository methods: Keep repository focused on data access.

  2. Not using async/await: Makes database calls synchronous and reduces performance.

  3. Writing complex queries in controllers: Always keep queries inside repositories.

  4. Skipping interfaces: Without interfaces, testing becomes difficult.

  5. Overusing repository pattern for tiny projects: Can add unnecessary complexity.

Conclusion

The Repository Pattern in ASP.NET Core is a simple yet powerful design pattern that promotes clean, maintainable, and testable code. By separating database access logic from controllers and services, it allows developers to focus on business logic without worrying about how data is stored or retrieved.

For beginners, understanding and implementing the repository pattern is an important step towards building scalable and professional ASP.NET Core applications.

Once you master this, you can combine it with Unit of Work pattern, Service Layer, and Dependency Injection to create a complete, enterprise-ready architecture.