Skip to main content

How to Use AsNoTracking in EF Core

· 6 min read
Jagdish Kumawat
Founder @ Dewiride

By default, EF Core remembers every entity a query returns, in case you change it. AsNoTracking turns that off for one query, which saves work when you only want to read. This guide shows what it changes and what it doesn't, measures the difference on 5,000 rows, and explains the cases where you don't need it at all.

Prerequisites​

  • A .NET C# project with EF Core. The examples use EF Core 10, Oracle's MySql.EntityFrameworkCore, MySQL 9.7, and a products table. SQL logging is on, as in How to See the SQL That EF Core Sends.

Step 1: Add It to a Query​

It goes on the query, before the method that runs it:

A temporary endpoint
var products = await db.Products.AsNoTracking().ToListAsync();

To see the effect, these two endpoints count what the context is tracking after the query:

Temporary endpoints
// Tracking, the default
var products = await db.Products.ToListAsync();
return Results.Ok(new { rows = products.Count, tracked = db.ChangeTracker.Entries().Count() });

// No tracking
var products = await db.Products.AsNoTracking().ToListAsync();
return Results.Ok(new { rows = products.Count, tracked = db.ChangeTracker.Entries().Count() });
Response (tracking)
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
"rows": 8,
"tracked": 8
}
Response (AsNoTracking)
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
"rows": 8,
"tracked": 0
}

And the SQL is the same in both cases:

API log (AsNoTracking)
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `p`.`Id`, `p`.`ArchivedAtUtc`, `p`.`CategoryId`, `p`.`CreatedAtUtc`, `p`.`Description`, `p`.`IsArchived`, `p`.`Name`, `p`.`QuantityOnHand`, `p`.`ReorderLevel`, `p`.`Sku`, `p`.`UnitPrice`, `p`.`UpdatedAtUtc`, `p`.`Version`
FROM `products` AS `p`

So MySQL does the same work. The saving is all in your app: no entries in the change tracker.

Step 2: Measure It​

I read 5,000 rows, five times each way, on a new context each time, after two warm-up rounds:

A temporary endpoint (the part that matters)
var query = tracking ? context.Products : context.Products.AsNoTracking();
var watch = Stopwatch.StartNew();
var rows = await query.Where(p => p.Sku.StartsWith("BULK-")).ToListAsync();
watch.Stop();
Response body
[
{
"run": 1,
"rows": 5000,
"trackedMs": 33,
"noTrackingMs": 27
},
{
"run": 2,
"rows": 5000,
"trackedMs": 37,
"noTrackingMs": 27
},
{
"run": 3,
"rows": 5000,
"trackedMs": 32,
"noTrackingMs": 26
},
{
"run": 4,
"rows": 5000,
"trackedMs": 51,
"noTrackingMs": 18
},
{
"run": 5,
"rows": 5000,
"trackedMs": 22,
"noTrackingMs": 18
}
]

A terminal with the five timing runs on 5,000 rows: tracked queries taking 22 to 51 milliseconds, and no-tracking queries 18 to 27 milliseconds.

No tracking won all five. A handful of milliseconds on 5,000 rows isn't dramatic, and your numbers will vary, but it adds up on a busy API. It matches EF Core's tracking page: no-tracking queries are "generally quicker to execute because there's no need to set up the change tracking information."

When to Use It​

SituationUse AsNoTracking?Why
Loading entities only to read themYesThere's no tracking to pay for
Select to a record or anonymous typeNot neededNothing is tracked anyway
Loading an entity to change and save itNoSaveChangesAsync wouldn't see your change
Reading a row you've just changed in this contextNoNo-tracking results come from the database

The second row surprises people. The page says: "If the result set doesn't contain any entity types, then no tracking is done." I checked:

Response (Select to an anonymous type, without AsNoTracking)
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
"rows": 8,
"tracked": 0,
"products": [
{
"sku": "CBL-USBC-2M",
"name": "USB-C cable, 2 m"
},
{
"sku": "CBL-USBC-1M",
"name": "USB-C cable, 1 m"
},
{
"sku": "CBL-HDMI-2M",
"name": "HDMI cable, 2 m"
},
{
"sku": "CBL-LTNG-1M",
"name": "Lightning cable, 1 m"
},
{
"sku": "CHG-USBC-20W",
"name": "USB-C charger, 20 W"
},
{
"sku": "CHG-USBC-65W",
"name": "USB-C charger, 65 W"
},
{
"sku": "ADP-HDMI-VGA",
"name": "HDMI to VGA adapter"
},
{
"sku": "ADP-USBC-USBA",
"name": "USB-C to USB-A adapter"
}
]
}

tracked is 0, with no AsNoTracking. So an API that always uses Select, like the one in Part 3 of the MySQL series, rarely needs this call.

Common Mistakes​

MistakeWhat happensDo this instead
Changing an AsNoTracking entity and savingNothing is saved, and no error appearsLoad it with tracking
Adding AsNoTracking to every SelectIt does nothingLeave it out
Turning tracking off for the whole contextUpdates silently stop working unless you opt back inUse it per query
Using Update on a no-tracking entity without thinkingIt writes every columnLoad with tracking, and change the property

FAQ​

Can I make no-tracking the default?​

Yes, with UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) in the context's options. Then use AsTracking on the queries that update. It's reasonable for a mostly read-only app.

What about the same row appearing twice?​

A tracking query returns one object per row. A no-tracking query can return separate objects for the same row, for example with Include. AsNoTrackingWithIdentityResolution is for that case.

Conclusion​

AsNoTracking() tells EF Core not to remember the entities a query returns. The SQL stays the same, the change tracker stays empty, and reads get a bit faster: five out of five runs here. Use it when you load entities only to read them, skip it on projections, where nothing is tracked, and never use it on something you're about to save.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.