EF Core 9 vs. Dapper: Performance Face-Off

This blog is presented as a part of C# Advent 2024. Follow the link to check out the rest of the excellent C# and .NET content coming out in 2 blogs per day between December 1 - 25.

Every time I decide which ORM to use in a project, I find myself personally conflicted between Dapper and Entity Framework (EF). Both are incredibly powerful ORMs and excel in the roles they were designed for, albeit in slightly different ways. Both promise their own advantages and disadvantages.

Dapper is renowned for its speed and simplicity. As long as you write efficient SQL queries, the rest is straightforward. On the other hand, EF competes by offering a convenient and modern approach to writing queries using LINQ—a style many developers love and frequently use. It also provides robust tools for managing database versioning (migrations), though sometimes at the cost of speed and memory usage. I wanted to know, though, is that still the case?

With every new release of .NET or Dapper, I eagerly explore what’s new in each. Has Dapper widened its speed advantage, or has EF closed the gap? Are there new features that could sway the decision for our next project? The release of .NET 9 brings a host of improvements. If you’re interested in learning more, check out Trailhead’s recent podcast and blog on the latest features in .NET 9!

Below I review the performance of EF Core 9, the newest version of Entity Framework released with .NET 9, and compare it to the perennial favorite in performance, Dapper. I think you will be pleasantly surprised by the results like I was.

The Benchmark Tests

I created three tests to help me benchmark the most common CRUD tasks in a software application accessing a database: reading, inserting, and updating:

  • Get Benchmark: Single or multiple entities
  • Insert Benchmark: Single or multiple entities
  • Update Benchmark: Single entity

Test Project

I created a new .NET 9 Console Application and incorporated the following essential packages:

  • BenchmarkDotNet: For benchmarking and testing.
  • Bogus: To generate realistic test data.
  • Dapper: A simple object mapper for .NET.
  • Microsoft.EntityFrameworkCore.SqlServer: Entity Framework Core provider for SQL Server.
  • Microsoft.EntityFrameworkCore.Design: Design-time tools for EF Core.

Database

For my tests, I utilized the ever-popular AdventureWorks database without any optimizations or adjustments—straight out of the box—to ensure fair comparisons.You can download the AdventureWorks sample database from Microsoft’s GitHub repository.

Code

I split the benchmarks into Get, Insert, and Update to isolate each test and run them independently. I also created a simple AppDbContext to represent the model configuration used by Entity Framework.

Additionally, I added a DbFactory a class whose sole purpose is to provide database connections for both EF and Dapper simultaneously. Here’s how that class looks:

public static class DBFactory
{
    public static async Task<IDbConnection> CreateConnectionAsync()
    {
        var connectionString = "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=AdventureWorksDW2019";
        var connection = new SqlConnection(connectionString);
        await connection.OpenAsync();
        return connection;
    }
}

Test 1: Get a List of Entities

Retrieve a simple list of entities based on a specified WHERE condition.

 [Benchmark]
    public async Task<IList<FactResellerSale>> EFCore_To_List_Raw()
    {
        return await _dbContext.FactResellerSales
            .FromSqlRaw("SELECT * FROM FactResellerSales WHERE  OrderDateKey = {0}", new SqlParameter("@OrderDateKey", _orderDateKey))
            .ToListAsync();
    }

    [Benchmark]
    public async Task<IList<FactResellerSale>> EFCore_To_List_LINQ()
    {
        return await _dbContext.FactResellerSales
            .Where(frs => frs.OrderDateKey == _orderDateKey)
            .ToListAsync();
    }

    [Benchmark]
    public async Task<IList<FactResellerSale>> Dapper_To_List()
    {
        var qry = await _dbConnection.QueryAsync<FactResellerSale>(
            sql: "SELECT * FROM FactResellerSales WHERE OrderDateKey = @OrderDateKey", new { OrderDateKey = _orderDateKey });
        return qry.ToList();
    }

As you can see I am retrieving entities based on OrderDateKey. In this database, there are roughly 14,000 items, making it a fair test.

