AsNoTracking
Turn off change tracking for read-only queries with AsNoTracking, know what it saves and what it breaks, and when identity resolution matters.
public static IQueryable<TEntity> AsNoTracking<TEntity>(this IQueryable<TEntity> source) where TEntity : class- Added in .NET EF Core 1.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$source | IQueryable<TEntity> | An EF Core query. On a non-EF IQueryable the call is a no-op. |
Return value
IQueryable<TEntity>The same query, marked so that entity instances it materialises are not added to the change tracker and no original-value snapshot is kept.
Errors
Nothing at call time. Attaching or updating a no-tracking instance later throws InvalidOperationException if another instance with the same key is already tracked.
Changelog
- PHP EF Core 3.0No-tracking queries stopped resolving identity: a row that appears more than once in the results becomes separate instances.
- PHP EF Core 5.0AsNoTrackingWithIdentityResolution and QueryTrackingBehavior.NoTrackingWithIdentityResolution added for no-tracking queries that need shared instances.
By default a query that returns entity types is tracking: for each row EF Core looks the key up in the context's identity map, returns the existing instance if there is one, otherwise creates the entity, stores it and takes a snapshot of every property so SaveChangesAsync can work out what changed. AsNoTracking() skips all of that. The entities are plain objects that the context forgets as soon as they are returned.
What you save, and what you lose
The saving is allocation and CPU: no snapshot, no identity-map lookups, and nothing for DetectChanges to scan later in the request. It grows with row count; a list endpoint returning 500 orders with their lines allocates noticeably less. What you lose:
- Writes. Changing a no-tracking entity and calling
SaveChangesAsyncwrites nothing and returns 0. No exception, no warning. - Shared instances. Load 100 orders with
Include(o => o.Customer)where 3 customers placed them, and you get 100Customerobjects, not 3. Serialisation output is identical, but memory is not, and reference comparisons fail. - Fix-up. Navigations are only populated from what this query includes, never from entities tracked earlier.
The options side by side
| Choice | Tracked | Identity resolution | Use for |
|---|---|---|---|
| Default (tracking) | Yes | Yes | Entities you will modify |
AsNoTracking() | No | No | Read-only entity results |
AsNoTrackingWithIdentityResolution() | No | Within this query | Read-only graphs with many repeated references |
Select into a DTO | No (unless the DTO holds entities) | n/a | API responses: fewest columns, no tracking |
A projection that returns only scalars or DTOs is never tracked, so AsNoTracking on it changes nothing. A projection that contains an entity instance (new { Order = o, o.Customer.Name }) tracks that entity unless you add AsNoTracking.
Choosing a default
For a context that serves mostly reads, set UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) on the options and opt back in with AsTracking() on write paths. The risk is that a developer who does not know the default writes load-modify-save code that silently saves nothing, so cover write endpoints with integration tests that read the row back. For a mixed context, keep the tracking default and add AsNoTracking to read queries, or project to DTOs, which is better anyway.
Production failure mode
A read endpoint without AsNoTracking is not wrong, only slower, until the same context is long-lived: a batch job that queries 50,000 rows with tracking keeps them all, and each later SaveChangesAsync scans every one. The opposite bug, a write endpoint whose query was copied from a read endpoint with AsNoTracking, returns 204 and changes nothing.
Example
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<ShopDb>(o =>
o.UseNpgsql(builder.Configuration.GetConnectionString("Shop")));
var app = builder.Build();
// Read path: entities are materialised but never tracked.
app.MapGet("/orders/{id:int}", async (int id, ShopDb db, CancellationToken ct) =>
{
var order = await db.Orders
.AsNoTracking()
.Include(o => o.Lines)
.SingleOrDefaultAsync(o => o.Id == id, ct);
return order is null
? Results.NotFound()
: Results.Ok(new OrderView(order.Id, order.Lines.Sum(l => l.Quantity * l.UnitPrice),
order.Lines.Select(l => new LineView(l.Sku, l.Quantity)).ToList()));
});
// Write path: tracking is required, otherwise SaveChanges has nothing to save.
app.MapPut("/orders/{id:int}/lines/{sku}", async (int id, string sku, QuantityChange input, ShopDb db, CancellationToken ct) =>
{
var line = await db.OrderLines.SingleOrDefaultAsync(l => l.OrderId == id && l.Sku == sku, ct);
if (line is null) return Results.NotFound();
line.Quantity = input.Quantity;
await db.SaveChangesAsync(ct);
return Results.NoContent();
});
app.Run();
public record QuantityChange(int Quantity);
public record LineView(string Sku, int Quantity);
public record OrderView(int Id, decimal Total, List<LineView> Lines);
public class Order
{
public int Id { get; set; }
public List<OrderLine> Lines { get; } = [];
}
public class OrderLine
{
public int Id { get; set; }
public int OrderId { get; set; }
public string Sku { get; set; } = "";
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
public class ShopDb(DbContextOptions<ShopDb> options) : DbContext(options)
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<OrderLine> OrderLines => Set<OrderLine>();
}When to use it
- Serving a product listing endpoint that returns 50 entities per page without paying for snapshots it will never use
- Setting NoTracking as the default on a reporting context and using AsTracking on the two endpoints that write
- Loading 1,200 orders with their customers for a report while keeping one Customer instance per customer
Frequently asked questions
Does AsNoTracking matter if I project to a DTO?
No. A projection that contains only scalars and DTOs is not tracked whatever you write. It matters only when the result contains entity instances, directly or inside an anonymous type.
Can I update an entity I loaded with AsNoTracking?
Yes, by attaching it: db.Update(entity) or db.Attach(entity) then changing properties. Update marks every column modified, and either call throws if the context already tracks another instance with the same key. Loading with tracking is usually simpler.
More examples
No-tracking by default, AsTracking where you write
With the context-wide default set to <code>NoTracking</code>, <code>AsTracking()</code> is the explicit opt-in. <code>FindAsync</code> and <code>Add</code> still track whatever the default, because they are change-tracking operations rather than queries.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<CatalogDb>(o => o
.UseNpgsql(builder.Configuration.GetConnectionString("Catalog"))
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
var app = builder.Build();
app.MapGet("/products", (CatalogDb db, CancellationToken ct) =>
db.Products.OrderBy(p => p.Sku).Take(50).ToListAsync(ct)); // not tracked
app.MapPost("/products/{id:int}/discontinue", async (int id, CatalogDb db, CancellationToken ct) =>
{
var product = await db.Products.AsTracking().SingleOrDefaultAsync(p => p.Id == id, ct);
if (product is null) return Results.NotFound();
product.Discontinued = true;
await db.SaveChangesAsync(ct); // works because of AsTracking()
return Results.NoContent();
});
app.Run();
public class Product
{
public int Id { get; set; }
public string Sku { get; set; } = "";
public bool Discontinued { get; set; }
}
public class CatalogDb(DbContextOptions<CatalogDb> options) : DbContext(options)
{
public DbSet<Product> Products => Set<Product>();
}Pitfall: the silent no-op save
Checking the return value of <code>SaveChangesAsync</code> on single-row writes catches this class of bug: if you expected one row and got zero, something is wrong. A one-column change like this is also a good fit for <code>ExecuteUpdateAsync</code>, which needs no tracking at all.
using Microsoft.EntityFrameworkCore;
public sealed class InvoiceService(BillingDb db)
{
// Bug: copied from a read endpoint. Returns 0 and changes nothing.
public async Task<int> MarkPaidBroken(int invoiceId, CancellationToken ct)
{
var invoice = await db.Invoices.AsNoTracking().SingleAsync(i => i.Id == invoiceId, ct);
invoice.PaidAt = DateTimeOffset.UtcNow;
return await db.SaveChangesAsync(ct);
}
// Fix: a tracked load. SaveChanges returns 1 and writes one column.
public async Task<int> MarkPaid(int invoiceId, CancellationToken ct)
{
var invoice = await db.Invoices.SingleAsync(i => i.Id == invoiceId, ct);
invoice.PaidAt = DateTimeOffset.UtcNow;
return await db.SaveChangesAsync(ct);
}
}
public class Invoice
{
public int Id { get; set; }
public decimal Amount { get; set; }
public DateTimeOffset? PaidAt { get; set; }
}
public class BillingDb(DbContextOptions<BillingDb> options) : DbContext(options)
{
public DbSet<Invoice> Invoices => Set<Invoice>();
}Identity resolution for repeated references
Identity resolution uses a temporary tracker for the duration of the query, so it costs some of what <code>AsNoTracking</code> saves. Use it when results repeat large references many times, or when code relies on reference equality; otherwise plain <code>AsNoTracking</code> or a DTO projection is cheaper.
using Microsoft.EntityFrameworkCore;
public sealed class ShipmentReport(ShopDb db)
{
public async Task<(int Orders, int CustomerObjects)> Load(DateOnly day, CancellationToken ct)
{
// 1,200 orders from 40 customers.
// AsNoTracking: 1,200 Customer instances, one per row.
// AsNoTrackingWithIdentityResolution: 40 Customer instances, shared.
var orders = await db.Orders
.AsNoTrackingWithIdentityResolution()
.Include(o => o.Customer)
.Where(o => o.ShipDate == day)
.ToListAsync(ct);
var distinctCustomers = orders.Select(o => o.Customer).Distinct(ReferenceEqualityComparer.Instance).Count();
return (orders.Count, distinctCustomers);
}
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
public class Order
{
public int Id { get; set; }
public DateOnly ShipDate { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; } = null!;
}
public class ShopDb(DbContextOptions<ShopDb> options) : DbContext(options)
{
public DbSet<Order> Orders => Set<Order>();
}
Discussion