Skip to content

Commit b63f221

Browse files
Merge pull request #22 from dotnet-guide-com/feature/ef-core-hot-path-sample
Add lightweight EF Core hot-path sample
2 parents 4cbb79d + 22d6679 commit b63f221

14 files changed

Lines changed: 1298 additions & 0 deletions

File tree

‎.github/workflows/build-samples.yml‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ on:
2727
- "dotnet-8-essentials/configuration-secrets-environments/**"
2828
- "dotnet-8-essentials/core-features-get-started/**"
2929
- "dotnet-8-essentials/observability-opentelemetry/**"
30+
- "ef-core/advanced-modeling-performance/**"
3031
- ".github/workflows/build-samples.yml"
3132

3233
pull_request:
@@ -54,6 +55,7 @@ on:
5455
- "dotnet-8-essentials/configuration-secrets-environments/**"
5556
- "dotnet-8-essentials/core-features-get-started/**"
5657
- "dotnet-8-essentials/observability-opentelemetry/**"
58+
- "ef-core/advanced-modeling-performance/**"
5759
- ".github/workflows/build-samples.yml"
5860

5961
workflow_dispatch:
@@ -1561,3 +1563,107 @@ jobs:
15611563
grep --fixed-strings --quiet 'Checkout.Process' "${app_log}"
15621564
grep --fixed-strings --quiet 'checkout.requests' "${app_log}"
15631565
grep --fixed-strings --quiet 'Checkout accepted' "${app_log}"
1566+
1567+
test-ef-core-hot-path:
1568+
name: Test EF Core hot-path sample
1569+
runs-on: ubuntu-latest
1570+
1571+
permissions:
1572+
contents: read
1573+
1574+
steps:
1575+
- name: Check out repository
1576+
uses: actions/checkout@v5
1577+
1578+
- name: Install .NET 10 SDK
1579+
uses: actions/setup-dotnet@v5
1580+
with:
1581+
dotnet-version: "10.0.x"
1582+
1583+
- name: Restore
1584+
run: >
1585+
dotnet restore
1586+
ef-core/advanced-modeling-performance/EfCoreHotPathMinimal.slnx
1587+
1588+
- name: Build
1589+
run: >
1590+
dotnet build
1591+
ef-core/advanced-modeling-performance/EfCoreHotPathMinimal.slnx
1592+
--configuration Release
1593+
--no-restore
1594+
1595+
- name: Test
1596+
run: >
1597+
dotnet test
1598+
ef-core/advanced-modeling-performance/EfCoreHotPathMinimal.slnx
1599+
--configuration Release
1600+
--no-build
1601+
1602+
- name: Verify EF Core package baseline
1603+
shell: bash
1604+
run: |
1605+
project="ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/EfCoreHotPathMinimal.csproj"
1606+
1607+
test "$(
1608+
grep \
1609+
--count \
1610+
'<PackageReference' \
1611+
"${project}"
1612+
)" = "1"
1613+
1614+
grep \
1615+
--fixed-strings \
1616+
'Include="Microsoft.EntityFrameworkCore.Sqlite"' \
1617+
"${project}"
1618+
1619+
grep \
1620+
--fixed-strings \
1621+
'Version="10.0.11"' \
1622+
"${project}"
1623+
1624+
- name: Verify exact sample output
1625+
shell: bash
1626+
run: |
1627+
output="$(
1628+
dotnet run \
1629+
--project ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/EfCoreHotPathMinimal.csproj \
1630+
--configuration Release \
1631+
--no-build
1632+
)"
1633+
1634+
expected="$(
1635+
printf '%s\n' \
1636+
'EF Core Hot-Path Query Lab' \
1637+
'Visible products: 4' \
1638+
'Compiled lookup: SKU-003 | Dock | 12900 cents' \
1639+
'Restocked products: 3' \
1640+
'Purged soft-deleted products: 1' \
1641+
'Final rows: 4'
1642+
)"
1643+
1644+
printf '%s\n' "${output}"
1645+
1646+
if [[ "${output}" != "${expected}" ]]; then
1647+
echo "Unexpected deterministic sample output."
1648+
exit 1
1649+
fi
1650+
1651+
- name: Verify no generated database artifacts
1652+
shell: bash
1653+
run: |
1654+
if find \
1655+
ef-core/advanced-modeling-performance \
1656+
-type f \
1657+
\( \
1658+
-name '*.db' \
1659+
-o -name '*.sqlite' \
1660+
-o -name '*.sqlite3' \
1661+
\) \
1662+
-print \
1663+
-quit |
1664+
grep \
1665+
--quiet \
1666+
.; then
1667+
echo "Generated SQLite database file found in sample tree."
1668+
exit 1
1669+
fi

