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:
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.
This is enabled automatically for most providers.
22. Avoid Lazy Loading in APIs
Lazy loading often causes unexpected database queries.
Explicit loading or projection usually provides more predictable performance.
23. Profile Slow Queries
Use database execution plans and monitoring tools to identify expensive queries.
Optimize before adding additional infrastructure.
24. Archive Historical Data
Large tables slow down queries.
Move historical records into archive tables when appropriate.
25. Continuously Monitor Performance
Performance tuning is an ongoing process.
Monitor:
Regular monitoring helps identify regressions before users notice them.
Production Considerations
Dependency Injection
Register your DbContext using ASP.NET Core dependency injection.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")));
Managing the DbContext through dependency injection ensures proper lifetime management and simplifies testing.
Configuration
Store database settings in appsettings.json.
{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=StoreDb;Trusted_Connection=True;"
}
}
Use environment variables, Azure Key Vault, or Secret Manager to protect production connection strings.
Logging
Monitor database activity by logging:
Avoid enabling detailed SQL logging in production unless troubleshooting a specific issue, as it can affect performance and expose sensitive information.
Error Handling
Handle database failures gracefully.
Examples include:
Connection timeouts
Deadlocks
Constraint violations
Transaction failures
Provide meaningful application errors while recording detailed diagnostics in centralized logs.
Security
Secure your data access layer by:
Using parameterized queries.
Applying least-privilege database accounts.
Encrypting connection strings.
Enforcing authentication and authorization.
Keeping EF Core and database providers up to date.
Validating user input before executing queries.
Performance should never come at the expense of security.
Performance
Beyond query optimization:
Use asynchronous APIs consistently.
Cache frequently requested data.
Avoid unnecessary object allocations.
Optimize indexes regularly.
Monitor execution plans.
Benchmark changes before deployment.
Performance improvements should be measured rather than assumed.
Deployment
Before releasing an application:
Apply database migrations carefully.
Validate indexes.
Load test critical APIs.
Monitor query performance.
Review execution plans.
Configure automated backups.
A well-planned deployment minimizes downtime and reduces production risks.
Best Practices
Use projections whenever possible.
Keep queries simple.
Prefer AsNoTracking() for read-only operations.
Implement pagination.
Optimize indexes.
Cache expensive queries.
Monitor production performance continuously.
Common Mistakes
Avoid these common issues:
Loading unnecessary data.
Returning entire tables.
Using lazy loading without understanding its impact.
Calling SaveChanges() repeatedly.
Ignoring query execution plans.
Optimizing without measuring performance.
Effective optimization focuses on real bottlenecks rather than assumptions.
Troubleshooting
| Problem | Solution |
|---|
| Slow queries | Review generated SQL, indexes, and execution plans. |
| High memory usage | Use AsNoTracking(), projections, and pagination. |
| Excessive database calls | Eliminate N+1 queries using eager loading or projections. |
| Deadlocks | Keep transactions short and optimize update order. |
| Poor API performance | Profile database queries before optimizing application code. |
Conclusion
Entity Framework Core 10 provides excellent performance when used correctly. Most performance issues stem from inefficient query patterns rather than the framework itself. By applying techniques such as projection, AsNoTracking(), proper indexing, compiled queries, caching, and careful monitoring, you can build data access layers that remain fast and scalable under production workloads.
Performance optimization is not a one-time activity. Regular profiling, monitoring, and iterative improvements ensure your ASP.NET Core applications continue to deliver responsive and reliable experiences as your data and user base grow.