Skip to content

v10.1.0 - Channel-based write queue, BenchmarkDotNet suite, SEO docs refresh, and trust signals - #7

Merged
CornerstoneCode merged 16 commits into
mainfrom
IncreaseThroughput
Jul 6, 2026
Merged

v10.1.0 - Channel-based write queue, BenchmarkDotNet suite, SEO docs refresh, and trust signals#7
CornerstoneCode merged 16 commits into
mainfrom
IncreaseThroughput

Conversation

@CornerstoneCode

@CornerstoneCode CornerstoneCode commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Overview

This release delivers four categories of improvement to EntityFrameworkCore.Sqlite.Concurrency: a core throughput architecture change, a complete documentation and SEO overhaul, a real-world benchmark suite, and a community trust package.


What's Changed

Core: Channel-Based Write Queue

Replaces the per-database SemaphoreSlim(1,1) with a Channel<IWriteRequest>-based write queue backed by a single background writer task.

Why this matters: Under concurrent write load, SemaphoreSlim.Release() wakes all N waiters simultaneously. N-1 immediately go back to sleep. This thundering-herd pattern causes CPU spikes, unfair scheduling, and elevated context-switch counts at high concurrency. A Channel<T> parks callers in FIFO order and drains them one at a time, eliminating the wakeup storm entirely.

  • New SqliteWriteQueue class owns the Channel<IWriteRequest> and the background writer loop
  • WriteQueueCapacity option added to SqliteConcurrencyOptions: null = unbounded (default), integer = bounded channel with BoundedChannelFullMode.Wait for producer back-pressure
  • IsWriteLockHeld reentrancy guard moved from SqliteConnectionEnhancer into SqliteWriteQueue as AsyncLocal<bool> IsWriteLoopExecuting
  • Zero breaking changes: all existing public call sites compile and behave correctly without modification

Bug fixes included:

  • ThreadSafeSqliteContext.ExecuteWriteAsync was constructing new SqliteConcurrencyOptions(), ignoring registered options. Fixed via TryGetInterceptor lookup.
  • BulkInsertSafeAsync used Skip/Take in a loop (O(n^2) LINQ-to-objects). Replaced with Chunk(1000).
  • BulkInsertSafeAsync did not call ChangeTracker.Clear() between batches, causing memory to grow linearly with entity count. Now cleared after each batch SaveChangesAsync.

netstandard 2.0 TFM

The connection and queue layer (SqliteConnectionEnhancer, SqliteWriteQueue, SqliteConcurrencyOptions) now targets netstandard2.0 in addition to net10.0. EF Core-dependent APIs remain net10.0-only (EF Core 10 requires it).

BenchmarkDotNet Suite

New EFCore.Sqlite.Concurrency.Benchmarks project with three benchmark classes:

Class What it measures
BulkInsertBenchmarks SaveChangesAsync-per-entity vs BulkInsertOptimizedAsync at 1,000 and 10,000 entities
ConcurrentWriteBenchmarks Task.WhenAll with 10 and 50 concurrent writers via SaveChangesSerializedAsync
ReadBenchmarks 20 parallel CountAsync readers: default journal mode vs WAL mode

Measured results (BenchmarkDotNet v0.14.0, .NET 10.0.2, Windows 11, Intel i7-13700K):

Method EntityCount Baseline Package Ratio
Bulk insert 1,000 4,500 ms 28 ms ~161x faster
Concurrent writes (50 tasks) - throws SQLITE_BUSY completes cleanly N/A

Run: dotnet run -c Release --project EFCore.Sqlite.Concurrency.Benchmarks -- --job short

Documentation and SEO

Complete problem-first rewrite of all user-facing surfaces so the package is discoverable when developers search for SQLITE_BUSY C#, database is locked EF Core, SQLITE_BUSY_SNAPSHOT, and related terms:

  • README.md - leads with the exact exception string developers paste into search engines; includes real benchmark table, CI badge, configuration reference, and FAQ
  • doc/QUICKSTART.md - condensed two-pattern quick start
  • doc/v10_1_0.md - full release notes
  • docs/troubleshooting-sqlite-busy.md - deep dive on error codes 5, 517, and 6 with diagnostic checklist
  • docs/concurrent-efcore-patterns.md - Task.WhenAll, background service, and request-scoped patterns with working code
  • docs/performance-guide.md - WAL internals, Channel queue architecture, PRAGMA tuning table, real benchmark results, reproduce command
  • docs/migration-guide.md - step-by-step migration from plain UseSqlite, common pitfalls checklist

CI and Trust Signals

  • CI pipeline - test step added to .github/workflows/ci.yml with dorny/test-reporter@v1 for TRX result publishing
  • CHANGELOG.md - Keep a Changelog format, v10.0.0 through v10.1.0
  • CONTRIBUTING.md - prerequisites, build/test/benchmark commands, PR process, architecture invariants
  • SECURITY.md - supported versions table, vulnerability reporting contact
  • GitHub issue templates - structured bug report and feature request forms
  • Pull request template - type of change, testing checklist, CHANGELOG requirement, benchmark results section
  • NU1903 suppression - SQLitePCLRaw.lib.e_sqlite3 2.1.11 is both the latest published version and carries advisory GHSA-2m69-gcr7-jv3q; no patched release exists on NuGet yet. Advisory-scoped NuGetAuditSuppress added with a comment to remove once upstream ships a fix.

Test Plan

  • dotnet build -c Release - 0 errors, NU1903 warning resolved
  • dotnet test EFCore.Sqlite.Concurrency.Test/ --framework net10.0 - all 4 stress tests pass (50 concurrent writers, ThreadSafeSqliteContext, SaveChangesSerializedAsync, mixed read/write)
  • BenchmarkDotNet suite builds and runs - real numbers in docs/performance-guide.md
  • dotnet pack -c Release - package builds successfully

Breaking Changes

None. All existing public API signatures are preserved. WriteQueueCapacity is additive. The netstandard2.0 TFM is additive.

CornerstoneCode and others added 15 commits July 5, 2026 20:42
This commit fundamentally refactors the internal write serialization mechanism for SQLite from `SemaphoreSlim` to `System.Threading.Channels.Channel`.

This change allows for:
-   Improved write throughput and better resource utilization by processing writes through a single background task per database file.
-   Configurable back-pressure via the new `WriteQueueCapacity` option, enabling bounded queues to manage write load.
-   Broader compatibility for the core queue logic by multi-targeting the library to `netstandard2.0`.

Additionally, a new test project `EFCore.Sqlite.Concurrency.Test` has been introduced. This project contains comprehensive stress tests that verify the correctness and robustness of all concurrent write operations, including `SaveChangesSerializedAsync`, `BulkInsertOptimizedAsync`, and `ThreadSafeSqliteContext.ExecuteWriteAsync`, as well as mixed read/write scenarios. These tests run dozens of concurrent writers and readers to assert data integrity under heavy load.
Spec covers: problem-first README rewrite, csproj metadata update,
QUICKSTART rebrand, four new docs/ microsite pages, and v10.1.0
release notes. Primary SEO target: SQLITE_BUSY / database is locked
search terms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Updated description to lead with SQLITE_BUSY/SQLITE_BUSY_SNAPSHOT error
strings for NuGet/AI indexing. Expanded tags with error-5, channel-queue,
netstandard2, task-whenall. Release notes updated to v10.1.0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Leads with exact SQLITE_BUSY exception string. Error table maps each
exception to root cause and fix. FAQ adds 4 new entries covering
Channel queue, SQLITE_BUSY_SNAPSHOT semantics, netstandard2.0, overhead.
Benchmark table relabeled "Typical Results". Links to new docs/ pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oncurrency

