Skip to content

Marten 9.5.2; register MasterTenantSource in DI (#3016); empty master-table seed fix (#3019); foundational per-tenant-partitioned-events tests (#3018)#3020

Merged
jeremydmiller merged 1 commit into
mainfrom
feat-3016-3018-tenant-partitioning
Jun 4, 2026
Merged

Marten 9.5.2; register MasterTenantSource in DI (#3016); empty master-table seed fix (#3019); foundational per-tenant-partitioned-events tests (#3018)#3020
jeremydmiller merged 1 commit into
mainfrom
feat-3016-3018-tenant-partitioning

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #3016. Closes #3019. Starts #3018 (foundational slice; deferred matrix tracked on the issue). Bumps to 6.4.2.

Marten 9.5.2

Bumps the Marten family 9.4.0 → 9.5.2. Its floors (JasperFx 2.8.0, Weasel 9.0.2) are already on main, so this is a Marten-only bump. Validated by a full wolverine.slnx Release build + the Marten test runs below.

#3016 — register MasterTenantSource as IDynamicTenantSource<string>

When UseMasterTableTenancy() is configured, the MasterTenantSource is now resolvable from DI as IDynamicTenantSource<string>, so store-agnostic admin consumers (e.g. CritterWatch's tenant-management handlers) can drive Add/Disable/Enable/Remove through the abstraction without sniffing the concrete type.

Registration point matters. The issue suggested registering inside BuildMessageStore, but that runs lazily off the AddSingleton<IMessageStore>(s => ...) factory — after the container is built — so it would never reach host.Services. And Configure() runs eagerly via Include(), before the fluent .UseMasterTableTenancy() sets the flag. So the registration goes in the fluent UseMasterTableTenancy() method itself (config time, pre-build); the factory defers to IMessageStore resolution, which constructs the MasterTenantSource and assigns ConnectionStringTenancy.

Tests (positive resolves MasterTenantSource; negative — no UseMasterTableTenancy → empty). Both verified fail-without/pass-with.

#3019 — empty master-table seed crash

SeedDatabasesAsync threw "CommandText property has not been initialized" when the master tenant table starts empty (no seeded tenants): the empty assignment set appended no SQL, so the compiled command had no CommandText. Now it skips execution when there is nothing to seed (an empty master table is valid — tenants can be registered at runtime). Regression test boots a host with UseMasterTableTenancy(_ => {}) and StartAsync — verified fail-without/pass-with.

#3018 — foundational per-tenant-partitioned-events test slice

First Wolverine tests exercising the aggregate-handler workflow against Events.UseTenantPartitionedEvents (Conjoined + Quick), on string and guid stream identity:

  • tenant-routed append lands in the correct tenant partition (and is invisible to other tenants);
  • the same stream-id value in two tenants stays isolated;
  • a no-tenant command falls to the default-tenant partition, isolated from named tenants.

Findings (these correct the issue's premises — see the issue checklist comment)

  • Managed partitions, not lazy 42P01. Marten 9.5.2 requires store.Advanced.AddMartenManagedTenantsAsync(...) before a tenant's first append, else MT002 — there is no lazy 42P01 provisioning. The "fresh tenant 42P01" item is therefore reframed.
  • The projected aggregate must be MultiTenanted() to match conjoined events ("Tenancy storage style mismatch" otherwise).
  • The default tenant also needs an explicit partition, registered with a table-safe suffix (*DEFAULT* is not a legal suffix).
  • Observation for follow-up: an append for a never-registered tenant via Wolverine's invoke path did not surface MT002 — left out of this slice pending investigation.

The full 2a/2b matrix (multi-node distribution, sharded DBs, HTTP endpoints, exclusive concurrency, blue/green, subscriptions/RelayWithEventTenant) remains deferred — checklist on #3018.

Validation

🤖 Generated with Claude Code

…-table seed fix (#3019); foundational per-tenant-partitioned-events tests (#3018); bump 6.4.2

- Bump Marten family 9.4.0 -> 9.5.2 (JasperFx 2.8.0 + Weasel 9.0.2 already satisfy its floors).

- #3016: register the MasterTenantSource as IDynamicTenantSource<string> in DI when
  UseMasterTableTenancy() is configured, so store-agnostic admin consumers can drive the
  dynamic-tenancy lifecycle through the abstraction. Registered in the fluent
  UseMasterTableTenancy() method (config time, before the container is built) rather than in
  BuildMessageStore (runs lazily off the IMessageStore factory, post-build) or Configure()
  (Include() runs it eagerly, before the flag is set). The factory defers to IMessageStore
  resolution, which constructs the MasterTenantSource and assigns ConnectionStringTenancy.

- #3019: SeedDatabasesAsync threw "CommandText property has not been initialized" when the
  master tenant table starts empty (no seeded tenants) — an empty assignment set appended no
  SQL, so the compiled command had no CommandText. Skip execution when there is nothing to seed.

- #3018 (foundational slice): exercise the Wolverine aggregate-handler workflow against a
  Marten store with Events.UseTenantPartitionedEvents (Conjoined + Quick), on both string and
  guid stream identity: tenant-routed append lands in the correct partition; the same stream-id
  value in two tenants stays isolated; a no-tenant command falls to the default partition,
  isolated from named tenants. Finding: Marten 9.5.2 uses MANAGED partitions
  (AddMartenManagedTenantsAsync, MT002 if unregistered) — NOT the lazy 42P01 provisioning the
  issue assumed; the projected aggregate must be MultiTenanted; the default tenant needs an
  explicit (table-safe-suffixed) partition. Deferred matrix tracked on #3018.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit 8f362ff into main Jun 4, 2026
24 checks passed
jeremydmiller added a commit that referenced this pull request Jun 4, 2026
…hapes, WriteAggregate (#3021) (#3022)

Extends the foundational single-store partitioned-events slice (#3020) with the remaining
single-store aggregate-handler scenarios, each scoped by tenant against a Conjoined + Quick +
UseTenantPartitionedEvents store (string identity), reusing TenantTally / PartitionedTenancyHost:

- [ReadAggregate] reads the routed tenant's partition; Required=false returns null for a tenant
  with no such stream.
- Every append return shape lands only in the routed tenant's partition: single event,
  IEnumerable<object>, Events, IAsyncEnumerable<object>.
- [WriteAggregate] (optimistic) appends to the routed tenant and stays isolated from other tenants.

Findings (folded into the #3021 checklist):
- MartenOps.StartStream has no tenant overload (only Store/Insert/Update/Delete do) — StartStream
  uses the ambient session tenant. The issue's StartStream(id, tenantId, events) does not exist.
- [WriteAggregate] Required=true -> 404 is an HTTP-endpoint concept; a message handler does not
  throw for a missing required aggregate. The Required/404 wrong-tenant-isolation case therefore
  belongs in the deferred HTTP cluster.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 8, 2026
This was referenced Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant