Skip to content

Series: Optimizing ASP.NET Core & C#

We've learned a lot about making databases faster. But a fast database is only half the story. The performance of an application also depends heavily on how we write our code in C# and ASP.NET Core.

Welcome to our new series focused on optimizing ASP.NET Core & C# applications!

In this series, we'll explore topics like:

  • Using async/await correctly to keep your app responsive.
  • Techniques to make Entity Framework Core faster.
  • Managing your application's memory.
  • Effective caching strategies.
  • And much more.

Case 1: The Easy Win - AsNoTracking() for Read-Only Queries

The Scenario 📝

  • System: An ASP.NET Core application that uses Entity Framework (EF) Core to talk to a database.
  • Problem: An API endpoint that returns a list of products is getting slow as the number of products grows.

The Problematic Code (The Default Way)

csharp
public async Task<List<Product>> GetProductsAsync()
{
    // Get 100 products to display
    var products = await _context.Products
                                 .Where(p => p.IsActive)
                                 .Take(100)
                                 .ToListAsync();
    return products;
}

The Hidden Bottleneck 🧐

  1. What is Change Tracking?: By default, when EF Core fetches data, it "tracks" every item. This is called Change Tracking. It does this by creating a copy (a "snapshot") of each item and storing it in memory.

  2. Why does it do this?: Change Tracking is what allows EF Core to perform its magic. You can change a property on an object (e.g., product.Name = "New Name") and then call _context.SaveChanges(). EF Core automatically knows which UPDATE command to send to the database.

  3. The Hidden Cost: This magic comes at a price. Creating and managing snapshots for hundreds or thousands of items consumes both CPU power and memory (RAM). In our case, the API only needs to display the data. It never updates it. So, all the work of Change Tracking is completely wasted.

The Solution: Use AsNoTracking()

  • The Logic: We tell EF Core, "Get the data, but don't track it. I only need to read it."
  • The Optimized Code:
    csharp
    public async Task<List<Product>> GetProductsAsync()
    {
        var products = await _context.Products
                                     .Where(p => p.IsActive)
                                     .AsNoTracking() // Add this line
                                     .Take(100)
                                     .ToListAsync();
        return products;
    }

The Results ✨

  1. Faster Execution: The query runs significantly faster, especially with a large amount of data, because EF Core skips the expensive Change Tracking setup.
  2. Less Memory Usage: The application uses less RAM because it no longer needs to store snapshots of the data.

Conclusion:

  • Golden Rule: Always use .AsNoTracking() for all queries where you only read data and do not intend to update it within the same operation.
  • This is one of the easiest and most effective optimizations in EF Core. Make it a habit!