Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .github/workflows/build-samples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ on:
- "dotnet-8-essentials/core-features-get-started/**"
- "dotnet-8-essentials/observability-opentelemetry/**"
- "ef-core/advanced-modeling-performance/**"
- "ef-core/modern-data-access-dotnet/**"
- ".github/workflows/build-samples.yml"

pull_request:
Expand Down Expand Up @@ -56,6 +57,7 @@ on:
- "dotnet-8-essentials/core-features-get-started/**"
- "dotnet-8-essentials/observability-opentelemetry/**"
- "ef-core/advanced-modeling-performance/**"
- "ef-core/modern-data-access-dotnet/**"
- ".github/workflows/build-samples.yml"

workflow_dispatch:
Expand Down Expand Up @@ -1667,3 +1669,107 @@ jobs:
echo "Generated SQLite database file found in sample tree."
exit 1
fi

test-ef-core-relationships:
name: Test EF Core relationship loading sample
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Check out repository
uses: actions/checkout@v5

- name: Install .NET 10 SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"

- name: Restore
run: >
dotnet restore
ef-core/modern-data-access-dotnet/EfCoreRelationshipsMinimal.slnx

- name: Build
run: >
dotnet build
ef-core/modern-data-access-dotnet/EfCoreRelationshipsMinimal.slnx
--configuration Release
--no-restore

- name: Test
run: >
dotnet test
ef-core/modern-data-access-dotnet/EfCoreRelationshipsMinimal.slnx
--configuration Release
--no-build

- name: Verify EF Core package baseline
shell: bash
run: |
project="ef-core/modern-data-access-dotnet/src/EfCoreRelationshipsMinimal/EfCoreRelationshipsMinimal.csproj"

test "$(
grep \
--count \
'<PackageReference' \
"${project}"
)" = "1"

grep \
--fixed-strings \
'Include="Microsoft.EntityFrameworkCore.Sqlite"' \
"${project}"

grep \
--fixed-strings \
'Version="10.0.11"' \
"${project}"

- name: Verify exact relationship-loading output
shell: bash
run: |
output="$(
dotnet run \
--project ef-core/modern-data-access-dotnet/src/EfCoreRelationshipsMinimal/EfCoreRelationshipsMinimal.csproj \
--configuration Release \
--no-build
)"

expected="$(
printf '%s\n' \
'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'
)"

printf '%s\n' "${output}"

if [[ "${output}" != "${expected}" ]]; then
echo "Unexpected deterministic relationship-loading output."
exit 1
fi

