Use AsNoTracking and Compiled Queries in EF Core

 In this tutorial, you will learn how to use AsNoTracking and compiled queries in Entity Framework Core. These features improve read performance and reduce overhead in high-traffic applications. You use them when your API runs read-only queries often and you want the fastest response possible.


Why Tracking Creates Overhead

EF Core tracks every entity it loads by default. Tracking helps when you update data and call SaveChanges. Tracking creates CPU and memory overhead during read operations. When your API loads thousands of rows or runs the same query many times, the change tracker becomes expensive.

AsNoTracking removes this overhead by telling EF Core not to track entities.

Compiled queries remove overhead by avoiding repeated translation of LINQ expressions.

You use both features for fast reads.


How AsNoTracking Works

AsNoTracking() tells EF Core to skip the change tracker. This improves performance for read-only scenarios.

Example

var employees = await _context.Employees
  .AsNoTracking()
  .Where(e => e.IsActive)
  .OrderBy(e => e.LastName)
  .ToListAsync();

When to Use It

When to Avoid It

  • When you update the entity after reading
  • When you need identity resolution across the context
  • When lazy loading is required

Pros

  • Faster read operations
  • Lower memory use
  • No change tracker overhead
  • Good for large result sets

Cons

  • Cannot update these entities through the same DbContext
  • No identity resolution
  • Lazy loading does not work


How Compiled Queries Work

EF Core must translate LINQ to SQL before executing a query. When the same query shape runs many times, translation overhead becomes expensive. Compiled queries solve this by compiling the query once.

Example: Basic Compiled Query

private static readonly Func<AppDbContext, int, Task<List<Order>>> GetOrders =
  EF.CompileAsyncQuery((AppDbContext ctx, int customerId) =>
    ctx.Orders
      .Where(o => o.CustomerId == customerId)
      .OrderByDescending(o => o.CreatedAt)
  );

var list = await GetOrders(_context, 42);

Example: Compiled Query with DTO Projection

private static readonly Func<AppDbContext, int, Task<List<OrderDto>>> GetOrderDtos =
  EF.CompileAsyncQuery((AppDbContext ctx, int id) =>
    ctx.Orders
      .Where(o => o.CustomerId == id)
      .Select(o => new OrderDto { Id = o.Id, Total = o.Total, Date = o.CreatedAt })
  );

var dtos = await GetOrderDtos(_context, 42);

Pros

  • Eliminates repeated translation cost
  • Predictable performance in high-traffic apps
  • Works well with parameterized queries
  • Thread-safe and reusable

Cons

  • More code to maintain
  • Not good for dynamic queries
  • Too many compiled delegates increase memory


Combine AsNoTracking With Compiled Queries

You use both features for maximum performance.

Example

var data = await GetOrders(_context, id)
  .AsNoTracking()
  .ToListAsync();

This removes both translation overhead and tracking overhead.


Best Practices

  • Use AsNoTracking for all read-only operations
  • Use compiled queries only for frequently executed queries
  • Use projection for response models
  • Keep DbContext short-lived
  • Benchmark before and after changes
  • Avoid using compiled queries for complex or dynamic filters


Summary

AsNoTracking reduces tracking overhead. Compiled queries reduce translation overhead. Both improve read performance. Use AsNoTracking for read-only queries. Use compiled queries for high-frequency queries. Combine both for fast and efficient EF Core read operations.

This approach gives you faster APIs, lower memory use, and predictable performance in production.

Happy coding! 🚀

Post a Comment

0 Comments