Skip to content

Conjoined EF Core multi-tenancy, Phase 3: wolverine_tenants registry + IDynamicTenantSource for CritterWatch #3464

Description

@jeremydmiller

Phase 3 of the conjoined EF Core multi-tenancy epic (#3465). Builds on Phase 1 (registry + tenant source are useful without partitioning); integrates with Phase 2 when partitioning is enabled. CritterWatch-side gating work is tracked in the CritterWatch repo.

Goal

Runtime tenant lifecycle for conjoined EF Core tenancy, owned by Wolverine, and CritterWatch tenancy management working out of the box against a conjoined app.

Proposed design

wolverine_tenants registry table (Wolverine-owned, durability schema; a Weasel FeatureSchemaBase + IDatabaseInitializer like the partition registries):

column notes
tenant_id (PK) after TenantIdStyle correction
is_disabled disabled tenants rejected at context-build and interceptor time (UnknownTenantIdException parity with Marten's master-table tenancy)
partition_suffix (null) bucketing assignment when Phase 2 partitioning is on
created_utc / modified_utc

The registry is the authoritative tenant list for conjoined mode and exists in both plain-conjoined and partitioned-conjoined — CritterWatch tenant management works even without partitioning. Tenant lifecycle state deliberately lives here, not in Weasel's partition control tables.

ConjoinedTenantSource : IDynamicTenantSource<string>, registered in DI. That registration alone lights up CritterWatch's satellite tenant handlers (they resolve and fan out over every registered IDynamicTenantSource<string> — no satellite changes needed):

  • AddTenantAsync(tenantId) — registry insert; when partitioned, also AddPartitionToAllTables with suffix/bucket assignment. CritterWatch's existing auto-assign branch (empty connection string) maps to exactly this call.
  • AddTenantAsync(tenantId, connectionString) — invalid for conjoined ⇒ clear error ("conjoined tenants share the application database").
  • DisableTenantAsync / EnableTenantAsync / AllDisabledAsync — registry flag.
  • RemoveTenantAsync — registry delete; when partitioned, partition drop with data removal (Postgres detach+drop today; SQL Server pending ManagedTenantPartitions (SQL Server) parity with ManagedListPartitions: drop semantics, bucketing, back-fill weasel#362 item 1).
  • FindAsync / AllActiveByTenant / RefreshAsync — registry reads; the returned "connection value" is the shared app connection, masked in descriptors the way TenantedDbContextUsageSource already masks.

Admin convenience API mirroring Marten's: host.AddWolverineManagedTenantsAsync(params string[] tenantIds) / (Dictionary<string,string> tenantToSuffix) / RemoveWolverineManagedTenantsAsync(...), returning Weasel TablePartitionStatus[].

Descriptors: extend TenantedDbContextUsageSource<T> / DbContextUsage so a conjoined context advertises DatabaseCardinality.DynamicMultiple plus tenant ids on its single DatabaseDescriptor, giving CritterWatch what it needs to show the Tenants tab with actions.

Coexistence note: an app running Marten conjoined tenancy and EF Core conjoined tenancy registers two dynamic tenant sources; CritterWatch fans out to both by design (add-tenant hits both) — document this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions