Introduction
Entity Framework Core (EF Core) is the most widely used Object-Relational Mapper (ORM) for .NET applications. It simplifies data access by allowing developers to work with strongly typed entities instead of writing raw SQL for every operation.
While EF Core increases developer productivity, poorly optimized data access can quickly become a performance bottleneck. Slow queries, excessive database round trips, unnecessary tracking, and inefficient loading strategies can significantly impact application performance as traffic grows.
In this article, you'll learn 25 practical techniques to optimize EF Core 10 applications for production, helping you build faster, more scalable, and maintainable applications.
Why Performance Tuning Matters
Every database query consumes resources.
Poorly optimized data access can lead to:
Slow API responses
High database CPU usage
Increased memory consumption
Network overhead
Poor scalability
Higher cloud infrastructure costs
Small improvements in frequently executed queries can produce significant performance gains.
1. Use AsNoTracking for Read-Only Queries
EF Core tracks entities by default.
Disable tracking for read-only operations.
var products = await context.Products
.AsNoTracking()
.ToListAsync();
Skipping change tracking reduces memory usage and improves query performance.
2. Retrieve Only Required Columns
Avoid selecting entire entities when only a few fields are needed.
var products = await context.Products
.Select(p => new
{
p.Id,
p.Name,
p.Price
})
.ToListAsync();
Projection reduces network traffic and object materialization costs.
3. Filter Early
Always apply filtering before materializing data.
var activeUsers = await context.Users
.Where(u => u.IsActive)
.ToListAsync();
Filtering in the database is far more efficient than filtering in memory.
4. Limit Returned Records
Avoid returning unnecessary rows.
var latestOrders = await context.Orders
.OrderByDescending(o => o.CreatedAt)
.Take(20)
.ToListAsync();
Pagination improves responsiveness and reduces database workload.
5. Prefer Async APIs
Use asynchronous database operations.
await context.SaveChangesAsync();
Async operations improve application scalability by preventing thread blocking.
6. Avoid the N+1 Query Problem
Loading related data inside loops creates excessive database calls.
Use eager loading or projection instead.
7. Use Include Carefully
Load only the navigation properties that are actually required.
Overusing Include() increases query complexity and memory consumption.
8. Prefer Split Queries for Large Graphs
When loading multiple collections, use split queries.
var orders = await context.Orders
.Include(o => o.Items)
.AsSplitQuery()
.ToListAsync();
Split queries reduce Cartesian explosion in complex joins.
9. Create Proper Database Indexes
Indexes have a greater impact on performance than most application-level optimizations.
Index columns used for:
Filtering
Sorting
Joins
Foreign keys
10. Avoid Client-Side Evaluation
Ensure LINQ expressions are translated into SQL.
If EF Core cannot translate an expression, unnecessary processing may occur in memory.
11. Batch Updates
Minimize repeated database calls.
Update multiple entities within a single unit of work whenever possible.
12. Use ExecuteUpdate and ExecuteDelete
Bulk operations avoid loading entities into memory.
await context.Products
.Where(p => !p.IsActive)
.ExecuteDeleteAsync();
These APIs reduce memory usage and improve execution speed.
13. Cache Frequently Used Data
Combine EF Core with distributed caching for frequently accessed information.
Redis is an excellent choice for production environments.
14. Optimize Change Tracking
Disable automatic change detection during bulk operations when appropriate.
15. Reuse DbContext Correctly
Register DbContext using dependency injection.
Avoid manually creating multiple contexts within a single request.
16. Use Compiled Queries
Compiled queries reduce repeated query compilation overhead.
They are particularly useful for high-frequency queries.
17. Avoid Large Transactions
Keep database transactions short.
Long-running transactions increase locking and reduce concurrency.
18. Use Database Pagination
Always perform paging at the database level instead of loading all records.
19. Monitor Generated SQL
Review generated SQL during development.
Complex LINQ expressions sometimes produce inefficient queries.
20. Minimize SaveChanges Calls
Avoid calling SaveChanges() repeatedly inside loops.
Save related changes together whenever possible.
21. Use Connection Pooling
Connection pooling reduces the overhead of repeatedly opening database connections.
Join the conversation! Your thoughts help the community grow.