From f5cf2ff58b5de949a0ce000fec9819546b1f2f6b Mon Sep 17 00:00:00 2001 From: BALAJI BASHYAM Date: Fri, 14 Aug 2026 14:53:13 +0000 Subject: [PATCH 1/3] Add lightweight EF Core hot-path sample --- .../EfCoreHotPathMinimal.slnx | 8 + .../advanced-modeling-performance/README.md | 211 +++++++++ .../Data/CatalogDatabase.cs | 139 ++++++ .../Data/CatalogDbContext.cs | 56 +++ .../EfCoreHotPathMinimal.csproj | 17 + .../EfCoreHotPathMinimal/Models/Product.cs | 40 ++ .../Models/ProductSummary.cs | 7 + .../src/EfCoreHotPathMinimal/Program.cs | 25 + .../Queries/CatalogQueries.cs | 86 ++++ .../Services/CatalogWorkflow.cs | 108 +++++ .../EfCoreHotPathMinimal.Tests.csproj | 46 ++ .../EfCoreHotPathTests.cs | 426 ++++++++++++++++++ 12 files changed, 1169 insertions(+) create mode 100644 ef-core/advanced-modeling-performance/EfCoreHotPathMinimal.slnx create mode 100644 ef-core/advanced-modeling-performance/README.md create mode 100644 ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Data/CatalogDatabase.cs create mode 100644 ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Data/CatalogDbContext.cs create mode 100644 ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/EfCoreHotPathMinimal.csproj create mode 100644 ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Models/Product.cs create mode 100644 ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Models/ProductSummary.cs create mode 100644 ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Program.cs create mode 100644 ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Queries/CatalogQueries.cs create mode 100644 ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Services/CatalogWorkflow.cs create mode 100644 ef-core/advanced-modeling-performance/tests/EfCoreHotPathMinimal.Tests/EfCoreHotPathMinimal.Tests.csproj create mode 100644 ef-core/advanced-modeling-performance/tests/EfCoreHotPathMinimal.Tests/EfCoreHotPathTests.cs diff --git a/ef-core/advanced-modeling-performance/EfCoreHotPathMinimal.slnx b/ef-core/advanced-modeling-performance/EfCoreHotPathMinimal.slnx new file mode 100644 index 0000000..370b5aa --- /dev/null +++ b/ef-core/advanced-modeling-performance/EfCoreHotPathMinimal.slnx @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ef-core/advanced-modeling-performance/README.md b/ef-core/advanced-modeling-performance/README.md new file mode 100644 index 0000000..deaf681 --- /dev/null +++ b/ef-core/advanced-modeling-performance/README.md @@ -0,0 +1,211 @@ +# EF Core Hot-Path Queries & Set-Based Operations + +A focused EF Core companion demonstrating a lean read path, a stable compiled +query, a named soft-delete query filter, and server-side set-based updates and +deletes without external database infrastructure. + +## Full tutorial + +[EF Core Advanced Modeling & Performance: Owned Types, Converters, JSON/Temporal Tables, Compiled Queries](https://www.dotnet-guide.com/tutorials/ef-core/advanced-modeling-performance/) + +## 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 +SQLite in-memory + -> named soft-delete filter + -> DTO projection + -> AsNoTracking + -> EF.CompileAsyncQuery + -> ExecuteUpdateAsync + -> ExecuteDeleteAsync + -> fresh-context verification +``` + +This sample is intentionally not a benchmark. + +## Why SQLite? + +SQLite gives the companion a real relational database without: + +- credentials; +- a server; +- Docker; +- Testcontainers; +- generated database files. + +The low-level in-memory connection remains open for the lifetime of each +scenario so multiple DbContext instances see the same database. + +SQLite is still a different provider from SQL Server and PostgreSQL. + +This sample does not validate provider-specific JSON, temporal-table, +concurrency-token, migration, query-plan, or batching behavior. + +## Soft-delete filter + +The companion targets EF Core 10 and uses a named query filter: + +```csharp +entity.HasQueryFilter( + "SoftDeleteFilter", + product => !product.IsDeleted); +``` + +The purge path selectively disables it: + +```csharp +IgnoreQueryFilters( + ["SoftDeleteFilter"]) +``` + +The EF Core 8 tutorial must continue to explain that older versions use one +combined filter expression when several filters apply to the same entity. + +## Projection and tracking + +The read path projects only: + +```text +Sku +Name +PriceCents +StockQuantity +``` + +into `ProductSummary`. + +It also uses `AsNoTracking` to make the read-only intent explicit. + +The tests verify that no entity entries remain in the DbContext change tracker. + +## Compiled query + +The sample uses one: + +```text +EF.CompileAsyncQuery +``` + +for a stable SKU lookup. + +This does not mean every query should be manually compiled. + +EF Core already caches ordinary queries by expression-tree shape. Explicit +compiled queries bypass the normal cache lookup and are intended for measured +hot paths with stable query shapes and simple scalar parameters. + +The sample makes no fixed latency-savings claim. + +## Set-based update + +```text +ExecuteUpdateAsync +``` + +increments stock for all visible products below the threshold in one +set-based command. + +The application does not load those products before updating them. + +## Change-tracker caveat + +`ExecuteUpdateAsync` does not synchronize entity instances that were already +tracked by the DbContext. + +The test suite deliberately proves this behavior. + +Use a clear context boundary, reload, or clear tracking when mixing set-based +operations with tracked entities. + +## Set-based delete + +The soft-deleted row is hidden by default. + +The purge operation explicitly disables the named soft-delete filter and calls: + +```text +ExecuteDeleteAsync +``` + +The affected-row count is asserted. + +## Deterministic output + +```text +EF Core Hot-Path Query Lab +Visible products: 4 +Compiled lookup: SKU-003 | Dock | 12900 cents +Restocked products: 3 +Purged soft-deleted products: 1 +Final rows: 4 +``` + +## Restore, build, and test + +```powershell +dotnet restore ` + .\EfCoreHotPathMinimal.slnx + +dotnet build ` + .\EfCoreHotPathMinimal.slnx ` + --configuration Release ` + --no-restore + +dotnet test ` + .\EfCoreHotPathMinimal.slnx ` + --configuration Release ` + --no-build +``` + +## Run + +```powershell +dotnet run ` + --project .\src\EfCoreHotPathMinimal\EfCoreHotPathMinimal.csproj ` + --configuration Release ` + --no-build +``` + +## Deliberately omitted + +- SQL Server; +- PostgreSQL; +- temporal tables; +- JSON columns; +- owned/complex types; +- value converters; +- encryption; +- concurrency tokens; +- migrations; +- Testcontainers; +- split queries; +- logging/interceptors; +- benchmarking. + +The complete tutorial covers the broader modeling and provider-specific +concepts. + +## 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 +- Benchmarks: none +- Last reviewed: 2026-08-14 \ No newline at end of file diff --git a/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Data/CatalogDatabase.cs b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Data/CatalogDatabase.cs new file mode 100644 index 0000000..f7f0de2 --- /dev/null +++ b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Data/CatalogDatabase.cs @@ -0,0 +1,139 @@ +using EfCoreHotPathMinimal.Models; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace EfCoreHotPathMinimal.Data; + +public sealed class CatalogDatabase : + IAsyncDisposable +{ + private readonly SqliteConnection + _connection; + + private readonly + DbContextOptions< + CatalogDbContext> + _options; + + private CatalogDatabase( + SqliteConnection connection, + DbContextOptions< + CatalogDbContext> + options) + { + _connection = + connection; + + _options = + options; + } + + public static async Task< + CatalogDatabase> + CreateAsync( + CancellationToken + cancellationToken = + default) + { + var connection = + new SqliteConnection( + "Data Source=:memory:"); + + await connection + .OpenAsync( + cancellationToken); + + try + { + DbContextOptions< + CatalogDbContext> + options = + new DbContextOptionsBuilder< + CatalogDbContext>() + .UseSqlite( + connection) + .EnableDetailedErrors() + .Options; + + var database = + new CatalogDatabase( + connection, + options); + + await using + CatalogDbContext context = + database + .CreateContext(); + + await context.Database + .EnsureCreatedAsync( + cancellationToken); + + context.Products.AddRange( + new Product + { + Id = 1, + Sku = "SKU-001", + Name = "Keyboard", + PriceCents = 7_500, + StockQuantity = 2 + }, + new Product + { + Id = 2, + Sku = "SKU-002", + Name = "Mouse", + PriceCents = 3_500, + StockQuantity = 8 + }, + new Product + { + Id = 3, + Sku = "SKU-003", + Name = "Dock", + PriceCents = 12_900, + StockQuantity = 1 + }, + new Product + { + Id = 4, + Sku = "SKU-004", + Name = "Webcam", + PriceCents = 6_500, + StockQuantity = 0, + IsDeleted = true + }, + new Product + { + Id = 5, + Sku = "SKU-005", + Name = "Stand", + PriceCents = 4_500, + StockQuantity = 4 + }); + + await context + .SaveChangesAsync( + cancellationToken); + + return database; + } + catch + { + await connection + .DisposeAsync(); + + throw; + } + } + + public CatalogDbContext + CreateContext() => + new( + _options); + + public ValueTask + DisposeAsync() => + _connection + .DisposeAsync(); +} \ No newline at end of file diff --git a/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Data/CatalogDbContext.cs b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Data/CatalogDbContext.cs new file mode 100644 index 0000000..9184723 --- /dev/null +++ b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Data/CatalogDbContext.cs @@ -0,0 +1,56 @@ +using EfCoreHotPathMinimal.Models; +using Microsoft.EntityFrameworkCore; + +namespace EfCoreHotPathMinimal.Data; + +public sealed class CatalogDbContext( + DbContextOptions + options) : + DbContext( + options) +{ + public const string + SoftDeleteFilterName = + "SoftDeleteFilter"; + + public DbSet Products => + Set(); + + protected override void + OnModelCreating( + ModelBuilder modelBuilder) + { + modelBuilder + .Entity( + entity => + { + entity.HasKey( + product => + product.Id); + + entity.Property( + product => + product.Sku) + .HasMaxLength( + 32) + .IsRequired(); + + entity.Property( + product => + product.Name) + .HasMaxLength( + 120) + .IsRequired(); + + entity.HasIndex( + product => + product.Sku) + .IsUnique(); + + entity.HasQueryFilter( + SoftDeleteFilterName, + product => + !product.IsDeleted); + }); + } +} \ No newline at end of file diff --git a/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/EfCoreHotPathMinimal.csproj b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/EfCoreHotPathMinimal.csproj new file mode 100644 index 0000000..5342fe8 --- /dev/null +++ b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/EfCoreHotPathMinimal.csproj @@ -0,0 +1,17 @@ + + + + Exe + net10.0 + enable + enable + true + + + + + + + \ No newline at end of file diff --git a/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Models/Product.cs b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Models/Product.cs new file mode 100644 index 0000000..fd7d211 --- /dev/null +++ b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Models/Product.cs @@ -0,0 +1,40 @@ +namespace EfCoreHotPathMinimal.Models; + +public sealed class Product +{ + public int Id + { + get; + set; + } + + public string Sku + { + get; + set; + } = ""; + + public string Name + { + get; + set; + } = ""; + + public int PriceCents + { + get; + set; + } + + public int StockQuantity + { + get; + set; + } + + public bool IsDeleted + { + get; + set; + } +} \ No newline at end of file diff --git a/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Models/ProductSummary.cs b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Models/ProductSummary.cs new file mode 100644 index 0000000..af08c37 --- /dev/null +++ b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Models/ProductSummary.cs @@ -0,0 +1,7 @@ +namespace EfCoreHotPathMinimal.Models; + +public sealed record ProductSummary( + string Sku, + string Name, + int PriceCents, + int StockQuantity); \ No newline at end of file diff --git a/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Program.cs b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Program.cs new file mode 100644 index 0000000..b657241 --- /dev/null +++ b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Program.cs @@ -0,0 +1,25 @@ +using EfCoreHotPathMinimal.Services; + +CatalogWorkflowResult result = + await CatalogWorkflow + .RunAsync(); + +Console.WriteLine( + "EF Core Hot-Path Query Lab"); + +Console.WriteLine( + $"Visible products: {result.VisibleProducts}"); + +Console.WriteLine( + $"Compiled lookup: {result.CompiledLookup.Sku} | " + + $"{result.CompiledLookup.Name} | " + + $"{result.CompiledLookup.PriceCents} cents"); + +Console.WriteLine( + $"Restocked products: {result.RestockedProducts}"); + +Console.WriteLine( + $"Purged soft-deleted products: {result.PurgedProducts}"); + +Console.WriteLine( + $"Final rows: {result.FinalRows}"); \ No newline at end of file diff --git a/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Queries/CatalogQueries.cs b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Queries/CatalogQueries.cs new file mode 100644 index 0000000..207ac53 --- /dev/null +++ b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Queries/CatalogQueries.cs @@ -0,0 +1,86 @@ +using EfCoreHotPathMinimal.Data; +using EfCoreHotPathMinimal.Models; +using Microsoft.EntityFrameworkCore; + +namespace EfCoreHotPathMinimal.Queries; + +public static class CatalogQueries +{ + private static readonly + Func< + CatalogDbContext, + string, + IAsyncEnumerable< + ProductSummary>> + FindBySkuCompiled = + EF.CompileAsyncQuery( + ( + CatalogDbContext + context, + string sku) => + context.Products + .AsNoTracking() + .Where( + product => + product.Sku + == sku) + .OrderBy( + product => + product.Id) + .Select( + product => + new ProductSummary( + product.Sku, + product.Name, + product.PriceCents, + product.StockQuantity)) + .Take( + 1)); + + public static Task< + List> + ListVisibleAsync( + CatalogDbContext + context, + CancellationToken + cancellationToken = + default) => + context.Products + .AsNoTracking() + .OrderBy( + product => + product.Id) + .Select( + product => + new ProductSummary( + product.Sku, + product.Name, + product.PriceCents, + product.StockQuantity)) + .ToListAsync( + cancellationToken); + + public static async Task< + ProductSummary?> + FindBySkuAsync( + CatalogDbContext + context, + string sku, + CancellationToken + cancellationToken = + default) + { + await foreach ( + ProductSummary product + in FindBySkuCompiled( + context, + sku) + .WithCancellation( + cancellationToken)) + { + return product; + } + + return null; + } +} \ No newline at end of file diff --git a/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Services/CatalogWorkflow.cs b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Services/CatalogWorkflow.cs new file mode 100644 index 0000000..2365a2b --- /dev/null +++ b/ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/Services/CatalogWorkflow.cs @@ -0,0 +1,108 @@ +using EfCoreHotPathMinimal.Data; +using EfCoreHotPathMinimal.Models; +using EfCoreHotPathMinimal.Queries; +using Microsoft.EntityFrameworkCore; + +namespace EfCoreHotPathMinimal.Services; + +public sealed record + CatalogWorkflowResult( + int VisibleProducts, + ProductSummary + CompiledLookup, + int RestockedProducts, + int PurgedProducts, + int FinalRows); + +public static class CatalogWorkflow +{ + public static async Task< + CatalogWorkflowResult> + RunAsync( + CancellationToken + cancellationToken = + default) + { + await using + CatalogDatabase database = + await CatalogDatabase + .CreateAsync( + cancellationToken); + + await using + CatalogDbContext context = + database + .CreateContext(); + + List + visible = + await CatalogQueries + .ListVisibleAsync( + context, + cancellationToken); + + ProductSummary lookup = + await CatalogQueries + .FindBySkuAsync( + context, + "SKU-003", + cancellationToken) + ?? throw new InvalidOperationException( + "Seeded SKU-003 was not found."); + + int restocked = + await context.Products + .Where( + product => + product.StockQuantity + < 5) + .ExecuteUpdateAsync( + setters => + setters.SetProperty( + product => + product.StockQuantity, + product => + product.StockQuantity + + 5), + cancellationToken); + + int purged = + await context.Products + .IgnoreQueryFilters( + [ + CatalogDbContext + .SoftDeleteFilterName + ]) + .Where( + product => + product.IsDeleted) + .ExecuteDeleteAsync( + cancellationToken); + + int finalRows = + await context.Products + .IgnoreQueryFilters( + [ + CatalogDbContext + .SoftDeleteFilterName + ]) + .CountAsync( + cancellationToken); + + return new CatalogWorkflowResult( + VisibleProducts: + visible.Count, + + CompiledLookup: + lookup, + + RestockedProducts: + restocked, + + PurgedProducts: + purged, + + FinalRows: + finalRows); + } +} \ No newline at end of file diff --git a/ef-core/advanced-modeling-performance/tests/EfCoreHotPathMinimal.Tests/EfCoreHotPathMinimal.Tests.csproj b/ef-core/advanced-modeling-performance/tests/EfCoreHotPathMinimal.Tests/EfCoreHotPathMinimal.Tests.csproj new file mode 100644 index 0000000..2748df6 --- /dev/null +++ b/ef-core/advanced-modeling-performance/tests/EfCoreHotPathMinimal.Tests/EfCoreHotPathMinimal.Tests.csproj @@ -0,0 +1,46 @@ + + + + net10.0 + enable + enable + true + false + true + Exe + + + + + + + + + all + + runtime; + build; + native; + contentfiles; + analyzers; + buildtransitive + + + + + + + + + + + + + \ No newline at end of file diff --git a/ef-core/advanced-modeling-performance/tests/EfCoreHotPathMinimal.Tests/EfCoreHotPathTests.cs b/ef-core/advanced-modeling-performance/tests/EfCoreHotPathMinimal.Tests/EfCoreHotPathTests.cs new file mode 100644 index 0000000..0aaff9a --- /dev/null +++ b/ef-core/advanced-modeling-performance/tests/EfCoreHotPathMinimal.Tests/EfCoreHotPathTests.cs @@ -0,0 +1,426 @@ +using EfCoreHotPathMinimal.Data; +using EfCoreHotPathMinimal.Models; +using EfCoreHotPathMinimal.Queries; +using EfCoreHotPathMinimal.Services; +using Microsoft.EntityFrameworkCore; + +namespace EfCoreHotPathMinimal.Tests; + +public sealed class EfCoreHotPathTests +{ + [Fact] + public async Task + Global_filter_excludes_soft_deleted_rows() + { + CancellationToken token = + TestContext.Current + .CancellationToken; + + await using + CatalogDatabase database = + await CatalogDatabase + .CreateAsync( + token); + + await using + CatalogDbContext context = + database + .CreateContext(); + + int visible = + await context.Products + .CountAsync( + token); + + int all = + await context.Products + .IgnoreQueryFilters( + [ + CatalogDbContext + .SoftDeleteFilterName + ]) + .CountAsync( + token); + + Assert.Equal( + 4, + visible); + + Assert.Equal( + 5, + all); + } + + [Fact] + public async Task + Read_projection_does_not_track_entities() + { + CancellationToken token = + TestContext.Current + .CancellationToken; + + await using + CatalogDatabase database = + await CatalogDatabase + .CreateAsync( + token); + + await using + CatalogDbContext context = + database + .CreateContext(); + + List + products = + await CatalogQueries + .ListVisibleAsync( + context, + token); + + Assert.Equal( + 4, + products.Count); + + Assert.Empty( + context.ChangeTracker + .Entries()); + } + + [Fact] + public async Task + Compiled_lookup_returns_projected_product() + { + CancellationToken token = + TestContext.Current + .CancellationToken; + + await using + CatalogDatabase database = + await CatalogDatabase + .CreateAsync( + token); + + await using + CatalogDbContext context = + database + .CreateContext(); + + ProductSummary? product = + await CatalogQueries + .FindBySkuAsync( + context, + "SKU-003", + token); + + Assert.NotNull( + product); + + Assert.Equal( + "Dock", + product.Name); + + Assert.Equal( + 12_900, + product.PriceCents); + + Assert.Equal( + 1, + product.StockQuantity); + + Assert.Empty( + context.ChangeTracker + .Entries()); + } + + [Fact] + public async Task + Compiled_lookup_respects_soft_delete_filter() + { + CancellationToken token = + TestContext.Current + .CancellationToken; + + await using + CatalogDatabase database = + await CatalogDatabase + .CreateAsync( + token); + + await using + CatalogDbContext context = + database + .CreateContext(); + + ProductSummary? product = + await CatalogQueries + .FindBySkuAsync( + context, + "SKU-004", + token); + + Assert.Null( + product); + + bool rowExists = + await context.Products + .IgnoreQueryFilters( + [ + CatalogDbContext + .SoftDeleteFilterName + ]) + .AnyAsync( + candidate => + candidate.Sku + == "SKU-004", + token); + + Assert.True( + rowExists); + } + + [Fact] + public async Task + ExecuteUpdate_updates_matching_rows_and_returns_count() + { + CancellationToken token = + TestContext.Current + .CancellationToken; + + await using + CatalogDatabase database = + await CatalogDatabase + .CreateAsync( + token); + + await using + CatalogDbContext context = + database + .CreateContext(); + + int updated = + await context.Products + .Where( + product => + product.StockQuantity + < 5) + .ExecuteUpdateAsync( + setters => + setters.SetProperty( + product => + product.StockQuantity, + product => + product.StockQuantity + + 5), + token); + + Assert.Equal( + 3, + updated); + + await using + CatalogDbContext verification = + database + .CreateContext(); + + Dictionary< + string, + int> + stock = + await verification + .Products + .AsNoTracking() + .ToDictionaryAsync( + product => + product.Sku, + product => + product.StockQuantity, + token); + + Assert.Equal( + 7, + stock["SKU-001"]); + + Assert.Equal( + 8, + stock["SKU-002"]); + + Assert.Equal( + 6, + stock["SKU-003"]); + + Assert.Equal( + 9, + stock["SKU-005"]); + } + + [Fact] + public async Task + ExecuteUpdate_does_not_synchronize_pretracked_entity() + { + CancellationToken token = + TestContext.Current + .CancellationToken; + + await using + CatalogDatabase database = + await CatalogDatabase + .CreateAsync( + token); + + await using + CatalogDbContext context = + database + .CreateContext(); + + Product tracked = + await context.Products + .SingleAsync( + product => + product.Sku + == "SKU-001", + token); + + Assert.Equal( + 2, + tracked.StockQuantity); + + int updated = + await context.Products + .Where( + product => + product.Sku + == "SKU-001") + .ExecuteUpdateAsync( + setters => + setters.SetProperty( + product => + product.StockQuantity, + product => + product.StockQuantity + + 10), + token); + + Assert.Equal( + 1, + updated); + + Assert.Equal( + 2, + tracked.StockQuantity); + + await using + CatalogDbContext verification = + database + .CreateContext(); + + int stored = + await verification + .Products + .AsNoTracking() + .Where( + product => + product.Sku + == "SKU-001") + .Select( + product => + product.StockQuantity) + .SingleAsync( + token); + + Assert.Equal( + 12, + stored); + } + + [Fact] + public async Task + ExecuteDelete_can_purge_soft_deleted_rows_explicitly() + { + CancellationToken token = + TestContext.Current + .CancellationToken; + + await using + CatalogDatabase database = + await CatalogDatabase + .CreateAsync( + token); + + await using + CatalogDbContext context = + database + .CreateContext(); + + int deleted = + await context.Products + .IgnoreQueryFilters( + [ + CatalogDbContext + .SoftDeleteFilterName + ]) + .Where( + product => + product.IsDeleted) + .ExecuteDeleteAsync( + token); + + Assert.Equal( + 1, + deleted); + + await using + CatalogDbContext verification = + database + .CreateContext(); + + int all = + await verification + .Products + .IgnoreQueryFilters( + [ + CatalogDbContext + .SoftDeleteFilterName + ]) + .CountAsync( + token); + + Assert.Equal( + 4, + all); + } + + [Fact] + public async Task + Workflow_returns_deterministic_summary() + { + CatalogWorkflowResult result = + await CatalogWorkflow + .RunAsync( + TestContext.Current + .CancellationToken); + + Assert.Equal( + 4, + result.VisibleProducts); + + Assert.Equal( + "SKU-003", + result.CompiledLookup.Sku); + + Assert.Equal( + "Dock", + result.CompiledLookup.Name); + + Assert.Equal( + 3, + result.RestockedProducts); + + Assert.Equal( + 1, + result.PurgedProducts); + + Assert.Equal( + 4, + result.FinalRows); + } +} \ No newline at end of file From e6747f1f7b7a7b9ef7dcbeb5c5b0c1c713cef78e Mon Sep 17 00:00:00 2001 From: BALAJI BASHYAM Date: Fri, 14 Aug 2026 14:53:17 +0000 Subject: [PATCH 2/3] Add EF Core hot-path sample to repository README --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index e762b00..1cb95ea 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Each sample folder contains a focused implementation of one tutorial topic. The | [`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/) | | [`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/) | ## Companion articles - [Common Microsoft.Extensions.AI mistakes](https://www.dotnet-guide.com/articles/dotnet-ai/microsoft-extensions-ai-common-mistakes/) @@ -469,6 +470,28 @@ tutorials/ | `-- MinimalApiPipeline.Tests/ | |-- MinimalApiPipeline.Tests.csproj | `-- MinimalApiPipelineTests.cs +|-- ef-core/ +| `-- advanced-modeling-performance/ +| |-- EfCoreHotPathMinimal.slnx +| |-- README.md +| |-- src/ +| | `-- EfCoreHotPathMinimal/ +| | |-- EfCoreHotPathMinimal.csproj +| | |-- Program.cs +| | |-- Data/ +| | | |-- CatalogDatabase.cs +| | | `-- CatalogDbContext.cs +| | |-- Models/ +| | | |-- Product.cs +| | | `-- ProductSummary.cs +| | |-- Queries/ +| | | `-- CatalogQueries.cs +| | `-- Services/ +| | `-- CatalogWorkflow.cs +| `-- tests/ +| `-- EfCoreHotPathMinimal.Tests/ +| |-- EfCoreHotPathMinimal.Tests.csproj +| `-- EfCoreHotPathTests.cs |-- .github/ | `-- workflows/ | `-- build-samples.yml From 22d66792803214b72feb053360d4b8a36701e620 Mon Sep 17 00:00:00 2001 From: BALAJI BASHYAM Date: Fri, 14 Aug 2026 14:53:29 +0000 Subject: [PATCH 3/3] Test EF Core hot-path sample in GitHub Actions --- .github/workflows/build-samples.yml | 106 ++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/.github/workflows/build-samples.yml b/.github/workflows/build-samples.yml index 9a4836c..382d3c2 100644 --- a/.github/workflows/build-samples.yml +++ b/.github/workflows/build-samples.yml @@ -27,6 +27,7 @@ on: - "dotnet-8-essentials/configuration-secrets-environments/**" - "dotnet-8-essentials/core-features-get-started/**" - "dotnet-8-essentials/observability-opentelemetry/**" + - "ef-core/advanced-modeling-performance/**" - ".github/workflows/build-samples.yml" pull_request: @@ -54,6 +55,7 @@ on: - "dotnet-8-essentials/configuration-secrets-environments/**" - "dotnet-8-essentials/core-features-get-started/**" - "dotnet-8-essentials/observability-opentelemetry/**" + - "ef-core/advanced-modeling-performance/**" - ".github/workflows/build-samples.yml" workflow_dispatch: @@ -1561,3 +1563,107 @@ jobs: grep --fixed-strings --quiet 'Checkout.Process' "${app_log}" grep --fixed-strings --quiet 'checkout.requests' "${app_log}" grep --fixed-strings --quiet 'Checkout accepted' "${app_log}" + + test-ef-core-hot-path: + name: Test EF Core hot-path 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/advanced-modeling-performance/EfCoreHotPathMinimal.slnx + + - name: Build + run: > + dotnet build + ef-core/advanced-modeling-performance/EfCoreHotPathMinimal.slnx + --configuration Release + --no-restore + + - name: Test + run: > + dotnet test + ef-core/advanced-modeling-performance/EfCoreHotPathMinimal.slnx + --configuration Release + --no-build + + - name: Verify EF Core package baseline + shell: bash + run: | + project="ef-core/advanced-modeling-performance/src/EfCoreHotPathMinimal/EfCoreHotPathMinimal.csproj" + + test "$( + grep \ + --count \ + '