With Entity Framework Core (EF Core), a potent ORM for.NET applications, developers may use LINQ rather than SQL to communicate with databases. However, EF Core queries can become slow if they are not utilized carefully, particularly when dealing with big tables and intricate interactions. The good news? You may greatly enhance EF Core performance with a variety of easy methods. This post will teach you how to improve your EF Core queries and speed up your.NET apps using simple, natural language.

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?

  • Reduces memory usage
  • Improves query execution speed
  • Recommended for all read-only operations

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

  • Smaller payload
  • Faster network transfer
  • Reduced materialization overhead

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?
Prevents unnecessary round trips to the database

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?

  • Does not fetch extra records
  • Reduces memory and improves SQL execution

Index Database Columns Properly
Indexes drastically improve filtering and lookups.

Add Index in Entity

[Index(nameof(Email), IsUnique = true)]
public class User { ... }


Performance Impact

  • Faster queries on WHERE clauses
  • Better JOIN performance

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?

  • Client-side evaluation slows down performance
  • Database engines are optimized for filtering

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

  • Reduces overhead of query translation
  • Useful for high-performance APIs

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

  • Lazy loading = more queries
  • Explicit loading = predictable performance


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

  •     Extremely complex queries
  •     Performance-critical reporting

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?

  • Avoids hitting the database repeatedly
  • High performance gain for read-heavy systems

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.