MethodMeanErrorStdDevAllocated
EFCore_To_List_Raw5.861 ms0.0777 ms0.0727 ms930.7 KB
EFCore_To_List_LINQ5.862 ms0.1122 ms0.2214 ms927.56 KB
Dapper_To_List5.643 ms0.0615 ms0.0575 ms1460.89 KB

Key Takeaways

It seems that EF has come incredibly close to matching Dapper in speed. The measured difference was minimal—5.862 ms for EF compared to 5.643 ms for Dapper—making EF only about 1.04x slower.

Surprisingly, Dapper used more memory—1460.89 KB compared to 930.7 KB for EF—making Dapper approximately 1.57x more memory-intensive. This was unexpected and might be due to the large number of columns being retrieved from the database.

Still, these results demonstrate that EF is continuously improving, especially with the advancements brought by .NET 9.

Test 2: Get a Single Entity

Retrieve a single entity based on a specified WHERE condition.

    [Benchmark]
    public async Task<FactResellerSale> Dapper_GetById_FirstAsync()
    {
        var qry = await _dbConnection.QueryFirstAsync<FactResellerSale>(
            sql: """
                 SELECT TOP 1 *
                 FROM dbo.FactResellerSales AS frs
                 WHERE 1 = 1
                       AND frs.SalesOrderNumber = @SalesOrderNumber
                       AND frs.SalesOrderLineNumber = @SalesOrderLineNumber
                 """, new { SalesOrderNumber = _salesOderNumber, SalesOrderLineNumber = _salesOrderLineNumber });
        return qry;
    }

    [Benchmark]
    public async Task<FactResellerSale?> Dapper_GetById_FirstOrDefaultAsync()
    {
        var qry = await _dbConnection.QueryFirstOrDefaultAsync<FactResellerSale>(
            sql: """
                 SELECT TOP 1 *
                 FROM dbo.FactResellerSales AS frs
                 WHERE 1 = 1
                       AND frs.SalesOrderNumber = @SalesOrderNumber
                       AND frs.SalesOrderLineNumber = @SalesOrderLineNumber
                 """, new { SalesOrderNumber = _salesOderNumber, SalesOrderLineNumber = _salesOrderLineNumber });
        return qry;
    }

    [Benchmark]
    public async Task<FactResellerSale> Dapper_GetById_SingleAsync()
    {
        var qry = await _dbConnection.QuerySingleAsync<FactResellerSale>(
            sql: """
                 SELECT TOP 1 *
                 FROM dbo.FactResellerSales AS frs
                 WHERE 1 = 1
                       AND frs.SalesOrderNumber = @SalesOrderNumber
                       AND frs.SalesOrderLineNumber = @SalesOrderLineNumber
                 """, new { SalesOrderNumber = _salesOderNumber, SalesOrderLineNumber = _salesOrderLineNumber });
        return qry;
    }

    [Benchmark]
    public async Task<FactResellerSale> EfCore_GetById_By_Qry()
    {
        return await _dbContext.FactResellerSales
            .FromSqlRaw("""
                        SELECT TOP 1 *
                        FROM dbo.FactResellerSales AS frs
                        WHERE 1 = 1
                              AND frs.SalesOrderNumber = {0}
                              AND frs.SalesOrderLineNumber = {1}
                        """,
                new SqlParameter("@SalesOrderNumber", _salesOderNumber),
                new SqlParameter("@SalesOrderLineNumber", _salesOrderLineNumber))
            .FirstAsync();
    }

    [Benchmark]
    public async Task<FactResellerSale> EfCore_GetById_By_FirstAsync()
    {
        return await _dbContext
            .FactResellerSales
            .FirstAsync(frs => frs.SalesOrderNumber == _salesOderNumber && frs.SalesOrderLineNumber == _salesOrderLineNumber);
    }

    [Benchmark]
    public async Task<FactResellerSale?> EfCore_GetById_By_FirstOrDefaultAsync()
    {
        return await _dbContext
            .FactResellerSales
            .FirstOrDefaultAsync(frs => frs.SalesOrderNumber == _salesOderNumber && frs.SalesOrderLineNumber == _salesOrderLineNumber);
    }

    [Benchmark]
    public async Task<FactResellerSale> EfCore_GetById_By_SingleAsync()
    {
        return await _dbContext.FactResellerSales
            .Where(frs => frs.SalesOrderNumber == _salesOderNumber && frs.SalesOrderLineNumber == _salesOrderLineNumber)
            .SingleAsync();
    }

I am testing the most commonly used methods with EF and Dapper: First, FirstOrDefault, and Single. These are widely utilized in everyday scenarios. I decided to exclude SingleOrDefault since I believe its behavior is sufficiently covered by FirstOrDefault.

Additionally, for EF Core, I wanted to test FromSqlRaw to compare its performance against raw SQL queries. This provides a more comprehensive evaluation of how EF handles raw SQL in comparison to Dapper.

MethodMeanErrorStdDevAllocated
Dapper_GetById_FirstAsync1.166 ms0.0224 ms0.0230 ms13.15 KB
Dapper_GetById_FirstOrDefaultAsync1.174 ms0.0212 ms0.0297 ms13.15 KB
Dapper_GetById_SingleAsync1.137 ms0.0146 ms0.0136 ms13.25 KB
EfCore_GetById_By_Qry1.213 ms0.0241 ms0.0226 ms28.55 KB
EfCore_GetById_By_FirstAsync1.200 ms0.0176 ms0.0156 ms19.97 KB
EfCore_GetById_By_FirstOrDefaultAsync1.219 ms0.0237 ms0.0273 ms19.98 KB
EfCore_GetById_By_SingleAsync3.543 ms0.0444 ms0.0415 ms21.14 KB
TestMean (ms)Dapper vs. EF Core (Time)Memory (KB)Dapper vs. EF Core (Memory)
GetById – FirstAsync1.200 vs. 1.166EF ~1.03x slower19.97 vs. 13.15EF ~1.52x
more memory
GetById – FirstOrDefaultAsync1.219 vs. 1.174EF ~1.04x slower19.98 vs. 13.15EF ~1.52x
more memory
GetById – SingleAsync3.543 vs. 1.137EF ~3.12x slower21.14 vs. 13.25EF ~1.60x
more memory

Key Takeaways

Dapper consistently outperforms EF Core in execution speed, but the difference is generally not substantial. However, for SingleAsync, Dapper significantly outpaces EF Core, being approximately 3.12x faster.

For GetById operations, EF Core is slightly slower while consuming ~1.52x to 1.60x more memory than Dapper. EF Core still faces challenges with SingleAsync, primarily due to how it generates SQL statements. Specifically, EF retrieves records using TOP 2 instead of TOP 1, which introduces unnecessary overhead in this scenario.

Test 3: Insert Entity and Bulk Insert

Insert a single entity or multiple entities (bulk insert) into the database.

    [Benchmark]
    public async Task Dapper_Insert_One_Async()
    {
        var f = _factResellerSales.First();
        await _dbConnection.ExecuteAsync(SQLTemplate.InsertTemplate, f);

        // Irrelevant for the benchmark since it is part of both methods
        await CleanUpAsync();
    }

    [Benchmark]
    public async Task EF_Core_Insert_One_Async()
    {
        await _dbContext.AddAsync(_factResellerSales.First());
        await _dbContext.SaveChangesAsync();

        // Irrelevant for the benchmark since it is part of both methods
        await CleanUpAsync();
    }

    [Benchmark]
    public async Task Dapper_Insert_Range_Async()
    {
        await _dbConnection.ExecuteAsync(SQLTemplate.InsertTemplate, _factResellerSales);

        // Irrelevant for the benchmark since it is part of both methods
        await CleanUpAsync();
    }

    [Benchmark]
    public async Task EF_Core_Insert_Range_Async()
    {
        await _dbContext.AddRangeAsync(_factResellerSales);
        await _dbContext.SaveChangesAsync();

        // Irrelevant for the benchmark since it is part of both methods
        await CleanUpAsync();
    }

    public async Task CleanUpAsync()
    {
        await _dbConnection.ExecuteAsync("DELETE FROM FactResellerSales WHERE CarrierTrackingNumber = 'EF_VS_DAPPER'");
    }