Removes all references to old package name ThreadSafeEFCore.SQLite.
Problem-first opening, two registration patterns only, links to new
docs/ microsite pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Channel queue, WriteQueueCapacity, netstandard2.0 TFM, bug fixes for
ThreadSafeSqliteContext options and BulkInsertSafeAsync O(n^2)/ChangeTracker.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
troubleshooting-sqlite-busy.md — owns SQLITE_BUSY/517/SQLITE_LOCKED search terms
concurrent-efcore-patterns.md — owns DbContext thread-safety / Task.WhenAll terms
performance-guide.md         — owns bulk insert / WAL tuning / Channel queue terms
migration-guide.md           — owns migration from plain UseSqlite terms

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Runs xUnit stress tests on every push/PR. Publishes TRX results via
dorny/test-reporter so failures are visible as PR checks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…plate

CHANGELOG covers v10.0.0 through v10.1.0 in Keep a Changelog format.
CONTRIBUTING documents build/test/benchmark commands and PR checklist.
SECURITY documents vulnerability reporting process.
Issue templates (bug/feature) use GitHub form syntax with structured fields.
PR template includes type-of-change, testing, and breaking-change checklist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three benchmark classes:
- BulkInsertBenchmarks: BulkInsertOptimizedAsync vs SaveChanges-per-entity (1k, 10k records)
- ConcurrentWriteBenchmarks: 10 and 50 concurrent SaveChangesSerializedAsync writers
- ReadBenchmarks: 20 parallel readers, WAL vs default journal mode

Run: cd EFCore.Sqlite.Concurrency.Benchmarks && dotnet run -c Release -- --job short --filter *
Full precision: dotnet run -c Release -- --filter *

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds gitignore rules for EFCore.Sqlite.Concurrency.Benchmarks/bin,
obj, and BenchmarkDotNet.Artifacts/. Removes previously tracked build
artifacts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…esults

BulkInsertOptimizedAsync vs plain SaveChanges-per-entity (1000 entities):
Baseline: 4500 ms | Package: 28 ms -> ~161x faster (N=84, BDN v0.14.0)
Measured on .NET 10.0.2 / Windows 11 / Intel i7-13700K.

Also adds CI badge to README and Running the Benchmarks section to
docs/performance-guide.md with reproduce commands.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SQLitePCLRaw.lib.e_sqlite3 2.1.11 is both the latest available release
and the vulnerable one - no patched version exists on NuGet yet. The
vulnerability is in the bundled native SQLite binary and comes in as a
transitive dependency via Microsoft.Data.Sqlite.

Added NuGetAuditSuppress scoped to the exact advisory URL so all other
high-severity advisories still surface. Remove suppression once a fixed
version ships upstream.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@CornerstoneCode, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 020c1192-0b55-45d7-8ecc-8482ca398a4b

📥 Commits

Reviewing files that changed from the base of the PR and between 9fe1515 and a15a801.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • EFCore.Sqlite.Concurrency.Test/EFCore.Sqlite.Concurrency.Test.csproj
📝 Walkthrough

Walkthrough

This PR replaces semaphore-based SQLite write serialization with a Channel-based write queue, adds a WriteQueueCapacity option, and multi-targets net10.0/netstandard2.0. It adds new benchmark and stress-test projects, updates CI, and adds extensive CHANGELOG, README, and documentation content for the v10.1.0 release.

Changes

Core Write Queue Implementation

Layer / File(s) Summary
WriteQueueCapacity option and polyfills
EntityFrameworkCore.Sqlite.Concurrency/src/Models/SqliteConcurrencyOptions.cs, .../src/Polyfills.cs
Adds nullable WriteQueueCapacity with equality/hash updates and netstandard2.0 IsExternalInit/HashCode polyfills.
SqliteWriteQueue implementation
.../src/SqliteWriteQueue.cs
Implements a Channel-based single-writer queue with reentrancy-aware EnqueueAsync, background RunAsync loop, and DisposeAsync.
Connection enhancer wiring
.../src/SqliteConnectionEnhancer.cs
Adds a per-connection-string write queue cache/accessor, deprecates GetWriteLock, and switches to synchronous using disposal in checkpoint/migration methods.
Interceptor and extension methods
.../src/SqliteConcurrencyInterceptor.cs, .../src/SqliteConcurrencyExtensions.cs
Replaces manual lock acquisition in SaveChangesSerializedAsync/BulkInsertOptimizedAsync and the interceptor constructor with queue-based enqueueing.
ThreadSafeSqliteContext refactor
.../src/ThreadSafeSqliteContext.cs
Rewrites ExecuteWriteAsync and BulkInsertSafeAsync to use the write queue with retry/backoff, and resolves Options from a connection-string interceptor.
Multi-targeting and packaging
.../EFCore.Sqlite.Concurrency.csproj, .../packages.lock.json
Adds netstandard2.0 target, bumps version to 10.1.0, restructures TFM-conditional dependencies, and updates lock file resolutions.

Benchmark Suite

Layer / File(s) Summary
Benchmark models and project setup
EFCore.Sqlite.Concurrency.Benchmarks/BenchmarkEntity.cs, BaselineSqliteContext.cs, ConcurrentSqliteContext.cs, EFCore.Sqlite.Concurrency.Benchmarks.csproj, Program.cs
Adds the benchmark entity/context types, project file, and BenchmarkSwitcher entry point.
Bulk insert benchmarks
BulkInsertBenchmarks.cs
Compares per-entity SaveChangesAsync vs BulkInsertOptimizedAsync with setup/cleanup lifecycle.
Concurrent write benchmarks
ConcurrentWriteBenchmarks.cs
Runs parallel writers via SaveChangesSerializedAsync to measure throughput.
Parallel read benchmarks
ReadBenchmarks.cs
Compares baseline vs WAL-mode parallel CountAsync reads.

Concurrency Stress Test Suite

Layer / File(s) Summary
Test infra and helpers
EFCore.Sqlite.Concurrency.Test/StressEntity.cs, StressDbContext.cs, ThreadSafeSqliteStressContext.cs, TempDatabase.cs, EFCore.Sqlite.Concurrency.Test.csproj
Adds test entity/context types and a temp-database helper with cleanup logic.
Stress test cases
ConcurrencyStressTests.cs
Adds four xUnit tests validating write correctness under concurrency (SaveChangesSerializedAsync, BulkInsertOptimizedAsync, ExecuteWriteAsync, mixed reads/writes).
Solution wiring
EntityFrameworkCore.Sqlite.Concurrency.sln, ...sln.DotSettings.user
Registers the new test project in the solution and build configurations.

Documentation, CI, and Community Files

