Repository navigation
Make the EF Core capture safe beside other SaveChanges interceptors (1.0.0-preview.3) - #4
Merged
Merged
Conversation
…micity AuditRecorder.RecordAsync splits into Prepare, which applies the model's rules and formatting and returns the entry, and Counted, which meters it. The EF Core capture will use the two to add entries to the context being saved instead of going through the store. OnLostAtomicity(LostAtomicity.Report | Throw), IAuditObserver.AtomicityLost and the dotnetboost.auditing.atomicity.lost counter are what the capture reports through when another interceptor commits the transaction before the deferred Created entries are written. AtomicityLost is a default interface member, so existing observers still compile; LoggingAuditObserver logs it as an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The save lifecycle, ported from DotNetBoost.MultiLingual's design: - A save another interceptor issues from inside this one is nested: it joins the save in progress, and only the outermost save writes the deferred entries, commits or rolls back. Nested is decided by whether EF has accepted the outer save's changes; anything else starts over, rolling back what an abandoned or replayed save left open and detaching the entries it captured. - With another interceptor after it, the capture commits from EF's DbContext.SavedChanges event, once every interceptor has finished. - A transaction someone else committed early is reported, or fails the save under OnLostAtomicity(Throw). And the standalone fixes: SaveChangesCanceled rolls back; a concurrency conflict rolls back from EF's SaveChangesFailed event, since a ROLLBACK during the ThrowingConcurrencyException hook fails on PostgreSQL and SQL Server; an ambient TransactionScope is used rather than refused; a suppressed save is not captured; entries go to the context being saved, and a context without the audit tables is refused. The tracker is read once per save, and rollbacks on failure paths are best-effort so they cannot hide why the save failed. Tests: stand-ins for another library in TestSupport (a post-save writer, a replaying interceptor under a retrying strategy, and others), run in both registration orders and through SaveChanges and SaveChangesAsync, on SQLite in ProviderTests and on PostgreSQL and SQL Server in IntegrationTests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/limitations.md gains the section on what the capture does in each hook, which orders are safe, and what to do; and the notes on a throw from a later SavingChanges, suppressed saves, TransactionScope, and every audited context mapping the audit tables. The observability and configuration references cover OnLostAtomicity and the new counter, and the changelog lists preview.3's changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the EF Core capture safe beside another library's
SaveChangesinterceptor — such asDotNetBoost.MultiLingual's — registered before it or after it, with or without a retrying strategy,
and fixes four bugs that show up with Auditing alone. This is the content of
1.0.0-preview.3; thefull list is in
CHANGELOG.mdunder[Unreleased].Commits
AuditRecorder.RecordAsyncsplits intoPrepareandCounted; adds the lost-atomicityreporting API.
every new test). The fixes and the redesign share the rewritten interceptor, so they are one
commit.
Each commit builds on its own.
What changes
Beside another interceptor, ported from MultiLingual's save lifecycle into
AuditingSaveChangesInterceptor.Lifecycle.cs:progress, and only the outermost save writes the deferred entries, commits or rolls back.
Nested is decided by whether EF has already accepted the outer save's changes. Anything else
starts over: a save abandoned when a later interceptor's
SavingChangesthrew, or one beingreplayed under a retrying strategy.
DbContext.SavedChangesevent, once every interceptor has finished, so their post-save work isinside the transaction.
Standalone fixes:
DbContext.SaveChangesFailedevent. AROLLBACKin theThrowingConcurrencyExceptionhook itself fails on PostgreSQL and SQL Server, because the connection is still reading the batchTransactionScopethrewPublic API (additions only; the surface baselines are updated)
AuditingBuilder.OnLostAtomicity(LostAtomicity)andAuditingOptions.OnLostAtomicityenum LostAtomicity { Report, Throw }inCore.ModelsIAuditObserver.AtomicityLost(string correlationId, int entries): a default interface member,so existing observers still compile.
LoggingAuditObserverlogs it as an error.AuditingTelemetry.AtomicityLost, thedotnetboost.auditing.atomicity.lostcounterBehaviour changes to look at
SaveChangesAsync, and only when anotherSaveChangesinterceptor follows this one.IAuditStore.AppendAsync. It adds entries to thecontext being saved. A store wrapped to observe entries no longer sees the capture's.
Explicit
IAuditManagercalls still go through the store.exception that explains why the save failed.
Tests
New suites, run in both registration orders and through both
SaveChangesandSaveChangesAsync:TestSupport/CompositionInterceptors.cs: stand-ins for another library, without referencingone: a post-save writer committing in
SavedChangesor from EF's event, a replaying interceptorunder a retrying strategy with tracker restore, a later
SavedChangesthat throws, and others.ProviderTests/EfCoreCompositionTests.csandEfCoreSaveLifecycleTests.cson SQLite.IntegrationTests/CompositionTests.cson PostgreSQL and SQL Server, standalone fixes included.The targeted SQLite tests were written first and failed on preview.2. The engine tests use the new
API, so they did not compile against it.
Release, locally: UnitTests 189, ProviderTests 288, ApiTests 59, SurfaceTests 7 (each on net8.0
and net10.0), IntegrationTests 280. All pass. Restore passes with fresh vulnerability data, and the
Release build has 0 warnings.
Also checked against the real DotNetBoost.MultiLingual 1.0.0-preview.3, in an application with
Auditing registered first and database-generated keys:
Createdentrycommitted on preview.2, and rolls back on this branch.
EnableRetryOnFailure, every change was recorded twice on preview.2, and once here.Still open, documented in
docs/limitations.mdSaveChangesthat other interceptors see. An interceptorsharing the context should count nested saves, as MultiLingual does.
SavingChangesleaves the transaction open until thecontext's next save, which cleans it up. EF gives no hook in between.
AddInterceptorscan't be seenfollowing this one.
MultiLingual rolls back inside
ThrowingConcurrencyExceptiontoo, so it probably has the samereal-engine bug fixed here. That's for a separate PR in its repository.
🤖 Generated with Claude Code