How it works
The explanation, with examples you can run.
Counting queries
The example below (EF Core 10 with SQLite in memory) counts the SELECT statements EF Core logs:
// three customers with two orders each are already savedforeach (var c in db.Customers.ToList()) db.Entry(c).Collection(x => x.Orders).Load(); // one query per customer// printed: load per customer: 4 queries var all = db.Customers.Include(c => c.Orders).ToList();// printed: Include: 1 query, 6 orders var totals = db.Customers .Select(c => new { c.Name, Sum = c.Orders.Sum(o => o.Total) }) .ToList();// printed: projection: 1 query, first c1=15.0The first loop is the N+1 pattern: one query for the list, then one per row. With 3 customers it is 4 round trips; with 3,000 it is a slow page. The same shape appears when lazy loading is enabled (it is off by default and needs a proxies package) and a loop touches a navigation property. Two fixes:
- Include loads the related rows in the same query with a JOIN. For several collection navigations, a single JOIN multiplies rows (a "cartesian explosion");
AsSplitQuery()loads each collection with its own query instead. - Projection with
Selectasks only for the columns you need. The aggregate ran in SQL, nothing was tracked, and the result was a small anonymous object. For read endpoints this is usually the best option.
Change tracking
var first = all[0];first.Name = "renamed";Console.WriteLine(db.Entry(first).State); // ModifiedConsole.WriteLine(db.SaveChanges()); // 1var untracked = db.Customers.AsNoTracking().First();Console.WriteLine(db.Entry(untracked).State); // DetachedWhen a tracking query returns an entity, EF Core stores a snapshot of its original values. SaveChanges runs DetectChanges, compares each tracked entity with its snapshot, and writes only the columns that differ. The cost is memory and CPU for every tracked object, so read-only queries should use AsNoTracking() (or UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) as the context default). For bulk changes, ExecuteUpdate and ExecuteDelete (EF Core 7 and later) run one SQL statement without loading entities at all.
Because the context tracks state and isn't thread-safe, AddDbContext registers it as scoped. Two queries started concurrently on one context throw. In background services, inject IDbContextFactory<T> and create a context per operation.
Migrations
When the model changes, dotnet ef migrations add AddOrderStatus generates a migration class with Up and Down methods plus an updated model snapshot. Review the generated code like any other code: renaming a property can come out as drop column and add column, losing data. To apply migrations in production, prefer a reviewed artifact over calling Database.Migrate() at start-up from every instance: dotnet ef migrations script --idempotent produces SQL a DBA can run, and dotnet ef migrations bundle produces an executable for the deployment pipeline. EnsureCreated, used in the example, skips migrations entirely and is only for tests and prototypes.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.