Design Patterns & Practices  

Repository Pattern in .NET Explained with a Clear and Practical Example

Introduction

In many .NET applications, database code often gets mixed into the business logic, making the system difficult to read, test, and maintain.The Repository Pattern solves this problem by moving all data access into a dedicated class, providing the rest of the application with clean and meaningful methods. This article explains how to use this pattern through a simple user registration example. 

The Problem We Are Solving

Imagine a simple requirement: “Register a user and later fetch user details.”

Direct Database Access (Problematic)

public class UserService
{
    private List<User> _users = new();

    public void Register(string name)
    {
        _users.Add(new User { Id = 1, Name = name });
    }

    public User GetUser(int id)
    {
        return _users.FirstOrDefault(u => u.Id == id);
    }
}

Why This Is a Problem

  • Data storage logic is mixed with business logic

  • Changing storage (database, API, cache) affects business code

  • Hard to test without real data

Introducing the Repository Pattern

Building a clean Repository Pattern in .NET involves these four simple steps:

1. Define the Model and Interface

First, create a basic User model and an IUserRepository interface. The interface acts as a contract that defines which data operations are available without worrying about how the database works.

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

public interface IUserRepository
{
    void Add(User user);
    User GetById(int id);
}

2. Implement the Repository

The UserRepository class handles the actual data logic. In this example, it manages an in-memory list, keeping all data storage details isolated from the rest of the app.

public class UserRepository : IUserRepository
{
    private readonly List<User> _users = new();

    public void Add(User user) => _users.Add(user);
    public User GetById(int id) => _users.FirstOrDefault(u => u.Id == id);
}

3. Inject into Business Logic

The UserService now interacts only with the interface. This makes the business logic cleaner because it doesn't need to know if the data is stored in a SQL database, a file, or a simple list.

public class UserService
{
    private readonly IUserRepository _repo;
    public UserService(IUserRepository repo) => _repo = repo;

    public void RegisterUser(string name)
    {
        _repo.Add(new User { Id = 1, Name = name });
        Console.WriteLine("User registered successfully");
    }
}

4. Run the Application

Finally, you connect the pieces. You pass the specific repository implementation into your service, allowing the application to run smoothly.

var repository = new UserRepository();
var userService = new UserService(repository);

userService.RegisterUser("Bob");

What Changed After Using Repository?

Before

  • UserService handled data storage

  • Business logic was cluttered

  • Hard to change data source

After

  • Repository handles data access

  • UserService focuses on business rules

  • Data source can change without touching UserService

Replacing Storage Without Changing Business Logic

The true power of the Repository Pattern lies in its flexibility: you can swap out your storage method at any time without touching your business logic. Whether you transition from an

InMemoryUserRepository to a SqlUserRepository or even an ApiUserRepository, the UserService remains exactly the same. Because the service relies on an interface rather than a specific database implementation, your core application logic stays protected from infrastructure changes.

Common Mistakes to Avoid

To keep your architecture clean, avoid these common Repository Pattern pitfalls:

  • Mixing Logic: Never put business logic inside repositories; they should strictly handle data persistence, while services handle rules.

  • Overcrowded Interfaces: Avoid creating repositories with too many methods; keep them focused on specific entities to prevent them from becoming unmanageable.

  • Over-Engineering: Don't use this pattern for tiny applications where the added complexity outweighs the benefits of abstraction.

Conclusion

In this article, we have seen how the Repository Pattern creates a clean separation between data access and business logic in .NET applications. By using this abstraction, you ensure your code is easier to test, more readable, and flexible enough to handle future storage changes without breaking your core logic. Hope this is useful!