One of the big debates in our C# niche is whether we should use Dapper or Entity Framework when making calls to the database.
Index
1 - What are Dapper and Entity Framework Core?
In C# we have various ways to connect to a relational database, the most common is to use libraries, with the most popular being Dapper and Entity Framework Core.
I have content for both libraries on this channel.
Dapper is a micro ORM that translates the data returned from the database into objects within C#. Link to the post.
Entity Framework is an ORM library created and maintained by Microsoft. It is much more complete and larger, containing many more functionalities than Dapper. Link to the Entity Framework course.
Dapper's functionalities are also available within EF Core, plus many more.
1.1- Why are there so many doubts about using Dapper or Entity Framework?
The answer to this question comes from older versions of Entity Framework, especially versions prior to .NET 5, where Entity Framework's performance was very slow when executing queries. Not for all types of queries, but when querying multiple tables, the SQL that Entity Framework generated under the hood wasn't the most efficient. In fact, it was often pretty poor.
So, despite the benefits that Entity Framework brings, such as migrations, entity tracking, interceptors, etc., many companies preferred not to use it and opted for Dapper or even for using their database's connector directly.
Since .NET 5, performance in this area has improved and is supposed to be on par, or very close, to Dapper, which is what we are going to check in this post.
In fact, in the 2021 Netconf it was stated that previously Dapper was 55% faster than EF, but from that version onwards it was just 5%.
This difference is quite significant, but in many cases it "doesn't matter". If an app receives a call per minute, it doesn't matter if it takes 30ms or 50ms to respond, but if you have several thousand calls per minute, those milliseconds do matter.
2 - Setting up the scenario to test Dapper and Entity Framework.
For this test, I am not going to 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).
Taking advantage of the fact that I already have a large part 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 using Entity Framework in the real world, you can buy my book, the Complete Full Stack Development Guide with .NET.
2.1 - Running the tests
We are not going to introduce extra configuration such as adding cache interceptors to Entity Framework. What we will do is try to replicate a real production scenario.
For example, in Entity Framework we will do it through the dbContext entity (which requires reading first) and the Unit of Work since this is how companies do it in the real world.
Similarly, I have added code so Dapper can do transactions, which obviously makes the result a little slower compared to making plain calls, but it is more in line with what a real production application would do.
Finally, the test is not exclusively for database communication but will run against the API. Both options have the same code and the result should be conclusive. If you are unsure how everything is set up, the code is fully 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, we simply pass an ID to the use case, which will be used to identify 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 we see, inserting with Dapper is slightly faster than doing it with Entity Framework Core. Although in this process we are calling an API, not just performing the insertion, since, as I said, the goal is to replicate a real scenario.
Note: each test creates about 3.5k records in the user database and about 7k in the experience 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 how Dapper is slightly faster, but not by much, which is expected since Dapper simply translates the SQL response to an object in C#.
3 - Can we use both Dapper and EF Core?
A question you might be asking yourself is whether we can use a combination of both. We could use EF Core for everything to do with inserts, updates, and deletes, since the speed is practically the same and Entity Framework provides a lot of extra features such as interceptors, the repository pattern and Unit Of Work, ease of use, etc.
And on the other hand, Dapper is faster when reading.
The answer is yes. Now, in my opinion, it's not worth it since both have their own specific configurations and it's always a hassle to have to update several elements and settings.
My recommendation is to know when to use each one. If you have an app that receives a couple of calls per minute, Dapper is not going to give you an advantage or a benefit that justifies replacing EF.
On the other hand, if you have an app that receives 100,000 calls per minute, and 99% of them are reads, in that case Dapper will give you a performance improvement over Entity Framework.
If there is any problem you can add a comment bellow or contact me in the website's contact form
 
                    