Improve queries reading performances with AsNoTracking on C# EF Core

What is AsNoTracking

Entity Framework Core is a massive .NET tool to facilitate database queries.

If you delve into it, it offers many configuration possibilities to improve the speed depending on the use you make of it.

Here we will talk about the AsNoTracking function, usable from IQueryable.

If we take a look at the Microsoft :

Returns a new query where the entities returned will not be cached in the DbContext or ObjectContext

The question is: what is cached in the DbContext?

I'm not going to go into detail because it would take a whole article to have a proper answer.

The important part is the tracking graph. It is an internal property of the DbContext to record all the modified items that were obtained from the database.

Basically, it helps the DbContext to maintain all the updates made to the database objects in order to save the data.

This tracking has a cost, both on the performance of requests and on the use of memory.

As we are sure not to modify the retrieved data, the AsNoTracking function is an excellent way to improve performance.

But beware.

AsNoTracking can only be used for read requests.

Since DbContext no longer tracks updates, you will get errors if you try to SaveChanges on the data you have retrieved.

Benchmark

To validate, we built a benchmark to compare the impact of AsNoTracking on contextual queries.

public class Goat
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

The fixture generates 100.000 goats (yeah, that's huge!) in an InMemoryDatabase to make sure it has enough data to monitor differences.

public class DbContextBenchmark
{
    [GlobalSetup]
    public void Setup()
    {
        var goats = new Fixture().CreateMany<Goat>(100000);
        var dbContext = new AppDbContext();
        dbContext.AddRange(goats);
        dbContext.SaveChanges();
    }

    [Benchmark]
    public void GetInDatabase_WithAsNoTracking()
    {
        var dbContext = new AppDbContext();
        dbContext.Goats.AsNoTracking().ToList();
    }

    [Benchmark]
    public void GetInDatabase_WithoutAsNoTracking()
    {
        var dbContext = new AppDbContext();
        dbContext.Goats.ToList();
    }
}

We instantiate 2 functions:

  • GetInDatabase_WithAsNoTracking to test with AsNoTracking
  • GetInDatabase_WithoutAsNoTracking to test without AsNoTracking

Obviously, we need to create a new AppDbContext for each method to make sure that no cache elements are contained in the DbContext after the first resolution.

Results

The benchmark shows us that with AsNoTracking, the read request is 4x faster on average.

|                            Method |     Mean |    Error |   StdDev |   Median |
|---------------------------------- |---------:|---------:|---------:|---------:|
|    GetInDatabase_WithAsNoTracking | 119.5 ms |  7.38 ms | 21.75 ms | 113.2 ms |
| GetInDatabase_WithoutAsNoTracking | 483.5 ms | 23.88 ms | 69.65 ms | 467.9 ms |

It's pretty massive, and it's a good trick to improve database access speed.

But remember, it's READ ONLY. I have to say that out loud!

To go further:

EntityFrameworkQueryableExtensions.AsNoTracking<TEntity> Method (Microsoft.EntityFrameworkCore)
The change tracker will not track any of the entities that are returned from a LINQ query. If the entity instances are modified, this will not be detected by the change tracker and SaveChanges() will not persist those changes to the database. Disabling change trackin…

Have fun 🐐