Introduction
Entity Framework Core (EF Core) is a powerful ORM for .NET applications, allowing developers to interact with databases using LINQ instead of SQL. However, if not used carefully, EF Core queries can become slow, especially with large tables and complex relationships. The good news? There are many simple techniques that can help you improve EF Core performance significantly. In this article, you will learn practical tips—written in plain and natural language—to optimize your EF Core queries and make your .NET applications run faster.
Use AsNoTracking for Read-Only Queries
Tracking changes adds overhead. If you’re only reading data, disable tracking.
Example (Slow)
var users = await _context.Users.ToListAsync();
Optimized
var users = await _context.Users.AsNoTracking().ToListAsync();
Why It Helps
Select Only the Columns You Need (Projections)
Fetching entire entities loads unnecessary columns.
Slow Query
var users = await _context.Users.ToListAsync();
Efficient Query
var users = await _context.Users
.Select(u => new { u.Id, u.Name })
.ToListAsync();
Benefits
Use Pagination for Large Result Sets
Never return thousands of rows at once.
Example
var page = 1;
var pageSize = 20;
var users = await _context.Users
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
Why It Helps
Reduces memory usage
Prevents slow UI loading
Avoid N+1 Query Problems with Include
If you load related data in a loop, EF will run multiple queries.
Bad (N+1 queries)
var users = await _context.Users.ToListAsync();
foreach (var user in users)
{
var orders = user.Orders; // triggers additional queries
}
Good
var users = await _context.Users
.Include(u => u.Orders)
.ToListAsync();
Why It Matters
Use Filter Before Include (Important)
Filtering after Include loads unnecessary data.
Inefficient
var users = await _context.Users
.Include(u => u.Orders)
.Where(u => u.IsActive)
.ToListAsync();
Optimized
var users = await _context.Users
.Where(u => u.IsActive)
.Include(u => u.Orders)
.ToListAsync();
Why This Helps
Index Database Columns Properly
Indexes drastically improve filtering and lookups.
Add Index in Entity
[Index(nameof(Email), IsUnique = true)]
public class User { ... }
Performance Impact
Avoid Client Evaluation (Let Database Do the Work)
EF Core may switch to client-side evaluation if query contains unsupported logic.
Bad
var users = await _context.Users
.Where(u => SomeCSharpFunction(u.Name)) // runs on client
.ToListAsync();
Good
var users = await _context.Users
.Where(u => u.Name.Contains(search)) // SQL compatible
.ToListAsync();
Why This Matters
Use Compiled Queries for Frequently Used Queries
If a query runs millions of times, precompile it.
Example
static readonly Func<AppDbContext, int, Task<User>> GetUserById =
EF.CompileAsyncQuery((AppDbContext ctx, int id) =>
ctx.Users.FirstOrDefault(u => u.Id == id));
Why It Helps
Avoid Using Lazy Loading (Prefer Explicit Loading)
Lazy loading causes hidden queries.
Bad
var user = await _context.Users.FirstAsync();
var orders = user.Orders; // triggers separate query
Good
var user = await _context.Users
.Include(u => u.Orders)
.FirstAsync();
Why
Keep Queries Simple and Use Raw SQL When Necessary
Sometimes LINQ creates inefficient SQL.
Example
var data = await _context.Users
.FromSqlRaw("SELECT * FROM Users WHERE IsActive = 1")
.ToListAsync();
When Useful
Cache Frequently Requested Data
Use caching for data that rarely changes.
var cacheKey = "ActiveUsers";
if (!_memoryCache.TryGetValue(cacheKey, out List<User> users))
{
users = await _context.Users.Where(u => u.IsActive).ToListAsync();
_memoryCache.Set(cacheKey, users, TimeSpan.FromMinutes(10));
}
Why Cache?
Disable Change Tracking for Bulk Operations
Manual tracking causes overhead in insert/update loops.
Efficient Bulk Insert
_context.ChangeTracker.AutoDetectChangesEnabled = false;
foreach (var record in records)
{
_context.Add(record);
}
await _context.SaveChangesAsync();
_context.ChangeTracker.AutoDetectChangesEnabled = true;
Or Use Bulk Libraries
EFCore.BulkExtensions
Dapper + BulkCopy
Best Practices Summary
Use AsNoTracking for read-only queries
Use projections to select only required columns
Apply pagination for large datasets
Avoid N+1 problems with Include
Let the database handle filtering
Use compiled queries when necessary
Index your database properly
Prefer explicit loading over lazy loading
Cache frequently used data
Use raw SQL for complex cases
Conclusion
Improving EF Core query performance is all about reducing unnecessary work—both in your code and inside the database. By applying these practical tips like AsNoTracking, pagination, projections, correct indexing, and avoiding lazy loading, you can significantly speed up your application. With the right approach, EF Core becomes both powerful and performant for building fast, scalable .NET applications.