Skip to content

feat: support serialized SaveChanges overrides - #10

Merged
CornerstoneCode merged 2 commits into
CornerstoneCode:mainfrom
JasonLandbridge:fix/serialized-save-core
Jul 21, 2026
Merged

feat: support serialized SaveChanges overrides#10
CornerstoneCode merged 2 commits into
CornerstoneCode:mainfrom
JasonLandbridge:fix/serialized-save-core

Conversation

@JasonLandbridge

@JasonLandbridge JasonLandbridge commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bug fix (non-breaking — fixes incorrect behavior)
  • New feature (non-breaking — adds functionality)
  • Breaking change (changes existing public API or behavior)
  • Documentation update
  • CI / build change
  • Performance improvement

Summary

This PR adds a delegate-based overload for SaveChangesSerializedAsync so applications can safely serialize all DbContext.SaveChangesAsync(...) calls from inside a DbContext override.

The existing SaveChangesSerializedAsync(this DbContext context, ...) API works well when call sites explicitly opt in:

await context.SaveChangesSerializedAsync();

However, it cannot currently be used to make serialization automatic at the DbContext level:

public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) =>
    this.SaveChangesSerializedAsync(cancellationToken: cancellationToken);

That override causes infinite recursion because SaveChangesSerializedAsync internally calls context.SaveChangesAsync(...), which re-enters the override.

This PR fixes that by adding an overload that accepts the actual save operation as a delegate:

public static Task<int> SaveChangesSerializedAsync(
    this DbContext context,
    Func<CancellationToken, Task<int>> saveChangesAsync,
    int maxRetries = 3,
    CancellationToken cancellationToken = default)

Consumers can now safely write:

public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) =>
    this.SaveChangesSerializedAsync(
        ct => base.SaveChangesAsync(ct),
        cancellationToken: cancellationToken);

Why this is useful

Some applications want SQLite write serialization to be a database-context invariant rather than a call-site convention.

Without this change, every write path must remember to call:

await context.SaveChangesSerializedAsync();

instead of:

await context.SaveChangesAsync();

That is easy to miss in larger applications, especially when writes happen across:

  • background jobs
  • API handlers
  • command handlers
  • domain services
  • third-party integrations
  • shared library code
  • inherited or overridden DbContext implementations

A missed call site can still bypass the shared write queue and hit SQLITE_BUSY / database is locked under concurrent load.

By allowing a DbContext override to pass base.SaveChangesAsync as the actual save delegate, applications can centralize the serialization rule once:

public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) =>
    this.SaveChangesSerializedAsync(
        ct => base.SaveChangesAsync(ct),
        cancellationToken: cancellationToken);

After that, existing application code can continue calling normal EF Core APIs:

await context.SaveChangesAsync();

and still receive the package’s serialized-write behavior.

This is especially useful for applications migrating from normal EF Core SQLite usage to this package, because they can preserve existing SaveChangesAsync call sites while enforcing serialized writes consistently.

Backward compatibility

This is additive and keeps the existing public API intact.

The existing overload remains:

SaveChangesSerializedAsync(this DbContext context, int maxRetries = 3, CancellationToken cancellationToken = default)

It now delegates to the new overload using context.SaveChangesAsync.

Existing consumers should not need to change anything.

Test coverage

This PR adds a regression test using a DbContext subclass that overrides SaveChangesAsync and calls the new delegate overload with base.SaveChangesAsync.

The test verifies that:

  • the override does not recurse;
  • concurrent writers complete successfully;
  • all expected rows are persisted.

Validation

Ran:

dotnet test EntityFrameworkCore.Sqlite.Concurrency.sln --no-restore --verbosity minimal

Result:

Passed: 5
Failed: 0
Skipped: 0

Testing

  • New tests added covering the change
  • All existing tests pass (dotnet test EFCore.Sqlite.Concurrency.Test/)
  • If performance-sensitive: benchmarks run and results included below

Checklist

  • CHANGELOG.md updated under [Unreleased]
  • XML doc comments added/updated for all public API changes
  • No Cache=Shared introduced in any connection string
  • packages.lock.json updated if dependencies changed (dotnet restore)
  • Breaking changes documented (or none — confirm below)

Breaking Changes

"None"

Summary by CodeRabbit

  • Improvements

    • Improved serialized SQLite save operations to support customized save workflows.
    • Enhanced concurrent write handling, including retry behavior, while maintaining serialized access to shared databases.
    • Existing save functionality continues to work without changes.
  • Tests

    • Added coverage for concurrent writers using customized save operations, validating that all expected records are persisted successfully.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

SaveChangesSerializedAsync now accepts custom save delegates, allowing overridden DbContext.SaveChangesAsync implementations to use serialized SQLite writes. A concurrency stress test validates that concurrent writers persist the expected number of entities.

Changes

Serialized Save Override

Layer / File(s) Summary
Delegate-based serialized save execution
EntityFrameworkCore.Sqlite.Concurrency/src/SqliteConcurrencyExtensions.cs
Adds delegate-based save execution for direct and queued serialized paths, including retries, while preserving the existing overload.
Override integration and concurrency validation
EFCore.Sqlite.Concurrency.Test/OverrideSaveChangesDbContext.cs, EFCore.Sqlite.Concurrency.Test/ConcurrencyStressTests.cs
Routes overridden saves through serialization and verifies concurrent writers persist the expected total row count.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ConcurrentWriters
  participant OverrideSaveChangesDbContext
  participant SaveChangesSerializedAsync
  participant SQLite
  ConcurrentWriters->>OverrideSaveChangesDbContext: call SaveChangesAsync
  OverrideSaveChangesDbContext->>SaveChangesSerializedAsync: pass base save delegate
  SaveChangesSerializedAsync->>SQLite: serialize and persist changes
  SQLite-->>ConcurrentWriters: return saved data
  ConcurrentWriters->>SQLite: verify final entity count
  SQLite-->>ConcurrentWriters: return expected total
Loading

Possibly related PRs

Suggested reviewers: cornerstonecode

🚥 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 is concise and accurately summarizes the main change: enabling serialized SaveChanges overrides.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@CornerstoneCode
CornerstoneCode merged commit 457e6f1 into CornerstoneCode:main Jul 21, 2026
2 checks passed
@JasonLandbridge
JasonLandbridge deleted the fix/serialized-save-core branch July 21, 2026 21:49
@JasonLandbridge

Copy link
Copy Markdown
Contributor Author

@CornerstoneCode thanks for the merge, could you publish a new release?

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