I am leveraging Bogus to generate realistic data for insertion into the database. I believe covering realistic test scenarios is crucial in this case to ensure the benchmarks reflect real-world performance. For the Bulk insert, I am generating 30 entities and Bulk Inserting them

NOTE: I am using the same generated entities for test based on specific seed

MethodMeanErrorStdDevAllocated
Dapper_Insert_One_Async18.27 ms0.320 ms0.300 ms18.23 KB
EF_Core_Insert_One_Async17.91 ms0.234 ms0.195 ms39.09 KB
Dapper_Insert_Range_Async22.96 ms0.437 ms0.569 ms427.73 KB
EF_Core_Insert_Range_Async22.58 ms0.201 ms0.178 ms753.61 KB

Key Takeaways

Dapper and EF Core exhibit similar performance when it comes to inserting records into the database, but the efficiency varies depending on the scale of the operation.

For single inserts, EF Core is 1.02x faster than Dapper (17.91 ms vs. 18.27 ms). However, EF Core consumes ~2.14x more memory (39.09 KB vs. 18.23 KB), which could be a consideration for memory-constrained applications.

For bulk inserts, EF Core is again 1.02x faster than Dapper (22.58 ms vs. 22.96 ms) when handling smaller datasets, such as 30 records. Yet, the memory usage difference becomes more pronounced, with EF Core consuming ~1.76x more memory (753.61 KB vs. 427.73 KB).

However, as the number of records increases, the advantage shifts dramatically. For larger datasets, such as 10,000 records, Dapper outperforms EF Core by a factor of 2.00x, highlighting Dapper’s superior scalability for high-volume operations.

Test 4: Update Single Entity

Update single entity based on Key or Id

    [Benchmark]
    public async Task Dapper_Update_Single_Async()
    {
        var qry = SQLTemplate.UpdateDimProductTemplate;
        await _dbConnection.ExecuteAsync(qry, new
        {
            ProductKey = _productKey,
            FrenchProductName = "DAPPER"
        });
    }

    [Benchmark]
    public async Task EFCore_Update_Single_Async()
    {
        var entity = await _dbContext
            .DimProducts
            .FirstAsync(t => t.ProductKey == _productKey);
        entity.FrenchProductName = "EF_VS_DAPPER_EF";
        await _dbContext.SaveChangesAsync();
    }

For this test, I am updating a single record with a specific ProductKey (ID). From the start, EF Core is at a disadvantage because the record needs to be fetched before it can be updated. Let’s take a look at the results.

MethodMeanErrorStdDevAllocated
Dapper_Update_Single_Async169.2 us3.24 us4.10 us3.68 KB
EFCore_Update_Single_Async209.1 us4.07 us3.81 us61.33 KB

Key Takeaways

Dapper consistently outperforms EF Core in execution speed, but the difference is generally not substantial for single record updates. However, in this test, Dapper significantly outpaces EF Core, being approximately 1.24x faster.

For memory usage, EF Core shows a clear disadvantage, consuming ~16.67x more memory than Dapper. This is primarily due to EF Core’s requirement to fetch the record before updating it and its use of change tracking, which adds overhead.

Conclusion

I personally love Dapper and am firmly on the pro-Dapper team mostly because I enjoy having complete control over my queries and how they are produced. Even if it takes a bit more time to double-check for typos in property names, ensure queries are optimized, or address other minor details, I value this level of control. However, I must remain objective and acknowledge that EF Core has shown significant improvement with .NET 9.

The choice between Dapper and EF Core is now much more of a toss-up in my opinion, and could ultimately be guided by the specific needs of your project. For example:

  • Choose Dapper for your most performance-critical scenarios, lightweight operations, and high scalability with large datasets.
  • Choose EF Core for more feature-rich applications that require advanced ORM capabilities, LINQ query support, and database versioniong.

Both tools are exceptionally powerful, and understanding their trade-offs allows you to make informed decisions that align with your project’s requirements. The Trailhead team specializes in optimizing data access layers in .NET applications. Ready to enhance your system’s performance? Contact us today!

Picture of Vladan Petrovic

Vladan Petrovic

Vladan earned his B.S in Computer at Faculty of Organisational Sciences of the University in Belgrade.Vladan is a result-oriented engineer with over 11 years of experience in the development of web and desktop applications. He has a deep knowledge of C#, .NET, and has a strong background in algorithms and data structures, mathematics, statistics, and data analysis. Vladan enjoys tackling complex tasks and is committed to staying up-to-date with the latest technologies and industry trends. Overall, his combination of technical expertise, experience, and passion for software development make him a valuable asset to the team. In his free time, he enjoys traveling to new and interesting places, basketball, photography, blogging, and hiking.

Free Consultation

Sign up for a FREE consultation with one of Trailhead's experts.

"*" indicates required fields

This field is for validation purposes and should be left unchanged.

Related Blog Posts

We hope you’ve found this to be helpful and are walking away with some new, useful insights. If you want to learn more, here are a couple of related articles that others also usually find to be interesting:

Our Gear Is Packed and We're Excited to Explore With You

Ready to come with us? 

Together, we can map your company’s software journey and start down the right trails. If you’re set to take the first step, simply fill out our contact form. We’ll be in touch quickly – and you’ll have a partner who is ready to help your company take the next step on its software journey. 

We can’t wait to hear from you! 

Main Contact

This field is for validation purposes and should be left unchanged.

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the form below. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Montage Portal

Montage Furniture Services provides furniture protection plans and claims processing services to a wide selection of furniture retailers and consumers.

Project Background

Montage was looking to build a new web portal for both Retailers and Consumers, which would integrate with Dynamics CRM and other legacy systems. The portal needed to be multi tenant and support branding and configuration for different Retailers. Trailhead architected the new Montage Platform, including the Portal and all of it’s back end integrations, did the UI/UX and then delivered the new system, along with enhancements to DevOps and processes.

Logistics

We’ve logged countless miles exploring the tech world. In doing so, we gained the experience that enables us to deliver your unique software and systems architecture needs. Our team of seasoned tech vets can provide you with:

Custom App and Software Development

We collaborate with you throughout the entire process because your customized tech should fit your needs, not just those of other clients.

Cloud and Mobile Applications

The modern world demands versatile technology, and this is exactly what your mobile and cloud-based apps will give you.

User Experience and Interface (UX/UI) Design

We want your end users to have optimal experiences with tech that is highly intuitive and responsive.

DevOps

This combination of Agile software development and IT operations provides you with high-quality software at reduced cost, time, and risk.

Trailhead stepped into a challenging project – building our new web architecture and redeveloping our portals at the same time the business was migrating from a legacy system to our new CRM solution. They were able to not only significantly improve our web development architecture but our development and deployment processes as well as the functionality and performance of our portals. The feedback from customers has been overwhelmingly positive. Trailhead has proven themselves to be a valuable partner.

– BOB DOERKSEN, Vice President of Technology Services
at Montage Furniture Services

Technologies Used

When you hit the trails, it is essential to bring appropriate gear. The same holds true for your digital technology needs. That’s why Trailhead builds custom solutions on trusted platforms like .NET, Angular, React, and Xamarin.

Expertise

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

  • Project Management
  • Architecture
  • Web App Development
  • Cloud Development
  • DevOps
  • Process Improvements
  • Legacy System Integration
  • UI Design
  • Manual QA
  • Back end/API/Database development

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

Our Gear Is Packed and We're Excited to Explore with You

Ready to come with us? 

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the contact form. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Thank you for reaching out.

You’ll be getting an email from our team shortly. If you need immediate assistance, please call (616) 371-1037.