If you’re familiar with the world of software development in .NET, you’ve probably heard about the repository pattern. Sometimes, this pattern is considered “outdated” in the era of microservices and NoSQL databases. But I’m here to tell you that it is still just as relevant and useful as ever.
Index
1 - What is the Repository Pattern?
The repository pattern is an abstraction of the data access 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 which type of database you’re using or how it’s implemented. In other words, we are abstracting the implementation.
Let’s say you have an application with a MySQL database. Your app needs to retrieve and save data in this database. Instead of scattering 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 will be 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 you ever decide to switch to a different database in the future, you’ll only need to update your repositories, not all your code. (Although in reality almost nobody ever changes the database).
Moreover, the repository pattern makes your code much easier to test. You can use a mock of your repository in unit tests, which simulates the behavior of your database. This allows you to test your business logic without having to worry about setting up a test database.
2.1 - Is DbContext and DbSet an implementation of the repository pattern?
Technically, it’s true that when we use Entity Framework, DbContext is similar to the unit of work and DbSet is an implementation of the repository.
Despite this, some people, myself included, prefer to have an additional layer of abstraction on top of the database, as it makes testing easier and decouples us from Entity Framework.
But as always, this is a design decision with pros and cons, and it depends on the team’s needs.
3 - When should you use the repository pattern?
Like all design patterns, the repository pattern isn’t mandatory in every implementation.
In some situations, it could be an unnecessary overhead, especially if your application is very simple and you only need to perform basic CRUD operations.
Also, sometimes you might want to use specific features of your database 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 have it available on GitHub. For this post, we’re going to continue with the code we’ve been working on in the Entity Framework Core course.
For this example, we’ll take the code we saw 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<Wokringexperience>()
{
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’re NOT going to modify this one, since to insert into two tables independently we need a transaction, and we’ll see this transaction when we cover the unitOfWork
pattern in the next post.
Let’s get back to business. If you’ve been paying attention, what we do with the repository pattern is abstract the implementation, and in C# we abstract with interfaces.
For this, we create an interface called IUserRepository
and a class which will act as that repository.
public interface IUserRepository
{
}
public class UserRepository : IUserRepository
{
}
Now what we need to do in our repository is handle the interaction with the database; in our case, we 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 also add another to read by id
.
public interface IUserRepository
{
Task<User> Insert(User user);
Task<User?> GetById(int id);
}
public class UserRepository : IUserRepository
{
private readonly CursoEfContext _context;
public UserRepository(CursoEfContext context)
{
_context = context;
}
public async Task<User> Insert(User user)
{
EntityEntry<User> insertedUser = await _context.Users.AddAsync(user);
await _context.SaveChangesAsync();
return insertedUser.Entity;
}
public async Task<User?> 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 thing that’s very common in companies is to have a class/interface called IGenericRepository<T>
where T
is a generic of a specific type, and it already provides 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 is done with 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<Wokringexperience>()
{
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<User?> GetExample(int userId)
=> await _userRepository.GetById(userId);
And if we run the code, we’ll see how it is inserted correctly. If we call the Get endpoint by id, we’ll 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 the system spins up.
If there is any problem you can add a comment bellow or contact me in the website's contact form