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