GH-3975: add a declarative way to run work AFTER the transactional commit - #3976
Merged
Conversation
…mmit "After" reads like a post-handler hook that runs at the end. It does not run after the commit. The transactional commit is itself a postprocessor -- each persistence provider's ApplyTransactionSupport appends one -- and After methods are INSERTED at the front of the postprocessor list, so they run before the write is durable. There was no supported way to express the other side of it, even though Wolverine uses that position itself for FlushOutgoingMessages. Adds an AfterCommit / AfterCommitAsync convention and a [WolverineAfterCommit] attribute, with the same parameter binding After already has, on message handlers, sagas and HTTP endpoints. The position is STRUCTURAL, not positional. Frames go into a new IChain.PostCommitPostprocessors list that is concatenated after every postprocessor at frame-assembly time, rather than being appended to Postprocessors from a policy sequenced after the persistence policy. That distinction is the whole point of the issue: an application that gets the position right by luck of policy ordering gets it wrong the moment that ordering changes, silently and with every test still green. After's pre-commit position is deliberately unchanged. Plenty of post-handler work has nothing to do with durability, and moving it would be a silent behaviour break. Verified per provider rather than once. Each of Marten, Polecat, Fisher, EF Core, RavenDb and CosmosDb gets a codegen test asserting the emitted AfterCommit call really does land after that provider's own commit frame, plus the compatibility guard that After still lands before it. These are pure codegen assertions, so none of them needs a live server -- including RavenDb, whose DocumentStore.Initialize() does not open a connection. Worth noting CosmosDb has NO commit postprocessor at all: its ApplyTransactionSupport puts a TransactionalFrame in the MIDDLEWARE and only FlushOutgoingMessages in the postprocessors, so its test asserts against the flush instead of a SaveChangesAsync it never emits. Core tests pin the rest: the frame lands in PostCommitPostprocessors and NOT in Postprocessors, the attribute works without the conventional name, the observed call order is Handle -> After -> AfterCommit, and an after-commit method does NOT run when an earlier postprocessor throws (frames are concatenated without a try/finally, so the exception unwinds straight past them -- pinned so it stays true). Docs call out the distinction directly, since the issue's point is that the name currently points the wrong way for anyone thinking about durability. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JG8Un6iNeyXECKJk3jo5uC
Member
Author
|
Full Per-provider after-commit tests: Marten 2/2, Polecat 2/2, Fisher 2/2, EF Core 2/2, RavenDb 2/2, CosmosDb 2/2. Full |
This was referenced Aug 18, 2026
This was referenced Aug 26, 2026
Open
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.
Closes #3975.
Afterreads like a post-handler hook that runs at the end. It does not run after the commit. The commit is itself a postprocessor — each provider'sApplyTransactionSupportappends one — andAftermethods are inserted at the front of that list, so they run before the write is durable. There was no supported way to express the other side of it, even though Wolverine uses that position itself forFlushOutgoingMessages.What's added
AfterCommit/AfterCommitAsyncas a conventional method name, and[WolverineAfterCommit]for methods named something else. Same parameter bindingAfteralready has; works on message handlers, sagas and HTTP endpoints.The position is structural, not positional
This is the part that matters. Frames go into a new
IChain.PostCommitPostprocessorslist that is concatenated after every postprocessor at frame-assembly time — not appended toPostprocessorsfrom a policy sequenced after the persistence policy.That's the issue's actual complaint: an application that gets the position right by luck of policy ordering gets it wrong the moment that ordering changes, silently, with every test still green. A separate list can't drift that way.
After's pre-commit position is deliberately unchanged, and there's a compatibility guard per provider asserting it stays there. Plenty of post-handler work has nothing to do with durability.Verified per provider, not once
Each of Marten, Polecat, Fisher, EF Core, RavenDb and CosmosDb gets a codegen test asserting the emitted
AfterCommitcall really does land after that provider's commit frame, plus the guard thatAfterstill lands before it. List membership alone would only prove the frame went into the right list; the guarantee is about where it ends up relative to a frame contributed from a completely different code path, and only the emitted source shows that.These are pure codegen assertions, so none needs a live server — including RavenDb, whose
DocumentStore.Initialize()doesn't open a connection.One provider difference worth flagging: CosmosDb has no commit postprocessor at all. Its
ApplyTransactionSupportputs aTransactionalFramein the middleware (the outbox enlistment) and onlyFlushOutgoingMessagesin the postprocessors. Its test asserts against the flush rather than aSaveChangesAsyncit never emits.Core behaviour pinned
PostCommitPostprocessorsand not inPostprocessorsHandle → After → AfterCommittry/finally, so the exception unwinds past them. The issue asked for this to be pinned so it stays true.Docs
The issue's nice-to-have was that the
Aftername "points the wrong way for anyone thinking about durability."docs/guide/handlers/middleware.mdnow shows the emitted order explicitly, with the two caveats: these don't run when the commit throws, and they run after the outbox flush, so a cascade from one isn't atomic with the write.Verification
CoreTestsmiddleware area: 33/33; newafter_commit_middleware4/4wolverine.slnxbuilds clean pinned tonet9.0🤖 Generated with Claude Code
https://claude.ai/code/session_01JG8Un6iNeyXECKJk3jo5uC