A focused EF Core companion showing how relationship-loading choices change database command shape.
EF Core 8 Fundamentals: Modern Data Access with .NET 8
The full tutorial was written around .NET 8 / EF Core 8.
This companion targets:
.NET 10
EF Core 10
Microsoft.EntityFrameworkCore.Sqlite 10.0.11
because that is the current DOTNET GUIDE repository baseline.
Category -> TodoItem
TodoItem <-> Tag
↓
intentional N+1 baseline
↓
Include
↓
filtered Include
↓
AsSplitQuery
↓
explicit loading
The sample uses one open SQLite in-memory connection so it can demonstrate real relational behavior without credentials, Docker, or a database server.
SQLite is still not equivalent to SQL Server or PostgreSQL.
Provider-specific production behavior should be tested against the actual production provider.
The sample intentionally includes an inefficient pattern:
1 query for todos
+ 1 category query per todo
With three seeded todos, that is:
4 SELECT commands
This is an anti-pattern included for comparison, not a recommendation.
When the operation knows it needs the category relationship, the companion uses:
Include(todo => todo.Category)For this exact SQLite/EF Core 10.0.11 query shape, that produces one SELECT.
Do not generalize this count to arbitrary relationship graphs.
Todo 1 has two tags:
urgent
planning
The filtered Include deliberately loads only:
urgent
The scenario uses AsNoTracking so prior tracked entities cannot change the
filtered navigation through relationship fix-up.
The nested graph:
Category -> Todos -> Tags
is loaded with:
AsSplitQuery()For this pinned sample it issues three SELECT commands.
Split queries aren't universally faster. They trade JOIN result size for multiple commands/round trips.
The explicit-load scenario first loads a TodoItem without its Category.
It then calls:
context.Entry(todo)
.Reference(item => item.Category)
.LoadAsync(...)The test proves the reference changes from not loaded to loaded.
Use explicit loading when the relationship is conditional rather than always required.
This companion intentionally does not install:
Microsoft.EntityFrameworkCore.Proxies
Lazy loading can hide extra database round trips, making N+1 behavior harder to see in a learning sample.
EF Core Relationship Loading Lab
Seeded graph: categories=2, todos=3, tags=3
N+1 baseline: todos=3, SELECTs=4
Eager Include: todos=3, SELECTs=1
Split graph: categories=2, todos=3, tags=3, SELECTs=3
Explicit reference load: todo=Prepare release, category=Work, SELECTs=2
dotnet restore `
.\EfCoreRelationshipsMinimal.slnx
dotnet build `
.\EfCoreRelationshipsMinimal.slnx `
--configuration Release `
--no-restore
dotnet test `
.\EfCoreRelationshipsMinimal.slnx `
--configuration Release `
--no-builddotnet run `
--project .\src\EfCoreRelationshipsMinimal\EfCoreRelationshipsMinimal.csproj `
--configuration Release `
--no-build- web API CRUD;
- migrations;
- owned/complex types;
- JSON;
- compiled queries;
- ExecuteUpdate/Delete;
- concurrency;
- transactions;
- retries;
- SQL Server;
- PostgreSQL;
- lazy-loading proxies;
- raw SQL;
- Testcontainers;
- benchmarks.
The complete tutorial covers the broader EF Core fundamentals.
- Target framework: .NET 10
- EF Core provider: SQLite 10.0.11
- Direct application packages: 1
- Tests: 8
- External services: none
- Generated database files: none
- Benchmark claims: none
- Last reviewed: 2026-08-14