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.
Frequently filtered columns such as:
Email
CategoryId
Status
CreatedDate
should typically be indexed based on application requirements.
EF Core generates SQL, but database performance still depends on proper schema design.
Anti-Pattern 8: Executing Multiple Queries Instead of One
Consider:
var activeUsers = await context.Users
.Where(u => u.IsActive)
.ToListAsync();
var activeCount = activeUsers.Count;
When only the count is required:
var activeCount = await context.Users
.CountAsync(u => u.IsActive);
Allowing SQL Server to perform aggregation is more efficient than loading entire datasets.
Anti-Pattern 9: Missing Pagination
Loading thousands of records into memory affects both performance and user experience.
Instead of:
var products = await context.Products.ToListAsync();
Use pagination:
var products = await context.Products
.OrderBy(p => p.Id)
.Skip(20)
.Take(20)
.ToListAsync();
Pagination keeps memory usage predictable and improves API responsiveness.
Performance Optimization Checklist
| Anti-Pattern | Recommended Solution |
|---|
| Loading entire entities | Use projections (Select) |
| N+1 queries | Use Include() or optimized projections |
| Read-only tracking | Use AsNoTracking() |
| Frequent SaveChanges() | Batch updates |
| Large result sets | Implement pagination |
| Client-side filtering | Let SQL perform filtering |
| Missing indexes | Optimize database schema |
| Excessive Includes | Load only required relationships |
Best Practices
Project only the columns you need.
Use AsNoTracking() for read-only queries.
Implement pagination for large datasets.
Review generated SQL during development.
Use eager loading carefully.
Keep database indexes aligned with query patterns.
Batch updates whenever possible.
Profile slow queries before optimizing application code.
Common Mistakes
Assuming EF Core Automatically Optimizes Queries
EF Core generates SQL, but it cannot determine the most efficient query for every scenario. Developers must understand how LINQ translates into SQL.
Loading Entire Object Graphs
Fetching multiple related collections without necessity increases memory usage and query complexity.
Ignoring SQL Execution Plans
When queries become slow, inspect the generated SQL and execution plan instead of assuming EF Core is the bottleneck.
Optimizing Prematurely
Not every query requires optimization. Focus on endpoints identified through profiling and performance monitoring.
Conclusion
Entity Framework Core is a powerful ORM that enables developers to build data-driven applications quickly, but achieving good performance requires understanding how it interacts with the database. Most production bottlenecks stem from common anti-patterns such as retrieving unnecessary data, triggering N+1 queries, tracking read-only entities, or executing excessive database operations.
By using projections, AsNoTracking(), pagination, efficient relationship loading, and batching updates, you can significantly improve application scalability while reducing database load. Combined with proper indexing and query profiling, these practices help ensure your EF Core applications remain fast, maintainable, and production-ready as they grow.