One of the big debates within our C# niche is whether we should use Dapper or Entity Framework when making database calls.
Index
1 - What is Dapper and Entity Framework Core?
Within C# we have different ways to connect to a relational database. The most common way is to use libraries, and the most popular are Dapper
and Entity Framework Core
.
I have content for both libraries on this channel.
Dapper is a micro ORM that translates the information returned from the database into objects within C#. Post link.
Entity Framework is an ORM library created and maintained by Microsoft. It's much more complete and larger, with many more functionalities than Dapper. Entity Framework course link.
Dapper's functionalities are also available within EF Core, in addition to many others.
1.1- Why is there so much uncertainty about using Dapper or Entity Framework?
The answer to this question comes from the fact that, especially in older versions of Entity Framework before .NET 5, the performance of Entity Framework was very slow when it came to making queries. Not for all types of queries, but when you had to query multiple tables, the SQL generated behind the scenes by Entity Framework wasn't the most efficient—in fact, quite the opposite, it was usually pretty bad.
So, despite the benefits that Entity Framework brings, like migrations, entity tracking, interceptors, and so on, many companies preferred not to use it and instead opted for Dapper or even the database connector they were using directly.
Since .NET 5, performance has improved and is supposed to be on par, or at least very close to Dapper, which is what we'll be checking in this post.
In fact, during the netconf 2021 event, they said previously Dapper was about 55% faster than EF, but from that version on, it was only about 5% faster.
This difference is quite significant, but in many cases, "it doesn't matter"—if an app receives just one call per minute, it doesn't really matter if it takes 30ms or 50ms to respond. However, if instead of two calls per minute you have several thousand, those milliseconds do matter.
2 - Building the Scenario for Testing Dapper and Entity Framework.
For this testing, I won't complicate things too much, and the architecture will be as follows:
- PostgreSQL database running in Docker.
- Application using the database
- Test project that will call the database through the API layer (yes, API calls).
Since I already have most of the code written, let's use my GitHub project from the Entity Framework Core course, where we have two entities: the User entity with Email, name, and Id, and the WorkingExperience entity with Id and several fields. The relationship is 1-n, where a user can have multiple WorkingExperiences.
If you want more details about real-world use of Entity Framework, you can buy my book, the Complete Guide to Full Stack Development with .NET.
2.1 - Executing the Tests
We won't introduce extra configuration like adding interceptors to the Entity Framework cache, but we will try to replicate a real production scenario.
For example, in Entity Framework, we will use the entity from DbContext (which requires reading first) and the Unit of Work since that's how companies do it in the real world.
Similarly, I've added code so Dapper can handle transactions. Obviously, this makes the result a bit slower compared to making plain calls, but it's closer to what a real production application would be.
Finally, the test is not exclusive to database communication, but is executed against the API. Both options have the same code, and the result should be conclusive. If you have doubts about how it's all set up, the code is entirely available on GitHub.
Packages to use and versions:
Package | version |
Npgsql.EntityFrameworkCore.PostgreSQL | 9.0.4 |
Microsfot.EntityFrameworkCore | 9.0.1 |
Dapper | 2.1.66 |
2.2 - Insert Test
To insert, and to save time, I simply pass an id to the use case, which will be used to identify each case more easily, but all the data is hardcoded to simplify the process.
This is the Entity Framework scenario
public class InsertUser(IUnitOfWork unitOfWork){ public async Task<User> Execute(int id) { User user = new User() { Email = $"{Guid.NewGuid()}@mail.com", UserName = $"id{id}" }; List<Wokringexperience> workingExperiences = [ new() { User = user, Name = $"experience1 user {id}", Details = "details1", Environment = "environment" }, new() { User = user, Name = $"experience user {id}", Details = "details2", Environment = "environment" } ]; user = await unitOfWork.UserRepository.Insert(user); // 👈 await unitOfWork.WorkingExperienceRepository.Insert(workingExperiences); // 👈 _ = await unitOfWork.Save();// 👈 return user; }}
For Dapper, we do the same:
public class InsertUserDapper(UoWDapper unitOfWork){ public async Task<UserDto> Execute(int id) { await unitOfWork.OpenTransaction(); // 👈 UserDto userDto = await unitOfWork.UserDapperRepository.InsertSingle(new UserDto() { Email = $"{Guid.NewGuid()}@mail.com", UserName = $"id{id}" }); List<WorkingExperienceDto> workingExperiences = [ new() { UserId = userDto.Id, Name = $"experience1 user {id}", Details = "details1", Environment = "environment" }, new() { UserId = userDto.Id, Name = $"experience user {id}", Details = "details2", Environment = "environment" } ]; List<WorkingExperienceDto> updatedExperiences = await unitOfWork .WorkingExperienceDapperRepository .InsertList(workingExperiences); // 👈 await unitOfWork.CommitTransaction(); // 👈 userDto.WorkingExperiences = updatedExperiences; return userDto; }}
And the results are very similar:
As you can see, inserting with Dapper is slightly faster than with Entity Framework Core. However, in this process, we're calling an API, not just inserting, since, as I mentioned, I'm trying to replicate a real situation.
Note: each test creates about 3.5k records in the users database and about 7k in the experiences database.
2.3 - Read Test
Just like the previous case, we simply read the results.
Entity Framework code:
public class GetUser(IUnitOfWork unitOfWork){ public async Task<User?> Execute(int id) { return await unitOfWork.UserRepository.GetByIdWithWorkingExperiences(id); }}public async Task<User?> GetByIdWithWorkingExperiences(int id) => await Entities .Include(a => a.Wokringexperiences) .FirstOrDefaultAsync(x => x.Id == id);
Dapper code:
public class GetUserDapper(UoWDapper unitOfWork){ public async Task<UserDto?> Execute(int id) { return await unitOfWork.UserDapperRepository.GetById(id); }}public async Task<UserDto?> GetById(int id){ DbConnection connection = await _transaction.GetConnectionAsync(); string sql = "select u.*, w.* " + " from users u " + " inner join workingexperiences w on u.id = w.userid " + " where u.id = @id"; UserDto? user = null; await connection.QueryAsync<UserDto, WorkingExperienceDto, UserDto>( sql, (userResult, workingExperience) => { if (user == null) { user = userResult; user.WorkingExperiences = new List<WorkingExperienceDto>(); } if (workingExperience != null) { user.WorkingExperiences.Add(workingExperience); } return user; }, new { id }, splitOn: "id" ); return user;}
And the result:
We can see that Dapper is slightly faster, but not by much, which is expected since Dapper simply translates the SQL response to a C# object.
3 - Can we use both Dapper and EF Core?
A question you might be asking yourself is if it's possible to combine both. We use EF Core for all inserts, updates, and deletes since the speed is practically the same, and Entity Framework gives us a lot of extra functionality like interceptors, the repository and Unit Of Work pattern, ease of use, and so on.
On the other hand, Dapper is faster for reading.
The answer is yes. However, in my opinion, it's not worth it, since both have their own specific configurations, and it's always a hassle to update multiple elements and settings.
My recommendation is to know when to use each one. If you have an app that gets just a couple of calls per minute, Dapper won’t provide enough advantage to justify replacing EF.
On the other hand, if you have an app that receives 100,000 calls per minute, and 99% are reads, in that case Dapper will give you a performance boost compared to Entity Framework.
If there is any problem you can add a comment bellow or contact me in the website's contact form