Skip to content

fix(payment): flush Wolverine outbox in ExecuteInTransactionAsync#26

Merged
emeraldleaf merged 2 commits into
mainfrom
fix/payment-outbox-savechanges
May 24, 2026
Merged

fix(payment): flush Wolverine outbox in ExecuteInTransactionAsync#26
emeraldleaf merged 2 commits into
mainfrom
fix/payment-outbox-savechanges

Conversation

@emeraldleaf

@emeraldleaf emeraldleaf commented May 24, 2026

Copy link
Copy Markdown
Owner

ExecuteInTransactionAsync opens an EF transaction, runs the caller's work delegate, then commits. Missing: a SaveChangesAsync between work() and Commit. Without it, Wolverine's outbox staging — which happens via UseEntityFrameworkCoreTransactions' SaveChanges interceptor — never reaches the wolverine.outgoing_envelopes table. The entity write commits, the event publish silently disappears.

Concrete failure mode: PaymentRecoveryJob calls
ExecuteInTransactionAsync(async ct => { await UpdateAsync(payment, ct); await eventPublisher.PublishAsync(failedEvent, ct); }, ct). UpdateAsync's internal SaveChanges flushes the Payment row. PublishAsync then stages the envelope to the tracker. work() returns. tx.CommitAsync commits the Payment update, but no SaveChanges has run to flush the envelope — it stays in the tracker, the transaction commits without it, and PaymentFailedEvent is dropped. The saga stalls — Order never sees the payment failed, never marks itself failed, customer never sees a refund offer.

Fix is one line: await context.SaveChangesAsync(ct) between work() and Commit. The teaching comment is generous because this is exactly the kind of "looks correct, silently wrong" outbox bug that the architecture review caught and that a junior reader needs to understand to not regress.

Discovered by: architecture-reviewer agent pass on PaymentRecoveryJob.

Follow-up not in this PR: integration test that asserts a row appears in wolverine.outgoing_envelopes inside the same transaction as the Payment update. Should be added to tests/PaymentService.Tests.Integration when that slice exists.

What changed

How it was built

Verification

Touches (check only if applies)

  • EF Core migration (immutable once applied — see CLAUDE.md "Performance Rules")
  • CLAUDE.md or a See CLAUDE.md paraphrase (run /check-rules to audit drift)

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where payment-related outbox events could be lost during transaction processing by ensuring proper persistence within the same transaction.

Review Change Stack

ExecuteInTransactionAsync opens an EF transaction, runs the caller's work
delegate, then commits. Missing: a SaveChangesAsync between work() and
Commit. Without it, Wolverine's outbox staging — which happens via
UseEntityFrameworkCoreTransactions' SaveChanges interceptor — never reaches
the wolverine.outgoing_envelopes table. The entity write commits, the event
publish silently disappears.

Concrete failure mode: PaymentRecoveryJob calls
ExecuteInTransactionAsync(async ct => { await UpdateAsync(payment, ct);
await eventPublisher.PublishAsync(failedEvent, ct); }, ct).
UpdateAsync's internal SaveChanges flushes the Payment row. PublishAsync
then stages the envelope to the tracker. work() returns. tx.CommitAsync
commits the Payment update, but no SaveChanges has run to flush the
envelope — it stays in the tracker, the transaction commits without it,
and PaymentFailedEvent is dropped. The saga stalls — Order never sees the
payment failed, never marks itself failed, customer never sees a refund
offer.

Fix is one line: `await context.SaveChangesAsync(ct)` between work() and
Commit. The teaching comment is generous because this is exactly the kind
of "looks correct, silently wrong" outbox bug that the architecture review
caught and that a junior reader needs to understand to not regress.

Discovered by: architecture-reviewer agent pass on PaymentRecoveryJob.

Follow-up not in this PR: integration test that asserts a row appears in
wolverine.outgoing_envelopes inside the same transaction as the Payment
update. Should be added to tests/PaymentService.Tests.Integration when
that slice exists.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown

Walkthrough

PaymentRepository's ExecuteInTransactionAsync method adds an explicit SaveChangesAsync call after the work delegate completes, ensuring Wolverine outbox envelopes staged during that work are flushed to the database within the same transaction boundary before commit.

Changes

Outbox envelope transactional persistence

