Overview
When you only need to display data without updating it in Entity Framework Core (EF Core), you can significantly improve performance by using the AsNoTracking method. This method skips the “Change Tracking” snapshot process performed by the DbContext. As a result, your application uses less memory and executes queries faster.
Specifications (Input/Output)
- Input: Maximum number of articles to retrieve.
- Output: Displays the titles and author names of the retrieved articles in the console.
- Prerequisites: .NET 6.0 or higher,
Microsoft.EntityFrameworkCore.InMemory(for testing).
Basic Usage
Simply add .AsNoTracking() to your query method chain. It is usually placed just before calling terminal methods like ToListAsync.
// Disable change tracking (Faster and uses less memory)
var products = await context.Products
.AsNoTracking() // ★ Add here
.Where(p => p.Price > 1000)
.ToListAsync();
Full Code Example
The following implementation shows a scenario for displaying a “New Articles List” where editing is not required. This is especially effective for batch processing or GET requests in Web APIs.
You will need the Microsoft.EntityFrameworkCore.InMemory package. dotnet add package Microsoft.EntityFrameworkCore.InMemory
using System;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.EntityFrameworkCore;
public class Program
{
public static async Task Main()
{
var options = new DbContextOptionsBuilder<BlogContext>()
.UseInMemoryDatabase("BlogDb_NoTracking")
.Options;
// --- 1. Prepare Initial Data ---
using (var context = new BlogContext(options))
{
if (!await context.Posts.AnyAsync())
{
var author = new User { Name = "TechUser" };
context.Users.Add(author);
// Create a large amount of data
for (int i = 1; i <= 1000; i++)
{
context.Posts.Add(new Post
{
Title = $"C# Tips Vol.{i}",
Content = "Content...",
SentTime = DateTime.Now.AddHours(-i),
User = author
});
}
await context.SaveChangesAsync();
}
}
// --- 2. Execute Read-Only Query ---
using (var context = new BlogContext(options))
{
var worker = new BlogReader(context);
await worker.ShowRecentPostsAsync(10);
}
}
}
// Business Logic Class
public class BlogReader
{
private readonly BlogContext _context;
public BlogReader(BlogContext context)
{
_context = context;
}
public async Task ShowRecentPostsAsync(int count)
{
Console.WriteLine($"--- Retrieving the latest {count} articles ---");
// Using AsNoTracking
// EF Core will not save a copy of these entities in the tracking manager
var posts = await _context.Posts
.Include(p => p.User) // Include related data
.OrderByDescending(p => p.SentTime)
.Take(count)
.AsNoTracking() // ★ Key to performance improvement
.ToArrayAsync();
foreach (var post in posts)
{
// Safety check for null even with Include
var authorName = post.User?.Name ?? "Unknown";
Console.WriteLine($"[{post.SentTime:MM/dd HH:mm}] {authorName}: {post.Title}");
}
// Note: Even if you set post.Title = "Changed"; here,
// the database will NOT be updated by SaveChangesAsync.
}
}
// Entity Definitions
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTime SentTime { get; set; }
public int UserId { get; set; }
public User? User { get; set; }
}
// DbContext Definition
public class BlogContext : DbContext
{
public BlogContext(DbContextOptions<BlogContext> options)
: base(options) { }
public DbSet<Post> Posts => Set<Post>();
public DbSet<User> Users => Set<User>();
}
Customization Points
- Changing Default Settings: If you want to disable tracking for the entire
DbContextinstead of individual queries, you can setQueryTrackingBehavior.NoTrackingin the context options.C#optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); - NoTracking with Identity Resolution: If your results contain multiple references to the same entity (e.g., the same
User), standardAsNoTrackingcreates separate instances for each reference, which wastes memory. In EF Core 5.0+, useAsNoTrackingWithIdentityResolution()to share instances with the same key.
Important Notes
- Cannot Update: If you change the properties of an entity retrieved with
AsNoTrackingand callSaveChangesAsync, the changes will not be reflected in the database. Do not use it when updates are needed. - Lazy Loading Disabled: Lazy loading for navigation properties does not work for non-tracked entities. You must explicitly load necessary data using
Include. - Find Method: The
DbSet.Find(id)method returns tracked data from the cache if available. In contrast,AsNoTrackingalways executes a query against the database. Use them according to your specific needs.
Advanced Application
Projection with Select (Further Optimization)
If you do not need the entire entity, using Select to retrieve only the necessary fields into an anonymous class (or DTO) is the fastest approach. This is automatically treated as a non-tracking query.
// Lightest approach: Retrieve only necessary columns without materializing entities
var dtos = await _context.Posts
.OrderByDescending(p => p.SentTime)
.Take(10)
.Select(p => new
{
p.Title,
AuthorName = p.User.Name // Access related data without using Include
})
.ToListAsync();
Conclusion
This approach reduces both memory usage and CPU load, improving scalability under heavy traffic.
Always use AsNoTracking for processes that do not involve saving changes, such as displaying data, generating reports, or reference endpoints in Web APIs.
If you need even more optimization, use Select to limit the columns you retrieve.
