BenchmarkDotNet and EF Core vs EF6 - Part 2
This is part 2 of a multi-part series on benchmarking EF Core 6 and EF6 using BenchmarkDotNet. In this post I will explore and compare the performance of EF Core and EF 6 for multiple scenarios designed to mock real world use cases.
1. See Introduction to BenchmarkDotNet if you're just getting started on the BenchmarkDotNet journey or looking for Hello World with simple examples.
2. See BenchmarkDotNet and EF Core vs EF6 - Part 1 for the configuration of benchmarks and database setup.
3. For all code in this post and all subsequent posts on EF Core 6 vs EF6 Benchmarking, see the following project on my GitHub at https://github.com/BrianMikinski/EfPerformance
TLDR;
EF Core 6 is fast. Very Fast. Across all benchmark scenarios, you should be using EF Core if performance is critical to your success.
Introduction
These days, EF Core 6 is certainly one of the most exciting ORM libraries on the .Net landscape. Beyond the near feature parity with EF6, the best part of EF Core is that performance continues to improve with each release. The latest EF Core 6 version is no exception to what we’ve come to expect from the EF team – they truly are Magic Unicorns when it comes to making EF Core as fast as it possibly can be.
But how does EF Core 6 stack up against EF6? In this post, I’m going to continue my benchmarking of EF6 and EF Core but this time with more realistic application scenarios. As you probably already know, EF Core continues to blow EF6 out of the water in nearly all aspects of performance but these benchmarks put some solid data behind both technologies.
Real World LINQ Scenarios and EF DbContext Configurations
While the previous EF Core 6 vs EF6 benchmarks focused on simple Hello World examples, this post will attempt to test more real-world scenarios. We will explore the following commonly encountered application requirements of EF Core -
- Insert Single Entity
- Retrieve Entity, Update and Save
- Insert Multiple Entities
- EF Extentions Bulk Insert Entities vs AddRange Entities Performance
EF DbContext Configuration
For each application scenario, we will run three separate benchmarks targeting common EF DbContext configurations across EF6 and EF Core 6.
- EF6
- The Baseline EF6 configuration. What you would expect to see in a classic .Net Framework application using Entity Framework.
- EF Core
- Most common EF Core 6 DbContext configuration. Nearly identical to the EF6 .Net Framework setup except primarily for .Net Core and .Net 5+ applications.
- EF Core Pooled
- A new feature as of EF Core 2.0, Pooled DbContexts provide many benefits to performance from both a memory and CPU perspective. As you will see, they are the DbContext that everyone should be using. The only downside to Pooled Ef Core DbContexts is that they cannot be used if your DbContext manages internal state via private fields. For more information see the Limitations section of What Is New and Advanced Performance Topics
Insert Single Entity
What are we benchmarking?
One of the most classic Entity Framework app scenarios, in this LINQ command we will be inserting a single Post
entity into the database using DbContext.Add()
. As you will see, EF6 handles this type of operation fairly reasonably but when we get to EF Core, the Pooled Db Contexts really start to shine from both a CPU and Memory perspective. This same pattern will begin to develop for nearly all of our benchmarks.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using BenchmarkDotNet.Attributes;
using Blog.Models;
using CoreBlog.Models;
namespace Blog.Benchmarks;
public class AddSingleEntityBenchmark : BenchmarkBase
{
[GlobalSetup]
public void GlobalSetup()
{
ConfigDatabases();
}
[Benchmark(Baseline = true)]
public void Ef6()
{
using var context = new BlogContext();
var post = Post.NewPost();
context.Posts.Add(post);
context.SaveChanges();
}
[Benchmark]
public void EfCore()
{
using var context = new CoreBlogContext(CoreBlogContext.NewDbContextOptions());
var post = PostCore.NewPost();
context.Posts.Add(post);
context.SaveChanges();
}
[Benchmark]
public void EfCorePooled()
{
using var context = _corePooledDbContextFactory.CreateDbContext();
var post = PostCore.NewPost();
context.Posts.Add(post);
context.SaveChanges();
}
}
Results
1
2
3
4
5
6
7
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1826 (21H1/May2021Update)
Intel Core i5-6200U CPU 2.30GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=6.0.302
[Host] : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT
Job-IDQVNR : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT
WarmupCount=1
Method | Mean | Error | StdDev | Min | Max | Ratio | RatioSD | Gen 0 | Allocated |
---|---|---|---|---|---|---|---|---|---|
Ef6 | 12.471 ms | 0.8909 ms | 2.627 ms | 5.419 ms | 18.03 ms | 1.00 | 0.00 | 93.7500 | 155 KB |
EfCore | 6.356 ms | 1.1345 ms | 3.237 ms | 1.492 ms | 15.70 ms | 0.54 | 0.33 | - | 89 KB |
EfCorePooled | 5.428 ms | 1.0484 ms | 3.025 ms | 1.059 ms | 14.61 ms | 0.46 | 0.30 | - | 22 KB |


