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

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

After

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:

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!