fix(marten): publish IEvent.TenantId verbatim under conjoined tenancy#2693
Merged
jeremydmiller merged 2 commits intomainfrom May 7, 2026
Merged
fix(marten): publish IEvent.TenantId verbatim under conjoined tenancy#2693jeremydmiller merged 2 commits intomainfrom
jeremydmiller merged 2 commits intomainfrom
Conversation
Under conjoined tenancy `PublishingRelay`'s default-tenant fallthrough let `WolverineSubscriptionRunner`'s `bus.TenantId` (= `operations.Database.Identifier`) leak onto the outbound envelope. Default-tenant events were dispatched under the database identifier (e.g. `"Main"`) instead of `*DEFAULT*`, the handler chain ran under the wrong tenant scope (or silently no-op'd), but Marten still advanced the subscription sequence because publishing succeeded. Closes GH-2675. Supersedes #2676 with a simpler implementation: bind the per-event publish path once at construction time so the hot loop in `ProcessEventsAsync` needs neither a `TenancyStyle` comparison nor a downcast through `IDocumentSession.DocumentStore`. The constructor now takes the `TenancyStyle` from the surrounding `ConfigureMarten` lambda and selects between two static method-group delegates: - `RelayWithEventTenant` (conjoined): always propagates `e.TenantId` verbatim — including `StorageConstants.DefaultTenantId` for default-tenant events. - `RelayWithLegacyFallthrough` (everything else): preserves the existing fallthrough so setups that legitimately use the database identifier as the message tenant (e.g. per-tenant ancillary stores with `TenancyStyle.Single`) keep working. Compatibility table — only the conjoined-default-tenant cell changes: | Setup | Pre-fix `Envelope.TenantId` | Post-fix `Envelope.TenantId` | |--------------------------------------------------|------------------------------|--------------------------------| | No tenancy (default Marten store) | `Database.Identifier` | `Database.Identifier` (unchanged) | | Conjoined, default-tenant event | `Database.Identifier` (bug) | `*DEFAULT*` (correct) | | Conjoined, named-tenant event | `"one"` | `"one"` (unchanged) | | `MultiTenantedDatabases(...)` per-tenant DB | `"tenant1"` | `"tenant1"` (unchanged) | | Per-tenant ancillary store, `TenancyStyle.Single`| custom identifier | custom identifier (unchanged) | Tests in `MartenSubscriptionTests` (carried over from #2676): 1. `carry_default_tenant_id_through_under_conjoined_tenancy` — publishes a default-tenant event through a conjoined store; asserts `Envelope.TenantId == StorageConstants.DefaultTenantId`. Reproduces the bug on main. 2. `non_conjoined_store_preserves_legacy_default_tenant_fallthrough` — companion test on a non-conjoined store; asserts the relay still falls through to the bus context's tenant id, guarding the per-tenant-ancillary-store pattern against accidental over-application. Both pass; full `MartenSubscriptionTests` suite is 12/12 green on net9.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#2693 supersedes #2676. The original tests + bug analysis are @svenclaesson's work; this trailer attaches the attribution that the amended trailer on the prior commit couldn't carry because force-push is disabled by the branch protection rule. Co-Authored-By: Sven Claesson <sven.claesson@devshift.se>
This was referenced May 7, 2026
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 #2675. Supersedes #2676.
Summary
PublishingRelay's default-tenant fallthrough letsWolverineSubscriptionRunner'sbus.TenantId(=operations.Database.Identifier) leak onto the outbound envelope. Default-tenant events were dispatched under the database identifier (e.g."Main") instead of*DEFAULT*, the handler chain ran under the wrong tenant scope (or silently no-op'd), but Marten still advanced the subscription sequence because publishing succeeded.ProcessEventsAsynchas noTenancyStylecomparison and no downcast throughIDocumentSession.DocumentStore.Why this is simpler than #2676
#2676 added a per-event runtime check inside the loop:
This PR binds the publish path once at construction by capturing
TenancyStylefrom the surroundingConfigureMartenlambda and picking between two static method-group delegates:The downcast is gone entirely. The conjoined hot path has zero runtime checks. The legacy path keeps its single comparison — that one's intrinsic to its semantics, not avoidable without changing behaviour.
Compatibility
Envelope.TenantIdEnvelope.TenantIdDatabase.IdentifierDatabase.Identifier(unchanged)Database.Identifier(the bug)*DEFAULT*(correct)"one""one"(unchanged)MultiTenantedDatabases(...AddSingleTenantDatabase(...))"tenant1""tenant1"(unchanged)Database.Identifier(TenancyStyle.Single)Only the conjoined-default-tenant cell changes. Every other row keeps its pre-fix behaviour because the relay only switches into
RelayWithEventTenantmode under conjoined.Tests (carried over from #2676)
carry_default_tenant_id_through_under_conjoined_tenancy— publishes a default-tenant event through a conjoined store; assertsEnvelope.TenantId == StorageConstants.DefaultTenantId. Reproduces the bug on main.non_conjoined_store_preserves_legacy_default_tenant_fallthrough— companion on a non-conjoined store; asserts the relay still falls through, guarding the per-tenant-ancillary-store pattern against accidental over-application.Test plan
dotnet test src/Persistence/MartenSubscriptionTests --framework net9.0— 12/12 passed (both regression tests included)dotnet build src/Persistence/MartenTestsclean (thePublishingRelayconsumers in the broader Marten test project still compile)🤖 Generated with Claude Code