Summary
Method | Mean | Ratio | Improvement | Allocated | Improvement |
---|---|---|---|---|---|
Ef6 | 12.471 ms | 1.00 | - | 155 KB | - |
EfCore | 6.356 ms | 0.54 | 1.96x | 89 KB | 1.74x |
EfCorePooled | 5.428 ms | 0.46 | 2.29x | 22 KB | 7.04x |
Analysis
While the roughly 2x increase in CPU and Memory allocation efficiency between EF6 and EF Core is a nice improvement, the EF Core Pooled DbContext really takes the cake. Coming in at a ~2.3x increase in CPU efficiency and 86% decrease in allocated KB, this is the DbContext you should be using. Why is the EF Core Pooled DbContext faster than the non pooled context and why is the allocated memory so much less? The enhanced performance is the result of the skipping the startup configuration of the internal EF Core DbContext services.
How does this work? DbContextFactory initially configures a DbContext that is reset at every call to _corePooledDbContextFactory.CreateDbContext()
. For more information related to configuring DbContextFactories, see previous blog posts Introduction to BenchmarkDotNet
and BenchmarkDotNet and EF Core vs EF6 - Part 1.
Retrieve, Update and Save
What are we benchmarking?
Another very common scenario in real world applications, in this benchmark, we want to analyze
- Retrieving a single entity with DbContext.FirstOrDefault()
- Updating a property on the entity with
post?.UpdateTitle("Is this faster than EF Core")
and - Saving the updated post title back to the database with DbContext.SaveChanges();
FirstOrDefaultUpdateBenchmark.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using BenchmarkDotNet.Attributes;
using Blog.Models;
using CoreBlog.Models;
namespace Blog.Benchmarks;
public class FirstOrDefaultUpdateBenchmark : BenchmarkBase
{
[GlobalSetup]
public void GlobalSetup()
{
ConfigDatabases();
AddPostsToSeedLimit();
}
[Benchmark(Baseline = true)]
public void Ef6()
{
using var context = new BlogContext();
var post = context.Posts.FirstOrDefault();
post?.UpdateTitle("Is this faster than EF Core");
context.SaveChanges();
}
[Benchmark]
public void EfCore()
{
using var context = new CoreBlogContext(CoreBlogContext.NewDbContextOptions());
var post = context.Posts.FirstOrDefault();
post?.UpdateTitle("EF Core will Rock your socks off!");
context.SaveChanges();
}
[Benchmark]
public void EfCorePooled()
{
using var context = _corePooledDbContextFactory.CreateDbContext();
var post = context.Posts.FirstOrDefault();
post?.UpdateTitle("EF Core will Rock your socks off!");
context.SaveChanges();
}
}
Results
1
2
3
4
5
6
7
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1826 (21H1/May2021Update)
Intel Core i5-6200U CPU 2.30GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=6.0.302
[Host] : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT
Job-ZWIYRJ : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT
WarmupCount=1
Method | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|---|---|
Ef6 | 8,641.5 μs | 430.79 μs | 1,207.98 μs | 8,234.9 μs | 6,681.9 μs | 12,181.9 μs | 1.00 | 0.00 | 125.0000 | 194.03 KB | 1.00 |
EfCore | 917.6 μs | 37.21 μs | 106.16 μs | 906.4 μs | 632.8 μs | 1,186.0 μs | 0.11 | 0.02 | 46.8750 | 73.93 KB | 0.38 |
EfCorePooled | 332.3 μs | 15.99 μs | 47.15 μs | 321.6 μs | 262.8 μs | 438.5 μs | 0.04 | 0.01 | 5.3711 | 8.81 KB | 0.05 |


