[C#] Speeding Up Read-Only Data Retrieval with Entity Framework Core

目次

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 DbContext instead of individual queries, you can set QueryTrackingBehavior.NoTracking in 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), standard AsNoTracking creates separate instances for each reference, which wastes memory. In EF Core 5.0+, use AsNoTrackingWithIdentityResolution() to share instances with the same key.

Important Notes

  • Cannot Update: If you change the properties of an entity retrieved with AsNoTracking and call SaveChangesAsync, 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, AsNoTracking always 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.

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次