TIL: You Don't Need AsNoTracking() When Projecting Entities With Entity Framework Core
11 Jun 2025 #csharp #todayilearnedEvery time we retrieve an entity with Entity Framework Core, it will track those entities in its change tracker. And when calling .SaveChanges()
, those changes are persisted to the database.
For read-only queries, we can add .AsNoTracking()
to make them faster.
But when projecting an entity into a custom object, there’s no need to add AsNoTracking()
since Entity Framework doesn’t track query results with a type different from the underlying entity type. Source
For example, let’s save some movies and retrieve them with and without a custom projection,
using Microsoft.EntityFrameworkCore;
namespace LookMaEntityFrameworkDoesNotTrackProjections;
public class Movie
{
public int Id { get; set; }
public string Name { get; set; }
public int ReleaseYear { get; set; }
}
public class MovieContext : DbContext
{
public MovieContext(DbContextOptions<MovieContext> options) : base(options)
{
}
public DbSet<Movie> Movies { get; set; }
}
[TestClass]
public class MovieTests
{
[TestMethod]
public void EFDoesNotTrackProjections()
{
var options = new DbContextOptionsBuilder<MovieContext>()
.UseInMemoryDatabase(databaseName: "MovieDB")
.Options;
// 0. Saving two movies
using (var context = new MovieContext(options))
{
context.Movies.AddRange(
new Movie { Name = "Matrix", ReleaseYear = 1999 },
new Movie {Name = "Titanic", ReleaseYear = 1997 }
);
context.SaveChanges();
}
// 1. Using a custom projection
using (var context = new MovieContext(options))
{
var firstMovieNameAndReleaseYear
= context.Movies
.Where(m => m.ReleaseYear >= 1990)
.Select(m => new { m.Name, m.ReleaseYear })
// ^^^^
// This is a custom projection
.First();
var noTracking = context.ChangeTracker.Entries();
Assert.AreEqual(0, noTracking.Count());
// ^^^
// No entities tracked
}
// 2. Using AsNoTracking
using (var context = new MovieContext(options))
{
var firstMovieWithNoTracking
= context.Movies
.Where(m => m.ReleaseYear >= 1990)
.AsNoTracking()
// ^^^^^
.First();
var withAsNoTracking = context.ChangeTracker.Entries();
Assert.AreEqual(0, withAsNoTracking.Count());
// ^^^
// As imply by its name, no entities tracked here
}
// 3. Retrieving a Movie
using (var context = new MovieContext(options))
{
var firstMovie = context.Movies
.Where(m => m.ReleaseYear >= 1990)
.First();
var beingTracked = context.ChangeTracker.Entries();
Assert.AreEqual(1, beingTracked.Count());
// ^^^
// Since we're retrieving only one Movie, tracking happens here
}
}
}
Only when we queried the first movie without a projection and without .AsNoTracking()
, Entity Framework Core tracked the underlying entity.
Et voilà!
For more tricks with Entity Framework Core, read how to configure default values for nullable columns with default constraints.