Layer / File(s) Summary
Transactional SaveChanges for outbox envelopes
PaymentService/Infrastructure/PaymentRepository.cs
ExecuteInTransactionAsync adds an explicit SaveChangesAsync(ct) after invoking work(ct) and before committing the transaction, ensuring Wolverine outbox envelopes are flushed within the same transaction scope.

🎯 2 (Simple) | ⏱️ ~8 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(payment): flush Wolverine outbox in ExecuteInTransactionAsync' directly and specifically describes the main change: adding an explicit SaveChangesAsync call to flush Wolverine outbox envelopes within a transaction.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/payment-outbox-savechanges

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

@codecov

codecov Bot commented May 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
PaymentService/Infrastructure/PaymentRepository.cs 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@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.

Caution

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

⚠️ Outside diff range comments (1)
PaymentService/Infrastructure/PaymentRepository.cs (1)

5-60: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use a true file-scoped namespace in this C# file.

This file still uses block-scoped layout; switch to file-scoped namespace style consistently.

♻️ Suggested update
-namespace PaymentService.Infrastructure;
-
-/// <summary>
-/// EF Core implementation of <see cref="IPaymentRepository"/>. Both <see cref="GetByIdAsync"/>
-/// and <see cref="GetByOrderIdAsync"/> are shared with the command path
-/// (<c>ProcessPaymentHandler</c> uses <c>GetByOrderIdAsync</c> as the idempotency check then
-/// later mutates and updates the entity), so tracking stays ON for both — see
-/// <c>docs/cqrs-data-access.md</c> for the rationale.
-/// </summary>
-public class PaymentRepository(PaymentDbContext context) : IPaymentRepository
-{
+namespace PaymentService.Infrastructure;
+
+/// <summary>
+/// EF Core implementation of <see cref="IPaymentRepository"/>. Both <see cref="GetByIdAsync"/>
+/// and <see cref="GetByOrderIdAsync"/> are shared with the command path
+/// (<c>ProcessPaymentHandler</c> uses <c>GetByOrderIdAsync</c> as the idempotency check then
+/// later mutates and updates the entity), so tracking stays ON for both — see
+/// <c>docs/cqrs-data-access.md</c> for the rationale.
+/// </summary>
+public class PaymentRepository(PaymentDbContext context) : IPaymentRepository
+{
   ...
 }

As per coding guidelines: "**/*.cs: File-scoped namespaces required."

🤖 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 `@PaymentService/Infrastructure/PaymentRepository.cs` around lines 5 - 60, The
file currently uses a block-scoped namespace; change it to a file-scoped
namespace by declaring "namespace PaymentService.Infrastructure;" with a
trailing semicolon and removing the surrounding namespace { ... } braces so the
top-level class PaymentRepository (and its constructor
PaymentRepository(PaymentDbContext context) and methods like GetByIdAsync,
GetByOrderIdAsync, AddAsync, UpdateAsync, GetStalePendingPaymentIdsAsync,
ExecuteInTransactionAsync) are declared at file scope; keep all existing
comments, usings and members intact and adjust indentation accordingly.
🤖 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.

Outside diff comments:
In `@PaymentService/Infrastructure/PaymentRepository.cs`:
- Around line 5-60: The file currently uses a block-scoped namespace; change it
to a file-scoped namespace by declaring "namespace
PaymentService.Infrastructure;" with a trailing semicolon and removing the
surrounding namespace { ... } braces so the top-level class PaymentRepository
(and its constructor PaymentRepository(PaymentDbContext context) and methods
like GetByIdAsync, GetByOrderIdAsync, AddAsync, UpdateAsync,
GetStalePendingPaymentIdsAsync, ExecuteInTransactionAsync) are declared at file
scope; keep all existing comments, usings and members intact and adjust
indentation accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4068bb4f-7697-4633-bf48-e0c0668775c8

📥 Commits

Reviewing files that changed from the base of the PR and between 709f9d9 and 64dd4c5.

📒 Files selected for processing (1)
  • PaymentService/Infrastructure/PaymentRepository.cs

@emeraldleaf
emeraldleaf merged commit 0dca379 into main May 24, 2026
6 of 7 checks passed
@emeraldleaf
emeraldleaf deleted the fix/payment-outbox-savechanges branch June 4, 2026 00:40
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