Skip to content

GH-3531: cover EF Core + Marten conjoined tenancy in one database - #4043

Merged
jeremydmiller merged 2 commits into
mainfrom
gh-3531-mixed-conjoined-tenancy
Aug 24, 2026
Merged

GH-3531: cover EF Core + Marten conjoined tenancy in one database#4043
jeremydmiller merged 2 commits into
mainfrom
gh-3531-mixed-conjoined-tenancy

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Covers scenario 1 of #3531.

Every existing conjoined battery gives EF Core the database to itself. A real Critter Stack app does not. This adds the mixed-persistence coverage the issue asks for: Wolverine-managed conjoined EF tenancy with PartitionPerTenant, and Marten conjoined tenancy with Marten-managed partitioning, against one Postgres database, each in its own schema.

Asserted against pg_catalog, not against either engine

This is the design decision worth reviewing. Every assertion reads pg_inherits / pg_tables directly rather than asking Wolverine or Marten what they think they did — because an engine reporting on its own partitions cannot show that it left the other one alone, which is the entire question the issue poses.

test what it pins
each_engine_keeps_its_control_tables_in_its_own_schema wolverine_tenants + wolverine_tenant_partitions in the Wolverine schema, mt_tenant_partitions in Marten's, neither in the other's or in EF's
registering_a_tenant_with_wolverine_partitions_only_the_ef_table AddWolverineManagedTenantsAsync partitions mixed_items and leaves Marten's document table exactly as it was
registering_a_tenant_with_marten_does_not_partition_the_ef_table the mirror, through AddMartenManagedTenantsAsync
cross_tenant_isolation_holds_for_both_engines_in_one_database an EF entity and a Marten document written per tenant, each readable only by its own tenant, from the same database

A finding that probably wants its own issue

.IntegrateWithWolverine() cannot currently be combined with conjoined EF tenancy. Handing envelope storage to Marten leaves ConjoinedDbContextBuilder unable to resolve a connection string — it reads _database.Settings.ConnectionString from the Wolverine message store, and Marten's store surfaces none — so the host dies at startup with:

Unable to determine the database connection string for the conjoined multi-tenanted DbContext ...

That is a plausible real-world combination (Marten for envelopes + conjoined EF for entities), so it looks like a genuine gap rather than an unsupported pairing. These tests therefore keep Wolverine on its own Postgres message store, which does not weaken them — they are about partition and tenant ownership between the engines, not about who owns envelope storage. The constraint is recorded in a comment at the exact place the next reader will hit it. Happy to file it separately.

Scenario 2 (EF + Polecat) is not here

Its stated gate, JasperFx/polecat#335, has since closed, so it is genuinely unblocked now. But it needs a Wolverine.Polecat reference this test project does not carry, and it is better as its own change than bolted onto this one.

Verified

  • EfCoreTests.MultiTenancy: 197 tests, 0 failed (193 before these four).
  • dotnet build wolverine.slnx -c Release -f net9.0 clean.
  • No new CI target needed — this project already runs under CIEfCore and already referenced Wolverine.Marten, so nothing has to be wired into a workflow (and GH-3816: fail the build when a CI target runs nowhere #4037's guard would have caught it if it did).

Two notes from building it, in case they save someone time later: MixedDoc needs m.Schema.For<MixedDoc>().MultiTenanted() — with only RegisterDocumentType it is single-tenant, so Marten creates no partitions and isolation silently does not apply. And Marten and EF Core both ship an AnyAsync extension, so a file that sees both must qualify the call.

🤖 Generated with Claude Code

Every existing conjoined battery gives EF Core the database to itself. A real
Critter Stack app does not, so this adds the mixed-persistence coverage:
Wolverine-managed conjoined EF tenancy with PartitionPerTenant and Marten
conjoined tenancy with Marten-managed partitioning, against ONE Postgres
database, each in its own schema.

Four tests, all asserted against pg_catalog rather than against either engine's
own API -- an engine reporting on its own partitions cannot show that it left
the other one alone:

- each_engine_keeps_its_control_tables_in_its_own_schema: wolverine_tenants and
  wolverine_tenant_partitions in the Wolverine schema, mt_tenant_partitions in
  Marten's, and neither appears in the other's schema or in EF's.
- registering_a_tenant_with_wolverine_partitions_only_the_ef_table: adding a
  tenant through AddWolverineManagedTenantsAsync creates the partition on
  mixed_items and leaves Marten's document table byte-for-byte as it was.
- registering_a_tenant_with_marten_does_not_partition_the_ef_table: the mirror,
  through AddMartenManagedTenantsAsync.
- cross_tenant_isolation_holds_for_both_engines_in_one_database: an EF entity
  and a Marten document written per tenant, each readable only by its own
  tenant, from the same database.

Scenario 2 of the issue (EF + Polecat) is not here. Its stated gate --
JasperFx/polecat#335 -- has since closed, so it is unblocked, but it needs a
Wolverine.Polecat reference this project does not carry and is better as its own
change than bolted onto this one.

One finding worth its own issue: .IntegrateWithWolverine() cannot currently be
combined with conjoined EF tenancy. Handing envelope storage to Marten leaves
ConjoinedDbContextBuilder unable to resolve a connection string -- it reads
_database.Settings.ConnectionString from the Wolverine message store, and
Marten's store surfaces none -- so the host dies at startup with "Unable to
determine the database connection string for the conjoined multi-tenanted
DbContext". These tests therefore keep Wolverine on its own Postgres message
store, which does not weaken them: they are about partition and tenant
OWNERSHIP between the engines, not about who owns envelope storage. The
constraint is recorded in the file where the next reader will hit it.

Verified: EfCoreTests.MultiTenancy 197 tests, 0 failed (193 before these four).
wolverine.slnx -c Release -f net9.0 clean. No new CI target needed -- this
project already runs under CIEfCore and already referenced Wolverine.Marten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jeremydmiller

Copy link
Copy Markdown
Member Author

Filed the IntegrateWithWolverine() interaction noted above as #4044, with the root cause traced.

Short version: BuildSinglePostgresqlMessageStore (WolverineOptionsMartenExtensions.cs:314) hands the NpgsqlDataSource to the PostgresqlMessageStore but never puts it on the DatabaseSettings it builds — so ConjoinedDbContextBuilder.buildOptions() finds both Settings.ConnectionString and Settings.DataSource null. Its two sibling paths (createTenantWolverineStore, and the master settings in BuildMultiTenantedMessageDatabase) both do populate DataSource, so the single-database case is the odd one out — likely a one-line fix rather than a design limitation.

The comment in this PR pointing at the constraint now references #4044.

@jeremydmiller
jeremydmiller merged commit 4b615a9 into main Aug 24, 2026
39 checks passed
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