Entity Framework Core (EF Core) is one of the most popular Object-Relational Mappers (ORMs) for .NET applications. It simplifies data access, reduces boilerplate code, and integrates seamlessly with ASP.NET Core. However, inefficient EF Core usage can lead to slow queries, excessive memory consumption, and unnecessary database round trips.
Many performance problems are not caused by EF Core itself but by how it is used. Simple changes such as disabling tracking for read-only queries, selecting only required columns, or avoiding the N+1 query problem can significantly improve application performance.
In this article, you'll learn practical EF Core 11 performance optimization techniques, understand when to apply them, and learn how to evaluate their impact using a structured benchmarking methodology.
Note: This article focuses on optimization techniques and benchmark methodology. It does not present fabricated benchmark numbers.
Why EF Core Performance Matters
Every database operation consumes resources.
Poorly optimized queries can result in:
Higher database CPU usage
Increased memory allocations
Longer response times
More network traffic
Lower application scalability
Optimizing data access is often one of the most effective ways to improve application performance.
Sample Entity
We'll use the following model throughout the examples.
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; } = null!;
}
Use AsNoTracking for Read-Only Queries
By default, EF Core tracks every entity it loads.
var products = await context.Products
.ToListAsync();
If the data will not be modified, disable change tracking.
var products = await context.Products
.AsNoTracking()
.ToListAsync();
Why It Helps
AsNoTracking():
Use tracking only when updates are required.
Select Only Required Columns
Avoid retrieving entire entities when only a few fields are needed.
Instead of:
var products = await context.Products
.ToListAsync();
Project the required columns.
var products = await context.Products
.Select(p => new
{
p.Id,
p.Name,
p.Price
})
.ToListAsync();
Smaller result sets reduce network traffic and memory consumption.
Avoid the N+1 Query Problem
Consider the following code.
var products = await context.Products
.ToListAsync();
foreach (var product in products)
{
Console.WriteLine(product.Category.Name);
}
This may execute one query for products and additional queries for each category.
Use eager loading instead.
var products = await context.Products
.Include(p => p.Category)
.ToListAsync();
This retrieves related data in a single query.
Use Filtered Includes
If only part of a navigation property is required, filter it.
var categories = await context.Categories
.Include(c => c.Products
.Where(p => p.Price > 100))
.ToListAsync();
Filtered includes reduce unnecessary data retrieval.
Use Pagination
Loading thousands of rows into memory is rarely necessary.
Instead of:
var products = await context.Products
.ToListAsync();
Use pagination.
var products = await context.Products
.OrderBy(p => p.Id)
.Skip(0)
.Take(20)
.ToListAsync();
Pagination improves response times and reduces memory usage.
Use Compiled Queries
Frequently executed queries can be compiled.
private static readonly Func<AppDbContext, int, Task<Product?>>
GetProduct =
EF.CompileAsyncQuery(
(AppDbContext db, int id) =>
db.Products.FirstOrDefault(p => p.Id == id));
Execute the compiled query.
var product = await GetProduct(context, 10);
Compiled queries reduce query compilation overhead for frequently executed operations.
Execute Bulk Updates
Instead of loading entities into memory before updating them:
var products = await context.Products.ToListAsync();
foreach (var product in products)
{
product.Price += 10;
}
await context.SaveChangesAsync();
Use ExecuteUpdateAsync().
await context.Products
.ExecuteUpdateAsync(p =>
p.SetProperty(
x => x.Price,
x => x.Price + 10));
This performs the update directly in the database.
Execute Bulk Deletes
Similarly, avoid loading rows before deleting them.
await context.Products
.Where(p => p.Price == 0)
.ExecuteDeleteAsync();
This generates a direct SQL DELETE statement.
Add Database Indexes
Frequently filtered columns should be indexed.
modelBuilder.Entity<Product>()
.HasIndex(p => p.CategoryId);
Indexes can significantly improve query performance for search and join operations.
Monitor Generated SQL
Inspect the SQL generated by EF Core.
var query = context.Products
.Where(p => p.Price > 500);
Console.WriteLine(query.ToQueryString());
Reviewing generated SQL helps identify unnecessary joins, filters, and inefficient queries.
End-to-End Query Flow
A typical optimized request follows these steps:
API receives a request.
EF Core generates SQL.
Database executes the query.
Only required columns are returned.
Change tracking is skipped for read-only data.
Results are serialized and returned.
Optimizing each stage improves overall application performance.
Optimization Comparison
| Technique | Performance Benefit | Best For |
|---|
| AsNoTracking | Lower CPU and memory | Read-only queries |
| Projection | Reduced network traffic | APIs |
| Include | Eliminates N+1 queries | Related data |
| Filtered Include | Smaller result sets | Large relationships |
| Pagination | Lower memory usage | Large tables |
| Compiled Queries | Lower query compilation overhead | Frequently executed queries |
| ExecuteUpdateAsync | Faster bulk updates | Batch operations |
| ExecuteDeleteAsync | Faster bulk deletes | Cleanup tasks |
Performance Evaluation Methodology
The research brief emphasizes performance but does not provide benchmark data. Use the following methodology to evaluate optimizations.
Test Environment
Keep these variables consistent:
.NET SDK version
EF Core version
Database engine
Hardware
Dataset size
Build configuration
Test Scenarios
Compare:
Tracking vs No Tracking
Full entity vs Projection
Include vs Lazy Loading
Regular queries vs Compiled Queries
Traditional updates vs ExecuteUpdateAsync
Traditional deletes vs ExecuteDeleteAsync
Metrics to Measure
Collect:
Query execution time
Memory allocations
Logical reads
Database CPU usage
Network traffic
Rows returned
Useful Tools
Use:
Always benchmark with production-like datasets instead of small development databases.
Best Practices
Use AsNoTracking() for read-only operations.
Project only required columns.
Avoid the N+1 query problem.
Apply pagination for large datasets.
Use compiled queries for frequently executed operations.
Create indexes for commonly filtered columns.
Prefer ExecuteUpdateAsync() and ExecuteDeleteAsync() for bulk operations.
Review generated SQL during development.
Common Mistakes
| Mistake | Impact |
|---|
| Returning entire entities unnecessarily | Increased memory and network usage |
| Loading all records into memory | Poor scalability |
| Ignoring indexes | Slow queries |
| Excessive lazy loading | N+1 query problem |
| Tracking read-only entities | Higher CPU and memory usage |
| Not reviewing generated SQL | Hidden performance issues |
Troubleshooting
Queries Are Slow
Check:
Missing indexes
Execution plans
Query projections
Tracking configuration
Database statistics
High Memory Usage
Review:
Entity tracking
Result set size
Pagination
Projection
Too Many SQL Queries
Investigate:
Use logging and SQL profiling to identify unexpected database calls.
FAQs
Should I always use AsNoTracking()?
No. Use it for read-only queries. If you plan to update entities, tracking is required.
When should I use compiled queries?
Compiled queries are beneficial for queries executed frequently with the same structure, especially in high-throughput applications.
Is projection faster than loading full entities?
In many cases, yes. Retrieving only the required columns reduces network traffic, memory usage, and serialization overhead.
Are ExecuteUpdateAsync() and ExecuteDeleteAsync() better than SaveChanges()?
For bulk operations, yes. They execute directly in the database without loading entities into memory.
How do I identify EF Core performance issues?
Review generated SQL, analyze execution plans, monitor database metrics, and benchmark representative workloads using tools such as BenchmarkDotNet and Query Store.
Conclusion
EF Core provides excellent productivity, but achieving high performance requires thoughtful query design and efficient data access patterns. Techniques such as disabling tracking for read-only queries, projecting only required columns, avoiding N+1 queries, using compiled queries, and leveraging bulk operations can significantly improve scalability and responsiveness.
Rather than applying every optimization indiscriminately, measure each change using realistic workloads and production-like datasets. A data-driven approach ensures your EF Core applications remain both maintainable and performant as they grow.