If you're familiar with the .NET software development world, you've probably heard of the repository pattern. Sometimes, this pattern is considered "old-fashioned" in the age of microservices and NoSQL databases. But I'm here to tell you it's still as relevant and useful as ever.
Table of Contents
1 - What Is the Repository Pattern?
The repository pattern is an abstraction of the data layer. Essentially, a repository is like a black box that can receive and send data to and from your database. The good thing about the repository pattern is that, from the perspective of the rest of your code, it doesn’t matter what kind of database you’re using or how it's implemented. In other words, we're abstracting the implementation.
Let’s say you have an app with a MySQL database. Your app needs to fetch and save data into this database. Instead of spreading SQL queries throughout your code, you can encapsulate all these operations in a repository. This way, if in the future you want to change your database, the process is much simpler.
2 - Why Is the Repository Pattern Useful?
An obvious benefit of the repository pattern is that it makes your code much cleaner and easier to maintain.
But it also has other advantages. For example, if at some point in the future you decide to switch to a different database, you’ll only have to update your repositories, not your entire codebase. (Even though in reality, nobody ever changes the database).
Additionally, the repository pattern makes your code much easier to test. You can use a mock repository in your unit tests to simulate the behavior of your database. This lets you test your business logic without having to set up a test database.
2.1 - Are Dbcontext and DbSet an Implementation of the Repository Pattern?
Technically speaking, when we use Entity Framework, DbContext is similar to what we consider the unit of work, and DbSet to a repository implementation.
That said, some people (myself included) prefer to have an extra layer of abstraction on top of the database, because that way it's easier to test and it's decoupled from Entity Framework.
But like everything, it’s a design decision that has pros and cons and will depend on the team's needs.
3 - When Should You Use the Repository Pattern?
Like all design patterns, the repository pattern is not mandatory in every implementation.
In some situations, it can be unnecessary overhead, especially if your app is very simple and you only need to do basic CRUD operations.
Also, sometimes you'll want to use database-specific features that can't be easily encapsulated in a repository.
4 - Implementing the Repository Pattern in C#
Before we dive into the code, remember that you can find it on GitHub. For this post, we’ll continue with the code we have been working on in the Entity Framework Core course.
For this example, we’ll use the code from the other day, where we insert a user into the Users
table.
[HttpPost("InsertDataExample2")]public async Task InsertDataExample2(){ User user1 = new User() { Email = $"{Guid.NewGuid()}@mail.com", UserName = "id1", Wokringexperiences = new List() { new Wokringexperience() { Name = "experience 1 same object", Details = "details1", Environment = "environment" }, new Wokringexperience() { Name = "experience 2 same object", Details = "details2", Environment = "environment" } } }; await _context.Users.AddAsync(user1); await _context.SaveChangesAsync();}
What we’re going to do is modify the code to use the repository pattern.
Note: If you followed the previous post where we worked with foreign keys, you’ll know that in another endpoint we did the following code:
await _context.Users.AddAsync(user1);await _context.Wokringexperiences.AddRangeAsync(workingExperiences1);await _context.SaveChangesAsync();
We WILL NOT modify this, as inserting into two tables independently requires a transaction, and we'll look at that when we see the unitOfWork
pattern in the next post.
Back to our main point: if you paid attention, what we do with the repository pattern is abstract the implementation, and in C# we abstract with interfaces.
For this, we'll create an interface called IUserRepository
and a class that will act as that repository.
public interface IUserRepository{ }public class UserRepository : IUserRepository{ }
Now, what we need to do in our repository is interact with the database; in our case, inject the DBContext
and create a method called Insert
, which will insert the record into the database.
And while we're at it, let's do another to read by id
.
public interface IUserRepository{ Task Insert(User user); Task GetById(int id);}public class UserRepository : IUserRepository{ private readonly CursoEfContext _context; public UserRepository(CursoEfContext context) { _context = context; } public async Task Insert(User user) { EntityEntry insertedUser = await _context.Users.AddAsync(user); await _context.SaveChangesAsync(); return insertedUser.Entity; } public async Task GetById(int id) => await _context.Users .Include(a => a.Wokringexperiences) .FirstOrDefaultAsync(x => x.Id == id);}
As you can see, we’re saving the insertion inside the repository itself. This will change when we have transactions.
Another common thing in companies is having a class/interface called IGenericRepository<T>
where T
is the generic of a specific type, and it already brings implemented the basic actions like insert, delete, update, and read.
Now all that’s left is to include our new repository in the dependencies and change the logic so that insertion happens through the repository instead of directly with the DBContext
.
[HttpPost("InsertDataExample2")]public async Task InsertDataExample2(){ User user1 = new User() { Email = $"{Guid.NewGuid()}@mail.com", UserName = "id1", Wokringexperiences = new List() { new Wokringexperience() { Name = "experience 1 same object", Details = "details1", Environment = "environment" }, new Wokringexperience() { Name = "experience 2 same object", Details = "details2", Environment = "environment" } } }; await _userRepository.Insert(user1);}[HttpGet("{userId}")]public async Task GetExample(int userId) => await _userRepository.GetById(userId);
And if we run the code, we see how it is inserted correctly. If we execute the Get by id endpoint, we can see the result.
GET https://localhost:44383/Relations/3{ "id": 3, "userName": "id1", "email": "[email protected]", "wokringexperiences": [ { "id": 1, "userId": 3, "name": "experience 1 same object", "details": "details1", "environment": "environment", "startDate": null, "endDate": null }, { "id": 2, "userId": 3, "name": "experience 2 same object", "details": "details2", "environment": "environment", "startDate": null, "endDate": null } ]}
Note: It's id:3
because the other two are created when spinning up the system.
If there is any problem you can add a comment bellow or contact me in the website's contact form