Layer / File(s) Summary
CI and gitignore
.github/workflows/ci.yml, .gitignore
Adds build/test/publish steps with TRX reporting and new ignore entries for benchmark/test artifacts.
Community/governance files
.github/ISSUE_TEMPLATE/*.yml, .github/PULL_REQUEST_TEMPLATE.md, CONTRIBUTING.md, SECURITY.md
Adds issue/PR templates, contribution guide, and security policy.
CHANGELOG
CHANGELOG.md
Adds full release history from 10.0.0 through 10.1.0.
README, QUICKSTART, release notes
README.md, doc/QUICKSTART.md, doc/v10_1_0.md
Rewrites README sections and adds quick-start and v10.1.0 release documentation.
New SEO documentation pages
docs/troubleshooting-sqlite-busy.md, docs/concurrent-efcore-patterns.md, docs/migration-guide.md, docs/performance-guide.md
Adds error troubleshooting, usage pattern, migration, and performance-tuning guides.
Planning/spec documents
docs/superpowers/plans/*.md, docs/superpowers/specs/*.md
Adds internal design-spec and implementation-plan documents for the docs/SEO and trust-signals initiatives.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main themes of the changeset and is specific enough for history scanning.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch IncreaseThroughput

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

dorny/test-reporter@v1 uses git ls-files on pull_request events and
never sees the runtime-generated .trx file, causing fail-on-empty to
kill the build even when all tests pass. The dotnet test exit code
already gates the build correctly so the reporter adds no value here.

Also adds NuGetAuditSuppress for GHSA-2m69-gcr7-jv3q to the test
project csproj to silence the SQLitePCLRaw.lib.e_sqlite3 NU1903
warning that was appearing during test project restore and build.
@CornerstoneCode
CornerstoneCode merged commit fc681a1 into main Jul 6, 2026
2 of 3 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/ci.yml (1)

10-48: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Tighten the workflow permissions.

This job inherits the default GITHUB_TOKEN scope, which is broader than this build/test/report flow needs. Add an explicit permissions block so the reporter gets only the access it needs and the rest stays read-only.

Suggested change
permissions:
  contents: read
  checks: write
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 10 - 48, The build-and-verify workflow
currently relies on the default GITHUB_TOKEN permissions, which are broader than
needed for the checkout/build/test/report flow. Add an explicit permissions
block to the build job in ci.yml so the job is read-only by default and only
grants the reporter the access it needs, using the build job and Publish Test
Results / dorny/test-reporter@v1 setup as the place to apply it.

Source: Linters/SAST tools

🧹 Nitpick comments (7)
docs/superpowers/specs/2026-07-05-trust-signals-design.md (1)

34-35: 🚀 Performance & Scalability | 🔵 Trivial

Keep a regression guard for the published benchmark numbers.

If the README is going to advertise real benchmark figures, excluding the benchmark project from CI means those numbers can drift without any automated signal. Consider at least a lightweight scheduled or smoke benchmark so the published results stay attributable to a known commit/environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-07-05-trust-signals-design.md` around lines 34 -
35, The spec currently excludes the benchmark project from CI, but the published
benchmark numbers need a regression guard so they do not drift unnoticed. Update
the trust-signals design to add a lightweight scheduled or smoke benchmark path
alongside the existing benchmark project, and reference the benchmark project /
README benchmark claims so the published figures stay tied to a known commit and
environment.
EntityFrameworkCore.Sqlite.Concurrency/src/SqliteWriteQueue.cs (1)

44-70: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

No supervision if the background writer loop dies.

RunAsync wraps each request's execution in try/finally that only resets IsWriteLockHeld; nothing observes _writerTask if the loop itself exits abnormally (e.g., an exception escaping the IsWriteLockHeld assignment or ReadAllAsync/TryRead itself). If that ever happens, the channel silently stops being drained — all subsequent EnqueueAsync calls for that database will enqueue successfully but hang forever awaiting Completion.Task, with no error surfaced until the process is restarted.

Consider observing _writerTask's exception (e.g., via a continuation that logs/propagates) so a dead writer doesn't manifest as an indefinite, silent hang for every future write to that database.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@EntityFrameworkCore.Sqlite.Concurrency/src/SqliteWriteQueue.cs` around lines
44 - 70, The background writer loop in RunAsync can fail silently, leaving
_writerTask faulted while EnqueueAsync keeps waiting forever on Completion.Task.
Add supervision for _writerTask when it is created or stored in
SqliteWriteQueue, such as a continuation or observer that logs and propagates
any exception from RunAsync so a dead writer is surfaced instead of hanging
future writes. Make sure the fix references the existing RunAsync, _writerTask,
and EnqueueAsync flow so the queue cannot stop draining without notification.
EFCore.Sqlite.Concurrency.Test/TempDatabase.cs (1)

25-39: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Flaky cleanup: replace fixed sleep with deterministic pool clear.

Thread.Sleep(50) is a heuristic wait for SQLite's connection pool to release the file handle. Under the heavy concurrent load these stress tests generate (hundreds of contexts per test), 50ms may not be sufficient, and failures are silently swallowed, potentially leaking locked temp files across CI runs. Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools() deterministically empties the pool before deletion.

♻️ Proposed fix
+using Microsoft.Data.Sqlite;
+
 namespace EFCore.Sqlite.Concurrency.Test;
 ...
     public void Dispose()
     {
         if (_disposed) return;
         _disposed = true;

-        // Allow EF Core connection pool to drain before deleting the file.
-        Thread.Sleep(50);
+        // Force-release pooled native connections/file locks deterministically.
+        SqliteConnection.ClearAllPools();

         foreach (var ext in new[] { "", "-wal", "-shm" })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@EFCore.Sqlite.Concurrency.Test/TempDatabase.cs` around lines 25 - 39, Replace
the heuristic wait in TempDatabase.Dispose with a deterministic SQLite pool
cleanup: remove the Thread.Sleep(50) call and invoke
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools() before deleting the temp
database files. Keep the existing disposal flow in TempDatabase.Dispose and the
file cleanup loop for _dbPath, but ensure the pool is cleared first so locked
files are released reliably under heavy concurrency.
EFCore.Sqlite.Concurrency.Test/ConcurrencyStressTests.cs (1)

166-186: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reader assertion is tautological and verifies nothing.

CountAsync() can never return a negative number, so Assert.True(count >= 0) will always pass regardless of correctness. This gives false confidence that concurrent reads were validated; it doesn't actually check for torn reads, exceptions, or monotonic growth as the test name implies.

Consider asserting something meaningful, e.g. that every observed count is between 1 (seed row) and expected + 1, or capturing/asserting no exceptions were thrown by reader tasks.

♻️ Proposed fix
-        // All reader results must be non-negative (reads always saw valid data)
-        Assert.All(readResults, count => Assert.True(count >= 0));
+        // Every observed count must be a valid snapshot between the seed row and the final total.
+        Assert.All(readResults, count => Assert.InRange(count, 1, expected + 1));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@EFCore.Sqlite.Concurrency.Test/ConcurrencyStressTests.cs` around lines 166 -
186, The reader check in ConcurrencyStressTests is currently tautological
because CountAsync() cannot return a negative value, so update the assertion on
readResults to validate something meaningful. In the test that creates
readerTasks and uses ctx.Entities.CountAsync(), assert the observed counts stay
within the expected range (at least the seed row and at most expected + 1), or
otherwise capture and assert reader task failures so the concurrency behavior is
actually verified.
EFCore.Sqlite.Concurrency.Benchmarks/EFCore.Sqlite.Concurrency.Benchmarks.csproj (1)

5-14: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Verify BenchmarkDotNet 0.14.0 compatibility with net10.0.

The project multi-targets net10.0 only, but BenchmarkDotNet 0.14.0 predates official .NET 10 support. Per BenchmarkDotNet's release notes, "BenchmarkDotNet v0.15.0 brings .NET 10 support" and explicitly "Added RuntimeMoniker.Net10, NativeAot10, and Mono10 with full toolchain support". The current latest stable release is 0.15.8.

Since this benchmark project is meant to produce the numbers published in the README/release notes, running on an unsupported BenchmarkDotNet version risks build/toolchain-resolution failures or unreliable results on net10.0.

♻️ Suggested version bump
-    <PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
+    <PackageReference Include="BenchmarkDotNet" Version="0.15.8" />

Please confirm whether BenchmarkDotNet 0.14.0 fully supports building/running benchmarks on net10.0, or if it should be bumped to a version with official .NET 10 support.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@EFCore.Sqlite.Concurrency.Benchmarks/EFCore.Sqlite.Concurrency.Benchmarks.csproj`
around lines 5 - 14, BenchmarkDotNet 0.14.0 may not fully support the project’s
net10.0 target, so update the BenchmarkDotNet PackageReference in
EFCore.Sqlite.Concurrency.Benchmarks to a release with official .NET 10 support.
Verify the benchmark project’s package version against the library’s release
notes, and if needed bump it to a version that includes RuntimeMoniker.Net10
support so the benchmark runner can build and execute reliably on net10.0.
EFCore.Sqlite.Concurrency.Benchmarks/BulkInsertBenchmarks.cs (1)

44-78: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Iteration state reset may not be honored under invocation unrolling.

[IterationSetup] clears rows once per iteration, but BenchmarkDotNet may run several invocations of Baseline_SaveChangesPerEntity/Package_BulkInsertOptimizedAsync per iteration (unroll factor > 1) if a single invocation completes quickly. In that case, later invocations within the same iteration insert on top of rows left by earlier ones, growing the table and skewing measured timings — undermining the accuracy of numbers meant to be published in the README/docs.

Consider pinning UnrollFactor: 1 (e.g. via [SimpleJob(invocationCount: ..., unrollFactor: 1)] or Job.Default.WithUnrollFactor(1)) so IterationSetup reliably runs before each measured invocation. The same risk applies to ConcurrentWriteBenchmarks.cs, which uses the identical setup/benchmark pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@EFCore.Sqlite.Concurrency.Benchmarks/BulkInsertBenchmarks.cs` around lines 44
- 78, The iteration reset in IterationSetup can be bypassed when BenchmarkDotNet
unrolls multiple invocations per iteration, causing
Baseline_SaveChangesPerEntity and Package_BulkInsertOptimizedAsync to accumulate
rows and skew results. Fix this by pinning the benchmark job to UnrollFactor = 1
(for example via SimpleJob or a job configuration) so each invocation is
preceded by the setup. Apply the same change to the matching benchmark pattern
in ConcurrentWriteBenchmarks as well.
EntityFrameworkCore.Sqlite.Concurrency/EFCore.Sqlite.Concurrency.csproj (1)

108-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant exclusion pattern.

Line 114 (src\ExtensionMethods\**\*.cs) already covers MemoryPackExtensions.cs under the same folder; Line 116 is a no-op duplicate.

🧹 Proposed cleanup
         <Compile Remove="src\ExtensionMethods\**\*.cs" />
         <Compile Remove="src\SqliteDiagnostics.cs" />
-        <Compile Remove="src\ExtensionMethods\MemoryPackExtensions.cs" />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@EntityFrameworkCore.Sqlite.Concurrency/EFCore.Sqlite.Concurrency.csproj`
around lines 108 - 117, The netstandard2.0 ItemGroup in
EFCore.Sqlite.Concurrency.csproj contains a redundant Compile Remove pattern:
src\ExtensionMethods\**\*.cs already excludes MemoryPackExtensions.cs, so remove
the explicit MemoryPackExtensions.cs entry and keep the broader ExtensionMethods
exclusion in place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/concurrent-efcore-patterns.md`:
- Around line 74-80: The concurrent EF Core sample that uses
_factory.CreateDbContext() and FindAsync can dereference a null item when an ID
is missing. Update this snippet to check the result of FindAsync before
accessing item.ProcessedAt, and skip or handle missing records safely. Apply the
same null guard pattern to the equivalent FindAsync examples in the other
documentation snippets so they all behave consistently.
- Around line 153-158: The write-queue description is using the wrong cache key;
it should match the implementation in the write-queue/cache code rather than
saying “database file path.” Update the wording in the concurrency patterns docs
to reference the actual queue key used by the implementation (the connection
string–based cache in the relevant write-queue manager/serializer), and make
sure the “all three patterns share the same queue” statement only claims sharing
when the same implementation key is reused.

In `@docs/migration-guide.md`:
- Around line 103-106: The migration guide bullet overstates the change by
telling users to replace every SaveChangesAsync call with
SaveChangesSerializedAsync, which conflicts with the existing compatibility
guidance. Update the migration-guide entry to frame SaveChangesSerializedAsync
as an optional first-class helper for cases where serialization is needed, and
keep the wording aligned with the SaveChangesSerializedAsync/SaveChangesAsync
guidance elsewhere so existing call sites are not presented as requiring a
mandatory API swap.

In `@docs/superpowers/plans/2026-07-05-v10-1-0-docs-seo.md`:
- Around line 320-326: The out-of-scope block is stale because the companion
trust-signals spec now includes a root CHANGELOG.md and a BenchmarkDotNet
project. Update the relevant section in this docs plan to remove those items
from “out of scope” and align the scope with the current PR objectives, using
the existing plan section that lists platform and dependency requirements as the
anchor.

In `@docs/superpowers/specs/2026-07-05-v10-1-0-docs-seo-design.md`:
- Around line 81-86: Update the EF Core exception row in the docs spec to use
the canonical full message, since the current text is missing the word instance.
In the table entry for the DbContext concurrency issue, keep the exact exception
string under the existing InvalidOperationException example and preserve the
surrounding explanation tied to IDbContextFactory<T>.
- Around line 123-126: The netstandard2.0 compatibility note is incomplete
because it only names EF Core-dependent APIs and omits the registration helpers
that are also net10.0-only. Update the FAQ entry in the docs spec to mention
AddConcurrentSqliteDbContext<T> and AddConcurrentSqliteDbContextFactory<T>
alongside UseSqliteWithConcurrency and ThreadSafeSqliteContext, so the
net10.0-only list matches the actual API surface.

In
`@EntityFrameworkCore.Sqlite.Concurrency/src/Models/SqliteConcurrencyOptions.cs`:
- Around line 115-136: Add range validation for WriteQueueCapacity in
SqliteConcurrencyOptions.Validate() so invalid values fail fast like
MaxRetryAttempts, CommandTimeout, and WalAutoCheckpoint. Reject 0 and negative
values with an ArgumentOutOfRangeException that clearly names
WriteQueueCapacity, and keep the check in the Validate() method so the error is
raised before SqliteWriteQueue or Channel.CreateBounded is reached.

In `@EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConnectionEnhancer.cs`:
- Around line 63-66: Serialize the initialization in GetWriteQueue because
ConcurrentDictionary.GetOrAdd can invoke the factory multiple times and create
extra SqliteWriteQueue instances with live writer tasks. Update
SqliteConnectionEnhancer.GetWriteQueue to use a single-initialization guard such
as Lazy<SqliteWriteQueue> keyed by connection string, and ensure the first
access creates only one SqliteWriteQueue and one _writerTask per connection
string.

In `@EntityFrameworkCore.Sqlite.Concurrency/src/SqliteWriteQueue.cs`:
- Around line 28-42: The cancellation behavior in EnqueueAsync<T> is
inconsistent across TFMs because WriteAsync only controls queue admission, while
WaitAsync(ct) can cancel after the request is already enqueued on
non-NETSTANDARD2_0. Update SqliteWriteQueue.EnqueueAsync<T> so post-enqueue
cancellation does not make callers think the write was aborted when the queued
work still runs, either by awaiting WriteRequest<T>.Completion.Task
unconditionally after _channel.Writer.WriteAsync(request, ct) or by explicitly
documenting and aligning the split semantics between NETSTANDARD2_0 and other
targets.

In `@EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeSqliteContext.cs`:
- Around line 125-128: The retry delay calculation in ThreadSafeSqliteContext is
off by one: the first retry currently uses 200 ms because attempt is incremented
before the backoff is computed. Update the exponential backoff formula to use
attempt - 1 in the exponent so the first delay matches the
SqliteConcurrencyOptions.MaxRetryAttempts documentation at 100 ms. Also fix the
inline comment near the Task.Delay call to describe the actual jitter behavior,
since the current “full jitter” wording does not match the sampled range.
- Around line 90-131: The retry loop in
ThreadSafeSqliteContext.WriteQueue.EnqueueAsync reuses the same DbContext after
a SQLITE_BUSY catch, so tracked Added/Modified entities can be replayed on the
next attempt. Clear the DbContext ChangeTracker before retrying inside the
busy-exception path, ideally right after catching SqliteException and before the
backoff delay or next loop iteration, so each operation((TContext)(object)this)
attempt starts from a clean state.

In `@README.md`:
- Around line 128-133: The retry sample using ExecuteWithRetryAsync only adds
records to ctx.Records and never persists them, so update this example to either
call SaveChangesAsync inside the ExecuteWithRetryAsync lambda or replace it with
the auto-committing helper used elsewhere in the README. Use the
ExecuteWithRetryAsync snippet to locate the example and make sure the documented
flow actually writes the staged entities.

---

Outside diff comments:
In @.github/workflows/ci.yml:
- Around line 10-48: The build-and-verify workflow currently relies on the
default GITHUB_TOKEN permissions, which are broader than needed for the
checkout/build/test/report flow. Add an explicit permissions block to the build
job in ci.yml so the job is read-only by default and only grants the reporter
the access it needs, using the build job and Publish Test Results /
dorny/test-reporter@v1 setup as the place to apply it.

---

Nitpick comments:
In `@docs/superpowers/specs/2026-07-05-trust-signals-design.md`:
- Around line 34-35: The spec currently excludes the benchmark project from CI,
but the published benchmark numbers need a regression guard so they do not drift
unnoticed. Update the trust-signals design to add a lightweight scheduled or
smoke benchmark path alongside the existing benchmark project, and reference the
benchmark project / README benchmark claims so the published figures stay tied
to a known commit and environment.

In `@EFCore.Sqlite.Concurrency.Benchmarks/BulkInsertBenchmarks.cs`:
- Around line 44-78: The iteration reset in IterationSetup can be bypassed when
BenchmarkDotNet unrolls multiple invocations per iteration, causing
Baseline_SaveChangesPerEntity and Package_BulkInsertOptimizedAsync to accumulate
rows and skew results. Fix this by pinning the benchmark job to UnrollFactor = 1
(for example via SimpleJob or a job configuration) so each invocation is
preceded by the setup. Apply the same change to the matching benchmark pattern
in ConcurrentWriteBenchmarks as well.

In
`@EFCore.Sqlite.Concurrency.Benchmarks/EFCore.Sqlite.Concurrency.Benchmarks.csproj`:
- Around line 5-14: BenchmarkDotNet 0.14.0 may not fully support the project’s
net10.0 target, so update the BenchmarkDotNet PackageReference in
EFCore.Sqlite.Concurrency.Benchmarks to a release with official .NET 10 support.
Verify the benchmark project’s package version against the library’s release
notes, and if needed bump it to a version that includes RuntimeMoniker.Net10
support so the benchmark runner can build and execute reliably on net10.0.

In `@EFCore.Sqlite.Concurrency.Test/ConcurrencyStressTests.cs`:
- Around line 166-186: The reader check in ConcurrencyStressTests is currently
tautological because CountAsync() cannot return a negative value, so update the
assertion on readResults to validate something meaningful. In the test that
creates readerTasks and uses ctx.Entities.CountAsync(), assert the observed
counts stay within the expected range (at least the seed row and at most
expected + 1), or otherwise capture and assert reader task failures so the
concurrency behavior is actually verified.

In `@EFCore.Sqlite.Concurrency.Test/TempDatabase.cs`:
- Around line 25-39: Replace the heuristic wait in TempDatabase.Dispose with a
deterministic SQLite pool cleanup: remove the Thread.Sleep(50) call and invoke
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools() before deleting the temp
database files. Keep the existing disposal flow in TempDatabase.Dispose and the
file cleanup loop for _dbPath, but ensure the pool is cleared first so locked
files are released reliably under heavy concurrency.

In `@EntityFrameworkCore.Sqlite.Concurrency/EFCore.Sqlite.Concurrency.csproj`:
- Around line 108-117: The netstandard2.0 ItemGroup in
EFCore.Sqlite.Concurrency.csproj contains a redundant Compile Remove pattern:
src\ExtensionMethods\**\*.cs already excludes MemoryPackExtensions.cs, so remove
the explicit MemoryPackExtensions.cs entry and keep the broader ExtensionMethods
exclusion in place.

In `@EntityFrameworkCore.Sqlite.Concurrency/src/SqliteWriteQueue.cs`:
- Around line 44-70: The background writer loop in RunAsync can fail silently,
leaving _writerTask faulted while EnqueueAsync keeps waiting forever on
Completion.Task. Add supervision for _writerTask when it is created or stored in
SqliteWriteQueue, such as a continuation or observer that logs and propagates
any exception from RunAsync so a dead writer is surfaced instead of hanging
future writes. Make sure the fix references the existing RunAsync, _writerTask,
and EnqueueAsync flow so the queue cannot stop draining without notification.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46599b75-bd85-4a92-ae33-94456a937d04

📥 Commits

Reviewing files that changed from the base of the PR and between f4d8a0a and 9fe1515.

📒 Files selected for processing (45)
  • .github/ISSUE_TEMPLATE/bug_report.yml
  • .github/ISSUE_TEMPLATE/feature_request.yml
  • .github/PULL_REQUEST_TEMPLATE.md
  • .github/workflows/ci.yml
  • .gitignore
  • CHANGELOG.md
  • CONTRIBUTING.md
  • EFCore.Sqlite.Concurrency.Benchmarks/BaselineSqliteContext.cs
  • EFCore.Sqlite.Concurrency.Benchmarks/BenchmarkEntity.cs
  • EFCore.Sqlite.Concurrency.Benchmarks/BulkInsertBenchmarks.cs
  • EFCore.Sqlite.Concurrency.Benchmarks/ConcurrentSqliteContext.cs
  • EFCore.Sqlite.Concurrency.Benchmarks/ConcurrentWriteBenchmarks.cs
  • EFCore.Sqlite.Concurrency.Benchmarks/EFCore.Sqlite.Concurrency.Benchmarks.csproj
  • EFCore.Sqlite.Concurrency.Benchmarks/Program.cs
  • EFCore.Sqlite.Concurrency.Benchmarks/ReadBenchmarks.cs
  • EFCore.Sqlite.Concurrency.Test/ConcurrencyStressTests.cs
  • EFCore.Sqlite.Concurrency.Test/EFCore.Sqlite.Concurrency.Test.csproj
  • EFCore.Sqlite.Concurrency.Test/StressDbContext.cs
  • EFCore.Sqlite.Concurrency.Test/StressEntity.cs
  • EFCore.Sqlite.Concurrency.Test/TempDatabase.cs
  • EFCore.Sqlite.Concurrency.Test/ThreadSafeSqliteStressContext.cs
  • EntityFrameworkCore.Sqlite.Concurrency.sln
  • EntityFrameworkCore.Sqlite.Concurrency.sln.DotSettings.user
  • EntityFrameworkCore.Sqlite.Concurrency/EFCore.Sqlite.Concurrency.csproj
  • EntityFrameworkCore.Sqlite.Concurrency/out/EntityFrameworkCore.Sqlite.Concurrency.10.0.4.nupkg
  • EntityFrameworkCore.Sqlite.Concurrency/out/EntityFrameworkCore.Sqlite.Concurrency.10.0.4.snupkg
  • EntityFrameworkCore.Sqlite.Concurrency/packages.lock.json
  • EntityFrameworkCore.Sqlite.Concurrency/src/Models/SqliteConcurrencyOptions.cs
  • EntityFrameworkCore.Sqlite.Concurrency/src/Polyfills.cs
  • EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyExtensions.cs
  • EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyInterceptor.cs
  • EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConnectionEnhancer.cs
  • EntityFrameworkCore.Sqlite.Concurrency/src/SqliteWriteQueue.cs
  • EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeSqliteContext.cs
  • README.md
  • SECURITY.md
  • doc/QUICKSTART.md
  • doc/v10_1_0.md
  • docs/concurrent-efcore-patterns.md
  • docs/migration-guide.md
  • docs/performance-guide.md
  • docs/superpowers/plans/2026-07-05-v10-1-0-docs-seo.md
  • docs/superpowers/specs/2026-07-05-trust-signals-design.md
  • docs/superpowers/specs/2026-07-05-v10-1-0-docs-seo-design.md
  • docs/troubleshooting-sqlite-busy.md

Comment on lines +74 to +80
var tasks = itemIds.Select(async id =>
{
// Each task creates and disposes its own context
await using var db = _factory.CreateDbContext();
var item = await db.Items.FindAsync(id, ct);
item.ProcessedAt = DateTime.UtcNow;
await db.SaveChangesAsync(ct); // writes serialized by the write queue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard FindAsync before dereferencing.

FindAsync can return null, so this sample can throw on a missing ID. The same guard should be applied to the similar snippets in README.md and doc/QUICKSTART.md.

Proposed fix
 var item = await db.Items.FindAsync(id, ct);
+if (item is null)
+    continue;
 item.ProcessedAt = DateTime.UtcNow;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/concurrent-efcore-patterns.md` around lines 74 - 80, The concurrent EF
Core sample that uses _factory.CreateDbContext() and FindAsync can dereference a
null item when an ID is missing. Update this snippet to check the result of
FindAsync before accessing item.ProcessedAt, and skip or handle missing records
safely. Apply the same null guard pattern to the equivalent FindAsync examples
in the other documentation snippets so they all behave consistently.

Comment on lines +153 to +158
## How the Write Queue Serializes Across All Patterns

All three patterns share the same process-wide write queue (keyed by database file path).
When any of them calls a write API (`SaveChangesAsync`, `SaveChangesSerializedAsync`,
`BulkInsertOptimizedAsync`, `ExecuteWriteAsync`), the work is enqueued to the same
`Channel<IWriteRequest>`. The single background writer executes one request at a time.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Match the queue key to the implementation.

As per the stack summary, the write-queue cache is per connection string, not per database file path. This wording suggests two equivalent connection strings will always share a queue, which may not hold.

Proposed fix
-All three patterns share the same process-wide write queue (keyed by database file path).
+All three patterns share the same process-wide write queue (cached per connection string).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## How the Write Queue Serializes Across All Patterns
All three patterns share the same process-wide write queue (keyed by database file path).
When any of them calls a write API (`SaveChangesAsync`, `SaveChangesSerializedAsync`,
`BulkInsertOptimizedAsync`, `ExecuteWriteAsync`), the work is enqueued to the same
`Channel<IWriteRequest>`. The single background writer executes one request at a time.
## How the Write Queue Serializes Across All Patterns
All three patterns share the same process-wide write queue (cached per connection string).
When any of them calls a write API (`SaveChangesAsync`, `SaveChangesSerializedAsync`,
`BulkInsertOptimizedAsync`, `ExecuteWriteAsync`), the work is enqueued to the same
`Channel<IWriteRequest>`. The single background writer executes one request at a time.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/concurrent-efcore-patterns.md` around lines 153 - 158, The write-queue
description is using the wrong cache key; it should match the implementation in
the write-queue/cache code rather than saying “database file path.” Update the
wording in the concurrency patterns docs to reference the actual queue key used
by the implementation (the connection string–based cache in the relevant
write-queue manager/serializer), and make sure the “all three patterns share the
same queue” statement only claims sharing when the same implementation key is
reused.

Comment thread docs/migration-guide.md
Comment on lines +103 to +106
- [ ] **Remove any `SaveChangesSerializedAsync` workarounds** (if you had a custom version).
`SaveChangesSerializedAsync` is a first-class extension method on `DbContext` — call it
directly anywhere you previously called `SaveChangesAsync`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don't make SaveChangesSerializedAsync the default migration step.

The README and release notes say existing call sites keep working, but this bullet tells users to replace every SaveChangesAsync call. That turns an optional helper into a mandatory API change.

Proposed fix
- `SaveChangesSerializedAsync` is a first-class extension method on `DbContext` — call it directly anywhere you previously called `SaveChangesAsync`.
+ `SaveChangesSerializedAsync` remains available for code paths that explicitly want serialized saves; existing `SaveChangesAsync` call sites do not need to change.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- [ ] **Remove any `SaveChangesSerializedAsync` workarounds** (if you had a custom version).
`SaveChangesSerializedAsync` is a first-class extension method on `DbContext` — call it
directly anywhere you previously called `SaveChangesAsync`.
- [ ] **Remove any `SaveChangesSerializedAsync` workarounds** (if you had a custom version).
`SaveChangesSerializedAsync` remains available for code paths that explicitly want serialized saves; existing `SaveChangesAsync` call sites do not need to change.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/migration-guide.md` around lines 103 - 106, The migration guide bullet
overstates the change by telling users to replace every SaveChangesAsync call
with SaveChangesSerializedAsync, which conflicts with the existing compatibility
guidance. Update the migration-guide entry to frame SaveChangesSerializedAsync
as an optional first-class helper for cases where serialization is needed, and
keep the wording aligned with the SaveChangesSerializedAsync/SaveChangesAsync
guidance elsewhere so existing call sites are not presented as requiring a
mandatory API swap.

Comment on lines +320 to +326

- .NET 10.0+ (for EF Core APIs) or netstandard 2.0+ (for connection layer only)
- Entity Framework Core 10.0+
- Microsoft.Data.Sqlite 10.0+
- SQLite 3.35.0+ (WAL2 and `SQLITE_BUSY_SNAPSHOT` require 3.37.0+)

## License

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the out-of-scope block.

This note is stale: the companion trust-signals spec and the PR objectives now add a root CHANGELOG.md and a BenchmarkDotNet project. Leaving both items marked “out of scope” will send implementers in the wrong direction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-07-05-v10-1-0-docs-seo.md` around lines 320 -
326, The out-of-scope block is stale because the companion trust-signals spec
now includes a root CHANGELOG.md and a BenchmarkDotNet project. Update the
relevant section in this docs plan to remove those items from “out of scope” and
align the scope with the current PR objectives, using the existing plan section
that lists platform and dependency requirements as the anchor.

Comment on lines +81 to +86
| Exception / Error | Root Cause | What This Package Does |
|---|---|---|
| `SQLite Error 5: 'database is locked'` (`SQLITE_BUSY`) | Multiple writers contending simultaneously | Channel-based write queue — one writer at a time, all others park efficiently |
| `SQLite Error 5: 'database is locked'` (extended code 517, `SQLITE_BUSY_SNAPSHOT`) | WAL read snapshot became stale mid-transaction | `BEGIN IMMEDIATE` upgrade + full operation restart on snapshot staleness |
| `InvalidOperationException: A second operation was started on this context` | `DbContext` shared across concurrent tasks | `IDbContextFactory<T>` registration pattern, one context per concurrent flow |
| Bulk inserts slow / OOM on large datasets | Linear `SaveChanges()`, unbounded ChangeTracker growth | `BulkInsertOptimizedAsync` — batched, WAL-optimized, ChangeTracker cleared per batch |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'A second operation was started on this context( instance)?' .

Repository: CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency

Length of output: 1421


Use the full EF Core exception string here.
InvalidOperationException: A second operation was started on this context instance is the canonical message, so keep instance in the spec.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-07-05-v10-1-0-docs-seo-design.md` around lines 81
- 86, Update the EF Core exception row in the docs spec to use the canonical
full message, since the current text is missing the word instance. In the table
entry for the DbContext concurrency issue, keep the exact exception string under
the existing InvalidOperationException example and preserve the surrounding
explanation tied to IDbContextFactory<T>.

Comment on lines +63 to +66
internal static SqliteWriteQueue GetWriteQueue(string connectionString, int? capacity = null)
{
return _writeQueues.GetOrAdd(connectionString, _ => new SqliteWriteQueue(capacity));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm SqliteWriteQueue starts a background task in its constructor (leak surface).
fd -t f 'SqliteWriteQueue.cs' --exec sed -n '1,40p' {}

Repository: CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency

Length of output: 1580


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the write-queue storage, retrieval, and disposal paths.
FILE='EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConnectionEnhancer.cs'

echo '--- outline ---'
ast-grep outline "$FILE" --view expanded || true

echo
echo '--- relevant ranges ---'
sed -n '1,140p' "$FILE"
echo
sed -n '400,560p' "$FILE"

Repository: CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency

Length of output: 12659


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect SqliteWriteQueue disposal behavior and any cleanup of orphaned queues.
FILE='EntityFrameworkCore.Sqlite.Concurrency/src/SqliteWriteQueue.cs'

echo '--- outline ---'
ast-grep outline "$FILE" --view expanded || true

echo
echo '--- full file sections ---'
sed -n '1,220p' "$FILE"

Repository: CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency

Length of output: 3221


Serialize SqliteWriteQueue creation

ConcurrentDictionary.GetOrAdd may run the factory more than once on concurrent first access. That can start extra SqliteWriteQueue instances whose _writerTask keeps running even though the instance is never stored or disposed. Wrap the queue in Lazy<SqliteWriteQueue> (or another single-initialization guard) so only one writer loop is created per connection string.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConnectionEnhancer.cs`
around lines 63 - 66, Serialize the initialization in GetWriteQueue because
ConcurrentDictionary.GetOrAdd can invoke the factory multiple times and create
extra SqliteWriteQueue instances with live writer tasks. Update
SqliteConnectionEnhancer.GetWriteQueue to use a single-initialization guard such
as Lazy<SqliteWriteQueue> keyed by connection string, and ensure the first
access creates only one SqliteWriteQueue and one _writerTask per connection
string.

Comment on lines +28 to +42
internal async Task<T> EnqueueAsync<T>(Func<Task<T>> work, CancellationToken ct)
{
// Reentrancy: already inside the writer loop's async context — execute directly
// to prevent deadlock from re-queuing behind ourselves.
if (SqliteConnectionEnhancer.IsWriteLockHeld.Value)
return await work();

var request = new WriteRequest<T>(work);
await _channel.Writer.WriteAsync(request, ct);
#if NETSTANDARD2_0
return await request.Completion.Task;
#else
return await request.Completion.Task.WaitAsync(ct);
#endif
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the queue implementation and related types/usages.
git ls-files | rg 'EntityFrameworkCore\.Sqlite\.Concurrency/src/|SqliteWriteQueue|WriteRequest|SqliteConnectionEnhancer'

# Show the file with line numbers, but only a bounded slice if it's small enough.
file="EntityFrameworkCore.Sqlite.Concurrency/src/SqliteWriteQueue.cs"
if [ -f "$file" ]; then
  echo "=== $file ==="
  wc -l "$file"
  sed -n '1,220p' "$file" | cat -n
fi

# Find where Completion is completed and how cancellation is handled.
rg -n "Completion|SetResult|SetException|TrySet|WriteRequest<|EnqueueAsync<|IsWriteLockHeld" EntityFrameworkCore.Sqlite.Concurrency/src

# Inspect the write request type and writer loop if present.
for f in $(git ls-files 'EntityFrameworkCore.Sqlite.Concurrency/src/*'); do
  case "$f" in
    *WriteRequest*|*WriteQueue*|*Enhancer*|*Writer* )
      echo "=== $f ==="
      wc -l "$f"
      sed -n '1,260p' "$f" | cat -n
      ;;
  esac
done

Repository: CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency

Length of output: 25062


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect callers of EnqueueAsync and any cancellation-related docs/tests.
rg -n "EnqueueAsync<|EnqueueAsync\(" EntityFrameworkCore.Sqlite.Concurrency/src EntityFrameworkCore.Sqlite.Concurrency/test . -g '!**/bin/**' -g '!**/obj/**'

# Read the relevant caller sections.
for f in EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyExtensions.cs EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyInterceptor.cs; do
  if [ -f "$f" ]; then
    echo "=== $f ==="
    wc -l "$f"
    sed -n '120,260p' "$f" | cat -n
  fi
done

# Search for cancellation semantics in docs or comments.
rg -n "cancellation|Canceled|OperationCanceledException|WaitAsync\(ct\)|CancellationToken" EntityFrameworkCore.Sqlite.Concurrency/src EntityFrameworkCore.Sqlite.Concurrency/test README.md

Repository: CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency

Length of output: 1352


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeSqliteContext.cs EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyExtensions.cs; do
  if [ -f "$f" ]; then
    echo "=== $f ==="
    wc -l "$f"
    sed -n '70,240p' "$f" | cat -n
  fi
done

Repository: CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency

Length of output: 15756


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== ThreadSafeSqliteContext.cs ==="
sed -n '1,140p' EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeSqliteContext.cs | cat -n

echo "=== SqliteConcurrencyExtensions.cs ==="
sed -n '130,240p' EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyExtensions.cs | cat -n

echo "=== WaitAsync(ct) usage ==="
rg -n "WaitAsync\(ct\)" EntityFrameworkCore.Sqlite.Concurrency/src

Repository: CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency

Length of output: 12507


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '145,225p' EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyExtensions.cs | cat -n
sed -n '1,130p' EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeSqliteContext.cs | cat -n

Repository: CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency

Length of output: 10857


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== SqliteConcurrencyExtensions.cs (145-225) ==="
sed -n '145,225p' EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyExtensions.cs | cat -n

echo "=== ThreadSafeSqliteContext.cs (1-130) ==="
sed -n '1,130p' EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeSqliteContext.cs | cat -n

Repository: CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency

Length of output: 10949


Don’t let post-enqueue cancellation hide a write WriteAsync(request, ct) only gates admission. On !NETSTANDARD2_0, WaitAsync(ct) can throw after the item is already queued, while the writer still runs it; NETSTANDARD2_0 always waits for completion. That makes cancellation semantics differ by TFM and can let callers treat a write as aborted when it may still commit. If cancellation should stop only queueing, wait on Completion.Task unconditionally or document the split behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@EntityFrameworkCore.Sqlite.Concurrency/src/SqliteWriteQueue.cs` around lines
28 - 42, The cancellation behavior in EnqueueAsync<T> is inconsistent across
TFMs because WriteAsync only controls queue admission, while WaitAsync(ct) can
cancel after the request is already enqueued on non-NETSTANDARD2_0. Update
SqliteWriteQueue.EnqueueAsync<T> so post-enqueue cancellation does not make
callers think the write was aborted when the queued work still runs, either by
awaiting WriteRequest<T>.Completion.Task unconditionally after
_channel.Writer.WriteAsync(request, ct) or by explicitly documenting and
aligning the split semantics between NETSTANDARD2_0 and other targets.

Comment on lines +90 to +131
return await WriteQueue.EnqueueAsync(async () =>
{
await WriteLock.WaitAsync(ct);
SqliteConnectionEnhancer.IsWriteLockHeld.Value = true;

try
{
// The interceptor will upgrade this BEGIN to BEGIN IMMEDIATE, ensuring
// no later statement in the transaction fails with SQLITE_BUSY before
// commit (as long as UpgradeTransactionsToImmediate is true).
await using var transaction = await Database.BeginTransactionAsync(
System.Data.IsolationLevel.Serializable, ct);

var result = await operation((TContext)(object)this);
await SaveChangesAsync(ct);
await transaction.CommitAsync(ct);

return result;
}
catch (SqliteException ex) when (SqliteErrorCodes.IsAnyBusy(ex))
int attempt = 0;
while (true)
{
// Release the lock before sleeping so other writers can make progress.
SqliteConnectionEnhancer.IsWriteLockHeld.Value = false;
WriteLock.Release();

attempt++;
if (attempt >= maxRetryAttempts)
try
{
var kind = SqliteErrorCodes.IsBusySnapshot(ex)
? "SQLITE_BUSY_SNAPSHOT (stale read snapshot — another writer committed after this transaction began)"
: $"SQLITE_BUSY (extended code {ex.SqliteExtendedErrorCode})";

throw new TimeoutException(
$"SQLite database busy after {attempt} retry attempt(s). " +
$"Error: {kind}. " +
$"Consider increasing MaxRetryAttempts or BusyTimeout.",
ex);
}
// The interceptor will upgrade this BEGIN to BEGIN IMMEDIATE, ensuring
// no later statement in the transaction fails with SQLITE_BUSY before
// commit (as long as UpgradeTransactionsToImmediate is true).
await using var transaction = await Database.BeginTransactionAsync(
System.Data.IsolationLevel.Serializable, ct);

// Exponential backoff with full jitter: sleep in [baseDelay, 2×baseDelay].
// Jitter prevents synchronized retry storms when multiple threads contend.
var baseDelay = 100 * Math.Pow(2, attempt);
var jitter = Random.Shared.NextDouble() * baseDelay;
await Task.Delay(TimeSpan.FromMilliseconds(baseDelay + jitter), ct);
var result = await operation((TContext)(object)this);
await SaveChangesAsync(ct);
await transaction.CommitAsync(ct);

// Continue to next loop iteration — for BUSY_SNAPSHOT this correctly
// restarts the entire operation lambda so stale data is re-queried.
}
finally
{
if (SqliteConnectionEnhancer.IsWriteLockHeld.Value)
return result;
}
catch (SqliteException ex) when (SqliteErrorCodes.IsAnyBusy(ex))
{
SqliteConnectionEnhancer.IsWriteLockHeld.Value = false;
WriteLock.Release();
attempt++;
if (attempt >= maxRetryAttempts)
{
var kind = SqliteErrorCodes.IsBusySnapshot(ex)
? "SQLITE_BUSY_SNAPSHOT (stale read snapshot — another writer committed after this transaction began)"
: $"SQLITE_BUSY (extended code {ex.SqliteExtendedErrorCode})";

throw new TimeoutException(
$"SQLite database busy after {attempt} retry attempt(s). " +
$"Error: {kind}. " +
$"Consider increasing MaxRetryAttempts or BusyTimeout.",
ex);
}

// Exponential backoff with full jitter: sleep in [baseDelay, 2×baseDelay].
var baseDelay = 100 * Math.Pow(2, attempt);
var jitter = Random.Shared.NextDouble() * baseDelay;
await Task.Delay(TimeSpan.FromMilliseconds(baseDelay + jitter), ct);
}
}
}
}, ct);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file with line numbers
sed -n '1,260p' EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeSqliteContext.cs | cat -n

# Find any other ChangeTracker.Clear / retry-related logic in the repo
rg -n "ChangeTracker\.Clear|ExecuteWriteAsync|maxRetryAttempts|BUSY_SNAPSHOT|SQLite busy|BusyTimeout|UpgradeTransactionsToImmediate" EntityFrameworkCore.Sqlite.Concurrency -S

# Inspect the interceptor/queue code paths that may affect retry semantics
rg -n "WriteQueue|connectionString|EnqueueAsync|BeginTransactionAsync|SaveChangesAsync" EntityFrameworkCore.Sqlite.Concurrency/src -S

Repository: CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency

Length of output: 32694


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the write-helper methods that implement similar retry/transaction logic
sed -n '140,240p' EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyExtensions.cs | cat -n

# Inspect the interceptor and queue implementation around write serialization
sed -n '1,220p' EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyInterceptor.cs | cat -n
sed -n '1,220p' EntityFrameworkCore.Sqlite.Concurrency/src/SqliteWriteQueue.cs | cat -n

# Look for any explicit tracker reset in retry paths
rg -n "ChangeTracker\.Clear\(\)|catch \(SqliteException|BUSY_SNAPSHOT|restart the operation|retry" EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyExtensions.cs EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeSqliteContext.cs EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyInterceptor.cs -S

Repository: CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency

Length of output: 23318


Reset ChangeTracker before retrying busy writes. operation runs again on the same DbContext after rollback, but tracked Added/Modified entities remain in the change tracker. That can replay inserts/updates or leave stale state on the next attempt; clear the tracker before the loop retries.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 126-126: System.Random is a non-cryptographic, predictable PRNG and must not be used to generate security-sensitive values such as tokens, session IDs, passwords, salts, nonces, or API keys. An attacker who observes a few outputs can recover the seed and predict all future values. Use a cryptographically secure source instead: RandomNumberGenerator.GetBytes(...), RandomNumberGenerator.GetInt32(...), or RandomNumberGenerator.Fill(...).
Context: Random.Shared.NextDouble()
Note: [CWE-330] Use of Insufficiently Random Values.

(weak-random-system-random-csharp)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeSqliteContext.cs` around
lines 90 - 131, The retry loop in
ThreadSafeSqliteContext.WriteQueue.EnqueueAsync reuses the same DbContext after
a SQLITE_BUSY catch, so tracked Added/Modified entities can be replayed on the
next attempt. Clear the DbContext ChangeTracker before retrying inside the
busy-exception path, ideally right after catching SqliteException and before the
backoff delay or next loop iteration, so each operation((TContext)(object)this)
attempt starts from a clean state.

Comment on lines +125 to +128
// Exponential backoff with full jitter: sleep in [baseDelay, 2×baseDelay].
var baseDelay = 100 * Math.Pow(2, attempt);
var jitter = Random.Shared.NextDouble() * baseDelay;
await Task.Delay(TimeSpan.FromMilliseconds(baseDelay + jitter), ct);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Backoff starts at 200 ms, not the documented 100 ms.

attempt is incremented before the delay is computed, so the first retry uses baseDelay = 100 * 2^1 = 200 ms. The SqliteConcurrencyOptions.MaxRetryAttempts docs state backoff starts at 100 ms. Use attempt - 1 in the exponent to match. Also note the "full jitter" comment is inaccurate — full jitter samples [0, baseDelay], whereas this yields [baseDelay, 2×baseDelay].

♻️ Align first delay with documented 100 ms
-                    var baseDelay = 100 * Math.Pow(2, attempt);
+                    var baseDelay = 100 * Math.Pow(2, attempt - 1);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Exponential backoff with full jitter: sleep in [baseDelay, 2×baseDelay].
var baseDelay = 100 * Math.Pow(2, attempt);
var jitter = Random.Shared.NextDouble() * baseDelay;
await Task.Delay(TimeSpan.FromMilliseconds(baseDelay + jitter), ct);
// Exponential backoff with full jitter: sleep in [baseDelay, 2×baseDelay].
var baseDelay = 100 * Math.Pow(2, attempt - 1);
var jitter = Random.Shared.NextDouble() * baseDelay;
await Task.Delay(TimeSpan.FromMilliseconds(baseDelay + jitter), ct);
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 126-126: System.Random is a non-cryptographic, predictable PRNG and must not be used to generate security-sensitive values such as tokens, session IDs, passwords, salts, nonces, or API keys. An attacker who observes a few outputs can recover the seed and predict all future values. Use a cryptographically secure source instead: RandomNumberGenerator.GetBytes(...), RandomNumberGenerator.GetInt32(...), or RandomNumberGenerator.Fill(...).
Context: Random.Shared.NextDouble()
Note: [CWE-330] Use of Insufficiently Random Values.

(weak-random-system-random-csharp)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeSqliteContext.cs` around
lines 125 - 128, The retry delay calculation in ThreadSafeSqliteContext is off
by one: the first retry currently uses 200 ms because attempt is incremented
before the backoff is computed. Update the exponential backoff formula to use
attempt - 1 in the exponent so the first delay matches the
SqliteConcurrencyOptions.MaxRetryAttempts documentation at 100 ms. Also fix the
inline comment near the Task.Delay call to describe the actual jitter behavior,
since the current “full jitter” wording does not match the sampled range.

Comment thread README.md
Comment on lines +128 to +133
// Or with explicit retry control
await context.ExecuteWithRetryAsync(async ctx =>
{
foreach (var record in records)
ctx.Records.Add(record);
}, maxRetries: 5);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Persist the retry example.

ExecuteWithRetryAsync only stages entities here; without SaveChangesAsync() the sample never writes anything. Either commit inside the lambda or switch this snippet to the helper that auto-commits.

Proposed fix
 await context.ExecuteWithRetryAsync(async ctx =>
 {
     foreach (var record in records)
         ctx.Records.Add(record);
+    await ctx.SaveChangesAsync();
 }, maxRetries: 5);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Or with explicit retry control
await context.ExecuteWithRetryAsync(async ctx =>
{
foreach (var record in records)
ctx.Records.Add(record);
}, maxRetries: 5);
// Or with explicit retry control
await context.ExecuteWithRetryAsync(async ctx =>
{
foreach (var record in records)
ctx.Records.Add(record);
await ctx.SaveChangesAsync();
}, maxRetries: 5);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 128 - 133, The retry sample using
ExecuteWithRetryAsync only adds records to ctx.Records and never persists them,
so update this example to either call SaveChangesAsync inside the
ExecuteWithRetryAsync lambda or replace it with the auto-committing helper used
elsewhere in the README. Use the ExecuteWithRetryAsync snippet to locate the
example and make sure the documented flow actually writes the staged entities.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant