Entity Framework Core is one of the most productive tools in the .NET ecosystem.
It allows teams to move fast, model domains cleanly, and ship features quickly. You should not dive into details how database works, which allows you to write code fast. And it's an advantage and disadvantage at the same time.
EF Core works great — until the project grows.
More data.
More relations.
More edge cases.
More performance-sensitive paths.
At that point, problems start appearing — and EF often gets blamed. Someone starts to add additional indexes, which just temporarily masks the issue.
But the real root cause: engineers have no idea how EF query is converted into SQL.
In many mature codebases, I’ve seen LINQ queries written without any consideration for how they translate to SQL.
At a small scale, this often goes unnoticed:
- Few records
- Low concurrency
- Acceptable latency
But as data grows, those “innocent” queries turn into:
- Multiple joins you didn’t expect
- N+1 queries
- Full table scans
- Excessive memory usage
- Unoptimized Execution plan
EF Core doesn’t hide SQL — it generates it.
If you don’t understand the generated SQL, you are effectively coding blind.
A Real Example from Production
A few years ago, while investigating performance issues in a production system, I found legacy code that did the following:
- Loaded a few hundred entities into memory
- Enabled change tracking
- Updated them in a loop
- Called SaveChangesAsync()
var cutoff = nowUtc.AddDays(-90);
var users = await db.Users
.Where(u =>
u.Status == UserStatus.Active &&
(u.LastLoginUtc == null || u.LastLoginUtc < cutoff))
.ToListAsync(ct);
foreach (var u in users)
{
u.Status = UserStatus.Archived;
u.UpdatedAtUtc = nowUtc;
}
await db.SaveChangesAsync(ct);
All of this — just to update a small subset of fields.
At that time, the correct approach was a set-based SQL operation (a stored procedure).
Later, EF Core introduced ExecuteUpdateAsync, which solves this problem cleanly and efficiently.
await db.Users
.Where(u =>
u.Status == UserStatus.Active &&
(u.LastLoginUtc == null || u.LastLoginUtc < cutoff))
.ExecuteUpdateAsync(setters => setters
.SetProperty(u => u.Status, UserStatus.Archived)
.SetProperty(u => u.UpdatedAtUtc, nowUtc),
ct);
But the root cause was not “EF is slow”.
The root cause was using an ORM abstraction without understanding its cost.
EFCore is cross-database and flexible. It's designed to support a wide range of use cases, and it cannot be perfectly optimized for every scenario out of the box. And it shouldn’t be.
Expecting EF to magically generate optimal SQL for every complex business case is unrealistic.
The Responsibility Is on Us. As software engineers, especially at senior levels, we must:
- Understand how LINQ translates to SQL
- Know when tracking is needed — and when it’s not
- Recognize when a query should be "set-based", compiled, or raw query
- Use the right tool for the job
Sometimes EF Core is the best solution. Sometimes stored procedure, view or raw query is better suite.
Do not blame tool, if you do not understand how it works.
Top comments (0)