‎README.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Each sample folder contains a focused implementation of one tutorial topic. The
3232
| [`dotnet-8-essentials/configuration-secrets-environments`](dotnet-8-essentials/configuration-secrets-environments/) | Focused .NET 10 Minimal API demonstrating layered configuration precedence, prefixed environment variables, command-line overrides, strongly typed options, startup validation, User Secrets metadata, a lightweight feature flag, and safe Development-only diagnostics | [.NET 8 Configuration & Secrets Management: Typed Options, User Secrets & Feature Flags](https://www.dotnet-guide.com/tutorials/dotnet-8-essentials/configuration-secrets-environments/) |
3333
| [`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/) |
3434
| [`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/) |
35+
| [`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/) |
3536

3637
## Companion articles
3738
- [Common Microsoft.Extensions.AI mistakes](https://www.dotnet-guide.com/articles/dotnet-ai/microsoft-extensions-ai-common-mistakes/)
@@ -469,6 +470,28 @@ tutorials/
469470
| `-- MinimalApiPipeline.Tests/
470471
| |-- MinimalApiPipeline.Tests.csproj
471472
| `-- MinimalApiPipelineTests.cs
473+
|-- ef-core/
474+
| `-- advanced-modeling-performance/
475+
| |-- EfCoreHotPathMinimal.slnx
476+
| |-- README.md
477+
| |-- src/
478+
| | `-- EfCoreHotPathMinimal/
479+
| | |-- EfCoreHotPathMinimal.csproj
480+
| | |-- Program.cs
481+
| | |-- Data/
482+
| | | |-- CatalogDatabase.cs
483+
| | | `-- CatalogDbContext.cs
484+
| | |-- Models/
485+
| | | |-- Product.cs
486+
| | | `-- ProductSummary.cs
487+
| | |-- Queries/
488+
| | | `-- CatalogQueries.cs
489+
| | `-- Services/
490+
| | `-- CatalogWorkflow.cs
491+
| `-- tests/
492+
| `-- EfCoreHotPathMinimal.Tests/
493+
| |-- EfCoreHotPathMinimal.Tests.csproj
494+
| `-- EfCoreHotPathTests.cs
472495
|-- .github/
473496
| `-- workflows/
474497
| `-- build-samples.yml
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Solution>
2+
<Folder Name="/src/">
3+
<Project Path="src/EfCoreHotPathMinimal/EfCoreHotPathMinimal.csproj" />
4+
</Folder>
5+
<Folder Name="/tests/">
6+
<Project Path="tests/EfCoreHotPathMinimal.Tests/EfCoreHotPathMinimal.Tests.csproj" />
7+
</Folder>
8+
</Solution>
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# EF Core Hot-Path Queries & Set-Based Operations
2+
3+
A focused EF Core companion demonstrating a lean read path, a stable compiled
4+
query, a named soft-delete query filter, and server-side set-based updates and
5+
deletes without external database infrastructure.
6+
7+
## Full tutorial
8+
9+
[EF Core Advanced Modeling & Performance: Owned Types, Converters, JSON/Temporal Tables, Compiled Queries](https://www.dotnet-guide.com/tutorials/ef-core/advanced-modeling-performance/)
10+
11+
## Version note
12+
13+
The full tutorial was written around .NET 8 / EF Core 8.
14+
15+
This companion targets:
16+
17+
```text
18+
.NET 10
19+
EF Core 10
20+
Microsoft.EntityFrameworkCore.Sqlite 10.0.11
21+
```
22+
23+
because that is the current DOTNET GUIDE repository baseline.
24+
25+
## Focus
26+
27+
```text
28+
SQLite in-memory
29+
-> named soft-delete filter
30+
-> DTO projection
31+
-> AsNoTracking
32+
-> EF.CompileAsyncQuery
33+
-> ExecuteUpdateAsync
34+
-> ExecuteDeleteAsync
35+
-> fresh-context verification
36+
```
37+
38+
This sample is intentionally not a benchmark.
39+
40+
## Why SQLite?
41+
42+
SQLite gives the companion a real relational database without:
43+
44+
- credentials;
45+
- a server;
46+
- Docker;
47+
- Testcontainers;
48+
- generated database files.
49+
50+
The low-level in-memory connection remains open for the lifetime of each
51+
scenario so multiple DbContext instances see the same database.
52+
53+
SQLite is still a different provider from SQL Server and PostgreSQL.
54+
55+
This sample does not validate provider-specific JSON, temporal-table,
56+
concurrency-token, migration, query-plan, or batching behavior.
57+
58+
## Soft-delete filter
59+
60+
The companion targets EF Core 10 and uses a named query filter:
61+
62+
```csharp
63+
entity.HasQueryFilter(
64+
"SoftDeleteFilter",
65+
product => !product.IsDeleted);
66+
```
67+
68+
The purge path selectively disables it:
69+
70+
```csharp
71+
IgnoreQueryFilters(
72+
["SoftDeleteFilter"])
73+
```
74+
75+
The EF Core 8 tutorial must continue to explain that older versions use one
76+
combined filter expression when several filters apply to the same entity.
77+
78+
## Projection and tracking
79+
80+
The read path projects only:
81+
82+
```text
83+
Sku
84+
Name
85+
PriceCents
86+
StockQuantity
87+
```
88+
89+
into `ProductSummary`.
90+
91+
It also uses `AsNoTracking` to make the read-only intent explicit.
92+
93+
The tests verify that no entity entries remain in the DbContext change tracker.
94+
95+
## Compiled query
96+
97+
The sample uses one:
98+
99+
```text
100+
EF.CompileAsyncQuery
101+
```
102+
103+
for a stable SKU lookup.
104+
105+
This does not mean every query should be manually compiled.
106+
107+
EF Core already caches ordinary queries by expression-tree shape. Explicit
108+
compiled queries bypass the normal cache lookup and are intended for measured
109+
hot paths with stable query shapes and simple scalar parameters.
110+
111+
The sample makes no fixed latency-savings claim.
112+
113+
## Set-based update
114+
115+
```text
116+
ExecuteUpdateAsync
117+
```
118+
119+
increments stock for all visible products below the threshold in one
120+
set-based command.
121+
122+
The application does not load those products before updating them.
123+
124+
## Change-tracker caveat
125+
126+
`ExecuteUpdateAsync` does not synchronize entity instances that were already
127+
tracked by the DbContext.
128+
129+
The test suite deliberately proves this behavior.
130+
131+
Use a clear context boundary, reload, or clear tracking when mixing set-based
132+
operations with tracked entities.
133+
134+
## Set-based delete
135+
136+
The soft-deleted row is hidden by default.
137+
138+
The purge operation explicitly disables the named soft-delete filter and calls:
139+
140+
```text
141+
ExecuteDeleteAsync
142+
```
143+
144+
The affected-row count is asserted.
145+
146+
## Deterministic output
147+
148+
```text
149+
EF Core Hot-Path Query Lab
150+
Visible products: 4
151+
Compiled lookup: SKU-003 | Dock | 12900 cents
152+
Restocked products: 3
153+
Purged soft-deleted products: 1
154+
Final rows: 4
155+
```
156+
157+
## Restore, build, and test
158+
159+
```powershell
160+
dotnet restore `
161+
.\EfCoreHotPathMinimal.slnx
162+
163+
dotnet build `
164+
.\EfCoreHotPathMinimal.slnx `
165+
--configuration Release `
166+
--no-restore
167+
168+
dotnet test `
169+
.\EfCoreHotPathMinimal.slnx `
170+
--configuration Release `
171+
--no-build
172+
```
173+
174+
## Run
175+
176+
```powershell
177+
dotnet run `
178+
--project .\src\EfCoreHotPathMinimal\EfCoreHotPathMinimal.csproj `
179+
--configuration Release `
180+
--no-build
181+
```
182+
183+
## Deliberately omitted
184+
185+
- SQL Server;
186+
- PostgreSQL;
187+
- temporal tables;
188+
- JSON columns;
189+
- owned/complex types;
190+
- value converters;
191+
- encryption;
192+
- concurrency tokens;
193+
- migrations;
194+
- Testcontainers;
195+
- split queries;
196+
- logging/interceptors;
197+
- benchmarking.
198+
199+
The complete tutorial covers the broader modeling and provider-specific
200+
concepts.
201+
202+
## Verification
203+
204+
- Target framework: .NET 10
205+
- EF Core provider: SQLite 10.0.11
206+
- Direct application packages: 1
207+
- Tests: 8
208+
- External services: none
209+
- Generated database files: none
210+
- Benchmarks: none
211+
- Last reviewed: 2026-08-14

0 commit comments

Comments
 (0)