# EF Core From Zero

> Learn Entity Framework Core, .NET's flagship ORM: the DbContext and connecting, entity models and migrations, create and read, LINQ querying, change tracking and SaveChanges, relationships, loading strategies and the N+1 trap, and transactions. The data layer most ASP.NET Core apps use - including where to drop to SQL.


---

# EF Core From Zero

Entity Framework Core is the ORM most ASP.NET Core apps reach for. You define your tables as C# classes,
and EF Core writes the SQL for create, read, update, delete, relationships, and schema migrations. If
you've used an ORM elsewhere it'll feel familiar; if you haven't, it's a comfortable way into "describe
your data as types, let the library talk to the database." And because good engineers like to know what's
really happening, the most valuable habit you'll build here is watching the SQL EF Core generates - so you
can drop to raw SQL the moment the ORM gets in your way.

The mental model is two pieces plus a query language. A **`DbContext`** is your session with the database:
it holds **`DbSet<T>`** properties (one per table), **tracks the changes** you make to the objects it
hands you, and pushes them all to the database when you call **`SaveChanges`**. And you query with
**LINQ** - C#'s built-in query syntax - which EF Core translates into SQL. Hold "the DbContext is a
change-tracking session, DbSets are your tables, LINQ becomes SQL," and EF Core stops being magic and
becomes a tool you direct.

> 📝 This teaches the **library** - it assumes you know **C#** (classes, generics, LINQ basics,
> `async`/`await` - [C# From Zero](/guides/csharp-from-zero)) and basic **databases** (tables, keys,
> joins - [What a Database Is](/guides/what-a-database-is)). The ORM concepts transfer directly from
> [Hibernate & JPA](/guides/hibernate-and-jpa-from-zero), [SQLAlchemy](/guides/sqlalchemy-from-zero), and
> [GORM](/guides/gorm-from-zero). It pairs with [ASP.NET Core](/guides/aspnet-core-from-zero) as its data
> layer. Examples use SQLite for zero setup and are shown with their output.

## How to read this

Read in order - it builds one schema (a **blog**: blogs, posts, tags) from a bare `DbContext` up to
relationships, the N+1 trap, and migrations. Phases carry difficulty badges.

## The phases

**Part 1 - Foundations (🟢 → 🟡)**
1. **[What EF Core Is & the DbContext](01-what-efcore-is.md)** 🟢 - the ORM idea, the `DbContext`/`DbSet` model, and seeing the SQL.
2. **[Entity Models & Migrations](02-models-and-migrations.md)** 🟡 - classes as tables, conventions, and `dotnet ef` migrations.
3. **[Create & Read](03-create-and-read.md)** 🟡 - `Add` + `SaveChanges`, `Find`/`First`/`Single`, and how records round-trip.

**Part 2 - Real queries (🟡 → 🔴)**
4. **[Querying with LINQ](04-querying-with-linq.md)** 🟡 - `Where`/`OrderBy`/`Select`, deferred execution, and `AsNoTracking`.
5. **[Change Tracking & SaveChanges](05-change-tracking.md)** 🔴 - the unit of work: how EF detects edits and batches the writes.
6. **[Relationships](06-relationships.md)** 🔴 - navigation properties, one-to-many, many-to-many, and the fluent API.
7. **[Loading Strategies & the N+1 Trap](07-loading-and-n-plus-1.md)** 🔴 - `Include` vs lazy loading, and the query explosion that bites everyone.

**Part 3 - Real projects (🔴 → 🟢)**
8. **[Transactions & Migrations in Production](08-transactions-and-migrations.md)** 🔴 - transactions, concurrency, and applying migrations safely.
9. **[EF Core in the Real World & Where to Go Next](09-where-to-go-next.md)** 🟢 - when to drop to SQL, EF Core vs Dapper, and what to build.

> The throughline: a **`DbContext` is a change-tracking session**, **`DbSet`s are your tables**, and
> **LINQ becomes SQL**. Watch that SQL and you stay in command of the database.


---

# What EF Core Is & the DbContext

A .NET app that reads and writes SQL rows *could* hand-write every `INSERT`, `SELECT`, and `UPDATE` - 
opening a connection, building a command, reading columns back one at a time. It works, but it's repetitive
plumbing, and the day a column name changes, you're chasing it through every query that touched it.

EF Core - Entity Framework Core - is what most ASP.NET Core apps reach for instead. It's .NET's flagship
**ORM** (object-relational mapper): you describe your tables as plain C# classes, and EF Core writes the SQL
for create, read, update, delete, relationships, and schema migrations. Fewer typos in column names, less
boilerplate, and the shape of your data lives in one place.

> 📝 This phase teaches the **library**. It assumes you know **C#** (classes, generics, LINQ basics,
> `async`/`await` - [C# From Zero](/guides/csharp-from-zero)) and the basics of **databases** (tables,
> rows, keys - [What a Database Is](/guides/what-a-database-is)). If you've used an ORM in another
> language, the core ideas transfer straight across - see [GORM From Zero](/guides/gorm-from-zero) for the
> same concepts in Go.

## The real cost

Every convenience comes with a bill. When a library writes your SQL for you, you stop *seeing* your SQL - 
exactly where ORMs earn their bad reputation. One innocent-looking method call can fire off a query you'd
never have written by hand, and if you're not watching, you find out in production when a page is
mysteriously slow.

