Entity Framework Core (EF Core) simplifies data access in .NET applications by allowing developers to work with strongly typed entities instead of writing raw SQL. While this improves productivity, it can also introduce performance issues when used without understanding how queries are translated and executed.
Many production performance problems are caused not by EF Core itself but by inefficient usage patterns. Issues such as unnecessary database queries, excessive data loading, and improper tracking can significantly impact application performance under load.
This article explores common EF Core performance anti-patterns and practical strategies to avoid them.
Why EF Core Performance Matters
Database operations are often the slowest part of an application. Inefficient queries increase:
Database CPU usage
Network latency
Memory consumption
API response time
Infrastructure costs
Optimizing EF Core queries improves scalability without requiring hardware upgrades.
Anti-Pattern 1: Retrieving More Data Than Needed
One of the most common mistakes is loading entire entities when only a few properties are required.
Instead of:
var products = await context.Products.ToListAsync();
Project only the required columns:
var products = await context.Products
.Select(p => new
{
p.Id,
p.Name,
p.Price
})
.ToListAsync();
Selecting only the required fields reduces data transfer, memory allocation, and serialization time.
Anti-Pattern 2: The N+1 Query Problem
The N+1 problem occurs when a query retrieves a list of entities and then executes an additional query for each related entity.
Example:
var orders = await context.Orders.ToListAsync();
foreach (var order in orders)
{
Console.WriteLine(order.Customer.Name);
}
If lazy loading is enabled, EF Core may execute one query for the orders and another query for every customer.
Instead, eagerly load related data:
var orders = await context.Orders
.Include(o => o.Customer)
.ToListAsync();
This reduces multiple database round trips to a single query.
Anti-Pattern 3: Overusing Include()
Although Include() solves the N+1 problem, using too many includes can produce massive SQL joins and duplicate data.
Avoid this:
var customers = await context.Customers
.Include(c => c.Orders)
.Include(c => c.Addresses)
.Include(c => c.Payments)
.ToListAsync();
Instead, retrieve only the relationships required for the current operation or project the desired data into a DTO.
Anti-Pattern 4: Tracking Read-Only Queries
By default, EF Core tracks every entity it retrieves.
For read-only operations, change tracking is unnecessary.
Instead of:
var products = await context.Products.ToListAsync();
Use:
var products = await context.Products
.AsNoTracking()
.ToListAsync();
AsNoTracking() reduces memory usage and improves query performance for read-heavy applications.
Anti-Pattern 5: Calling SaveChanges() Repeatedly
Saving changes inside a loop generates multiple database transactions.
Avoid:
foreach (var product in products)
{
product.Price += 10;
await context.SaveChangesAsync();
}
Instead:
foreach (var product in products)
{
product.Price += 10;
}
await context.SaveChangesAsync();
Batching updates reduces transaction overhead and improves throughput.
Anti-Pattern 6: Client-Side Evaluation
Not every .NET method can be translated into SQL.
For example:
var users = context.Users
.Where(u => CustomMethod(u.Name))
.ToList();
If EF Core cannot translate the expression, unnecessary data may be loaded into memory for evaluation.
Prefer expressions that EF Core can convert into SQL, allowing filtering to occur in the database.
Anti-Pattern 7: Ignoring Database Indexes
Even well-written EF Core queries perform poorly without appropriate database indexes.
Join the conversation! Your thoughts help the community grow.