Skip to content

Conversation

@CornerstoneCode
Copy link
Owner

@CornerstoneCode CornerstoneCode commented Jan 25, 2026

Summary by CodeRabbit

  • New Features

    • Added automatic retry helpers, DI-friendly DbContext registration, renamed bulk-insert API to BulkInsertOptimizedAsync, and new UseSqliteWithConcurrency configuration.
  • Improvements

    • Release 10.0.2: connection string caching, runtime PRAGMA batching, write-locking with configurable retry/backoff, increased command timeout, optimized bulk/write flows.
  • Documentation

    • New comprehensive v10_0_0 and v10_0_2 guides with benchmarks, migration tips, and examples.
  • Packaging

    • Updated package metadata, version bump to 10.0.2, and distribution settings.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

A refactor replacing an event-driven write-queue with interceptor-driven concurrency management, adding DI-friendly factories and registration helpers, retry/lock-oriented write paths, connection/pragmas caching, a project version bump to 10.0.2, and extensive documentation and packaging metadata updates.

Changes

Cohort / File(s) Summary
Build & CI
\.github/workflows/ci.yml, \.github/workflows/deploy.yml, \.gitignore
Enable actions/setup-dotnet caching; make restore/build/pack target the project csproj; add new bin/ ignores.
Project Metadata & Packaging
EntityFrameworkCore.Sqlite.Concurrency/EFCore.Sqlite.Concurrency.csproj
Version → 10.0.2; add RootNamespace and packaging metadata (symbols, icon, release notes); adjust PackageReferences (add Microsoft.SourceLink.GitHub, Microsoft.Extensions.DependencyInjection; remove System.Threading.Channels, EFCore.BulkExtensions.Sqlite); reorganize packaging items.
Removed Build Artifacts
EntityFrameworkCore.Sqlite.Concurrency/bin/.../*.deps.json, .../*.xml
Deleted generated .deps.json and XML documentation artifacts from Debug/Release outputs.
Documentation
EntityFrameworkCore.Sqlite.Concurrency/doc/*, README.md
Add v10_0_0/v10_0_1/v10_0_2 docs; update README to new APIs (UseSqliteWithConcurrency, BulkInsertOptimizedAsync), options (MaxRetryAttempts, WalAutoCheckpoint), examples, and system requirements.
Service Registration
EntityFrameworkCore.Sqlite.Concurrency/src/ExtensionMethods/SqliteConcurrencyServiceCollectionExtensions.cs
Add AddConcurrentSqliteDbContext<TContext> to register DbContext with concurrency options and configurable lifetime.
Options Model
EntityFrameworkCore.Sqlite.Concurrency/src/Models/SqliteConcurrencyOptions.cs
Remove boolean flags (UseWriteQueue, EnableWalCheckpointManagement, EnableMemoryPack); add MaxRetryAttempts, BusyTimeout, CommandTimeout, WalAutoCheckpoint; implement IEquatable and equality members.
Public API & Extensions
EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyExtensions.cs
Replace per-connection queue logic with interceptor + optimized connection string; add ExecuteWithRetryAsync and BulkInsertOptimizedAsync with retry/backoff and batched writes; adjust UseSqliteWithConcurrency wiring.
Interceptor
EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyInterceptor.cs
Replace queue-based model with interceptor implementing IDbConnectionInterceptor and IDbTransactionInterceptor; constructor now requires connectionString; add connection/transaction lifecycle hooks and UpgradeToBeginImmediate helper; apply runtime pragmas on open.
Connection Enhancer
EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConnectionEnhancer.cs
Add connection-string caching, per-datasource initialization, write-lock and interceptor caches; ComputeOptimizedConnectionString and ApplyRuntimePragmas (database- and connection-scoped pragmas) with per-datasource locking.
Factory & Context Creation
EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeFactory.cs
DI-aware CreateContext<TContext> (optional IServiceProvider), new APIs: CreateOptionsAndConnection, CreateContextFromOptions, CreateContextWithSharedConnectionAsync; wire interceptor via GetInterceptor.
Thread-Safe Context
EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeSqliteContext.cs
Remove IAsyncDisposable from type; convert static write-lock to per-instance nullable SemaphoreSlim; rework ExecuteWriteAsync with reentrancy guard and exponential backoff on SQLITE_BUSY; add batched BulkInsertSafeAsync (1000 batch).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App as App
  participant Context as ThreadSafeSqliteContext
  participant Enhancer as SqliteConnectionEnhancer
  participant Interceptor as SqliteConcurrencyInterceptor
  participant DB as SQLite

  App->>Context: BulkInsertOptimizedAsync(entities)
  Context->>Enhancer: GetWriteLock(connectionString)
  Enhancer-->>Context: SemaphoreSlim (write lock)
  Context->>Context: Acquire lock (await)
  Context->>Context: Begin transaction (SERIALIZABLE)
  Context->>Enhancer: GetInterceptor(connectionString, options)
  Enhancer-->>Context: SqliteConcurrencyInterceptor
  Context->>DB: EF Core commands (go through Interceptor)
  Interceptor->>Enhancer: ConnectionOpened -> ApplyRuntimePragmas
  Interceptor->>DB: Upgrade BEGIN -> BEGIN IMMEDIATE (when applicable)
  DB-->>Interceptor: Responses
  Context->>Context: Commit transaction
  Context->>Enhancer: Release lock
  Context-->>App: Return result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through patches, swapped queues for hooks,

Pragmas set on open, retries in my books,
Locks held soft and batches stitched with care,
Docs and packs updated—version bump in the air,
Now SQLite hums, concurrency light as a hare.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'V10.0.2 optimizations' directly reflects the main changes: a version bump from 10.0.1 to 10.0.2 with comprehensive optimizations across CI/CD, caching, packaging, APIs, and performance tuning documented in v10_0_2.md.
Docstring Coverage ✅ Passed Docstring coverage is 95.52% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

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

18-22: Critical issue: Workflow will fail — packages.lock.json is required but missing.

The cache: true parameter in actions/setup-dotnet@v4 requires a packages.lock.json lock file to exist in the repository root. When the file is not found, the setup step fails and blocks the entire build. No lock file currently exists in this repository.

To fix this:

  • Generate packages.lock.json by running dotnet restore --locked-mode locally, then commit it to the repository, OR
  • Remove cache: true from the workflow if you don't want to maintain lock files.

If you choose to use caching, ensure packages.lock.json is committed before this workflow change is merged.

EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeFactory.cs (1)

133-148: Resource leak if context creation fails after connection opens.

If Activator.CreateInstance throws (line 148), the opened SqliteConnection (line 135) is never disposed. Wrap in try-catch or use a pattern that ensures cleanup.

🔧 Proposed fix
     public static async Task<TContext> CreateContextWithSharedConnectionAsync<TContext>(
         string connectionString,
         Action<SqliteConcurrencyOptions>? configure = null)
         where TContext : DbContext
     {
         var options = new SqliteConcurrencyOptions();
         configure?.Invoke(options);

         var enhancedConnectionString = SqliteConnectionEnhancer
             .GetOptimizedConnectionString(connectionString);

         // Create a shared connection
         var sharedConnection = new SqliteConnection(enhancedConnectionString);
-        await sharedConnection.OpenAsync();
-
-        // Apply runtime optimizations
-        SqliteConnectionEnhancer.ApplyRuntimePragmas(sharedConnection, options);
-
-        // We are not using the extension method here because we are passing an already open connection.
-        var optionsBuilder = new DbContextOptionsBuilder<TContext>()
-            .UseSqlite(sharedConnection);
-
-        // Manually add the interceptor for write queue
-        var interceptor = SqliteConnectionEnhancer.GetInterceptor(enhancedConnectionString, options);
-        optionsBuilder.AddInterceptors(interceptor);
-
-        return (TContext)Activator.CreateInstance(typeof(TContext), optionsBuilder.Options)!;
+        try
+        {
+            await sharedConnection.OpenAsync();
+
+            // Apply runtime optimizations
+            SqliteConnectionEnhancer.ApplyRuntimePragmas(sharedConnection, options);
+
+            var optionsBuilder = new DbContextOptionsBuilder<TContext>()
+                .UseSqlite(sharedConnection);
+
+            var interceptor = SqliteConnectionEnhancer.GetInterceptor(enhancedConnectionString, options);
+            optionsBuilder.AddInterceptors(interceptor);
+
+            return (TContext)Activator.CreateInstance(typeof(TContext), optionsBuilder.Options)!;
+        }
+        catch
+        {
+            await sharedConnection.DisposeAsync();
+            throw;
+        }
     }
EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeSqliteContext.cs (2)

185-199: User-configured options are ignored.

The Options property always creates a new SqliteConcurrencyOptions() with defaults if _options is null. User configurations passed to constructors are never captured, so MaxRetryAttempts will always be 3 (default), ignoring any custom settings.

🔧 Proposed fix

Capture options in constructors or retrieve from the DbContext configuration:

+    private SqliteConcurrencyOptions? _options;
+
     public ThreadSafeSqliteContext(string connectionString)
     {
         _connectionString = SqliteConnectionEnhancer.GetOptimizedConnectionString(connectionString);
         _writeLock = SqliteConnectionEnhancer.GetWriteLock(_connectionString);
+        _options = new SqliteConcurrencyOptions(); // Or accept as parameter
     }

     public ThreadSafeSqliteContext(DbContextOptions options) : base(options)
     {
         var extension = options.FindExtension<SqliteOptionsExtension>();
         if (extension?.ConnectionString != null)
         {
             _connectionString = SqliteConnectionEnhancer.GetOptimizedConnectionString(extension.ConnectionString);
             _writeLock = SqliteConnectionEnhancer.GetWriteLock(_connectionString);
         }
         // TODO: Extract SqliteConcurrencyOptions from options if available
     }

173-181: O(n²) complexity in bulk insert batching.

Using Skip(i).Take(batchSize) inside a loop creates O(n²) complexity for large entity lists since Skip must enumerate all preceding elements on each iteration.

🔧 Proposed fix using chunking
     public async Task BulkInsertSafeAsync<T>(
         IList<T> entities,
         CancellationToken ct = default) where T : class
     {
         await ExecuteWriteAsync(async ctx =>
         {
-            // Batch inserts
-            var batchSize = 1000;
-            for (int i = 0; i < entities.Count; i += batchSize)
-            {
-                var batch = entities.Skip(i).Take(batchSize).ToList();
-                await ctx.AddRangeAsync(batch, ct);
-                await ctx.SaveChangesAsync(ct);
-            }
+            const int batchSize = 1000;
+            for (int i = 0; i < entities.Count; i += batchSize)
+            {
+                var count = Math.Min(batchSize, entities.Count - i);
+                // Use list slicing for O(1) access
+                var batch = entities is List<T> list 
+                    ? list.GetRange(i, count) 
+                    : entities.Skip(i).Take(count).ToList();
+                await ctx.AddRangeAsync(batch, ct);
+                await ctx.SaveChangesAsync(ct);
+            }
         }, ct);
     }

Or use .NET's Chunk method if targeting .NET 6+:

foreach (var batch in entities.Chunk(1000))
{
    await ctx.AddRangeAsync(batch, ct);
    await ctx.SaveChangesAsync(ct);
}
🤖 Fix all issues with AI agents
In `@EntityFrameworkCore.Sqlite.Concurrency/doc/v10_0_0.md`:
- Line 135: The doc currently recommends setting
concurrencyOptions.UseWriteQueue = true but the PR states the system moved from
an event-driven write queue to interceptor-based concurrency management; update
the documentation around the concurrencyOptions.UseWriteQueue setting to clarify
its status: state whether UseWriteQueue is deprecated or retained for backward
compatibility, describe any remaining effects it has under the new interceptor
model, and replace or augment the recommendation with guidance to use the
interceptor-based approach (mention the concurrencyOptions object and the
UseWriteQueue property and note the interceptor-based alternative) so the text
is consistent with the architectural change.
- Line 22: The phrase "Guaranteed 100% write reliability and up to 10x faster
bulk operations." is an absolute claim; replace it with a softened, factual
statement — e.g. change the sentence in the v10_0_0.md content that currently
reads "Guaranteed 100% write reliability and up to 10x faster bulk operations."
to something like "Designed for high write reliability and up to 10x faster bulk
operations." or "Improved write reliability and up to 10x faster bulk operations
in typical scenarios." Ensure the revised wording removes "Guaranteed 100%" and
preserves the performance claim while avoiding absolute guarantees.
- Line 73: Replace the emphasized line "*Benchmark environment: .NET 10, Windows
11, Intel i7-13700K, 32GB RAM*" with a proper heading and plain text body:
create a heading like "Benchmark environment" (e.g., "### Benchmark
environment") and move the environment details to the next line as normal
paragraph text so the note is part of the document structure rather than
emphasized inline.
- Line 168: The migration guide currently references BulkInsertSafeAsync() which
is inconsistent with the primary docs; update the text to recommend
BulkInsertOptimizedAsync() instead. Locate the mention of BulkInsertSafeAsync
(used on ThreadSafeSqliteContext) and replace it with BulkInsertOptimizedAsync
(the method on SqliteConcurrencyExtensions) so the v10_0_0.md guide aligns with
README, QUICKSTART.md, and MemoryPackExtensions.
- Around line 116-121: The example uses a non-existent options property
(options.EnablePerformanceOptimizations) when calling
ThreadSafeFactory.CreateContext; update the example to either remove the
configuration lambda or replace that line with a valid SqliteConcurrencyOptions
property (e.g., set options.BusyTimeout or options.MaxRetryAttempts) so the
CreateContext call matches the actual SqliteConcurrencyOptions API, keeping
ExecuteWithRetryAsync usage unchanged.
- Around line 85-92: The documentation example incorrectly shows a non-existent
BulkConfig and properties (BatchSize, PreserveInsertOrder, EnableStreaming,
UseOptimalTransactionSize); update the doc to match the real API by replacing
the call with the actual signature—e.g. show usage as await
_context.BulkInsertSafeAsync(modernRecords, cancellationToken) and remove the
BulkConfig block (or alternatively, if you prefer the documented options,
refactor the BulkInsertSafeAsync<T> method to accept a config parameter and
implement the listed properties on a new BulkConfig type and wire them into
BulkInsertSafeAsync<T>).

In `@EntityFrameworkCore.Sqlite.Concurrency/doc/v10_0_2.md`:
- Around line 1-56: The markdown has inline "csharp" labels and un-fenced code
snippets causing rendering issues; fix by converting each example into proper
fenced code blocks with language tags (```csharp ... ```). Update occurrences
around the sample lines referencing _connectionStringCache initialization,
builder["Page Size"] assignment, command.CommandText = pragmas.ToString(),
PRAGMA cache_size example, the ApplyBulkOptimizationPragmas method mention, and
the VACUUM snippet so each code fragment is wrapped in ```csharp fences and
explanatory text stays outside the fences for consistent rendering.

In `@EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyExtensions.cs`:
- Around line 109-117: The batch insert loop (using entities.Chunk and calling
context.AddRangeAsync and context.SaveChangesAsync) leaves all inserted entities
tracked in the DbContext, causing memory to grow; after each successful
SaveChangesAsync call, clear the context's change tracker (e.g., call
context.ChangeTracker.Clear() or explicitly detach entries) so previously saved
entities are removed from tracking before processing the next batch; ensure this
is done inside the foreach after SaveChangesAsync to preserve batching memory
benefits.

In `@EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyInterceptor.cs`:
- Around line 161-164: Remove the two invalid methods
TransactionExplictlyStarted and TransactionExplictlyStartedAsync from
SqliteConcurrencyInterceptor: they are not part of EF Core's
IDbTransactionInterceptor and should not be implemented; locate the methods
named TransactionExplictlyStarted and TransactionExplictlyStartedAsync in
SqliteConcurrencyInterceptor and delete both their declarations (including the
XML doc lines), leaving the rest of the class and valid
IDbTransactionInterceptor implementations intact.

In `@EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConnectionEnhancer.cs`:
- Around line 60-63: GetInterceptor currently uses
_interceptors.GetOrAdd(connectionString, ...) which lets the first caller's
SqliteConcurrencyOptions silently win; update GetInterceptor to detect
mismatched options: attempt to get an existing interceptor from _interceptors by
connectionString, and if present compare its configured options with the
incoming SqliteConcurrencyOptions (expose a property or method on
SqliteConcurrencyInterceptor to obtain its options if needed); if they differ,
throw a clear ArgumentException describing the conflict (or alternatively change
the cache key to include a hash of relevant SqliteConcurrencyOptions fields if
you prefer multiple cached variants per connectionString). Also add XML doc on
GetInterceptor documenting the behavior and that callers must use consistent
options or expect an exception.
- Around line 100-116: The code marks a database as initialized via
_initializedDatabases.TryAdd(dataSource, true) before running
initCommand.ExecuteNonQuery(), so if ExecuteNonQuery throws the dataSource
remains marked and will never retry; fix by ensuring the marker is only set
after successful execution (or removed on failure): wrap the pragma execution in
a try/catch inside the lock around _pragmaLocks.GetOrAdd(dataSource,...), call
initCommand.ExecuteNonQuery() first, and then add
_initializedDatabases.TryAdd(dataSource, true) (or in the catch call
_initializedDatabases.TryRemove(dataSource, out _)) so that failures do not
leave the database marked as initialized.
🧹 Nitpick comments (10)
EntityFrameworkCore.Sqlite.Concurrency/doc/v10_0_0.md (2)

153-153: Consider proper unit formatting.

Grammar tools suggest adding a space between the number and unit: "1 ms" instead of "1ms" for better readability and adherence to SI unit conventions.

📝 Suggested fix
-A: Less than 1ms per write operation—negligible compared to the performance gains from optimized bulk operations and parallel reads.
+A: Less than 1 ms per write operation—negligible compared to the performance gains from optimized bulk operations and parallel reads.

182-182: Consider hyphenating compound adjective.

When "open source" is used as a compound adjective modifying "projects," it should be hyphenated: "open-source projects."

📝 Suggested fix
-EntityFrameworkCore.Sqlite.Concurrency is licensed under the MIT License. Free for commercial use, open source projects, and enterprise applications.
+EntityFrameworkCore.Sqlite.Concurrency is licensed under the MIT License. Free for commercial use, open-source projects, and enterprise applications.
.github/workflows/deploy.yml (1)

29-33: Ensure the cache key covers central package files.

With cache: true on Line 33, confirm the cache key includes any central package files (e.g., Directory.Packages.props, NuGet.config) so cache hits don’t go stale when dependencies change.

♻️ Optional cache-dependency-path addition (only if these files exist)
       - name: Setup .NET
         uses: actions/setup-dotnet@v4
         with:
           dotnet-version: '10.0.x'
           cache: true
+          cache-dependency-path: |
+            **/*.csproj
+            **/*.fsproj
+            **/*.vbproj
+            **/*.sln
+            Directory.Packages.props
+            NuGet.config
EntityFrameworkCore.Sqlite.Concurrency/src/ExtensionMethods/SqliteConcurrencyServiceCollectionExtensions.cs (1)

21-34: Minor: Use discard for unused provider parameter.

The provider parameter in the lambda is unused. Consider using a discard to make the intent clearer.

♻️ Suggested fix
-        services.AddDbContext<TContext>((provider, options) =>
+        services.AddDbContext<TContext>((_, options) =>
         {
             options.UseSqliteWithConcurrency(connectionString, configure);
         }, contextLifetime);
EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyExtensions.cs (2)

1-5: Remove unused import.

Microsoft.Extensions.DependencyInjection (line 5) is not used in this file.

♻️ Suggested fix
 using System.Data;
 using EntityFrameworkCore.Sqlite.Concurrency.Models;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Data.Sqlite;
-using Microsoft.Extensions.DependencyInjection;

58-77: Consider capping the retry delay.

With exponential backoff using 100 * Math.Pow(2, attempt), delays grow quickly. For maxRetries=5, the final delay would be 3.2 seconds; for maxRetries=10, it would be ~102 seconds. Consider capping the maximum delay.

♻️ Suggested fix
             catch (SqliteException ex) when (ex.SqliteErrorCode == 5 && attempt < maxRetries)
             {
                 attempt++;
-                await Task.Delay(TimeSpan.FromMilliseconds(100 * Math.Pow(2, attempt)), cancellationToken);
+                var delay = Math.Min(100 * Math.Pow(2, attempt), 5000); // Cap at 5 seconds
+                await Task.Delay(TimeSpan.FromMilliseconds(delay), cancellationToken);
             }
README.md (1)

111-116: Redundant retry configuration in example.

The example shows MaxRetryAttempts = 5 in the options (line 113), then passes maxRetries: 5 again to ExecuteWithRetryAsync (line 115). This is potentially confusing—clarify whether the method parameter overrides the configured option or if only one should be specified.

📝 Suggested clarification
     using var context = ThreadSafeFactory.CreateContext<AppDbContext>(
         "Data Source=app.db",
-        options => options.MaxRetryAttempts = 5);
+        options => options.MaxRetryAttempts = 5);  // Default retry attempts
     
-    return await context.ExecuteWithRetryAsync(operation, maxRetries: 5);
+    return await context.ExecuteWithRetryAsync(operation);  // Uses configured MaxRetryAttempts
EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeFactory.cs (1)

108-113: Null-forgiving operator without validation.

Activator.CreateInstance can return null if the type lacks a suitable constructor. Consider adding a null check or throwing a descriptive exception.

🔧 Proposed fix
     public static TContext CreateContextFromOptions<TContext>(
         DbContextOptions<TContext> options)
         where TContext : DbContext
     {
-        return (TContext)Activator.CreateInstance(typeof(TContext), options)!;
+        return (TContext?)Activator.CreateInstance(typeof(TContext), options)
+            ?? throw new InvalidOperationException(
+                $"Failed to create instance of {typeof(TContext).Name}. Ensure it has a constructor accepting DbContextOptions<{typeof(TContext).Name}>.");
     }
EntityFrameworkCore.Sqlite.Concurrency/src/ThreadSafeSqliteContext.cs (1)

108-127: Double-release risk on SQLITE_BUSY retry.

When SQLITE_BUSY is caught (lines 108-119), the lock is released at lines 111-112. However, IsWriteLockHeld.Value is set to false before releasing, so the finally block (lines 120-127) won't release again. This is correct but fragile—if the code is modified, this invariant could break.

Consider restructuring to make the single-release guarantee explicit:

🔧 Suggested restructure
             catch (SqliteException ex) when (ex.SqliteErrorCode == 5) // SQLITE_BUSY
             {
-                // Release lock before retry wait
-                SqliteConnectionEnhancer.IsWriteLockHeld.Value = false;
-                WriteLock.Release();
-
                 attempt++;
                 if (attempt >= maxRetryAttempts)
                     throw new TimeoutException($"Database busy timeout after {attempt} retries", ex);

+                // Release lock before retry wait (finally won't release since we continue the loop)
+                SqliteConnectionEnhancer.IsWriteLockHeld.Value = false;
+                WriteLock.Release();
+
                 await Task.Delay(100 * (int)Math.Pow(2, attempt), ct);
+                continue; // Explicit continue to re-enter loop and re-acquire lock
             }
EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyInterceptor.cs (1)

13-27: Unused _writeLock field.

The _writeLock field is initialized but never used within this interceptor. If write locking is handled elsewhere (e.g., ThreadSafeSqliteContext), consider removing this field to avoid confusion.

🔧 Proposed fix
 public class SqliteConcurrencyInterceptor : DbCommandInterceptor, IDbConnectionInterceptor, IDbTransactionInterceptor
 {
     private readonly SqliteConcurrencyOptions _options;
-    private readonly SemaphoreSlim _writeLock;
     private readonly string _connectionString;

     public SqliteConcurrencyInterceptor(SqliteConcurrencyOptions options, string connectionString)
     {
         _options = options;
         _connectionString = connectionString;
-        _writeLock = SqliteConnectionEnhancer.GetWriteLock(connectionString);
     }

Comment on lines +161 to +164
/// <inheritdoc />
public InterceptionResult TransactionExplictlyStarted(DbConnection connection, TransactionEndEventData eventData, InterceptionResult result) => result;
/// <inheritdoc />
public ValueTask<InterceptionResult> TransactionExplictlyStartedAsync(DbConnection connection, TransactionEndEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) => new(result);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

EF Core IDbTransactionInterceptor TransactionExplicitly method name documentation

💡 Result:

There is no TransactionExplicitly… method on EF Core’s IDbTransactionInterceptor / DbTransactionInterceptor.

In EF Core, transaction interception is exposed via methods like:

  • TransactionStarting / TransactionStarted – called around EF calling BeginTransaction(...) (this includes EF-created transactions, which may be implicit). [1]
  • TransactionUsed – called when you explicitly attach an existing transaction via RelationalDatabaseFacadeExtensions.UseTransaction(...) / UseTransactionAsync(...). [2]
  • TransactionCommitting / TransactionCommitted, TransactionRollingBack / TransactionRolledBack, plus savepoint methods. [2][3]

Docs to reference:

  • IDbTransactionInterceptor.TransactionStarting(...) (API doc) [1]
  • DbTransactionInterceptor method list (shows TransactionUsed, commit/rollback, etc.) [2]
  • Example commit hook doc: DbTransactionInterceptor.TransactionCommitting(...) [3]

Sources: [1] (learn.microsoft.com) [2] (learn.microsoft.com) [3] (learn.microsoft.com)

Citations:


Remove the TransactionExplictlyStarted and TransactionExplictlyStartedAsync methods—they do not exist in EF Core's IDbTransactionInterceptor interface.

The method name is not a typo but rather a method that has no correspondence in the interface. EF Core's IDbTransactionInterceptor defines methods like TransactionStarted, TransactionUsed, TransactionCommitting, TransactionCommitted, and others, but no variant of "TransactionExplicitly…". These methods should be removed as they are either dead code or incorrect implementations.

🤖 Prompt for AI Agents
In `@EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyInterceptor.cs`
around lines 161 - 164, Remove the two invalid methods
TransactionExplictlyStarted and TransactionExplictlyStartedAsync from
SqliteConcurrencyInterceptor: they are not part of EF Core's
IDbTransactionInterceptor and should not be implemented; locate the methods
named TransactionExplictlyStarted and TransactionExplictlyStartedAsync in
SqliteConcurrencyInterceptor and delete both their declarations (including the
XML doc lines), leaving the rest of the class and valid
IDbTransactionInterceptor implementations intact.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConnectionEnhancer.cs`:
- Around line 64-81: GetInterceptor currently has a TOCTOU where two threads can
race and bypass options validation; change it to always call
_interceptors.GetOrAdd(...) first to obtain the interceptor instance (var
interceptor = _interceptors.GetOrAdd(connectionString, cs => new
SqliteConcurrencyInterceptor(options, cs))) and then compare interceptor.Options
(type SqliteConcurrencyOptions) to the incoming options using the existing
equality logic (and FormatOptions for message) and throw the ArgumentException
if they differ; this ensures options are validated against the actual stored
SqliteConcurrencyInterceptor and prevents silent mismatches while keeping
interceptor creation via GetOrAdd.
♻️ Duplicate comments (1)
EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyInterceptor.cs (1)

166-169: Remove invalid interface methods: TransactionExplictlyStarted and TransactionExplictlyStartedAsync.

These methods do not exist in EF Core's IDbTransactionInterceptor interface. The interface defines TransactionStarted, TransactionUsed, TransactionCommitting, etc., but no "TransactionExplicitly..." variant. These appear to be dead code that will never be called.

🔧 Suggested fix - remove both methods
     /// <inheritdoc />
     public Task ReleasedSavepointAsync(DbTransaction transaction, TransactionEventData eventData, CancellationToken cancellationToken = default) => Task.CompletedTask;
-    /// <inheritdoc />
-    public InterceptionResult TransactionExplictlyStarted(DbConnection connection, TransactionEndEventData eventData, InterceptionResult result) => result;
-    /// <inheritdoc />
-    public ValueTask<InterceptionResult> TransactionExplictlyStartedAsync(DbConnection connection, TransactionEndEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) => new(result);

     // --- Connection Management (IConnectionInterceptor) ---
🧹 Nitpick comments (2)
EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyExtensions.cs (1)

64-76: Cancellation token should be checked before retry.

If the caller cancels the operation, the loop will only exit when Task.Delay throws OperationCanceledException. Adding an explicit cancellation check improves responsiveness.

♻️ Suggested improvement
         int attempt = 0;
         while (true)
         {
+            cancellationToken.ThrowIfCancellationRequested();
             try
             {
                 return await operation(context);
             }
             catch (SqliteException ex) when (ex.SqliteErrorCode == 5 && attempt < maxRetries)
             {
                 attempt++;
                 await Task.Delay(TimeSpan.FromMilliseconds(100 * Math.Pow(2, attempt)), cancellationToken);
             }
         }
EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyInterceptor.cs (1)

11-32: Unused field: _writeLock is stored but never used.

The _writeLock semaphore is retrieved from SqliteConnectionEnhancer.GetWriteLock and stored, but this interceptor never calls WaitAsync or Release on it. If locking is handled elsewhere (e.g., BulkInsertOptimizedAsync), consider removing this field to avoid confusion.

♻️ Suggested cleanup
 public class SqliteConcurrencyInterceptor : DbCommandInterceptor, IDbConnectionInterceptor, IDbTransactionInterceptor
 {
     private readonly SqliteConcurrencyOptions _options;
-    private readonly SemaphoreSlim _writeLock;
     private readonly string _connectionString;

     /// <summary>
     /// Gets the concurrency options configured for this interceptor.
     /// </summary>
     public SqliteConcurrencyOptions Options => _options;

     public SqliteConcurrencyInterceptor(SqliteConcurrencyOptions options, string connectionString)
     {
         _options = options;
         _connectionString = connectionString;
-        _writeLock = SqliteConnectionEnhancer.GetWriteLock(connectionString);
     }

@CornerstoneCode CornerstoneCode merged commit 4cdda43 into main Jan 25, 2026
0 of 3 checks passed
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.

2 participants