Summary
Method | Mean | Ratio | Improvement | Allocated | Improvement |
---|---|---|---|---|---|
Ef6 | 8,641.5 μs | 1.00 | - | 194.03 KB | - |
EfCore | 917.6 μs | 0.11 | 9.75x | 73.93 KB | 1.74 |
EfCorePooled | 332.3 μs | 0.04 | 26.00x | 8.81 KB | 19.40x |
Analysis
Once again, the EF Core Pooled DbContext takes the cake. Clocking in at a 26x CPU performance improvement and 19x improvement in allocated bytes from the EF6 DbContext, this context is the winner. The traditional EF Core DbContext still performed very well but the Pooled DbContext wins out overall. Let’s see what happens next when we start to insert multiple entities in one EF command…
Insert Multiple Entities
What are we benchmarking?
When building real world applications, engineers will often need to insert multiple entities in a single EF LINQ statement. Traditionally, EF6 has not performed well in these scenarios and as you’ll see, it definitely struggles against EF Core. This benchmark uses DbContext.AddRange()
to add and track multiple entities against the database and like all other entity updates, all create, update, and delete changes are saved to the database using DbContext.SaveChages()
FirstOrDefaultUpdateBenchmark.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
using BenchmarkDotNet.Attributes;
using Blog.Models;
using CoreBlog.Models;
namespace Blog.Benchmarks;
public class AddMultipleEntitiesBenchmark : BenchmarkBase
{
[GlobalSetup]
public void GlobalSetup()
{
ConfigDatabases();
}
[Benchmark(Baseline = true)]
public void Ef6()
{
using var context = new BlogContext();
for (int i = 0; i < SeedLimit; i++)
{
context.Posts.Add(new Post());
}
context.SaveChanges();
}
[Benchmark]
public void EfCore()
{
using var context = new CoreBlogContext(CoreBlogContext.NewDbContextOptions());
for (int i = 0; i < SeedLimit; i++)
{
context.Posts.Add(new PostCore());
}
context.SaveChanges();
}
[Benchmark]
public void EfCorePooled()
{
using var context = _corePooledDbContextFactory.CreateDbContext();
for (int i = 0; i < SeedLimit; i++)
{
context.Posts.Add(new PostCore());
}
context.SaveChanges();
}
}
Results
1
2
3
4
5
6
7
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1826 (21H1/May2021Update)
Intel Core i5-6200U CPU 2.30GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=6.0.302
[Host] : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT
Job-YWRNBA : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT
WarmupCount=1
Method | Mean | Error | StdDev | Median | Min | Max | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|---|---|---|---|
Ef6 | 6,819.43 ms | 290.312 ms | 842.249 ms | 7,044.13 ms | 5,222.54 ms | 8,392.86 ms | 1.000 | 516000.0000 | 11000.0000 | 1000.0000 | 804 MB |
EfCore | 76.39 ms | 4.655 ms | 13.506 ms | 69.15 ms | 60.42 ms | 110.74 ms | 0.011 | 1000.0000 | - | - | 10 MB |
EfCorePooled | 65.12 ms | 1.502 ms | 4.009 ms | 64.18 ms | 59.75 ms | 80.32 ms | 0.009 | 1000.0000 | - | - | 10 MB |


