Skip to content

Fix(eventing)/outbox with dedicated db - #1434

Open
HiroshiYoshino wants to merge 2 commits into
fullstackhero:mainfrom
HiroshiYoshino:fix(eventing)/outbox-with-dedicated-db
Open

HiroshiYoshino wants to merge 2 commits into
fullstackhero:mainfrom
HiroshiYoshino:fix(eventing)/outbox-with-dedicated-db

Conversation

@HiroshiYoshino

@HiroshiYoshino HiroshiYoshino commented Oct 14, 2026 •

Copy link
Copy Markdown

Related Issue:fullstackhero/dotnet-starter-kit/issues#1427
Related Docs:fullstackhero/docs/pull#268

Problem

The system stores events in an Outbox so that follow-up work can run in the background after a user operation. Preparing Webhook notifications after a file upload is one example. Some tenants use a dedicated database rather than the shared default database.

The problem is that an event can be stored in the correct database while its follow-up processing connects to the wrong one. Before invoking consumers, the system rebuilt the tenant context using only the tenant ID. This discarded the dedicated-database connection information and caused consumers to use default instead. Webhook subscriptions in the dedicated database could therefore be missed, and Inbox records used to track completed processing could be written to the wrong database.

The database holding an event is not necessarily the database its consumers should use.

  • For a tenant’s file upload, both the Outbox and tenant-specific follow-up processing use that tenant’s dedicated database.
  • For a root operator’s tenant renewal, the Outbox is in default, but Webhook subscriptions and Inbox belong in the target tenant’s dedicated database. Billing subscriptions and invoices intentionally remain in default.

Simply reusing the Outbox database for all follow-up processing would therefore not solve the problem.

Fix

Resolve the event tenant’s complete information before constructing consumers. The implementation looks up the tenant in the default database’s tenant catalog, including its dedicated connection information, and only then creates handlers and DbContexts.

This ordering matters because DbContexts select their database at construction. Setting tenant information afterwards cannot repair an already selected connection. IEventTenantScope.ExecuteAsync provides the flow for resolution and consumer execution, restoring the previous context after success, failure, or cancellation. Webhook fanout also stops replacing the resolved context with an ID-only context.

Completion and retry updates remain in the original Outbox database, while consumers and Inbox use the appropriate destination for the event tenant. The existing synchronous API and central Billing storage remain unchanged.
Created 1 todo

Tests

  • The dedicated-database regression test fails before the fix and passes afterwards, covering consumer writes, Inbox placement, source completion, and redelivery deduplication.
  • Coverage includes default-origin root renewal, dedicated Webhook subscription lookup, central Billing, context restoration, cancellation, concurrency, and default-drain isolation.
  • The latest reported local run of dotnet test src/FSH.Starter.slnx passed all 2,026 tests. A separate rebuild of Integration.Tests also succeeded with zero warnings and errors.

Webhook tests cover subscription lookup and fanout, not successful delivery to an external HTTP endpoint.

Behaviour Change and Limits

Consumers and Inbox now follow the resolved event tenant. Claim, completion, retry, and dead-letter updates remain in the source Outbox database; central Billing storage is unchanged. Custom buses and tenant scopes should verify that full context resolution precedes consumer construction.

No schema migration or new configuration is required. This change does not automatically repair historical misrouting, introduce distributed transactions, or guarantee exactly-once processing. Dedicated databases used exclusively by inactive or deleted tenants remain excluded from automatic drains and require operator review. The companion docs PR includes the reference updates and an Unreleased changelog entry, intended to be integrated alongside the backend change.

Framework-owned Outbox rows already follow a tenant's dedicated database,
but event dispatch rebuilds the ambient tenant from its ID alone. This
loses connection metadata and can write consumer effects and Inbox rows
to default while completing the correct source Outbox.

Add a same-tenant Finbuckle scope diagnostic and a public-dispatch
PostgreSQL regression using default plus one dedicated database. Keep
all 13 assertions for persisted business rows, per-handler Inbox placement,
source completion, tenant ownership, and unchanged redelivery snapshots.

Retain the supplemental same-tenant, cross-tenant, and global routing
test. Production remains unchanged; the two primary regressions
intentionally fail on the unfixed baseline.
Supplemental test host ownership is corrected in the following fix.

Red checkpoint: 12 nearby existing tests passed and the two primary
regressions failed for the expected metadata/routing mismatch, with no
skips. The original execution checkpoint is retained in backup history.
…mers

An Outbox drain target selects the source database, not the destination
for every event it contains. A root renewal can originate in default
while its tenant-aware consumers and Inbox belong in a dedicated database.
Preserve this separation instead of inheriting the source connection or
reconstructing an ID-only tenant for dispatch.

Add ExecuteAsync with a default synchronous bridge. The Finbuckle
implementation resolves full tenant metadata from the default catalog
through a short-lived scoped resolver, disposes the lookup scope, and
runs consumer construction inside the same async callback. Restore the
previous ambient context on success, error, and cancellation.

Keep source claim, completion, retry, and dead-letter updates in the
original database. Explicitly reset default drain context. Stop Webhook
fanout from overwriting resolved metadata; keep central Billing business
data in default while its framework Inbox follows the event tenant.

Preserve Begin and the original two-argument constructor. Same-ID Begin
retains full ambient metadata; different-ID Begin remains legacy ID-only.
Global async dispatch uses minimal root/default context and preserves a
null event TenantId in Inbox. Reject unknown and inactive tenants before
consumer construction; inactive-only dedicated databases are still not
automatically drained.

Cover real Finbuckle async propagation, construction order, cancellation,
isolation, two-database routing, Webhook subscriptions, root HTTP renewal,
and redelivery after source completion failure. Replace the supplemental
test's extra host with a lightweight provider to avoid disposing shared
Hangfire storage. Production Hangfire and its shared factory are unchanged.
Update the internal eventing and Webhook guidance.

The original Green tree passed all 2,067 solution tests (no failures or
skips), including 797 integration and 55 architecture tests; the full
build had no warnings or errors. An isolated ID-only mutation failed the
primary regression and passed again after restoration. The earlier history
cleanup removed only an unrelated .gitignore entry; validated production,
test, and guidance files remain unchanged.

No distributed transaction, exactly-once guarantee, schema migration,
or automatic repair of previously misrouted data is introduced. External
docs publication and deployment/data-recovery review remain separate.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T11:15:52.080093Z 549931f Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 549931f37f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/// The default bridge uses <see cref="Begin"/>; multitenancy implementations can resolve
/// full catalog metadata and define global routing before invoking the action.
/// </summary>
async Task ExecuteAsync(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Obtain approval before changing BuildingBlocks

When no explicit approval accompanies this change, do not land the new shared interface method: the repository's AGENTS.md marks all of src/BuildingBlocks as protected and prohibits modifications without explicit approval, while this commit changes both the public eventing abstraction and the framework event bus. Record the approval or move the behavior behind a module-owned adapter before merging.

AGENTS.md reference: AGENTS.md:L82-L82

Useful? React with 👍 / 👎.

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