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!