“EF Core is slow.”
It’s one of the most common complaints I hear from developers and one of the most misleading.
After years of working with .NET and building high-scale, enterprise applications, I can say with confidence:
Entity Framework Core isn’t inherently slow, most of the time, it’s being misused.
Yes, EF Core has its limits. No ORM is perfect. But in the majority of projects I’ve audited or worked on, performance issues weren’t caused by EF itself — they were caused by how developers used EF.
In this article, I’ll walk through the most common mistakes that lead to poor EF Core performance, and show you practical solutions to get things running smoothly.
🚫 Mistake 1: Not Using AsNoTracking()
for Read-Only Queries
By default, EF Core tracks all entities it retrieves from the database. That means it keeps a copy of every entity and monitors it for changes so that you can update it later.
That’s useful, but only when you actually need to update the data.
If you're just reading data (e.g., returning it from an API or showing it in a view), tracking is unnecessary and adds significant overhead.
🔧 The Fix: Use AsNoTracking()
// ❌ Slower: Tracking enabled
var products = await _context.Products.ToListAsync();
// ✅ Faster: Tracking disabled
var products = await _context.Products
.AsNoTracking()
.ToListAsync();
✅ When to Use
Report generation
Read-only admin panels
Any time you don’t call SaveChanges()
Using AsNoTracking()
can dramatically reduce memory usage and speed up query execution, especially on large datasets.
🐌 Mistake 2: Relying on Lazy Loading - The N+1 Query Problem
Lazy loading sounds convenient, EF loads related entities only when you access them. But this can easily cause what’s known as the N+1 query problem.
🧨 Example: The Pitfall
var students = await _context.Students.ToListAsync();
foreach (var student in students)
{
// Triggers a separate SQL query per student
var courseName = student.EnrolledCourse.Name;
}
This can destroy performance.
🔧 The Fix: Use Eager Loading with .Include()
var students = await _context.Students
.Include(s => s.EnrolledCourse)
.ToListAsync(); // Just 1 query
✅ Best Practices
Use
.Include()
when you know you’ll need related data.Avoid lazy loading unless you’re dealing with very small or optional relationships.
Disable lazy loading globally if it’s not required:
services.AddDbContext<AppDbContext>(options =>
options.UseLazyLoadingProxies(false));
🧼 Mistake 3: Keeping DbContext
Alive Too Long
The DbContext
class is designed to be short-lived and used for a single unit of work. Holding onto it for too long (like across multiple requests or operations) can lead to:
Memory leaks
Excessive tracking
Concurrency issues
🔧 The Fix: Use Scoped Lifetime
In an ASP.NET Core app, always register your DbContext
with a scoped lifetime:
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer("your-connection-string"),
ServiceLifetime.Scoped);
In short:
Don’t treat DbContext as a singleton.
Don’t inject it into background threads or async voids.
Do keep it scoped to the request or operation.
🎯 Mistake 4: Pulling Full Entities Instead of DTOs
One of the most common (and expensive) EF Core mistakes is loading entire entities from the database when you only need a few fields.
Let’s say you need to show a list of users with their name and email:
❌ Inefficient Query
var users = await _context.Users.ToListAsync();
// Loads all columns, including those you don't need
✅ Efficient Query with Projection
var users = await _context.Users
.Select(u => new UserDto
{
Id = u.Id,
Name = u.Name,
Email = u.Email
})
.ToListAsync();
This generates a lean SQL query that only selects the fields you need. It also skips navigation properties and lazy loading entirely.
Bonus: Use AutoMapper for Clean Projection
var users = await _context.Users
.ProjectTo<UserDto>(_mapper.ConfigurationProvider)
.ToListAsync();
🔧 Bonus Tips for Senior Software Engineers
📦 Use Bulk Operations for Performance at Scale
EF Core does not optimize batch inserts, updates, or deletes by default.
Instead of:
foreach (var item in list)
{
_context.Update(item);
}
await _context.SaveChangesAsync();
Use a library like EFCore.BulkExtensions
await _context.BulkUpdateAsync(list);
🕵️♂️ Monitor EF Core Queries with Logging
Add query logging to see exactly what SQL EF is executing:
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer("your-connection-string")
.LogTo(Console.WriteLine, LogLevel.Information));
This helps you:
Spot N+1 problems
Detect large object graphs
Optimize complex LINQ expressions
📌 TL;DR — EF Core Performance Checklist
❌ Mistake | ✅ Solution |
---|---|
Tracking unnecessary data | Use AsNoTracking()
|
N+1 queries via lazy loading | Use .Include()
|
Holding DbContext too long | Use scoped DbContext |
Fetching full entities | Project into DTOs |
Slow batch operations | Use BulkExtensions |
Hidden slow queries | Enable SQL logging |
🧠 Final Thoughts
EF Core is a powerful, flexible ORM. But like any powerful tool, it’s easy to misuse. Most performance issues I’ve seen in production applications come down to:
Not understanding how EF Core works under the hood
Relying on defaults instead of being intentional
Prioritizing convenience over performance
Once you learn how to use EF Core the right way, it becomes a fast, scalable, and elegant data access layer, even for complex, high-load applications.
🙋♂️ Over to You
Have you struggled with EF Core performance in the past? Have a tip I missed?
Let me know in the comments or connect with me directly. I’d love to hear your experience.
LinkedIn Account
: LinkedIn
Twitter Account
: Twitter
Credit: Graphics sourced from Gunnarpeipman Blog
Top comments (0)