- name: Verify no generated database artifacts
shell: bash
run: |
if find \
ef-core/modern-data-access-dotnet \
-type f \
\( \
-name '*.db' \
-o -name '*.sqlite' \
-o -name '*.sqlite3' \
\) \
-print \
-quit |
grep \
--quiet \
.; then
echo "Generated SQLite database file found in sample tree."
exit 1
fi
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Each sample folder contains a focused implementation of one tutorial topic. The
| [`dotnet-8-essentials/core-features-get-started`](dotnet-8-essentials/core-features-get-started/) | Focused .NET 10 Native AOT Minimal API demonstrating CreateSlimBuilder, source-generated JSON, typed DI, AOT-safe endpoints, analyzer-aware publishing, and direct native-binary smoke testing | [.NET 8 Essentials: Core Features & Getting Started](https://www.dotnet-guide.com/tutorials/dotnet-8-essentials/core-features-get-started/) |
| [`dotnet-8-essentials/observability-opentelemetry`](dotnet-8-essentials/observability-opentelemetry/) | Focused .NET 10 OpenTelemetry companion demonstrating ASP.NET Core request instrumentation, custom ActivitySource spans, low-cardinality Meter metrics, structured ILogger events, automatic log-to-trace correlation, console export, and deterministic tests without external observability infrastructure | [.NET 8 Observability with OpenTelemetry: Tracing, Metrics & Structured Logging](https://www.dotnet-guide.com/tutorials/dotnet-8-essentials/observability-opentelemetry/) |
| [`ef-core/advanced-modeling-performance`](ef-core/advanced-modeling-performance/) | Focused .NET 10 / EF Core 10 relational-data companion demonstrating DTO projection, no-tracking reads, a compiled hot-path query, named soft-delete filtering, ExecuteUpdate, ExecuteDelete, and explicit change-tracker caveats with deterministic SQLite tests | [EF Core Advanced Modeling & Performance: Owned Types, Converters, JSON/Temporal Tables, Compiled Queries](https://www.dotnet-guide.com/tutorials/ef-core/advanced-modeling-performance/) |
| [`ef-core/modern-data-access-dotnet`](ef-core/modern-data-access-dotnet/) | Focused .NET 10 / EF Core 10 relationship-loading companion demonstrating one-to-many and many-to-many modeling, an intentional N+1 baseline, eager and filtered Include, split-query loading, explicit loading, SELECT-command counting, and deterministic SQLite verification | [EF Core 8 Fundamentals: Modern Data Access with .NET 8](https://www.dotnet-guide.com/tutorials/ef-core/modern-data-access-dotnet/) |

## Companion articles
- [Common Microsoft.Extensions.AI mistakes](https://www.dotnet-guide.com/articles/dotnet-ai/microsoft-extensions-ai-common-mistakes/)
Expand Down Expand Up @@ -471,8 +472,8 @@ tutorials/
| |-- MinimalApiPipeline.Tests.csproj
| `-- MinimalApiPipelineTests.cs
|-- ef-core/
| `-- advanced-modeling-performance/
| |-- EfCoreHotPathMinimal.slnx
| |-- advanced-modeling-performance/
| | |-- EfCoreHotPathMinimal.slnx
| |-- README.md
| |-- src/
| | `-- EfCoreHotPathMinimal/
Expand All @@ -492,6 +493,26 @@ tutorials/
| `-- EfCoreHotPathMinimal.Tests/
| |-- EfCoreHotPathMinimal.Tests.csproj
| `-- EfCoreHotPathTests.cs
| `-- modern-data-access-dotnet/
| |-- EfCoreRelationshipsMinimal.slnx
| |-- README.md
| |-- src/
| | `-- EfCoreRelationshipsMinimal/
| | |-- EfCoreRelationshipsMinimal.csproj
| | |-- Program.cs
| | |-- Data/
| | | |-- RelationshipDatabase.cs
| | | `-- RelationshipDbContext.cs
| | |-- Diagnostics/
| | | `-- SelectCountingInterceptor.cs
| | |-- Models/
| | | `-- RelationshipModels.cs
| | `-- Services/
| | `-- LoadingScenarios.cs
| `-- tests/
| `-- EfCoreRelationshipsMinimal.Tests/
| |-- EfCoreRelationshipsMinimal.Tests.csproj
| `-- EfCoreRelationshipsTests.cs
|-- .github/
| `-- workflows/
| `-- build-samples.yml
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/EfCoreRelationshipsMinimal/EfCoreRelationshipsMinimal.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/EfCoreRelationshipsMinimal.Tests/EfCoreRelationshipsMinimal.Tests.csproj" />
</Folder>
</Solution>
211 changes: 211 additions & 0 deletions ef-core/modern-data-access-dotnet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# EF Core Relationship Loading & Query Shapes

A focused EF Core companion showing how relationship-loading choices change
database command shape.

## Full tutorial

[EF Core 8 Fundamentals: Modern Data Access with .NET 8](https://www.dotnet-guide.com/tutorials/ef-core/modern-data-access-dotnet/)

## Version note

The full tutorial was written around .NET 8 / EF Core 8.

This companion targets:

```text
.NET 10
EF Core 10
Microsoft.EntityFrameworkCore.Sqlite 10.0.11
```

because that is the current DOTNET GUIDE repository baseline.

## Focus

```text
Category -> TodoItem
TodoItem <-> Tag
intentional N+1 baseline
Include
filtered Include
AsSplitQuery
explicit loading
```

## Why SQLite?

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.

## N+1 baseline

The sample intentionally includes an inefficient pattern:

```text
1 query for todos
+ 1 category query per todo
```

With three seeded todos, that is:

```text
4 SELECT commands
```

This is an anti-pattern included for comparison, not a recommendation.

## Eager loading

When the operation knows it needs the category relationship, the companion uses:

```csharp
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.

## Filtered Include

Todo 1 has two tags:

```text
urgent
planning
```

The filtered Include deliberately loads only:

```text
urgent
```

The scenario uses `AsNoTracking` so prior tracked entities cannot change the
filtered navigation through relationship fix-up.

## Split query

The nested graph:

```text
Category -> Todos -> Tags
```

is loaded with:

```csharp
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.

## Explicit loading

The explicit-load scenario first loads a TodoItem without its Category.

It then calls:

```csharp
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.

## No lazy-loading proxies

This companion intentionally does not install:

```text
Microsoft.EntityFrameworkCore.Proxies
```

Lazy loading can hide extra database round trips, making N+1 behavior harder to
see in a learning sample.

## Deterministic output

```text
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
```

## Restore, build, and test

```powershell
dotnet restore `
.\EfCoreRelationshipsMinimal.slnx

dotnet build `
.\EfCoreRelationshipsMinimal.slnx `
--configuration Release `
--no-restore

dotnet test `
.\EfCoreRelationshipsMinimal.slnx `
--configuration Release `
--no-build
```

## Run

```powershell
dotnet run `
--project .\src\EfCoreRelationshipsMinimal\EfCoreRelationshipsMinimal.csproj `
--configuration Release `
--no-build
```

## Deliberately omitted

- 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.

## Verification

- 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
Loading
Loading