Skip to content

Share tenancy-neutral identity-map state across ForTenant sessions (#4947)#4948

Merged
jeremydmiller merged 1 commit into
masterfrom
fix-4947-tenantless-identity-map
Jul 13, 2026
Merged

Share tenancy-neutral identity-map state across ForTenant sessions (#4947)#4948
jeremydmiller merged 1 commit into
masterfrom
fix-4947-tenantless-identity-map

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Fixes #4947. Correctness regression against the shipped 9.13/9.14/9.15 line — targeting a 9.15.1 patch.

The bug

A ForTenant() view of an identity- or dirty-tracked session stopped seeing tenancy-neutral (global) documents the parent session tracks. A global document has exactly one row per id for the whole database, so LoadAsync through the ForTenant view missed the identity map, went to the database, found nothing (in @dervagabund's repro the document is Store()d but not yet committed) and returned null.

Silent wrong answer, not an error. Reported working on 9.12.0, broken on 9.15.0.

Root cause

Commit 74b11a461 (PR #4807, the fix for #4801) tenant-scoped the identity map and version tracker for ForTenant sessions:

src/Marten/Internal/Sessions/NestedTenantSession.cs:29-33ItemMap/Versions are shared from the parent only when tenant.TenantId == parent.TenantId; any other tenant gets a fresh, empty ItemMap. Same in NestedTenantQuerySession.cs:20-24.

That was right for conjoined documents — where the same id means a different document per tenant — but it was applied per session rather than per document type, so it also isolated document types that are tenancy-neutral and must be shared. Broken since 9.13.0.

The fix

Sharing is now decided per document type. A nested ForTenant session aliases the parent's identity-map entry (and version-tracker entry) for a type only when all of these hold:

Aliasing happens in selectStorage, which runs on every StorageFor<T>() — i.e. before any load or store can create a competing map entry — so it is deterministic rather than order-dependent.

Tests

Bug_4947_for_tenant_tenantless_documents — the reporter's repro, LoadManyAsync, committed-document instance identity, storing through ForTenant, plus two guard rails asserting conjoined documents stay isolated.

  • 5 of the 6 fail on unmodified master (the one that passes is the conjoined guard rail, as it should).
  • The Bug_4801 suite still passes — the isolation that fix introduced is intact.
  • Full DocumentDbTests: 1047 passed, 0 failed.

Honest caveats

  • SubClassDocumentStorage does not implement the new seam, so a subclassed global document resolved via the subclass type won't have the parent's map aliased at that moment (it delegates to the root storage, which aliases when accessed through the root type). Judged out of scope for a minimal patch; untested edge.
  • EjectAllOfType on a nested session removes the entry only from that session's ItemMap; for a now-shared global type the underlying dictionary is the parent's, so the parent keeps seeing it. Pre-existing asymmetry in the eject paths, unchanged in behavior here.
  • Only net9.0 was exercised locally.

🤖 Generated with Claude Code

…4947)

A ForTenant() view of an identity/dirty-tracked session stopped seeing
tenancy-neutral (global) documents that the parent session tracks. A global
document has exactly one row per id for the whole database, so LoadAsync
through the ForTenant view missed the identity map, queried the database, found
nothing (the doc was Store()d but not yet committed) and returned null. Silent
wrong answer, not an error.

Introduced by 74b11a4 (#4807, the fix for #4801), which tenant-scoped the
identity map and version tracker for ForTenant sessions. That was right for
CONJOINED documents -- where the same id means a different document per tenant
-- but it was applied per SESSION rather than per DOCUMENT TYPE, so it also
isolated document types that are tenancy-neutral and must be shared. Broken
since 9.13.0.

The sharing is now decided per document type. A nested ForTenant session aliases
the parent's identity-map entry (and version-tracker entry) for a type only
when all of the following hold:

  - the storage is identity-mapped (so it tracks documents at all),
  - the type is NOT Conjoined -- #4801's isolation is preserved exactly, and
  - the nested session's Database is the same instance as the parent's, because
    under database-per-tenant the same id in another tenant's database is a
    different document even for a tenancy-neutral type.

Aliasing happens in selectStorage, which runs on every StorageFor<T>() -- i.e.
before any load or store can create a competing map entry -- so it is
deterministic rather than order-dependent.

Tests: Bug_4947_for_tenant_tenantless_documents -- the reporter's repro,
LoadManyAsync, committed-document instance identity, storing through ForTenant,
plus two guard rails asserting conjoined documents stay isolated. 5 of the 6
fail on unmodified master. The Bug_4801 suite still passes, and full
DocumentDbTests is 1047/0.

Fixes #4947.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit 7e88c02 into master Jul 13, 2026
9 checks passed
@jeremydmiller jeremydmiller deleted the fix-4947-tenantless-identity-map branch July 13, 2026 13:00
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.

IdentitySessions - ForTenant issue with tenantless entities

1 participant