From c9416b4df309c229e73ddc9b6e200626059b77f0 Mon Sep 17 00:00:00 2001 From: Steven Hoang Date: Thu, 26 Mar 2026 17:17:44 +0800 Subject: [PATCH] update packages --- .claude/settings.local.json | 8 + .../AspCore.Extensions.Tests.csproj | 2 - ...spCore.Idempotency.MsSqlStore.Tests.csproj | 2 - .../AspCore.Idempotency.Tests.csproj | 2 - .../AspCore.Tasks.Tests.csproj | 1 - src/AspNet/CONCURRENT_IDEMPOTENCY_FIX.md | 176 ---------- ...KNet.AspCore.Idempotency.MsSqlStore.csproj | 5 +- .../Aspire.Hosting.ServiceBus.csproj | 3 - .../Fw.Extensions.Tests.csproj | 1 - .../RandomCreatorTests.csproj | 3 +- src/DKNet.FW.sln | 1 - src/DKNet.FW.sln.DotSettings.user | 62 +++- src/Directory.Build.props | 3 - src/Directory.Packages.props | 320 +++++++++--------- .../DKNet.EfCore.DtoEntities.csproj | 2 - .../DKNet.EfCore.DtoGenerator.csproj | 7 +- .../EfCore.DtoGenerator.Tests.csproj | 12 +- .../EfCore.Events.Tests.csproj | 1 - 18 files changed, 239 insertions(+), 372 deletions(-) create mode 100644 .claude/settings.local.json delete mode 100644 src/AspNet/CONCURRENT_IDEMPOTENCY_FIX.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..9dda0772 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet --version)", + "Bash(dotnet sdk:*)" + ] + } +} diff --git a/src/AspNet/AspCore.Extensions.Tests/AspCore.Extensions.Tests.csproj b/src/AspNet/AspCore.Extensions.Tests/AspCore.Extensions.Tests.csproj index 1663ddce..c0579d89 100644 --- a/src/AspNet/AspCore.Extensions.Tests/AspCore.Extensions.Tests.csproj +++ b/src/AspNet/AspCore.Extensions.Tests/AspCore.Extensions.Tests.csproj @@ -1,8 +1,6 @@  - enable - enable false false diff --git a/src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/AspCore.Idempotency.MsSqlStore.Tests.csproj b/src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/AspCore.Idempotency.MsSqlStore.Tests.csproj index b0b8b6a5..d99b75ec 100644 --- a/src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/AspCore.Idempotency.MsSqlStore.Tests.csproj +++ b/src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/AspCore.Idempotency.MsSqlStore.Tests.csproj @@ -1,8 +1,6 @@  - enable - enable false false diff --git a/src/AspNet/AspCore.Idempotency.Tests/AspCore.Idempotency.Tests.csproj b/src/AspNet/AspCore.Idempotency.Tests/AspCore.Idempotency.Tests.csproj index 2f78c579..9e48486d 100644 --- a/src/AspNet/AspCore.Idempotency.Tests/AspCore.Idempotency.Tests.csproj +++ b/src/AspNet/AspCore.Idempotency.Tests/AspCore.Idempotency.Tests.csproj @@ -1,8 +1,6 @@ - enable - enable false false diff --git a/src/AspNet/AspCore.Tasks.Tests/AspCore.Tasks.Tests.csproj b/src/AspNet/AspCore.Tasks.Tests/AspCore.Tasks.Tests.csproj index edde451d..7e9b9b59 100644 --- a/src/AspNet/AspCore.Tasks.Tests/AspCore.Tasks.Tests.csproj +++ b/src/AspNet/AspCore.Tasks.Tests/AspCore.Tasks.Tests.csproj @@ -2,7 +2,6 @@ false - default false false diff --git a/src/AspNet/CONCURRENT_IDEMPOTENCY_FIX.md b/src/AspNet/CONCURRENT_IDEMPOTENCY_FIX.md deleted file mode 100644 index 615993c6..00000000 --- a/src/AspNet/CONCURRENT_IDEMPOTENCY_FIX.md +++ /dev/null @@ -1,176 +0,0 @@ -# Concurrent Request Handling in Idempotency Implementation - -## Problem Identified - -The original test `CreateItem_ConcurrentRequestsWithSameKey_OnlyOneProcessed` assumed that when 5 concurrent requests arrived with the same idempotency key, only one would be processed. However, due to a **race condition** in the check-then-act pattern, this wasn't guaranteed: - -``` -Timeline of 5 concurrent requests with same key: -───────────────────────────────────────────────────── - -T1: Request A checks cache → "NOT FOUND" -T2: Request B checks cache → "NOT FOUND" -T3: Request C checks cache → "NOT FOUND" -T4: Request D checks cache → "NOT FOUND" -T5: Request E checks cache → "NOT FOUND" - -T6: Request A executes handler -T7: Request B executes handler ← All 5 execute! -T8: Request C executes handler -T9: Request D executes handler -T10: Request E executes handler - -T11: Request A tries to insert → SUCCESS -T12: Request B tries to insert → DUPLICATE KEY VIOLATION -T13: Request C tries to insert → DUPLICATE KEY VIOLATION -... -``` - -All 5 requests would pass the validation and execute the handler before any of them marked the key as processed. - -## Root Cause - -1. **No database-level constraint** - Without a unique index on `CompositeKey`, multiple inserts could succeed -2. **Check-then-act race condition** - The filter checks cache, finds nothing, then executes, but concurrent requests do the same before the first one caches - -## Solution Implemented - -### 1. **Add Unique Constraint on CompositeKey** (Database Level) -**File**: `IdempotencyKeyConfiguration.cs` - -```csharp -// Unique constraint on CompositeKey to prevent race conditions -builder.HasIndex(k => k.CompositeKey) - .IsUnique() - .HasDatabaseName("UX_CompositeKey"); -``` - -This ensures the database itself rejects duplicate keys, preventing multiple entries for the same idempotent operation. - -### 2. **Handle Unique Constraint Violations Gracefully** (Application Level) -**File**: `IdempotencySqlServerStore.cs` - -```csharp -public async ValueTask MarkKeyAsProcessedAsync(IdempotentKeyInfo keyInfo, CachedResponse cachedResponse) -{ - try - { - // ... insert logic ... - await dbContext.SaveChangesAsync().ConfigureAwait(false); - } - catch (DbUpdateException ex) when (ex.InnerException?.Message.Contains("UNIQUE") == true) - { - // Another concurrent request already inserted this key - safe to ignore - logger.LogInformation( - "Idempotency key already processed by concurrent request: {Key}. Continuing."); - } -} -``` - -When a concurrent request tries to insert a key that another request just inserted, the database constraint violation is caught and logged, then safely ignored. - -### 3. **Update Test Expectations** (Test Level) -**File**: `IdempotencyIntegrationTests.cs` - -The test now correctly reflects realistic concurrent behavior: - -```csharp -[Fact] -public async Task CreateItem_ConcurrentRequestsWithSameKey_OnlyOneProcessed() -{ - // Send 5 concurrent requests with the same key - var responses = await Task.WhenAll(tasks); - - // With concurrent requests, some or all may succeed (201) or get conflict (409) - var successCount = responses.Count(r => r.StatusCode == HttpStatusCode.Created); - var conflictCount = responses.Count(r => r.StatusCode == HttpStatusCode.Conflict); - - // At least one must succeed - successCount.ShouldBeGreaterThanOrEqualTo(1); - - // Verify only ONE entry in database (unique constraint enforces this) - var count = await dbContext.IdempotencyKeys - .CountAsync(k => k.IdempotentKey == idempotencyKey && ...); - count.ShouldBe(1, "Unique constraint should prevent duplicate idempotency keys"); -} -``` - -## How It Works Now - -### Successful Scenario -``` -Concurrent Requests (Same Key): -├─ Request A: Check (✓), Execute (✓), Insert (✓ WINS) -├─ Request B: Check (✓), Execute (✓), Insert (✗ CONSTRAINT), Continue (✓) -├─ Request C: Check (✓), Execute (✓), Insert (✗ CONSTRAINT), Continue (✓) -├─ Request D: Check (✓), Execute (✓), Insert (✗ CONSTRAINT), Continue (✓) -└─ Request E: Check (✓), Execute (✓), Insert (✗ CONSTRAINT), Continue (✓) - -Result: ✓ All 5 requests succeed (201 or 409) - ✓ Only 1 entry in database - ✓ Unique constraint enforced -``` - -## Limitations & Trade-offs - -### Current Implementation (Without Distributed Locking) -- **Pro**: Simple, no external dependencies -- **Con**: Multiple requests may execute the handler before the first one caches - - Better than caching *wrong* result - - Acceptable for most use cases (business logic handles duplicates) - -### Ideal Implementation (With Distributed Lock) -- Would require Redis/Memcached distributed locking -- Only 1 request would acquire lock and execute -- Others wait for result then return cached response -- **Not implemented** - adds complexity, single-instance deployments need this less - -## Best Practices for Consumers - -### 1. **Idempotent Handler Logic** -Ensure your handlers are idempotent - if they execute multiple times with the same input, the result is the same: - -```csharp -public async Task CreateOrderAsync(CreateOrderRequest request) -{ - // Good: Checking for existing order makes this idempotent - var existing = await _repo.FindByKeyAsync(request.IdempotencyKey); - if (existing != null) return Ok(existing); - - var order = new Order(request.Data); - await _repo.AddAsync(order); - return Created($"/orders/{order.Id}", order); -} -``` - -### 2. **Configuration** -```csharp -builder.Services.AddIdempotency(options => -{ - // Return conflict for duplicates (recommended for most APIs) - options.ConflictHandling = IdempotentConflictHandling.ConflictResponse; - - // TTL for cached responses - options.Expiration = TimeSpan.FromHours(4); -}); -``` - -## Testing Concurrency - -Run the integration test to verify: -```bash -dotnet test AspCore.Idempotency.MsSqlStore.Tests/... -``` - -The test validates: -- ✓ All concurrent requests complete successfully -- ✓ Only one database entry exists -- ✓ Unique constraint is enforced -- ✓ Violation is handled gracefully - -## References - -- **Issue**: Race condition in check-then-act pattern -- **Solution Type**: Database-level constraint + Application-level exception handling -- **Pattern**: Optimistic concurrency with conflict resolution -- **Standard**: HTTP 409 Conflict for duplicate requests diff --git a/src/AspNet/DKNet.AspCore.Idempotency.MsSqlStore/DKNet.AspCore.Idempotency.MsSqlStore.csproj b/src/AspNet/DKNet.AspCore.Idempotency.MsSqlStore/DKNet.AspCore.Idempotency.MsSqlStore.csproj index 89251a80..29d9313d 100644 --- a/src/AspNet/DKNet.AspCore.Idempotency.MsSqlStore/DKNet.AspCore.Idempotency.MsSqlStore.csproj +++ b/src/AspNet/DKNet.AspCore.Idempotency.MsSqlStore/DKNet.AspCore.Idempotency.MsSqlStore.csproj @@ -1,10 +1,9 @@  - enable - enable + true - + diff --git a/src/Aspire/Aspire.Hosting.ServiceBus/Aspire.Hosting.ServiceBus.csproj b/src/Aspire/Aspire.Hosting.ServiceBus/Aspire.Hosting.ServiceBus.csproj index 2b61baa2..0cb8c525 100644 --- a/src/Aspire/Aspire.Hosting.ServiceBus/Aspire.Hosting.ServiceBus.csproj +++ b/src/Aspire/Aspire.Hosting.ServiceBus/Aspire.Hosting.ServiceBus.csproj @@ -1,9 +1,6 @@  - default - enable - enable true false diff --git a/src/Core/Fw.Extensions.Tests/Fw.Extensions.Tests.csproj b/src/Core/Fw.Extensions.Tests/Fw.Extensions.Tests.csproj index 3a29dacd..48425c70 100644 --- a/src/Core/Fw.Extensions.Tests/Fw.Extensions.Tests.csproj +++ b/src/Core/Fw.Extensions.Tests/Fw.Extensions.Tests.csproj @@ -2,7 +2,6 @@ false - default false false diff --git a/src/Core/RandomCreatorTests/RandomCreatorTests.csproj b/src/Core/RandomCreatorTests/RandomCreatorTests.csproj index be365a74..f049b606 100644 --- a/src/Core/RandomCreatorTests/RandomCreatorTests.csproj +++ b/src/Core/RandomCreatorTests/RandomCreatorTests.csproj @@ -2,7 +2,7 @@ false - default + false false None @@ -10,6 +10,7 @@ false false false + $(NoWarn);CS1591;SA1600;CA1826 diff --git a/src/DKNet.FW.sln b/src/DKNet.FW.sln index 46c21cd2..868a17ca 100644 --- a/src/DKNet.FW.sln +++ b/src/DKNet.FW.sln @@ -57,7 +57,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Files", "_Files", "{9F7203 Directory.Packages.props = Directory.Packages.props coverage.runsettings = coverage.runsettings NugetLogo.png = NugetLogo.png - nuget.sh = nuget.sh ..\README.md = ..\README.md verify_nuget_package.sh = verify_nuget_package.sh ..\.github\copilot-instructions.md = ..\.github\copilot-instructions.md diff --git a/src/DKNet.FW.sln.DotSettings.user b/src/DKNet.FW.sln.DotSettings.user index 7495222a..18ad5218 100644 --- a/src/DKNet.FW.sln.DotSettings.user +++ b/src/DKNet.FW.sln.DotSettings.user @@ -585,17 +585,17 @@ </AssemblyExplorer> True True - <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &lt;AspNet&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <Project Location="/Users/steven/_CODE/DRUNK/DKNet/src" Presentation="&lt;AspNet&gt;" /> + <SessionState ContinuousTestingMode="0" Name="All tests from &lt;AspNet&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <Solution /> </SessionState> - <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &lt;AspNet&gt;\&lt;AspCore.Idempotency.Tests&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <SessionState ContinuousTestingMode="0" Name="All tests from &lt;AspNet&gt;\&lt;AspCore.Idempotency.Tests&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Or> <Project Location="/Users/steven/_CODE/DRUNK/DKNet/src/AspNet/AspCore.Idempotency.Tests" Presentation="&lt;AspNet&gt;\&lt;AspCore.Idempotency.Tests&gt;" /> <Project Location="/Users/steven/_CODE/DRUNK/DKNet/src/AspNet/AspCore.Idempotency.MsSqlStore.Tests" Presentation="&lt;AspNet&gt;\&lt;AspCore.Idempotency.MsSqlStore.Tests&gt;" /> </Or> </SessionState> - <SessionState ContinuousTestingMode="0" Name="Session" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <SessionState ContinuousTestingMode="0" IsActive="True" Name="Session" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Or> <Project Location="/Users/steven/_CODE/DRUNK/DKNet/src" Presentation="&lt;AspNet&gt;" /> <Project Location="/Users/steven/_CODE/DRUNK/DKNet/src" Presentation="&lt;Core&gt;" /> @@ -808,6 +808,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 3fa120b6..b7ba42cc 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -10,9 +10,6 @@ - - - diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 67f692d2..2110f423 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,169 +1,169 @@ - - net10.0 - Steven Hoang - https://drunkcoding.net - Copyright @ $([System.DateTime]::Now. Year) - https://github.com/baoduy/DKNet - https://github.com/baoduy/DKNet - git - aspnetcore,aws,azure,cloud,ddd,efcore,entityframework,events,extensions,generator,password,pdf,random,storage,string - + + net10.0 + Steven Hoang + https://drunkcoding.net + Copyright @ $([System.DateTime]::Now. Year) + https://github.com/baoduy/DKNet + https://github.com/baoduy/DKNet + git + aspnetcore,aws,azure,cloud,ddd,efcore,entityframework,events,extensions,generator,password,pdf,random,storage,string + DKNet is an enterprise-grade .NET library collection focused on advanced EF Core extensions, dynamic predicate building, and the Specification pattern. It provides production-ready tools for building robust, type-safe, and testable data access layers, including dynamic LINQ support, LinqKit integration. Designed for modern cloud-native applications, DKNet enforces strict code quality, async best practices, and full documentation for all public APIs. Enterprise-grade .NET library suite for modern application development, featuring advanced EF Core extensions (dynamic predicates, specifications, LinqKit), robust Domain-Driven Design (DDD) patterns, and domain event support. DKNet empowers scalable, maintainable, and testable solutions with type-safe validation, async/await, XML documentation, and high code quality standards. Ideal for cloud-native, microservices, and enterprise architectures. - false - README.md - default - en - MIT - NugetLogo.png - enable - true - false - true - true - All - true - true - - - true - true - true - snupkg - true - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - + false + README.md + default + en + MIT + NugetLogo.png + enable + true + false + true + true + All + true + true + + + true + true + true + snupkg + true + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + CA2208; ASPIRE002; CA1032; @@ -206,5 +206,5 @@ SA1636; CA1031; - + \ No newline at end of file diff --git a/src/EfCore/DKNet.EfCore.DtoEntities/DKNet.EfCore.DtoEntities.csproj b/src/EfCore/DKNet.EfCore.DtoEntities/DKNet.EfCore.DtoEntities.csproj index 9b5457f1..4eb4d673 100644 --- a/src/EfCore/DKNet.EfCore.DtoEntities/DKNet.EfCore.DtoEntities.csproj +++ b/src/EfCore/DKNet.EfCore.DtoEntities/DKNet.EfCore.DtoEntities.csproj @@ -1,8 +1,6 @@  - enable - enable false false diff --git a/src/EfCore/DKNet.EfCore.DtoGenerator/DKNet.EfCore.DtoGenerator.csproj b/src/EfCore/DKNet.EfCore.DtoGenerator/DKNet.EfCore.DtoGenerator.csproj index 16e59fcd..63765d71 100644 --- a/src/EfCore/DKNet.EfCore.DtoGenerator/DKNet.EfCore.DtoGenerator.csproj +++ b/src/EfCore/DKNet.EfCore.DtoGenerator/DKNet.EfCore.DtoGenerator.csproj @@ -3,9 +3,6 @@ netstandard2.0 - latest - enable - enable true true @@ -29,10 +26,10 @@ - + - + diff --git a/src/EfCore/EfCore.DtoGenerator.Tests/EfCore.DtoGenerator.Tests.csproj b/src/EfCore/EfCore.DtoGenerator.Tests/EfCore.DtoGenerator.Tests.csproj index 5de00df3..48ac8d12 100644 --- a/src/EfCore/EfCore.DtoGenerator.Tests/EfCore.DtoGenerator.Tests.csproj +++ b/src/EfCore/EfCore.DtoGenerator.Tests/EfCore.DtoGenerator.Tests.csproj @@ -1,8 +1,6 @@  - enable - enable false false @@ -17,7 +15,7 @@ - + CreatedBy,UpdatedBy,CreatedAt,UpdatedAt @@ -44,9 +42,13 @@ + + all + + - all - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/EfCore/EfCore.Events.Tests/EfCore.Events.Tests.csproj b/src/EfCore/EfCore.Events.Tests/EfCore.Events.Tests.csproj index 9f4fb22a..8b07c8d4 100644 --- a/src/EfCore/EfCore.Events.Tests/EfCore.Events.Tests.csproj +++ b/src/EfCore/EfCore.Events.Tests/EfCore.Events.Tests.csproj @@ -2,7 +2,6 @@ false - default false false