Summary
Method | Mean | Ratio | Improvement | Allocated | Improvement |
---|---|---|---|---|---|
Ef6 | 6,819.43 ms | 1.000 | - | 804 MB | - |
EfCore | 76.39 ms | 0.011 | 89.27x | 10 MB | 80.40x |
EfCorePooled | 65.12 ms | 0.009 | 104.72x | 10 KB | 80.40x |
Analysis
Although we often want to avoid it, EF DbContext’s can sometimes get trashed. The longer they live, the more items that get added causing additional allocations and increased CPU time to SaveChanges()
to the database. The benchmark perfectly demonstrates the performance implications of such DbContext
bloat and how it is handled in EF Core and EF6 during SaveChanges()
. With 104x improvement in CPU time and a 80.40x improvement to the allocated bytes, EF Core certainly performs better than EF6. When engineers inevitable encounter long lived DbContext
’s EF Core will out perform EF6.
Bulk Insert Entities
What are we testing?
Up until this point, all benchmarks have used vanilla EF Core 6 or EF6 libraries from Microsoft with no additional cruft. We are finally going to change that and utilize the very popular and very helpful extension from the folks at https://entityframework-extensions.net/ that allow engineers to bypass EF Change Tracking and SaveChanges() to bulk insert entities as rapidly as possible. Over the years I have found these extensions to be invaluable and worth ever penny for a license. Additionally, you can test the extensions out for free just by installing their NuGet hosted package. In this benchmark, we will be comparing the standard EF LINQ query DbContext.AddRange() coupled with DbContext.SaveChanges(), to the DbContext.BulkInsert(entities) extension.
BulkInsertEntitiesBenchmark.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using BenchmarkDotNet.Attributes;
using Blog.Models;
using CoreBlog.Models;
namespace Blog.Benchmarks;
public class BulkInsertEntitiesBenchmark : BenchmarkBase
{
[GlobalSetup]
public void GlobalSetup()
{
ConfigDatabases();
}
[Benchmark(Baseline = true)]
public void Ef6AddRange()
{
using var context = new BlogContext();
List<Post> posts = new();
for (int i = 0; i < SeedLimit; i++)
{
posts.Add(new Post());
}
context.Posts.AddRange(posts);
context.SaveChanges();
}
[Benchmark]
public void Ef6BulkInsertEfExtensions()
{
using var context = new BlogContext();
List<Post> posts = new();
for (int i = 0; i < SeedLimit; i++)
{
posts.Add(new Post());
}
context.BulkInsert(posts);
}
[Benchmark]
public void EfCoreAddRange()
{
using var context = _corePooledDbContextFactory.CreateDbContext();
List<PostCore> posts = new();
for (int i = 0; i < SeedLimit; i++)
{
posts.Add(new PostCore());
}
context.Posts.AddRange(posts);
context.SaveChanges();
}
[Benchmark]
public void EfCoreBulkInsertEfExtensions()
{
using var context = _corePooledDbContextFactory.CreateDbContext();
List<PostCore> posts = new();
for (int i = 0; i < SeedLimit; i++)
{
posts.Add(PostCore.NewPost());
}
context.BulkInsert(posts);
}
}
Results
1
2
3
4
5
6
7
BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19043.1889/21H1/May2021Update)
Intel Core i5-6200U CPU 2.30GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=6.0.303
[Host] : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2
Job-TYIMDK : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2
WarmupCount=1
Method | Mean | Error | StdDev | Median | Min | Max | Ratio | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Ef6AddRange | 3,477.80 ms | 66.229 ms | 173.310 ms | 3,450.21 ms | 3,066.94 ms | 4,056.29 ms | 1.000 | 21000.0000 | 5000.0000 | 1000.0000 | 64713.64 KB | 1.00 |
Ef6BulkInsertZzzEfExtensions | 33.89 ms | 1.209 ms | 3.508 ms | 32.79 ms | 28.75 ms | 43.04 ms | 0.010 | 285.7143 | - | - | 1244.82 KB | 0.02 |
EfCoreAddRange | 66.77 ms | 1.688 ms | 4.648 ms | 65.05 ms | 60.94 ms | 81.22 ms | 0.019 | 1666.6667 | 333.3333 | - | 10319.63 KB | 0.16 |
EfCoreBulkInsertZzzEfExtensions | 23.27 ms | 0.431 ms | 0.820 ms | 23.42 ms | 21.37 ms | 24.93 ms | 0.007 | 250.0000 | - | - | 810.9 KB | 0.01 |


Summary
Method | Mean | Ratio | Improvement | Allocated | Improvement |
---|---|---|---|---|---|
Ef6AddRange | 3,477.80 ms | 1.000 | - | 64,713.64 KB | - |
Ef6BulkInsertEfExtensions | 33.89 ms | 0.010 | 102.59x | 12,44.82 KB | 51.59x |
EfCoreAddRange | 66.77 ms | 0.019 | 52.08x | 103,19.63 KB | 6.27x |
EfCoreBulkInsertEfExtensions | 23.27 ms | 0.007 | 2.87x, 149.45x | 810.9 KB | 1.72x, 79.89x |
Analysis
Of all the benchmarks covered in this post, this one is by far my favorite. Look at the incredible difference between the EF6 AddRange()
to the EF6 BulkInsert()
extensions? A massive 102x improvement to the execution speed of inserting 1000 entities. Now compare the EF6 DbContext.AddRange()
to the EF Core Pooled BulkInserts extension – an even more massive 149x execution speed improvement and a 79x reduction in your memory footprint! If ever anyone needed backup to support a migration away from EF6 to EF 6 Core, this is it. I would also add, if you can’t move your app to EF Core and you do a large amount of bulk inserts, it is beyond a shadow of a doubt in your best interest to pick up a license of Entity Framework Extensions.
Conclusion
At this point, any concerns realted to the performance benefits of EF Core should be laid to rest. When it comes to both CPU and memory performance, EF Core beats EF6 hands down, no questions asked in all benchmark scenarios. While the enhanced CPU speed benefits will be most noticeable, the reduced memory footprint is equally important. Does it get any better? Well, actually it might - with the release of .Net 7 and EF7 in November of 2022, preliminary benchmarks of the updated EF7 pipeline indicate that for some scenarios, EF Core may be up to 4x faster. Come November, be on the lookout for future blog posts exploring EF7 performance enhancements…..but until then…..Happy Coding!
If you want to view all of the code from this post, you can access the source at my github repo https://github.com/BrianMikinski/EfPerformance
- ef core (3)
- ef6 (3)
- entity framework core (3)
- entity framework (3)
- benchmarkdotnet (4)
- benchmarking (4)
- performance (4)
- dotnet core (5)
- LINQ (2)