⚠️ The cure isn't avoiding EF Core - it's **watching the SQL it generates**. EF Core can log every statement
it runs. Turn that on while you learn (we'll do it in a minute), and the ORM stops being a black box. You'll
see the `INSERT` behind an `Add`, the `SELECT` behind a query, and the moment a call does something expensive.

## The mental model

Before any setup, hold this picture - it's the whole guide in one line:

> **A `DbContext` is a change-tracking session, a `DbSet<T>` is a table, and LINQ becomes SQL.**

Three pieces:

- **The `DbContext` = your session with the database.** You create one, do some work through it, and dispose
  it. While it's alive it **tracks the changes** you make to the objects it hands you, and pushes them all
  to the database when you call `SaveChanges`.
- **A `DbSet<T>` = a table.** Your context exposes one `DbSet` property per table - `DbSet<Blog>` is the
  `Blogs` table. You add to it, and you query through it.
- **LINQ becomes SQL.** You write queries in C#'s built-in query language (LINQ), and EF Core translates
  them into real `SELECT ... WHERE ...` statements. (Querying is Phase 4 - for now, just know that's the
  flow.)

💡 EF Core is a **SQL generator**, not a cage. When the high-level API gets awkward - a gnarly report, a
bulk update - you drop straight to raw SQL with `FromSql(...)` or `ExecuteSql(...)`, against the same
connection. You never lose access to the database underneath.

## Installing EF Core

EF Core is the core library plus a **provider** for your specific database. We'll use SQLite - zero setup
(the database is just a file), so you can run everything here without standing up a server.

From inside a console project (`dotnet new console -o blog` if you're starting fresh), pull in the SQLite
provider:

```bash
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
```

*What just happened:* `dotnet add package` downloaded the SQLite provider and added it to your `.csproj`.
That one package brings EF Core's core along as a dependency, plus the adapter that teaches it to speak
SQLite specifically. Moving to PostgreSQL or SQL Server later just means swapping the provider
(`Npgsql.EntityFrameworkCore.PostgreSQL` or `Microsoft.EntityFrameworkCore.SqlServer`) - little else changes.

## Defining a DbContext and an entity

You write two kinds of class: a **`DbContext`** subclass that names your tables and points at the database,
and an **entity** class for each table - a plain class whose properties become columns:

```csharp
using Microsoft.EntityFrameworkCore;

public class BlogContext : DbContext
{
    public DbSet<Blog> Blogs => Set<Blog>();
    public DbSet<Post> Posts => Set<Post>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=blog.db");
}

public class Blog
{
    public int Id { get; set; }
    public string Url { get; set; } = "";
}
```

*What just happened:* `BlogContext` derives from `DbContext`, which is what makes it a database session. Its
two `DbSet<T>` properties declare the tables - `Blogs` and `Posts` - and `=> Set<Blog>()` is the standard
way to wire each one up. `OnConfiguring` is where the context learns *which* database to talk to:
`UseSqlite("Data Source=blog.db")` says "use the SQLite provider, pointed at a file called `blog.db`"
(created automatically if it doesn't exist). `Blog` is an **entity** - an ordinary C# class. Its `Id` and
`Url` properties will become columns; EF Core treats a property named `Id` as the primary key by
convention. (We'll define `Post` and turn these into real tables in [Phase 2](02-models-and-migrations.md).)

📝 In an ASP.NET Core app you usually *don't* write `OnConfiguring`. Instead you register the context with
dependency injection via `AddDbContext<BlogContext>(...)`, and the framework hands a fresh, correctly-scoped
context to each request - see [ASP.NET Core From Zero](/guides/aspnet-core-from-zero). Same mental model,
different wiring.

## Opening, saving, and disposing

Using the context is three moves - create it, change something, save:

```csharp
using var ctx = new BlogContext();

ctx.Blogs.Add(new Blog { Url = "https://example.com" });
ctx.SaveChanges();
```

Run it with `dotnet run`.

*What just happened:* `new BlogContext()` opened a session. `ctx.Blogs.Add(...)` didn't touch the database
yet - it told the context "start tracking this new `Blog`, I intend to insert it." Nothing is written until
`ctx.SaveChanges()`, which looks at everything the context is tracking, generates the SQL, and runs it in a
single batch (here, one `INSERT`). `using var` makes this safe: `DbContext` is meant to be **short-lived**,
and `using` disposes it - releasing the connection - the moment the block ends. Create one, do a unit of
work, let it go. (`SaveChanges` has an `async` twin, `SaveChangesAsync`, for web apps.)

## Turn on the SQL log

This is the fix for the real cost, and the single best habit to build while learning. Chain `LogTo` onto
your provider setup and EF Core prints every statement it runs:

```csharp
protected override void OnConfiguring(DbContextOptionsBuilder options)
    => options.UseSqlite("Data Source=blog.db")
              .LogTo(Console.WriteLine);
```

*What just happened:* the only change is `.LogTo(Console.WriteLine)`, which hands EF Core a place to send
its log lines - here, straight to the console. From now on, every query leaves a trail you can read. (In
development you can also add `.EnableSensitiveDataLogging()` to see actual parameter *values*, not just
`@p0` placeholders - handy while learning, but keep it out of production since it can print real data.)

Re-run the save with logging on, and the `INSERT` from a moment ago shows up looking roughly like this:

```sql
INSERT INTO "Blogs" ("Url")
VALUES (@p0);
SELECT "Id"
FROM "Blogs"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
```

*What just happened:* that's the literal SQL behind `Add` + `SaveChanges`. The first statement inserts the
row; the second reads back the database-generated `Id` so EF Core can fill it into your `Blog` object in
memory. Two C# lines, and here's exactly what they became. 💡 Keep this on the entire time you're learning
 - the instant a single call fires five queries, or runs a `SELECT` with no `WHERE`, you'll see it.

## The running example: a blog

Rather than disconnected snippets, this guide builds one small, recognizable schema - a **blog** - and grows
it phase by phase. You've already met two of its tables; here's the cast and how they relate:

```mermaid
flowchart LR
  Blog -- "has many" --> Post
  Post -- "tagged with many" --> Tag
  Tag -- "applied to many" --> Post
```

*What just happened:* the diagram lays out where we're headed. A **`Blog`** has many **`Post`s** - a
one-to-many relationship. A **`Post`** can carry many **`Tag`s** while each `Tag` labels many posts - a
many-to-many. Right now they're just boxes and arrows; in [Phase 2](02-models-and-migrations.md) we turn
these into real entity classes, use migrations to create the tables, then create rows, query with LINQ,
watch change tracking batch our edits, and wire up these relationships.

The win for this phase: you can connect, you've saved a row, the SQL log is on, and you hold the mental
model. That's the foundation everything else stands on.

## Recap

1. **EF Core is .NET's flagship ORM** - you describe tables as C# classes and it writes the SQL for CRUD,
   relationships, and migrations, so you skip the hand-written query boilerplate.
2. **The real cost is invisible SQL.** The cure is `LogTo`: turn it on while learning so you see the exact
   statement behind every call.
3. **The mental model:** a `DbContext` is a change-tracking session, a `DbSet<T>` is a table, and LINQ
   becomes SQL - and you can always drop to raw SQL with `FromSql`/`ExecuteSql`.
4. **Install a provider** (`Microsoft.EntityFrameworkCore.Sqlite` here), define a `DbContext` subclass with
   `DbSet` properties, and point it at the database in `OnConfiguring` with `UseSqlite("Data Source=...")`.
5. **`Add` then `SaveChanges`:** `Add` only starts tracking; nothing hits the database until `SaveChanges`
   batches and runs the SQL. Keep the context **short-lived** - `using var ctx = new BlogContext();`.
6. In **ASP.NET Core** you register the context with `AddDbContext` and let DI hand one per request, instead
   of writing `OnConfiguring`.

## Quick check

Three questions on the framing that has to stick before Phase 2:

```quiz
[
  {
    "q": "In EF Core's mental model, what is a `DbContext`?",
    "choices": [
      "A change-tracking session with the database that exposes DbSets and pushes changes on SaveChanges",
      "A single row fetched from a table",
      "The SQLite file on disk",
      "A class that maps directly to exactly one table"
    ],
    "answer": 0,
    "explain": "A `DbContext` is your session with the database: it holds `DbSet<T>` properties (the tables), tracks the changes you make, and writes them all when you call `SaveChanges`. A class that maps to one table is an entity; a `DbSet<T>` represents the table."
  },
  {
    "q": "After `ctx.Blogs.Add(blog);`, when is the row actually written to the database?",
    "choices": [
      "When you call `ctx.SaveChanges()` - `Add` only starts tracking it",
      "Immediately, inside the `Add` call",
      "When the `DbContext` is garbage collected",
      "Only after you manually open a transaction"
    ],
    "answer": 0,
    "explain": "`Add` just tells the context to start tracking the new entity as something to insert. Nothing reaches the database until `SaveChanges`, which generates and runs the SQL (here an `INSERT`) in one batch."
  },
  {
    "q": "Why is `.LogTo(Console.WriteLine)` such a good habit while learning EF Core?",
    "choices": [
      "It prints the exact SQL behind every call, so the ORM stops being a black box",
      "It makes queries run faster by caching them",
      "It is required or EF Core refuses to connect",
      "It automatically rewrites slow queries for you"
    ],
    "answer": 0,
    "explain": "The real cost of an ORM is that you stop seeing your SQL. `LogTo` prints every generated statement, so you immediately catch surprises - like one call firing several queries or a `SELECT` with no `WHERE`."
  }
]
```


---

# Entity Models & Migrations

The mental model for this phase: **a class is a table.** You write an ordinary C# class, EF Core looks at it, and figures out the table's columns, types, and primary key - mostly without you saying a word. When you later change that class, you don't reach for a SQL editor. You generate a **migration**: a small, versioned record of "the schema went from *this* to *that*," committed alongside your code and applied when you deploy.

> 📝 In [Phase 1](01-what-efcore-is.md) you built a `DbContext` with `DbSet<T>` properties. Each `DbSet<Blog> Blogs` is one table - but EF Core needs the actual `Blog` *class* to know what goes in it. That class is what we're writing now.

## A class is a table

EF Core works from **conventions** - sensible defaults it applies automatically so you write less configuration. The big ones:

- A property named `Id` (or `<TypeName>Id`, like `BlogId`) becomes the **primary key**.
- Every public property with a getter and setter becomes a **column**.
- The column's SQL type is inferred from the C# type (`int` → integer, `string` → text, `DateTime` → timestamp, and so on).
- The **table name** comes from the `DbSet` property name on your context - so `DbSet<Blog> Blogs` produces a table called `Blogs`.

Let's give our blog a `Post` entity:

```csharp
public class Post
{
    public int Id { get; set; }
    [Required, MaxLength(200)]
    public string Title { get; set; } = "";
    public string Content { get; set; } = "";
}
```

*What just happened:* EF Core reads this class and concludes: a table with `Id` as the primary key (auto-incrementing, since it's an `int` named `Id`), a `Title` text column, and a `Content` text column. `[Required, MaxLength(200)]` on `Title` is the one place we overrode a convention. The `= ""` initializers aren't an EF thing; they just keep C#'s nullable-reference warnings quiet.

And the matching `Blog`:

```csharp
public class Blog
{
    public int Id { get; set; }
    public string Url { get; set; } = "";
}
```

*What just happened:* same story - `Id` is the key, `Url` is a column. No annotations here, so EF Core uses pure conventions: `Url` becomes a text column with no length cap. We'll tighten that next, since "no length cap" is rarely what you want.

> 💡 Conventions do real work for you. You didn't declare a single column type, key, or constraint by hand - you described your data as C# types and EF Core inferred a reasonable schema. You only step in when a guess is wrong.

## Two ways to override: annotations vs the Fluent API

When the conventions aren't enough - you need a max length, a different column name, a column that *isn't* mapped at all - you have two tools.

**Data annotations** are attributes you put right on the property. They live with the model, which makes them easy to read:

| Annotation | What it does |
|---|---|
| `[Required]` | Column is `NOT NULL` |
| `[MaxLength(200)]` | Caps string/array length (e.g. `varchar(200)`) |
| `[Column("url")]` | Maps the property to a differently named column |
| `[Key]` | Marks the primary key explicitly (when convention can't guess) |
| `[NotMapped]` | Exclude this property - no column for it |

The **Fluent API** is the other option: you configure everything in code inside your `DbContext`, in an `OnModelCreating` override.

```csharp
public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs => Set<Blog>();
    public DbSet<Post> Posts => Set<Post>();

    protected override void OnModelCreating(ModelBuilder b)
    {
        b.Entity<Blog>()
            .Property(x => x.Url)
            .IsRequired()
            .HasMaxLength(200);
    }
}
```

*What just happened:* we told EF Core that `Blog.Url` is required and capped at 200 characters - the same rule `[Required, MaxLength(200)]` expresses, but written centrally instead of on the property. `ModelBuilder` is the configuration surface; `b.Entity<Blog>().Property(...)` drills down to one column and chains the rules onto it.

Why two systems? Annotations are concise and live next to the data - great for simple rules. The Fluent API is more powerful: it can express things annotations can't (composite keys, relationships, indexes, default values), and keeps entity classes free of EF-specific attributes. **If you ever configure the same thing both ways, the Fluent API wins.** A confusing "but I set `[MaxLength]`!" bug is almost always a Fluent API line quietly overriding it.

> ⚠️ Don't sprinkle both for the *same* property and hope for the best. Pick a default approach (many teams lean Fluent API for anything non-trivial) and reserve mixing for deliberate overrides you actually understand.

## Migrations: versioning your schema

You've got classes. Now you need an actual database with actual tables - and a way to *change* that schema later without losing data or hand-writing `ALTER TABLE` statements.

First, install the design-time pieces: the `Design` package powers the tooling, and the `dotnet-ef` global tool gives you the commands.

```bash
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-ef
```

Now create your first migration:

```bash
dotnet ef migrations add InitialCreate
```

*What just happened:* EF Core compared your current model (the `Blog` and `Post` classes plus any Fluent config) against the *last* migration - since there isn't one yet, the "diff" is "create everything." It wrote a new C# migration class into a `Migrations/` folder, with two methods: `Up()` (apply the change) and `Down()` (undo it). Nothing has touched your database yet - a migration is just a plan.

Here's a peek at what that generated class looks like:

```csharp
public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Blogs",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("Sqlite:Autoincrement", true),
                Url = table.Column<string>(maxLength: 200, nullable: false)
            },
            constraints: table => table.PrimaryKey("PK_Blogs", x => x.Id));
        // ... CreateTable for "Posts" follows ...
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(name: "Blogs");
        // ... DropTable for "Posts" ...
    }
}
```

*What just happened:* the migration describes your schema as code, not raw SQL - `Up()` creates the tables (notice `Url` came out as `maxLength: 200, nullable: false`, exactly the Fluent rule we set), and `Down()` drops them so the change is reversible. EF Core translates this to dialect-specific SQL at apply time, so the same migration can target SQLite, SQL Server, or Postgres.

Now apply it:

```bash
dotnet ef database update
```

*What just happened:* EF Core ran the `Up()` of every pending migration against your database - creating the `Blogs` and `Posts` tables for real. It also created a bookkeeping table (`__EFMigrationsHistory`) that records which migrations have applied, so next time it only runs the new ones. Run `database update` again right now and nothing happens.

If you want to *see* the SQL without touching the database, `dotnet ef migrations script` prints it - same instinct as watching the SQL EF Core generates from Phase 1.

## Migrations vs `EnsureCreated()` - don't mix them

A tempting shortcut you'll see in tutorials: `ctx.Database.EnsureCreated()`. It looks at your model and creates the schema in one shot, no migration files, no tooling.

```csharp
// Dev/prototyping only - NOT a migration.
ctx.Database.EnsureCreated();
```

*What just happened:* EF Core created the tables directly from your current model - fast and convenient for a throwaway prototype or test database. But notice what it *didn't* do: no migration created, nothing recorded in the history table, no `Down()`.

> ⚠️ `EnsureCreated()` and migrations are two different worlds that don't cooperate. `EnsureCreated()` creates the schema **once** with no concept of evolving it - there's no "add a column later." Worse, a database made by `EnsureCreated()` has no migrations history, so `database update` won't know where to start. **Pick one per database.** For anything real, that's migrations.

## A migration per change, committed and deployed

The workflow once you're rolling, worth internalizing:

1. Change a model (add a property, a new entity, a constraint).
2. `dotnet ef migrations add DescribeTheChange` - generates the diff.
3. Review the generated `Up()`/`Down()`. Did it do what you expected?
4. Commit the migration files **with the code change** - they're part of your source history.
5. On deploy, run `dotnet ef database update` (or apply the migration as part of your release).

Each model change earns its own migration - a readable, reviewable timeline of how your schema evolved, with the ability to roll forward or back deliberately.

> 💡 Treat migration files like code, because they are: reviewed in pull requests, tracked in version control, order matters. We'll cover the *production* side - applying migrations safely on a live database, handling concurrency - in [Phase 8](08-transactions-and-migrations.md). For now, the habit to build is: model change → migration → commit.

## Recap

- **A class is a table.** EF Core reads your plain C# entity classes and maps them by **convention**: `Id`/`<Type>Id` becomes the primary key, public properties become columns, types are inferred, and the table name comes from the `DbSet` name.
- Override conventions with **data annotations** (`[Required]`, `[MaxLength]`, `[Column]`, `[Key]`, `[NotMapped]`) on the property, or with the **Fluent API** in `OnModelCreating`. The Fluent API is more powerful and **wins** when both configure the same thing.
- **Migrations** version your schema. Install `Microsoft.EntityFrameworkCore.Design` and the `dotnet-ef` tool, then `dotnet ef migrations add <Name>` generates a reversible `Up()`/`Down()` class from the diff, and `dotnet ef database update` applies pending migrations.
- A migration is a **plan**, not an action - nothing hits the database until `database update`. EF Core tracks applied migrations in `__EFMigrationsHistory`.
- `EnsureCreated()` is a **dev-only** shortcut that creates schema once with no migration history - never mix it with migrations on the same database.
- Make **one migration per model change**, review it, commit it with the code, and apply it on deploy.

## Quick check

```quiz
[
  {
    "q": "You have a Blog class with an int property named Id and a public string Url. With pure EF Core conventions, what schema does EF infer?",
    "choices": ["No table at all until you add [Table] and [Key] attributes", "A Blogs table with Id as the primary key and Url as a column", "A Blog table with no primary key", "A table where only Url is mapped, because Id is reserved"],
    "answer": 1,
    "explain": "Conventions: Id becomes the primary key, public properties become columns, and the table name comes from the DbSet name (Blogs)."
  },
  {
    "q": "A property has [MaxLength(50)] in an annotation, but OnModelCreating also calls HasMaxLength(200) for it. What max length does the column get?",
    "choices": ["50, annotations take priority", "200, the Fluent API wins", "It throws an error for conflicting config", "The smaller of the two, 50"],
    "answer": 1,
    "explain": "When both configure the same thing, the Fluent API wins. That precedence is a common source of 'but I set the annotation!' confusion."
  },
  {
    "q": "What does `dotnet ef migrations add InitialCreate` do?",
    "choices": ["Immediately creates the tables in the database", "Generates a versioned migration class with Up/Down from the model diff, without touching the database", "Deletes and recreates the database from scratch", "Runs EnsureCreated() for you"],
    "answer": 1,
    "explain": "migrations add only generates the migration (a plan). Nothing reaches the database until you run dotnet ef database update."
  }
]
```


---

# Create & Read

You've got a `DbContext`, a `Blog` class, and a real database with tables behind it (Phase 2). Now comes
the part you actually came for: putting rows in and getting them back out. This is the C and the R of
CRUD, and the half of EF Core you'll use every single day.

## The mental model: a session you stage into, then flush

Here's the one picture to carry through this whole phase:

**`Add` doesn't touch the database. It stages an insert in your `DbContext`'s memory. `SaveChanges` is
the moment EF turns everything you've staged into SQL and sends it - in one transaction. The finders
(`Find`, `First`, `Single`, `ToList`) go the other direction: they run a `SELECT` and pour the rows
back into C# objects.**

> 💡 Think of the `DbContext` as a notepad, not a phone line. You scribble intentions on it ("insert
> this blog"), and nothing reaches the database until you call `SaveChanges`. That's why a single
> `SaveChanges` can flush ten inserts at once - they were all just sitting on the notepad.

We'll lean on the SQL logger from Phase 1 throughout, because the fastest way to trust an ORM is to read
the SQL it writes.

## Creating rows: `Add` + `SaveChanges`

The pattern is two lines: make an object, hand it to the `DbSet`, then flush.

```csharp
var blog = new Blog { Url = "https://battle-hardened.dev" };

ctx.Blogs.Add(blog);   // staged, not saved - no SQL yet
ctx.SaveChanges();     // NOW the INSERT runs

Console.WriteLine(blog.Id);   // 1  ← EF filled this in for you
```

With the logger on, `SaveChanges` prints something like:

```sql
INSERT INTO "Blogs" ("Url")
VALUES (@p0);
SELECT "Id"
FROM "Blogs"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
```

*What just happened:* `Add` only marked the object as "to be inserted" - the first line did nothing to
the database. `SaveChanges` generated the `INSERT`, ran it inside a transaction, then read back the
auto-generated primary key with that second `SELECT`. EF Core writes that key **into your object** - 
`blog.Id` was `0` before the save and `1` after, so the object you're holding matches the row that now
exists.

> ⚠️ A brand-new object's `Id` is `0` (the default for `int`) until you call `SaveChanges`. If you try
> to use `blog.Id` before saving, you'll get `0`, not the real key. The Id exists only after the row
> exists.

### Inserting many at once: `AddRange`

When you have a batch, stage them all and save once:

```csharp
var posts = new[]
{
    new Post { Title = "Why I read the SQL", BlogId = blog.Id },
    new Post { Title = "The N+1 trap",       BlogId = blog.Id },
};

ctx.Blogs.Add(blog);
ctx.Posts.AddRange(posts);
ctx.SaveChanges();   // one trip, one transaction, all rows
```

*What just happened:* `AddRange` staged both posts alongside the blog. The single `SaveChanges` flushed
everything together in **one transaction** - if any insert failed, they'd all roll back, nothing left
half-written. That's the unit-of-work behavior we'll dig into in
[Phase 5](05-change-tracking.md); for now, hold "stage as much as you want, save once."

### Async, for web apps

In a web request you don't want to block the thread while the database works. Use the async variant:

```csharp
ctx.Blogs.Add(blog);
await ctx.SaveChangesAsync();
```

*What just happened:* same `INSERT`, same write-back of `blog.Id`, but the thread is freed to handle
other requests while the database works. In an ASP.NET Core app, this is the default to reach for. `Add`
itself stays synchronous (it's just touching the in-memory notepad) - only the flush has an async form.

## Reading rows: the four finders

Reading is where EF gives you a few tools that look similar but behave differently. Pick the one whose
**failure mode** matches what you mean.

### `Find(id)` - by primary key, tracker-first

```csharp
var blog = ctx.Blogs.Find(1);
```

*What just happened:* `Find` looks up a row by its **primary key**, checking the change tracker (the
notepad) *first*. If you already loaded blog #1 earlier in this `DbContext`, `Find` hands you the same
object with **no database trip at all**. Only if it's not already in memory does EF run:

```sql
SELECT "Id", "Url" FROM "Blogs" WHERE "Id" = @p
```

If no row has that key, `Find` returns `null`. Use it when you have an Id in hand (a route parameter
like `/blogs/1`) and want the cheapest possible lookup.

### `First` / `FirstOrDefault` - first match of a condition

```csharp
var blog  = ctx.Blogs.First(b => b.Url == "https://battle-hardened.dev");
var maybe = ctx.Blogs.FirstOrDefault(b => b.Url == "https://nope.dev");
```

*What just happened:* these take a predicate (any condition, not just the key) and return the **first**
match. EF translates it to:

```sql
SELECT "Id", "Url" FROM "Blogs" WHERE "Url" = @p LIMIT 1
```

The difference is what happens when nothing matches: `First` **throws** an
`InvalidOperationException`; `FirstOrDefault` returns `null`. The first line above succeeds; the second
returns `null` because no blog has that URL.

### `Single` / `SingleOrDefault` - exactly one match

```csharp
var blog = ctx.Blogs.Single(b => b.Url == "https://battle-hardened.dev");
```

*What just happened:* `Single` says "I expect exactly one row to match." It throws if **none** match
*and* throws if **more than one** matches - doubling as a sanity check on uniqueness. Behind the scenes EF
fetches up to two rows (`LIMIT 2`) to tell whether there was more than one. `SingleOrDefault` is the same
but returns `null` when none match (still throws on two or more). Reach for `Single` when a duplicate
would mean your data is broken and you want to find out loudly.

### `ToList` / `ToListAsync` - all the rows

```csharp
var all      = ctx.Blogs.ToList();
var dotnet   = ctx.Posts.Where(p => p.Title.Contains(".NET")).ToList();
var allAsync = await ctx.Blogs.ToListAsync();
```

*What just happened:* `ToList` runs the query and materializes **every** matching row into a `List<T>`.
On its own it's `SELECT * FROM Blogs`; with a `Where`, EF folds the condition into the SQL so only
matching rows come back - the database filters, not your C#. We'll go deep on `Where`, `OrderBy`,
`Select` in [Phase 4](04-querying-with-linq.md). The async `ToListAsync` is what you want in web apps,
for the same thread-freeing reason as `SaveChangesAsync`.

## `First` vs `FirstOrDefault`: the choice that bites people

The single most common stumble in EF Core reads.

> ⚠️ `First` and `Single` **throw** when nothing matches. `FirstOrDefault` and `SingleOrDefault`
> return **`null`**. They are not interchangeable - pick based on whether "not found" is an *error* or
> an *expected outcome*.

The clear way to choose: ask "if this returns nothing, is my program broken, or is that just a normal
'not found'?"

```csharp
// A user requested /blogs/999 - "not found" is normal, return a 404.
var blog = ctx.Blogs.FirstOrDefault(b => b.Id == id);
if (blog is null)
    return NotFound();

// We just inserted this and MUST be able to read it back - missing = bug, throw loud.
var settings = ctx.Blogs.First(b => b.Url == knownSeedUrl);
```

*What just happened:* the first case maps a missing row to an HTTP 404 - a user typing a bad Id isn't a
crash, so we check for `null` and respond gracefully. The second uses `First` because absence would mean
something is genuinely wrong; throwing surfaces the bug instead of letting a `null` slip downstream and
blow up later with a confusing `NullReferenceException`. Choosing the throwing or `*OrDefault` variant
*is* deciding how "missing" should be handled.

## Two things that come for free

> 📝 **Your queries are injection-safe by default.** Notice the `@p0` / `@p` in every generated
> statement above - EF Core turns the values in your LINQ predicates into **SQL parameters**, never
> string-concatenated into the query text. So `b.Url == userInput` is safe even when `userInput` is
> `"'; DROP TABLE Blogs; --"`; it goes in as a parameter value, not as SQL - one of the real reasons to
> let the ORM write the SQL.

> 📝 **Prefer the async finders in web apps.** `ToListAsync`, `FirstOrDefaultAsync`,
> `SingleOrDefaultAsync`, `FindAsync`, and `SaveChangesAsync` all exist. In a server handling many
> concurrent requests, blocking a thread on a synchronous database call wastes a thread that could serve
> someone else. It matters far less in a one-off script - but in ASP.NET Core, async is the house style.

## Recap

- **`Add` stages, `SaveChanges` flushes.** Nothing reaches the database until `SaveChanges` (or
  `SaveChangesAsync`) runs, and it runs everything staged in one transaction.
- **EF writes the generated `Id` back into your object** after the insert - it's `0` before saving and
  the real key after.
- **`Find(id)`** looks up by primary key and checks the in-memory tracker first (no DB hit if already
  loaded); **`ToList`/`ToListAsync`** pull every matching row.
- **`First`/`Single` throw on no match; `FirstOrDefault`/`SingleOrDefault` return `null`.** Pick by
  whether "missing" is an error or a normal outcome (e.g. a 404).
- **`Single` also throws on more than one match**, making it a built-in uniqueness check.
- **Values become SQL parameters automatically**, so reads are injection-safe; prefer the async finders
  in web apps.

## Quick check

```quiz
[
  {
    "q": "You call ctx.Blogs.Add(blog) but never call SaveChanges. What's in the database?",
    "choices": ["The new blog row", "Nothing - Add only stages the insert in memory", "A row with a null Url", "An empty row reserving the Id"],
    "answer": 1,
    "explain": "Add only marks the object for insertion on the DbContext. No SQL runs until SaveChanges (or SaveChangesAsync) flushes it."
  },
  {
    "q": "A user requests /blogs/999 and no blog has that Id. You want to return a 404, not crash. Which finder fits?",
    "choices": ["First, then catch the exception", "Single", "FirstOrDefault and check for null", "Find, then call SaveChanges"],
    "answer": 2,
    "explain": "FirstOrDefault returns null when nothing matches, so you can check for null and respond with a 404. First would throw, treating a normal 'not found' as a crash."
  },
  {
    "q": "After ctx.Blogs.Add(blog); ctx.SaveChanges();, what is blog.Id?",
    "choices": ["Still 0 - you must reload the object", "The auto-generated key EF wrote back into the object", "null until you call Find", "A random GUID"],
    "answer": 1,
    "explain": "SaveChanges runs the INSERT and reads the database-generated primary key back into your object, so blog.Id holds the real key right after the save."
  }
]
```


---

# Querying with LINQ

The mental model to lock in: **a LINQ query is not code that runs - it's a description that EF Core turns into SQL.** When you write `ctx.Posts.Where(...)`, nothing touches the database. You're building an *expression tree*, a blueprint that says "I want posts, filtered like this, sorted like that." EF holds that blueprint and translates it into a single SQL statement only when you actually ask for the results.

Once you internalize "LINQ describes, SQL executes, and execution happens *later* than you'd think," EF Core querying stops surprising you. Every weird behavior in this phase - why your query didn't run, why it ran twice, why it suddenly broke - traces back to that one idea.

We'll keep using the running **blog** schema: a `Blog` has many `Post`s, each `Post` has a `BlogId`, `Title`, and `Id`.

## A query is a blueprint, not a result

Look at this chain and the SQL it becomes:

```csharp
var recent = ctx.Posts
    .Where(p => p.BlogId == blogId)
    .OrderByDescending(p => p.Id)
    .Take(10)
    .ToList();
```

EF translates that into one statement:

```sql
SELECT "p"."Id", "p"."BlogId", "p"."Title"
FROM "Posts" AS "p"
WHERE "p"."BlogId" = @blogId
ORDER BY "p"."Id" DESC
LIMIT 10
```

*What just happened:* you wrote four C# method calls, and EF folded all of them into a single round-trip to the database. `Where` became `WHERE`, `OrderByDescending` became `ORDER BY ... DESC`, `Take(10)` became `LIMIT 10`. The database filters, sorts, and limits - your app gets back only the 10 rows it asked for, not the whole table.

> 📝 There are two ways to write LINQ. The **method syntax** above (`.Where(...).OrderBy(...)`) is what you'll see most. There's also **query syntax**, reading more like SQL: `from p in ctx.Posts where p.BlogId == blogId select p`. They compile to the same thing - pick whichever you find clearer. This guide uses method syntax throughout.

## The operators that map to SQL

Most LINQ operators you'd reach for have a direct SQL translation. The common ones:

```csharp
// Filtering and sorting
var titles = ctx.Posts.Where(p => p.Title.Contains("EF")).ToList();

// Paging: skip the first 20, take the next 10 (page 3, 10 per page)
var page = ctx.Posts.OrderBy(p => p.Id).Skip(20).Take(10).ToList();

// Aggregates - these run in SQL and return a single value
int total = ctx.Posts.Count(p => p.BlogId == blogId);
bool anyDrafts = ctx.Posts.Any(p => p.Title == "");

// Grouping: count posts per blog
var perBlog = ctx.Posts
    .GroupBy(p => p.BlogId)
    .Select(g => new { BlogId = g.Key, Count = g.Count() })
    .ToList();
```

*What just happened:* each operator pushed work down into the database. `Contains` became a SQL `LIKE`; an `In`-style filter (`Where(p => ids.Contains(p.Id))`) becomes a SQL `IN (...)`. `Skip`/`Take` became `OFFSET`/`LIMIT` - that's your paging. `Count` and `Any` came back as `COUNT(*)` and an `EXISTS` check, returning one number or one boolean instead of shipping rows across the wire. `GroupBy` became `GROUP BY`. None of these loaded the table into memory.

## ⚠️ Deferred execution: nothing runs until you enumerate

This is the part that bites everyone at least once. A LINQ query over `IQueryable<T>` is *lazy*. Building it does nothing. The SQL fires only when you **enumerate** the results - a specific list of things count as enumerating.

```csharp
// No database call yet. This is just a blueprint.
var query = ctx.Posts.Where(p => p.BlogId == blogId);

// Still nothing - you can keep composing.
query = query.OrderByDescending(p => p.Id);

// NOW the SQL runs - ToList() enumerates.
var results = query.ToList();
```

*What just happened:* the first two lines built and refined an expression tree without touching the database. You could pass `query` around, add more `.Where(...)` calls conditionally, and EF would fold them all into one statement. Only `ToList()` triggered the actual SQL. The triggers that force execution: `ToList()`/`ToListAsync()`, `First()`/`FirstOrDefault()`, `Single()`, `Count()`, `Any()`, `Sum()`, and a plain `foreach`. Until one of those, you're composing - not querying.

⚠️ The flip side of laziness: if you enumerate the *same* query twice (two `foreach` loops, or `.Count()` then `.ToList()`), you hit the database **twice**. When you need the results more than once, call `ToList()` once and reuse the list.

> 💡 In real apps, prefer the async versions - `ToListAsync()`, `FirstOrDefaultAsync()` - so the thread isn't blocked waiting on the database. You'll need `using Microsoft.EntityFrameworkCore;` for those extension methods.

## Select: project to exactly the columns you need

By default, querying `ctx.Posts` selects every column and materializes full `Post` entities. Often you don't need the whole row - just an id and a title for a list view. That's what **projection** with `Select` is for.

```csharp
public record PostDto(int Id, string Title);

var list = await ctx.Posts
    .Where(p => p.BlogId == blogId)
    .Select(p => new PostDto(p.Id, p.Title))
    .ToListAsync();
```

The generated SQL is narrower:

```sql
SELECT "p"."Id", "p"."Title"
FROM "Posts" AS "p"
WHERE "p"."BlogId" = @blogId
```

*What just happened:* instead of `SELECT Id, BlogId, Title`, EF generated `SELECT Id, Title` - only the columns your `PostDto` actually uses. Less data crosses the wire, and EF skips building full entity objects. For a posts table with a big `Content` or `Body` column you don't need in a list, this is a real, measurable win.

> 💡 For read-only API endpoints (your typical `GET /posts`), project straight to a DTO. You get a leaner query *and* avoid leaking your internal entity shape into your API response - two birds, one `Select`.

## AsNoTracking and keeping queries translatable

When EF hands you full entities, it **tracks** them - keeping a snapshot of each one so it can detect edits later when you call `SaveChanges` (Phase 5's topic). For a read-only query, that bookkeeping is pure overhead: you're never going to save these objects back.

```csharp
var posts = await ctx.Posts
    .AsNoTracking()
    .Where(p => p.BlogId == blogId)
    .ToListAsync();
```

*What just happened:* `AsNoTracking()` told EF "don't bother watching these for changes." The query runs faster and uses less memory because EF skips creating change-tracking snapshots. Trade-off: if you edit one of these objects and call `SaveChanges`, nothing happens - EF isn't watching them. That's exactly what you want for GET endpoints and any read you won't modify. (Projections with `Select` to a DTO are effectively untracked already.)

⚠️ **Client vs server evaluation** is the sharp edge here. Almost everything in your LINQ runs as SQL on the database (the *server*). But if your predicate calls a C# method EF can't translate, it can't push that into SQL.

```csharp
// EF can translate this - it knows StartsWith → LIKE 'EF%'
var ok = ctx.Posts.Where(p => p.Title.StartsWith("EF")).ToList();

// EF CANNOT translate a custom C# method - this throws at runtime
var bad = ctx.Posts.Where(p => MyCustomCheck(p.Title)).ToList();
```

*What just happened:* the first query translated cleanly - `StartsWith` maps to a SQL `LIKE`. The second referenced `MyCustomCheck`, a method that exists only in C#, with no SQL equivalent. Modern EF Core won't silently pull the whole table into memory to run it (older ORMs did, causing brutal performance surprises) - instead it **throws**, telling you the expression couldn't be translated. The fix: keep `Where` predicates built from things EF understands (entity properties, comparisons, `StartsWith`/`Contains`/`==`), or pull the data first and do the C# logic afterward on the in-memory list, knowing you've now loaded more rows.

The habit that saves you: **watch the SQL EF generates.** If a query is slow or behaving oddly, the logged SQL tells you whether the work is happening in the database or accidentally in your app. For a deep dive on diagnosing slow queries, see [Why Is My Query Slow?](/guides/why-is-my-query-slow).

## Recap

- A LINQ query is a **description**, not a result. EF builds an expression tree and translates it to one SQL statement.
- Execution is **deferred** - the SQL fires only when you enumerate: `ToList()`/`ToListAsync()`, `First()`, `Count()`, `Any()`, or `foreach`. Enumerate twice, hit the database twice.
- `Where`, `OrderBy`, `Skip`/`Take`, `Count`, `Any`, `Sum`, `GroupBy`, and `Contains` all map to SQL - the database does the work and returns just the answer.
- `Select` **projects** to a DTO, generating a narrower SELECT - fewer columns, less data, leaner read endpoints.
- `AsNoTracking()` skips change-tracking for read-only queries: faster and lighter.
- ⚠️ Keep predicates **translatable** - a C# method EF can't turn into SQL throws. Watch the generated SQL to stay in command.

## Quick check

```quiz
[
  {
    "q": "You write `var q = ctx.Posts.Where(p => p.BlogId == 1);` and stop there. What has hit the database?",
    "choices": ["The full filtered result set", "A COUNT of matching rows", "Nothing - execution is deferred until you enumerate", "An empty query that errors"],
    "answer": 2,
    "explain": "Building a LINQ query only creates an expression tree. No SQL runs until you enumerate with ToList(), First(), Count(), foreach, etc."
  },
  {
    "q": "For a read-only GET endpoint that returns posts you won't modify, which is the best fit?",
    "choices": ["ctx.Posts.AsNoTracking().Where(...)", "ctx.Posts.Where(...) with full tracking", "Load every column and filter in C#", "ctx.Posts.Find(id) in a loop"],
    "answer": 0,
    "explain": "AsNoTracking() skips the change-tracker bookkeeping you don't need for read-only data - faster and less memory."
  },
  {
    "q": "What does adding `.Select(p => new PostDto(p.Id, p.Title))` change about the generated SQL?",
    "choices": ["It adds a JOIN to every related table", "It narrows the SELECT to only the Id and Title columns", "It forces the query to run in memory", "It disables deferred execution"],
    "answer": 1,
    "explain": "Projection tells EF to select only the columns the DTO uses, producing a narrower SELECT that ships less data."
  }
]
```


---

# Change Tracking & SaveChanges

The thing that trips up almost everyone the first time: there is no `Update` method you call to save an edit. You load a `Post`, change its `Title`, call `SaveChanges`, and the `UPDATE` statement just... appears. That can feel like magic, and magic you don't understand is magic that will bite you. So before any code, the mental model.

> 💡 **The mental model.** A `DbContext` is a **unit of work** with a built-in **change tracker**. The moment a query hands you an entity, the context starts watching it: it stashes a private **snapshot** of every property value as they were when loaded. When you call `SaveChanges`, EF compares the current values of each tracked entity against its snapshot, works out the minimal set of inserts, updates, and deletes needed, and pushes them all to the database in **one transaction**. You don't tell EF *what* changed - it figures that out by diffing.

Hold that picture - **load, mutate, diff, save** - and everything in this phase follows from it.

## Update by mutation

Because the context already tracks the entity, updating a row is three steps: load it, change a property, save. No "update" call.

```csharp
using var ctx = new BlogContext();

var post = ctx.Posts.First(p => p.Id == 42);
post.Title = "Rewritten and better";
ctx.SaveChanges();
```

*What just happened:* the `First` query loaded the post **and** registered it with the change tracker, snapshot included. Setting `post.Title` only changed the in-memory object - nothing hit the database yet. `SaveChanges` diffed the entity against its snapshot, saw exactly one column differed, and emitted SQL touching only that column:

```sql
UPDATE Posts SET Title = 'Rewritten and better' WHERE Id = 42;
```

> 📝 Notice it does **not** rewrite every column - only `Title`. EF tracks changes per-property, so an `UPDATE` carries just the columns that actually moved. That keeps writes small and avoids stomping on columns another process may have touched.

Deleting is the same shape, except you tell the context to mark the entity for removal:

```csharp
var post = ctx.Posts.First(p => p.Id == 42);
ctx.Posts.Remove(post);
ctx.SaveChanges();
```

*What just happened:* `Remove` doesn't delete anything immediately - it flips the tracked entity's state to `Deleted`. `SaveChanges` runs `DELETE FROM Posts WHERE Id = 42;`. Until you save, the row's still there.

## Entity states: what the tracker is really storing

Every entity the context knows about sits in exactly one of five states. This is the vocabulary the change tracker thinks in:

| State | Meaning | What `SaveChanges` does |
|-------|---------|--------------------------|
| `Added` | New, not yet in the DB (you called `Add`) | `INSERT` |
| `Unchanged` | Loaded and untouched since | nothing |
| `Modified` | A tracked property changed | `UPDATE` (changed columns) |
| `Deleted` | Marked for removal (you called `Remove`) | `DELETE` |
| `Detached` | The context isn't tracking it at all | nothing - it's invisible |

You can read or set the state yourself through `ctx.Entry(...)`:

```csharp
var post = ctx.Posts.First(p => p.Id == 42);
Console.WriteLine(ctx.Entry(post).State);   // Unchanged

post.Title = "Edited";
Console.WriteLine(ctx.Entry(post).State);   // Modified
```

*What just happened:* fresh from the query, the post is `Unchanged`. The instant you mutate a tracked property, the change tracker notices (detected when you inspect state or call `SaveChanges`) and moves the entity to `Modified`. You never set this by hand in the normal flow - the diff drives it. `ctx.Entry(post).State` is your window into what the tracker believes about any entity.

> 💡 Want to see the whole picture? `ctx.ChangeTracker.Entries()` returns every tracked entity with its state - a great thing to dump when an update mysteriously does nothing (foreshadowing).

## SaveChanges: one batch, one transaction

`SaveChanges` isn't a per-entity operation. It collects **all** pending changes across every tracked entity, then sends them together, wrapped in a single database transaction.

```csharp
ctx.Posts.Add(new Post { Title = "Brand new" });        // Added
ctx.Posts.First(p => p.Id == 7).Title = "Touched up";   // Modified
ctx.Posts.Remove(ctx.Posts.First(p => p.Id == 9));      // Deleted

int rows = ctx.SaveChanges();
Console.WriteLine($"{rows} rows affected");              // 3 rows affected
```

*What just happened:* three different operations - an insert, an update, and a delete - accumulated in the tracker. The single `SaveChanges` call issued all three inside one transaction: if any statement fails, the whole batch rolls back and your database is left untouched. The return value is the **number of rows affected**, here `3`.

## ⚠️ The detached-entity trap - the #1 web-app surprise

Everything above assumes the entity was **loaded by this context**, so it's tracked. In a web app, that assumption quietly breaks - the single most common EF Core gotcha you'll hit.

When a controller receives an object deserialized from a JSON request body, that object was created by the model binder - **not** loaded by your `DbContext`. It is `Detached`. The context has never seen it, has no snapshot for it, isn't watching it. So this does **nothing**:

```csharp
// post came from the HTTP request body - it is DETACHED
public IActionResult UpdatePost(Post post)
{
    post.Title = "Changed in the controller";
    ctx.SaveChanges();   // ⚠️ no UPDATE runs - the context isn't tracking `post`
    return Ok();
}
```

*What just happened:* nothing, and that's the trap. `post` is detached, so there's no snapshot to diff and no tracked state to mark `Modified`. `SaveChanges` looks at its (empty) change tracker, finds nothing to do, and returns `0`. No error, no exception - just a silent no-op that looks like a database bug.

Three straightforward ways to fix it.

**1. `ctx.Update(...)` - mark the whole entity Modified.** Simplest, but it sets *every* column to `Modified`, so the `UPDATE` rewrites all columns regardless of what actually changed.

```csharp
ctx.Update(post);    // attaches as Modified (all properties)
ctx.SaveChanges();   // UPDATE Posts SET Title=..., Body=..., ... WHERE Id = post.Id
```

*What just happened:* `Update` attaches the detached object to the context and stamps it `Modified` wholesale, so `SaveChanges` has something to write. The cost: you overwrite every column from whatever's on `post` - including any the client didn't intend to change.

**2. `Attach` + set state explicitly.** When you want finer control over which properties are dirty.

```csharp
ctx.Attach(post);
ctx.Entry(post).Property(p => p.Title).IsModified = true;   // only Title
ctx.SaveChanges();
```

*What just happened:* `Attach` brings `post` in as `Unchanged`. Marking just `Title` as modified makes the resulting `UPDATE` touch only that one column - the same minimal-write behavior as the tracked path.

**3. Load-then-copy.** The safest pattern, and what most production code does: load the real tracked entity, copy the allowed fields onto it, then save.

```csharp
var existing = ctx.Posts.First(p => p.Id == post.Id);   // tracked, with snapshot
existing.Title = post.Title;                            // copy only what you allow
ctx.SaveChanges();                                      // normal diff-and-UPDATE
```

*What just happened:* you're back on the happy path. `existing` is tracked, so the change tracker diffs it normally and writes only the columns you copied - and it guards against a client smuggling in fields you never meant to expose, since you control which properties get copied.

> ⚠️ If an update "works locally but does nothing in the web app," your entity is almost certainly detached. Check `ctx.Entry(entity).State` - if it says `Detached`, that's your answer.

## Recap

- A `DbContext` is a **unit of work** with a **change tracker**: it snapshots every entity it loads and diffs against that snapshot on save.
- **You don't call an update method.** Load → mutate a property → `SaveChanges`, and EF emits an `UPDATE` for only the changed columns. Delete with `Remove`.
- Every tracked entity has a **state** - `Added`, `Unchanged`, `Modified`, `Deleted`, or `Detached` - which you can inspect or set via `ctx.Entry(e).State`.
- `SaveChanges` **batches** all pending inserts/updates/deletes into one transaction and returns the number of affected rows.
- **Detached entities** (objects from a web request, not loaded by this context) aren't tracked - mutating them does nothing on save. Fix with `ctx.Update(...)`, `Attach` + per-property state, or the safer load-then-copy.

## Quick check

```quiz
[
  {
    "q": "You load a Post, set post.Title, and call SaveChanges. What SQL does EF Core generate?",
    "choices": ["Nothing - you forgot to call an Update method", "An UPDATE touching only the Title column", "An UPDATE rewriting every column on the row", "An INSERT for a new Post"],
    "answer": 1,
    "explain": "The entity is tracked, so SaveChanges diffs it against its snapshot, sees only Title changed, and emits an UPDATE for just that column."
  },
  {
    "q": "A controller receives a Post deserialized from the request body, sets a property, and calls SaveChanges. Nothing changes in the database. Why?",
    "choices": ["The transaction rolled back", "The object is detached, so the context isn't tracking it and has nothing to save", "SaveChanges only handles inserts", "You must call Remove first"],
    "answer": 1,
    "explain": "An object from the request body was never loaded by this context, so it's Detached. With no snapshot and no tracked state, SaveChanges finds nothing to do - a silent no-op."
  },
  {
    "q": "What does SaveChanges return?",
    "choices": ["The saved entity", "true if any change was written", "The number of rows affected", "The new primary key"],
    "answer": 2,
    "explain": "SaveChanges batches all pending changes into one transaction and returns the count of rows affected."
  }
]
```


---

# Relationships

The mental model for this phase: **a relationship is a foreign key plus navigation properties.** That's it. The database side is the same boring thing it's always been - a column in one table that points at the primary key of another. EF Core's contribution is the *navigation property*: a C# reference (or list) that lets you walk from one object to its related objects without writing the join yourself. EF reads the **shapes** of your classes - a `List<Post>` here, a `Blog` reference there, a `BlogId` int - and infers the foreign key and the relationship from them.

If the underlying concepts feel shaky - what a foreign key *is*, why a join table exists for many-to-many - read [Relationships & Keys](/guides/relationships-and-keys) first. This phase assumes you know the database side and focuses on how EF Core projects it onto C# classes.

> 📝 We've been building a **blog** schema: `Blog`, `Post`, and now we'll add `Tag`. The relationships are the natural ones - a blog has many posts, and posts and tags belong to each other in a many-to-many. By the end you'll be able to read a pair of entity classes and predict exactly what foreign key EF will create.

## One-to-many: the FK convention

The bread-and-butter relationship. One blog, many posts. You express it with **two navigation properties and one foreign key**, and EF Core wires the rest by convention.

```csharp
public class Blog
{
    public int Id { get; set; }
    public string Url { get; set; } = "";
    public List<Post> Posts { get; set; } = new();   // one-to-many: a blog has many posts
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public int BlogId { get; set; }                   // foreign key (convention: <Nav>Id)
    public Blog Blog { get; set; } = null!;           // inverse navigation
}
```

*What just happened:* EF Core saw a collection navigation (`Blog.Posts`) and a matching reference navigation on the other side (`Post.Blog`), and concluded these are *the same relationship viewed from both ends*. Then it spotted `Post.BlogId` - an `int` named `<NavigationName>Id` - and recognized it as the foreign key by convention. The `= null!` on `Post.Blog` tells the C# compiler "trust me, this won't be null at runtime" so the nullable-reference warning goes away (EF populates it when you load the relationship).

When you run `dotnet ef migrations add AddPostBlogRelationship`, the generated migration creates the `BlogId` column **and an index on it** - relational databases index foreign keys because you almost always filter and join on them.

```sql
-- What the migration produces (SQLite dialect)
ALTER TABLE "Posts" ADD "BlogId" INTEGER NOT NULL DEFAULT 0;
CREATE INDEX "IX_Posts_BlogId" ON "Posts" ("BlogId");
-- plus a FOREIGN KEY constraint linking Posts.BlogId -> Blogs.Id
```

*What just happened:* the migration added the FK column, created the index EF generates automatically for it, and declared the foreign-key constraint so the database itself enforces that every `Post.BlogId` points at a real `Blog`. Two navigation properties and one `int` became a proper, indexed, constrained relationship.

> 💡 The convention `<NavigationName>Id` is why `BlogId` works without configuration. Named `OwnerId` instead, EF wouldn't recognize it as the FK for the `Blog` navigation - you'd point EF at it with the Fluent API (coming up). Match the convention and you write zero config.

## One-to-one and many-to-many

**One-to-one** is the same idea with a *single reference* on each side instead of a collection. Think a `Blog` and its `BlogHeader`:

```csharp
public class Blog
{
    public int Id { get; set; }
    public BlogHeader Header { get; set; } = null!;   // reference, not a list
}

public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId { get; set; }                   // FK lives on the dependent side
    public Blog Blog { get; set; } = null!;
}
```

*What just happened:* because both sides hold a single reference (no `List<>`), EF infers one-to-one. The foreign key goes on the **dependent** side - the entity that can't exist without the other (`BlogHeader` needs a `Blog`). EF often can't guess which side is dependent here, so one-to-one is the relationship most likely to need a Fluent API hint.

**Many-to-many** is where EF Core 5+ earns its keep. A post has many tags; a tag belongs to many posts. Put a collection on *each* side - these are called **skip navigations** - and EF creates the join table for you:

```csharp
public class Post
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public List<Tag> Tags { get; set; } = new();
}

public class Tag
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public List<Post> Posts { get; set; } = new();
}
// EF creates a PostTag join table; add an explicit join entity only if it needs extra columns.
```

*What just happened:* EF saw a collection on both ends with no foreign key on either entity, and recognized a many-to-many. It silently created a hidden join table (`PostTag`) with two FK columns - `PostsId` and `TagsId` - to record which posts wear which tags. You never declared that table; it's invisible in your C# model, and you navigate straight from `post.Tags` to `tag.Posts` as if the join didn't exist - the "skip" in skip navigation.

> ⚠️ The auto join table works only when the join holds *nothing but the two foreign keys*. The moment you need an extra column on the relationship itself - say, `AddedDate` recording when a tag was applied - define an **explicit join entity** (a `PostTag` class with `PostId`, `TagId`, and `AddedDate`) and map two one-to-many relationships through it. Reach for that only when the relationship genuinely carries data of its own.

## The Fluent API: taking control

Conventions handle the common cases. When they can't guess - a non-conventional FK name, a one-to-one's dependent side, a specific delete behavior - you configure the relationship explicitly in `OnModelCreating`. The vocabulary reads like a sentence:

```csharp
protected override void OnModelCreating(ModelBuilder b)
{
    b.Entity<Post>()
        .HasOne(p => p.Blog)          // a Post has one Blog
        .WithMany(bl => bl.Posts)     // a Blog has many Posts
        .HasForeignKey(p => p.BlogId) // the FK is Post.BlogId
        .OnDelete(DeleteBehavior.Cascade); // delete a Blog -> delete its Posts
}
```

*What just happened:* we spelled out the exact relationship EF had already inferred from the class shapes - `HasOne`/`WithMany` name both ends, `HasForeignKey` pins down which property is the FK, and `OnDelete` declares what happens to posts when their blog is deleted. With a conventional FK name like `BlogId` you don't *need* this - write it when conventions fall short, or when you want delete behavior explicit and reviewed rather than defaulted.

**Required vs optional** is controlled by whether the FK can be null:

```csharp
public int BlogId { get; set; }    // non-nullable FK = REQUIRED: a Post must have a Blog
public int? BlogId { get; set; }   // nullable FK = OPTIONAL: a Post may have no Blog
```

*What just happened:* a non-nullable `int BlogId` means the column is `NOT NULL` and every post is required to belong to a blog - and deleting a blog cascades to its posts by default. Making it `int?` flips the relationship to optional: a post can exist with `BlogId = NULL`, and the default delete behavior changes to setting that FK to null rather than deleting the post. The nullability of one property quietly decides both the constraint and the cascade rule.

> 💡 You don't have to choose Fluent-or-nothing. Let conventions handle the 90% they cover for free, and add a Fluent API line *only* for the specific thing a convention got wrong. Every line you add is a line a reviewer has to understand - add them for a reason.

## Creating with nested relations

Where navigation properties pay off: you don't insert a blog, read back its id, then insert posts with that id by hand. You build the **object graph** and save it once.

```csharp
var blog = new Blog
{
    Url = "https://example.com",
    Posts =
    {
        new Post { Title = "Hello, world" },
        new Post { Title = "Second post" }
    }
};

ctx.Blogs.Add(blog);
ctx.SaveChanges();
```

*What just happened:* you added one `Blog` whose `Posts` collection already held two `Post` objects with no `BlogId` set. On `SaveChanges`, EF inserted the blog first, got its generated `Id` back, then inserted both posts with their `BlogId` filled in to match - all in one transaction. You never touched a foreign key value; EF read it off the navigation. (Many-to-many works the same way: assign `post.Tags = new() { tag1, tag2 }` and EF writes the join rows for you.)

> ⚠️ Defining a navigation property does **not** mean it gets loaded when you read. Query `ctx.Blogs.First()` and `blog.Posts` will be empty - not because the blog has no posts, but because you didn't ask EF to fetch them. Loading related data on read (`Include`, lazy loading, and the N+1 query trap that catches everyone) is the subject of [Phase 7](07-loading-and-n-plus-1.md). For now: a navigation describes the relationship; it doesn't auto-populate.

## Recap

- **A relationship is a foreign key plus navigation properties.** EF Core infers it from class shapes - a collection navigation, a reference navigation, and an FK property - not from configuration.
- **One-to-many:** a `List<Post>` on the parent, a `Blog` reference and a `BlogId` on the child. The FK follows the `<NavigationName>Id` convention, and the migration creates the column *and* an index on it.
- **One-to-one** uses a single reference on each side with the FK on the dependent side; **many-to-many** uses a collection on each side (skip navigations) and EF auto-creates a hidden join table - add an explicit join entity only when the relationship needs extra columns.
- The **Fluent API** (`HasOne`/`WithMany`/`HasForeignKey`/`OnDelete`) takes control when conventions can't guess. A **non-nullable FK is a required** relationship; a **nullable FK (`int?`) is optional**, which also changes the default delete behavior.
- **Create graphs in one shot:** build the object tree, `Add` the root, `SaveChanges`. EF inserts in dependency order and fills the foreign keys for you.
- Defining a navigation does **not** load it on read - that's [Phase 7](07-loading-and-n-plus-1.md).

## Quick check

```quiz
[
  {
    "q": "In the blog schema, Post has `public int BlogId { get; set; }` and `public Blog Blog { get; set; }`, while Blog has `public List<Post> Posts { get; set; }`. What does EF Core infer?",
    "choices": ["Nothing until you add Fluent API config", "A one-to-many relationship with BlogId as the foreign key, by convention", "A many-to-many relationship needing a join table", "A one-to-one relationship between Blog and Post"],
    "answer": 1,
    "explain": "A collection navigation (Blog.Posts) plus a reference navigation (Post.Blog) plus an FK named <Nav>Id (BlogId) is the convention for one-to-many. No configuration needed."
  },
  {
    "q": "You give Post a `List<Tag> Tags` and Tag a `List<Post> Posts`, with no FK property on either. What does EF Core do in EF Core 5+?",
    "choices": ["Throws an error because there's no foreign key", "Creates a join table automatically and lets you navigate post.Tags directly", "Requires you to write an explicit PostTag join entity first", "Treats it as two unrelated one-to-many relationships"],
    "answer": 1,
    "explain": "A collection on both sides with no FK is a many-to-many. EF creates a hidden join table automatically; you only write an explicit join entity when it needs extra columns."
  },
  {
    "q": "You change a Post's foreign key from `public int BlogId` to `public int? BlogId`. What does that change?",
    "choices": ["Nothing - nullability of the FK is ignored by EF", "It makes the relationship optional: a Post can have no Blog, and the default delete behavior changes", "It deletes the relationship entirely", "It forces you to use the Fluent API to keep it working"],
    "answer": 1,
    "explain": "A non-nullable FK = required relationship; a nullable FK (int?) = optional. The nullability also changes the default on-delete behavior from cascade to setting the FK null."
  }
]
```


---

# Loading Strategies & the N+1 Trap

In [Phase 6](06-relationships.md) you wired up navigation properties - `blog.Posts`, `post.Blog`,
`post.Tags`. They look like ordinary C# collections, so it's tempting to assume that once you've loaded
a blog, its posts are right there waiting. They are not. The gap between "looks loaded" and "is loaded"
is where the single most common ORM performance disaster lives: the **N+1 query trap**.

## The mental model: EF loads related data only when you ask

Here is the one sentence that prevents 80% of the confusion in this phase:

> 💡 **Loading a parent does not load its navigations.** EF Core fetches related data only when you
> explicitly ask for it - by `Include`-ing it in the query, by opting into lazy loading, or by loading
> it explicitly afterward.

When you write `ctx.Blogs.ToList()`, EF Core runs *one* `SELECT` against the `Blogs` table and hands you
`Blog` objects. Each `blog.Posts` collection comes back **empty** - not null, empty - because EF never
went to the `Posts` table. The navigation property is just an in-memory container with no idea a database
exists. EF fills it only when instructed.

```csharp
var blogs = ctx.Blogs.ToList();
Console.WriteLine(blogs[0].Posts.Count);   // 0 - even if the blog has 50 posts in the DB
```

*What just happened:* one query went out for blogs, nothing for posts. `Posts.Count` is 0 because the
collection was never populated. Not a bug - EF refuses to silently drag the whole database into memory
behind your back. Your job is to tell it what graph you actually need, three ways.

## Way 1: Eager loading with `Include` (the default tool)

**Eager loading** means "fetch the related data *in the same trip* as the parent." You declare it with
`Include`, naming the navigation you want.

```csharp
var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .ToList();

Console.WriteLine(blogs[0].Posts.Count);   // 50 - populated
```

*What just happened:* `Include(b => b.Posts)` told EF to pull each blog *and* its posts. EF emits a
single `LEFT JOIN`, reads the flattened rows, and stitches the `Posts` collections back together. One
round trip, and now every `blog.Posts` is filled.

The SQL looks roughly like this:

```sql
SELECT b.Id, b.Url, p.Id, p.Title, p.BlogId
FROM Blogs AS b
LEFT JOIN Posts AS p ON p.BlogId = b.Id
ORDER BY b.Id;
```

*What just happened:* one `SELECT` with a join. The `ORDER BY b.Id` is EF's doing - grouping rows by blog
so it can assign each batch of posts to the right parent while reading down the result set.

### Going deeper with `ThenInclude`

`Include` loads one level. To follow a navigation *off the thing you just included*, chain
`ThenInclude`:

```csharp
var blogs = ctx.Blogs
    .Include(b => b.Posts)
        .ThenInclude(p => p.Tags)
    .ToList();
```

*What just happened:* you loaded blogs, their posts, and each post's tags - the whole three-level graph
(`Blog` → `Post` → `Tag`) in one query. `Include` jumps from blog to posts; `ThenInclude` continues from
each post to its tags. Chain as deep as your model goes.

> 📝 `Include` is the tool to reach for **by default**. It's explicit (you can see exactly what gets
> loaded), one round trip, and doesn't depend on opt-in magic. The other two strategies exist for specific
> situations - and one of them is a trap.

## Way 2: Lazy loading (off by default - and how it bites)

**Lazy loading** means: don't fetch the related data up front; fetch it *automatically the moment you
touch the navigation property*. Accessing `blog.Posts` silently fires a query.

It is **off by default in EF Core**, and turning it on takes deliberate setup:

1. Install the `Microsoft.EntityFrameworkCore.Proxies` package.
2. Enable it: `optionsBuilder.UseLazyLoadingProxies()`.
3. Make every navigation property `virtual` so EF can override it with a proxy.

```csharp
public class Blog
{
    public int Id { get; set; }
    public string Url { get; set; } = "";
    public virtual List<Post> Posts { get; set; } = new();   // virtual = lazy-loadable
}

// In OnConfiguring / DI setup:
optionsBuilder
    .UseLazyLoadingProxies()
    .UseSqlite("Data Source=blog.db");
```

*What just happened:* EF replaces your `Blog` with a generated subclass (a "proxy") that overrides the
`Posts` getter. When you read `blog.Posts`, the proxy notices the collection isn't loaded yet and fires a
`SELECT * FROM Posts WHERE BlogId = @id` right then. Convenient - `blog.Posts` "just works" with no
`Include`. That convenience is exactly what makes it dangerous, next section.

## Way 3: Explicit loading (load it yourself, later)

**Explicit loading** is the manual middle ground: no proxies, no `Include` at query time - you load a
specific navigation on demand using the `Entry` API.

```csharp
var blog = ctx.Blogs.First();

// Load a collection navigation:
ctx.Entry(blog).Collection(b => b.Posts).Load();

// Load a reference navigation:
var post = ctx.Posts.First();
ctx.Entry(post).Reference(p => p.Blog).Load();
```

*What just happened:* `ctx.Entry(blog)` gives you EF's tracking handle for that object.
`.Collection(...).Load()` runs one query to fill `blog.Posts`; `.Reference(...).Load()` does the same for
a single related entity like `post.Blog`. Useful when you only *sometimes* need the related data - but
call it inside a loop and you've reinvented the N+1 problem by hand.

## ⚠️ The N+1 trap - the heart of this phase

The disaster: you load a list of blogs, then loop over them and touch a navigation.

```csharp
var blogs = ctx.Blogs.ToList();                 // Query #1
foreach (var blog in blogs)
    Console.WriteLine(blog.Posts.Count);        // with lazy loading: one query PER blog
```

*What just happened:* line 1 runs **1** query to get the blogs. Then, with lazy loading enabled, *every
single* `blog.Posts` access fires its own `SELECT ... WHERE BlogId = @id`. With 100 blogs, that's
**1 + 100 = 101** queries to render one page. That's the N+1 trap: **1** query for the parents, plus
**N** more - one per parent - for the children. The loop looks innocent; the database is on fire.

It's insidious precisely *because* it looks like normal C#. Nothing in `blog.Posts.Count` screams
"network round trip." With lazy loading on, queries hide behind property access, so only the SQL log
(or a slow page under load) reveals 101 trips where there should be 1 or 2.

The SQL log under N+1 looks like this - and keeps going:

```sql
SELECT Id, Url FROM Blogs;                       -- 1
SELECT Id, Title, BlogId FROM Posts WHERE BlogId = 1;   -- + N
SELECT Id, Title, BlogId FROM Posts WHERE BlogId = 2;
SELECT Id, Title, BlogId FROM Posts WHERE BlogId = 3;
-- ... one more for every blog
```

### The fix: ask for the graph once

```csharp
var blogs = ctx.Blogs
    .Include(b => b.Posts)      // <-- one query, the graph comes with it
    .ToList();

foreach (var blog in blogs)
    Console.WriteLine(blog.Posts.Count);   // no queries here - already loaded
```

*What just happened:* the same loop now costs **1** query instead of **N+1**. `Include` pulled all the
posts in the original round trip via a join, so every `blog.Posts` is already in memory and `.Count`
touches nothing but RAM. One trip, not 101.

> 💡 This is why `Include` is the default and lazy loading is the footgun: it turns a missing `Include`
> into N silent queries instead of one loud error. The cure is the habit you've been building all
> guide - **watch the SQL EF generates**. The same query shape repeating in a loop in your logs is N+1.
> See [Why Is My Query Slow?](/guides/why-is-my-query-slow) for reading and diagnosing query plans.

## Avoiding over-fetch: projection with `Select`

Sometimes the issue isn't *too many* queries - it's *one query that drags too much*. If a page only needs
each blog's URL and post count, loading every full `Post` entity is waste. Project instead.

```csharp
var summaries = ctx.Blogs
    .Select(b => new
    {
        b.Url,
        PostCount = b.Posts.Count
    })
    .ToList();
```

*What just happened:* `Select` projects straight into a lightweight anonymous shape. EF turns
`b.Posts.Count` into a SQL aggregate - a correlated subquery or grouped count - so you get one efficient
query returning two columns per blog and loading **no entities at all**. When you only need a few
fields, projection beats `Include` hands down.

```sql
SELECT b.Url, (SELECT COUNT(*) FROM Posts AS p WHERE p.BlogId = b.Id) AS PostCount
FROM Blogs AS b;
```

*What just happened:* the count happens in the database, not in C#. No posts cross the wire, just the
URL and a number per blog.

## ⚠️ Cartesian explosion and `AsSplitQuery`

`Include` is great until you `Include` **two collections at once**. Then the join multiplies:

```csharp
var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .Include(b => b.Authors)    // a second collection on the same blog
    .ToList();
```

*What just happened:* joining `Blogs` to both `Posts` *and* `Authors` produces a row for every
*combination* - a blog with 50 posts and 10 authors yields 50 × 10 = 500 rows, most repeating the same
data. That's a **cartesian explosion**: the result set balloons and EF burns time de-duplicating. One
query, but a brutally fat one.

The fix is to let EF split it into separate queries:

```csharp
var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .Include(b => b.Authors)
    .AsSplitQuery()             // <-- one SELECT per collection instead of a mega-join
    .ToList();
```

*What just happened:* `AsSplitQuery()` tells EF to run one `SELECT` for blogs, one for their posts, one
for their authors - then stitch the graph together in memory. You trade a few extra round trips for
avoiding the multiplicative row blowup. (Tradeoff: separate queries aren't a single consistent snapshot - 
fine for most reads, something to weigh under heavy concurrent writes.)

> 📝 The N+1 problem is not an EF Core quirk - it's universal to every ORM. You'll meet the exact same
> trap (and the exact same `Include`-style fix) in Java's
> [Hibernate & JPA](/guides/hibernate-and-jpa-from-zero) and Go's [GORM](/guides/gorm-from-zero). The
> defense is always the same: watch the SQL, load the graph you need in as few trips as possible - see
> [Why Is My Query Slow?](/guides/why-is-my-query-slow).

## Recap

- **Loading a parent does not load its navigations.** `ctx.Blogs.ToList()` leaves every `blog.Posts`
  empty - EF fetches related data only when you ask.
- **Eager loading with `Include`** (and `ThenInclude` for deeper levels) pulls the graph in one round
  trip via a join. It's explicit and the right default.
- **Lazy loading** is off by default; it needs the `Proxies` package, `UseLazyLoadingProxies()`, and
  `virtual` navigations. It makes `blog.Posts` "just work" - by firing a hidden query, which is the
  classic N+1 source. **Explicit loading** (`Entry().Collection().Load()`) is the manual alternative.
- **The N+1 trap:** loading parents (1 query) then touching a navigation in a loop fires N more - 1+N
  total. The fix is `Include` (or projection) to make it one round trip. Watch the SQL log to catch it.
- **`Select` projection** avoids over-fetch by loading only the columns you need (no entities tracked);
  **`AsSplitQuery()`** avoids the cartesian explosion when you `Include` multiple collections at once.

## Quick check

```quiz
[
  {
    "q": "After `var blogs = ctx.Blogs.ToList();` with default settings, what is `blogs[0].Posts.Count`?",
    "choices": ["The real post count from the database", "0, because navigations aren't loaded unless you ask", "null, because Posts was never set", "Throws an exception"],
    "answer": 1,
    "explain": "Loading a parent does not load its navigations. EF ran one query for blogs and never touched Posts, so the collection is empty (0), not populated and not null."
  },
  {
    "q": "You loop over 100 blogs and read `blog.Posts.Count` each time, with lazy loading enabled. How many queries run?",
    "choices": ["1", "2", "101 (1 for blogs + 100 for posts)", "100"],
    "answer": 2,
    "explain": "This is the N+1 trap: 1 query loads the blogs, then each lazy `blog.Posts` access fires its own query - 100 more. Adding `Include(b => b.Posts)` collapses it back to 1."
  },
  {
    "q": "You `Include` two separate collections on the same blog and the row count explodes. What fixes it?",
    "choices": ["Add more `ThenInclude` calls", "`AsSplitQuery()` to run one SELECT per collection", "Switch to lazy loading", "Call `Load()` in a loop"],
    "answer": 1,
    "explain": "Joining two collections at once causes a cartesian explosion (rows multiply). `AsSplitQuery()` splits it into one query per collection and stitches the graph in memory, avoiding the blowup."
  }
]
```


---

# Transactions & Migrations in Production

Two mental models carry this phase, and they're cousins. The first: a **transaction makes several operations all-or-nothing** - either every write lands together, or none do, so the database never ends up half-updated. The second: in production, **schema changes ship as reviewed, ordered migrations** - not as something your app improvises at startup. Both come from the same instinct: when real data is on the line, you stop trusting "it'll probably work" and demand "all or nothing, reviewed, repeatable."

> 📝 You've already been using transactions without knowing it - every `SaveChanges` from [Phase 5](05-change-tracking.md) was one. We'll make that explicit, then take the migrations workflow from [Phase 2](02-models-and-migrations.md) and harden it for a live database.

## `SaveChanges` is already a transaction

The thing most people miss: you don't need to *add* transactions to make a single `SaveChanges` safe. It already is one.

When you call `SaveChanges`, EF Core batches all the pending inserts, updates, and deletes it's tracking and runs them inside a **single transaction**. If any one statement fails - a constraint violation, a deadlock, a dropped connection - the whole batch rolls back. You never get three of your five inserts.

```csharp
var blog = new Blog { Url = "https://battle-hardened.dev" };
blog.Posts.Add(new Post { Title = "Hello", Content = "First post" });
blog.Posts.Add(new Post { Title = "World", Content = "Second post" });

ctx.Blogs.Add(blog);
ctx.SaveChanges();   // blog + both posts: all of it, or none of it
```

*What just happened:* EF Core inserted the `Blog` row and both `Post` rows inside one implicit transaction. If the second post's insert had blown up, the blog and first post would roll back too - you'd be left with exactly what you started with. This atomicity is free, the default. The story only gets interesting when **one** `SaveChanges` isn't enough.

> 💡 If everything you need fits in a single `SaveChanges`, you're done - don't reach for `BeginTransaction`. Wrapping one `SaveChanges` in an explicit transaction is redundant ceremony.

## Explicit transactions: spanning multiple `SaveChanges`

So when *do* you need an explicit transaction? When a single unit of work spans **more than one** `SaveChanges` call - or mixes `SaveChanges` with raw SQL - and needs all of it to commit or roll back together.

A classic case: insert a blog, get its generated `Id`, then insert dependent rows in a second round - but if that second step fails, the blog must vanish too. `BeginTransaction` makes those separate `SaveChanges` calls one atomic unit.

```csharp
using var tx = ctx.Database.BeginTransaction();
try
{
    ctx.Blogs.Add(blog);
    ctx.SaveChanges();              // first write

    ctx.Posts.AddRange(posts);
    ctx.SaveChanges();              // second write

    tx.Commit();                   // both land together
}
catch
{
    tx.Rollback();                 // anything failed → undo everything
    throw;
}
```

*What just happened:* `BeginTransaction` opened one database transaction that both `SaveChanges` calls wrote into. Neither set of rows is visible to anyone else until `Commit()`. If either `SaveChanges` throws, we `Rollback()` and re-throw, leaving the database untouched. The `using` does quiet safety work too - if an exception skips past us, disposing the transaction without a commit also rolls it back.

> 💡 This is just ACID atomicity applied through EF Core. If "atomic," "isolation," and "commit/rollback" feel fuzzy, the underlying database concepts live in [Transactions & ACID](/guides/transactions-and-acid) - the *why* under this *how*.

> ⚠️ Keep transactions **short**. A transaction holds locks until it commits, so calling a slow web API or waiting on user input mid-transaction turns a quick write into a pile-up of blocked connections. Open it, do the writes, commit, get out.

## Optimistic concurrency: don't let the last writer win silently

Now the bug that *doesn't* announce itself. Two editors load the same blog. Editor A changes the URL and saves. Editor B - who loaded the old version a minute ago - changes the description and saves. With no protection, B's `SaveChanges` issues `UPDATE Blogs SET ... WHERE Id = 5`, overwriting A's change. A's edit is gone, no error, nobody notices until a customer asks where their data went. That's **last-write-wins**, and it's the silent default.

The fix is a **concurrency token**: a column EF Core checks during updates. The cleanest version is a `rowversion` (a value the database auto-bumps on every change), declared with `[Timestamp]`:

```csharp
public class Blog
{
    public int Id { get; set; }
    public string Url { get; set; } = "";

    [Timestamp]
    public byte[] RowVersion { get; set; } = null!;
}
```

*What just happened:* `[Timestamp]` tells EF Core this `byte[]` is a database-managed `rowversion` - the database stamps a new value into it every time the row changes. EF Core now treats it as a concurrency token, changing how it writes updates. (The Fluent-API equivalent is `.Property(x => x.RowVersion).IsRowVersion()`, or `.IsConcurrencyToken()` for any plain column you want to guard.)

With that token in place, EF Core adds it to the `WHERE` clause of every update:

```sql
UPDATE Blogs SET Url = @newUrl, RowVersion = @auto
WHERE Id = 5 AND RowVersion = @theVersionILoaded;
```

*What just happened:* the update only matches if `RowVersion` is **still** the value you loaded. If Editor A already changed the row, the version moved on, the `WHERE` matches **zero rows**, and EF Core notices the mismatch (it expected to affect one row) and throws **`DbUpdateConcurrencyException`**. The silent clobber became a loud, catchable signal.

Handle it by catching that exception, reloading the current values, and deciding what to do - retry, merge, or ask the user:

```csharp
try
{
    ctx.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
    var entry = ex.Entries.Single();
    entry.Reload();          // pull the database's current values
    // re-apply your change on top of the fresh data, then SaveChanges() again
}
```

*What just happened:* `ex.Entries` hands you the entities that failed the version check. `Reload()` refreshes one with what's actually in the database now, so you can re-apply your edit against current data instead of stale data. The "right" merge policy is yours to choose - but now you *get* to choose, instead of losing data quietly.

> ⚠️ Without a concurrency token, concurrent edits are **last-write-wins by default** - and EF Core gives you no warning. If two users can ever edit the same row (almost any real app), add the token *before* you ship, not after the support ticket.

## Applying migrations in production

You built migrations in [Phase 2](02-models-and-migrations.md): model change → `migrations add` → commit. That workflow is the same in production. What changes is **how the migration reaches the live database** - because now there's real data, possibly multiple app instances, and no undo button.

First, what *not* to lean on: `EnsureCreated()` has no concept of evolving a schema (Phase 2 covered why), and there's no "auto-migrate on every request" feature worth wiring up. You want a deliberate, reviewable step. Three solid options.

**Option 1 - `dotnet ef database update` in the deploy pipeline.** The same command from your dev machine, run as a controlled step during deployment (before or alongside rolling out the new app version). Simple, explicit, and it runs once where you can watch it.

```bash
dotnet ef database update
```

*What just happened:* the deploy pipeline applied every pending migration's `Up()` to the production database as a discrete, observable step - not buried inside app startup. It runs in one place, at a known moment, and the pipeline logs tell you exactly what happened.

**Option 2 - an idempotent SQL script for a human to review.** Generate plain SQL a DBA can read, approve, and run against the database - repeatable without harm:

```bash
dotnet ef migrations script --idempotent
```

*What just happened:* EF Core emitted a SQL script that checks `__EFMigrationsHistory` before each migration, applying only the ones that haven't run yet. The `--idempotent` flag is what makes it safe to run more than once - and the plain SQL is something a human (or a change-review process) can inspect before it touches production data. The option regulated or DBA-gated shops usually want.

**Option 3 - a migrations bundle.** A self-contained executable that applies your migrations, with no SDK or project files needed on the target machine:

```bash
dotnet ef migrations bundle
```

*What just happened:* EF Core packaged the migrations into a single executable you can drop onto a deploy server and run - more portable than needing the full `dotnet ef` tooling installed, more automated than hand-running a SQL script.

### The convenient-but-risky one: `Database.Migrate()` at startup

You'll see this in tutorials: call `context.Database.Migrate()` when the app boots, letting it apply pending migrations itself.

```csharp
// Convenient - but think hard before using this in production.
context.Database.Migrate();
```

*What just happened:* on startup, the app applied any pending migrations to its database automatically. For a solo project or single-instance app, that's genuinely convenient. The trouble shows up at scale.

> ⚠️ `Database.Migrate()` at startup is risky when **multiple app instances start at once** - a common deploy and autoscaling pattern. Several instances race to apply the same migrations against the same database, and a partially-applied schema is a bad afternoon. It also gives **no review step**: the schema changes the instant the app boots, with no DBA approval and no separate moment to catch a mistake. For anything multi-instance or production-critical, prefer a deploy-time step (Option 1, 2, or 3) where exactly one actor applies migrations at a known time.

> 💡 The migrations *workflow* - one migration per change, reviewed, committed, ordered - is the foundation here, and it generalizes beyond EF Core. For the framework-agnostic principles (forward-only changes, expand/contract, never editing an applied migration), see [Database Migrations](/guides/database-migrations).

## Recap

- **`SaveChanges` is already atomic.** Every call batches its writes into a single transaction - all of them land, or none do. You don't add a transaction to make one `SaveChanges` safe.
- **Use `BeginTransaction` only to span multiple `SaveChanges` calls** (or `SaveChanges` plus raw SQL) as one all-or-nothing unit. Commit on success; roll back (and re-throw) on failure. Keep transactions short.
- **Optimistic concurrency** needs a token: a `[Timestamp] byte[] RowVersion` (rowversion) or `.IsConcurrencyToken()`. EF Core adds it to the update's `WHERE`; a stale version matches zero rows and throws `DbUpdateConcurrencyException`. Catch it, `Reload()`, re-apply, retry.
- **Without a concurrency token, concurrent edits are silent last-write-wins** - no error, lost data. Add the token before you ship anything multi-user.
- **Apply migrations in production via a deliberate step**: `dotnet ef database update` in the pipeline, `migrations script --idempotent` for DBA review, or `migrations bundle` as a portable executable.
- **`Database.Migrate()` at startup is convenient but risky** - multiple instances race to migrate, and there's no review moment. Prefer a deploy-time step for anything production-critical.

## Quick check

```quiz
[
  {
    "q": "You call a single SaveChanges that inserts a Blog and three Posts, and the third Post violates a constraint. What ends up in the database?",
    "choices": ["The Blog and the first two Posts", "Nothing - the whole SaveChanges rolls back", "The Blog only", "Everything, with the bad Post nulled out"],
    "answer": 1,
    "explain": "A single SaveChanges runs as one transaction. If any statement in the batch fails, the entire batch rolls back - all or nothing."
  },
  {
    "q": "A Blog entity has a [Timestamp] RowVersion property. Two users load the same blog; user A saves first, then user B saves a stale copy. What happens to user B's SaveChanges?",
    "choices": ["It silently overwrites user A's change", "It throws DbUpdateConcurrencyException because the WHERE matches no rows", "It merges both changes automatically", "It blocks until user A's transaction releases"],
    "answer": 1,
    "explain": "The concurrency token goes into the update's WHERE clause. User A already bumped RowVersion, so user B's update matches zero rows and EF Core throws DbUpdateConcurrencyException - catch it, reload, and retry."
  },
  {
    "q": "Why is context.Database.Migrate() at startup risky for a production app running several instances?",
    "choices": ["It can't apply more than one migration at a time", "Multiple instances race to apply the same migrations, and there's no review step", "It only works with SQLite", "It deletes the __EFMigrationsHistory table each boot"],
    "answer": 1,
    "explain": "When several instances start together they race to migrate the same database, risking a partially-applied schema, and the schema changes with no DBA review. Prefer a single deploy-time step: database update, an idempotent script, or a bundle."
  }
]
```


---

# EF Core in the Real World & Where to Go Next

Look at the distance you've covered. You can model a table as a C# class and let a migration build it. You can `Add` a record and `SaveChanges`, then read it back with `Find`, `First`, or `Single`. You can chain `Where`, `OrderBy`, `Select`, and `Include` into the exact query you mean. You understand change tracking - that the `DbContext` quietly watches the objects it hands you and batches every edit into one round-trip. You can model one-to-many and many-to-many relationships, spot an N+1 explosion before it ships and reach for `Include`, wrap a sequence of writes in a transaction, and reason about migrations in production.

And most of all - the whole point of learning EF Core this way - you can read the SQL underneath. With logging on, EF Core stopped being a magic box and became a SQL generator whose output you can predict and debug. That skill outlives any single library.

This last phase isn't new mechanics. It's where EF Core actually lives in real .NET codebases, where it isn't the right tool, and what to build to make all of this stick.

## When to drop to raw SQL

Worth saying out loud, since it surprises people who expect an ORM to be a cage: **EF Core never traps you.** Any time the generated SQL gets awkward, go SQL-first for that one query and keep using EF Core for everything else.

When does that moment arrive?

- **Complex reporting queries** - a seven-way join, window functions, a recursive CTE. The ORM fights you here, and you shouldn't fight back.
- **Database-specific features** - something only your engine offers that EF Core's portable layer doesn't expose cleanly.
- **Performance-critical paths** - a hot query where the generated SQL is suboptimal and you want to hand-tune every clause.

The friendliest door is `FromSql`. Pass an interpolated string and EF Core turns the interpolation holes into real SQL parameters - and you get back tracked entities, exactly as if you'd queried normally:

```csharp
var posts = ctx.Posts
    .FromSql($"SELECT * FROM Posts WHERE Title = {title}")
    .ToList();
```

📝 That `{title}` is **not** string concatenation. The interpolated `FromSql` parameterizes the value for you - the same protection against SQL injection as LINQ. If you need to build parameters yourself, `FromSqlRaw` lets you pass them manually (and puts the safety on you). For writes that don't return rows, use `ExecuteSql`:

```csharp
ctx.Database.ExecuteSql($"UPDATE Posts SET Published = {true} WHERE BlogId = {blogId}");
```

Drop to raw SQL, yes; drop your guard, no.

## EF Core vs Dapper vs ADO.NET

EF Core is the default in .NET, but not the only way to talk to a database - knowing the landscape helps you pick well and read other people's code.

- **EF Core** - a full ORM. You describe data as classes, query with LINQ, and it writes the SQL, tracks changes, and manages migrations. It optimizes for productivity.
- **Dapper** - a *micro-ORM*. You write the SQL yourself; Dapper maps the result rows onto your objects. No change tracking, no LINQ-to-SQL - fast, predictable, and you own every query.
- **ADO.NET** - the lowest level: raw commands, readers, and parameters by hand. Maximum control, maximum boilerplate. Both EF Core and Dapper sit on top of it.

```mermaid
flowchart TD
  A[Need a data layer] --> B{Want to write the SQL yourself?}
  B -- No, give me productivity --> C[EF Core]
  B -- Yes, SQL-first --> D{Want it mapped for you?}
  D -- Yes, just map rows --> E[Dapper]
  D -- No, full control --> F[ADO.NET]
```

💡 The practical rule: reach for **EF Core when you want productivity** - fast CRUD, relationships handled, migrations baked in, covering most apps. Reach for **Dapper on hot read paths** or when you want SQL-first control without an ORM in the way. The part people miss: **they coexist beautifully.** A very common production setup is EF Core for writes and the everyday model, with Dapper dropped in for a handful of heavy read queries.

## The caveats, plainly - and ASP.NET Core

A battle-hardened friend tells you where the dragons are. The short list of EF Core's, all of which you've already met:

- **The detached-entity trap (Phase 5).** An object that didn't come from *this* context isn't tracked, so editing it and calling `SaveChanges` does nothing until you `Attach` or `Update` it. Know which context an entity belongs to.
- **Forgetting `Include` → N+1 (Phase 7).** Load a list, then touch each item's navigation property, and you fire one query per row. Eager-load with `Include` and watch the count collapse.
- **Generated SQL can be suboptimal.** EF Core aims for correctness and portability, not always the leanest query. Keep logging on and read what it emits. When a query is slow, the logged SQL is your first clue - see [Why Is My Query Slow?](/guides/why-is-my-query-slow).

Now, the place EF Core most often lives: as the data layer of an [ASP.NET Core](/guides/aspnet-core-from-zero) app. You register the context with the dependency injection container once at startup:

```csharp
builder.Services.AddDbContext<BlogContext>(o => o.UseSqlite(conn));
```

📝 `AddDbContext` registers the context as **Scoped** - one instance per HTTP request. Inject it into your endpoints or services and use it for that request; the framework disposes it when the request ends. **Don't share a `DbContext` across threads or requests** - a context is a unit of work for one request, not a long-lived singleton; sharing one is how you get tangled change-tracking and concurrency bugs.

## What to build

Reading got you here. Building is what makes it last. If you've done the [ASP.NET Core](/guides/aspnet-core-from-zero) guide, you built a products (or blog) API backed by an in-memory repository in Phase 6. Swap that repository for an EF Core-backed one and point it at a real database.

Concretely:

- **Back it with EF Core + a real DB.** Register the context with `AddDbContext`, inject it into your endpoints, and let your `POST`/`GET`/`PUT`/`DELETE` handlers do real CRUD against the database.
- **Go async everywhere.** Use `ToListAsync`, `FirstOrDefaultAsync`, and `SaveChangesAsync` so requests don't block a thread while the database works.
- **Tune reads.** Add `AsNoTracking` to read-only queries so EF Core skips change-tracking overhead, and reach for compiled queries on the hottest paths.
- **Use a real provider.** SQLite is a fine sandbox; for production, move to SQL Server or PostgreSQL (via Npgsql). The model and your LINQ stay the same - only the provider and connection string change.

Then deploy it somewhere, even a tiny instance. Keep logging on and watch your endpoints turn into SQL as requests come in. Whatever you build, **finish one** - a small API you actually debugged and deployed teaches more than three half-built ones.

You came in seeing an ORM as a trick that turned objects into rows somehow. You're leaving able to model, migrate, query with LINQ, track changes, relate, beat N+1, transact - and, when the ORM gets in your way, drop to the SQL it was writing all along. A **`DbContext` is a change-tracking session**, **`DbSet`s are your tables**, **LINQ becomes SQL** - and you can always see that SQL and reach past it.

## Recap

1. **EF Core never locks you in.** Drop to raw SQL with `ctx.Posts.FromSql($"...")` (interpolated → parameterized, returns tracked entities), `FromSqlRaw` for manual params, and `ctx.Database.ExecuteSql($"...")` for non-queries - for reporting, DB-specific features, or hot paths.
2. **Know the alternatives.** EF Core is the full ORM for productivity; **Dapper** is the micro-ORM where you write SQL and it maps rows; **ADO.NET** is the raw layer underneath both. They coexist - EF for writes, Dapper for heavy reads is common.
3. **Remember the caveats.** The detached-entity trap, forgetting `Include` and triggering N+1, and occasionally suboptimal generated SQL - all manageable once you keep logging on and read what EF emits.
4. **With ASP.NET Core, register via `AddDbContext`.** The context is **Scoped** (one per request) - inject it, don't share it across threads or requests.
5. **Build for real:** take the ASP.NET Core products/blog API, back it with EF Core and a real DB, add async + `AsNoTracking` reads, and deploy. Finish one.

## Quick check

One last check - on how EF Core shows up in real .NET apps:

```quiz
[
  {
    "q": "You need a gnarly reporting query - a multi-table join with window functions - and EF Core's LINQ gets awkward. What's the mature move?",
    "choices": [
      "Use ctx.Posts.FromSql($\"...\") for that one query (interpolated, so it stays parameterized) and keep using EF Core everywhere else",
      "Abandon EF Core entirely and rewrite the whole app on raw ADO.NET",
      "Force it through Include no matter how many queries it fires",
      "Build the SQL string by concatenating the user's input directly into FromSqlRaw"
    ],
    "answer": 0,
    "explain": "EF Core never traps you. Use FromSql with an interpolated string for the awkward query - it parameterizes the values and returns tracked entities - and keep the ORM for the rest. ExecuteSql covers non-query writes."
  },
  {
    "q": "Your app is mostly EF Core, but one read-heavy endpoint is hot and you want hand-tuned SQL mapped straight onto objects, with no change tracking. What fits?",
    "choices": [
      "Dapper - a micro-ORM where you write the SQL and it maps rows to objects; it coexists fine alongside EF Core",
      "EF Core only - never mix data libraries in one app",
      "ADO.NET, rewriting every other query by hand too",
      "Migrations - that's a schema tool, not a query layer"
    ],
    "answer": 0,
    "explain": "Dapper is the SQL-first micro-ORM: you write the query, it maps the rows, no tracking. EF for writes plus Dapper for heavy reads is a common, healthy production setup - they coexist."
  },
  {
    "q": "How should you register and use EF Core's DbContext in an ASP.NET Core app?",
    "choices": [
      "builder.Services.AddDbContext<BlogContext>(...) registers it as Scoped - one per request; inject it and don't share it across threads or requests",
      "Register it as a singleton and share one context across every request for speed",
      "Create a new DbContext manually inside each LINQ statement",
      "Skip DI entirely and store the context in a static field"
    ],
    "answer": 0,
    "explain": "AddDbContext registers the context as Scoped - one instance per HTTP request. Inject it into endpoints/services and let the framework dispose it. A DbContext is a unit of work for one request; sharing it across threads or requests causes tracking and concurrency bugs."
  }
]
```
