feat: MasterTableTenancy implements IDynamicTenantSource<string> (#377) - #385
Merged
Merged
Conversation
Polecat's MasterTableTenancy already had the full dynamic-tenancy behavior but did not *declare* JasperFx.MultiTenancy.IDynamicTenantSource<string> — the abstraction store-agnostic admin tooling (CritterWatch) resolves through. Polecat-backed services therefore showed their tenants read-only: no runtime add / disable / enable / remove. Mirroring Marten's MasterTableTenancy: * MasterTableTenancy : ITenancy, IDynamicTenantSource<string>, reporting Cardinality == DatabaseCardinality.DynamicMultiple. Cardinality is now a public member so it satisfies both ITenancy and ITenantedSource<string>. * The interface members are explicit implementations that adapt to the existing cancellable Polecat-native methods (AddDatabaseRecordAsync, DeleteDatabaseRecordAsync, Disable/EnableTenantAsync, AllDisabledAsync), which carry an optional CancellationToken and so cannot implement the token-less interface signatures implicitly. * FindAsync throws UnknownTenantIdException for an unknown *or* disabled tenant. AllActive() returns the tenant database identifiers rather than raw connection strings so credentials never reach an admin dashboard — same choice Marten makes. * The jasperfx#413 auto-assign overload keeps its default NotSupportedException: database-per-tenant has no pool to assign from. DI registration, mirroring MartenServiceCollectionExtensions: AddPolecat registers the tenancy as IDynamicTenantSource<string>, but only when the configured tenancy is a dynamic source. Non-dynamic stores must keep GetServices empty — that is the signal consumers use to fall back to a read-only tenant list. To make the probe possible while the container is still being assembled, AddPolecat(Action<StoreOptions>) now builds the StoreOptions eagerly and delegates to AddPolecat(StoreOptions), exactly as Marten's AddMarten(Action<StoreOptions>) does. The AddPolecat(Func<IServiceProvider, StoreOptions>) overload cannot probe and is documented accordingly. Also bumps the package version to 5.8.0 and documents the abstraction in docs/configuration/multitenancy.md. Closes #377 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #377. Marks the NuGet packages as 5.8.0.
What
MasterTableTenancyalready had the full dynamic-tenancy behavior (shipped in #165) but did not declareJasperFx.MultiTenancy.IDynamicTenantSource<string>— the abstraction store-agnostic admin tooling resolves through. Polecat-backed services therefore showed their tenants read-only in CritterWatch: no runtime add / disable / enable / remove.Modeled directly on Marten's
MasterTableTenancy: ITenancy, ITenancyWithMasterDatabase, IDynamicTenantSource<string>andMartenServiceCollectionExtensions.MasterTableTenancyNow
: ITenancy, IDynamicTenantSource<string>, reportingCardinality == DatabaseCardinality.DynamicMultiple.Cardinalitybecame a public member so it satisfies bothITenancyandITenantedSource<string>.The interface members are explicit implementations adapting to the existing cancellable Polecat-native methods. Those carry an optional
CancellationTokenand therefore cannot implement the token-less interface signatures implicitly; explicit implementation also keeps the concrete API unambiguous for existing callers.IDynamicTenantSource<string>AddTenantAsync(tenantId, connectionValue)AddDatabaseRecordAsyncRemoveTenantAsync(tenantId)DeleteDatabaseRecordAsyncDisableTenantAsync(tenantId)DisableTenantAsync(tenantId, token)EnableTenantAsync(tenantId)EnableTenantAsync(tenantId, token)AllDisabledAsync()AllDisabledAsync(token)FindAsyncthrowsUnknownTenantIdExceptionfor an unknown or disabled tenant — same as opening a session for it.RefreshAsyncclears the cache and re-reads the master table.AllActive()returns the tenant database identifiers, not raw connection strings, so credentials never reach an admin dashboard. This is the same choice Marten makes (it returnsMartenDatabase.Identifier).AllActiveByTenant()returnsAssignment<string>(tenantId, tenantId), matching Marten.The jasperfx#413 auto-assign overload
Task<string> AddTenantAsync(string, CancellationToken)is deliberately left on its default interface implementation, which throwsNotSupportedException. Database-per-tenant has no pool for Polecat to assign from, and CritterWatch skips sources that throw.DI registration
AddPolecatregisters the tenancy asIDynamicTenantSource<string>— but only when the configured tenancy is a dynamic source. Non-dynamic stores must keepGetServicesempty; that emptiness is the signal consumers use to fall back to a read-only tenant list.To make that probe possible while the container is still being assembled,
AddPolecat(Action<StoreOptions>)now builds theStoreOptionseagerly and delegates toAddPolecat(StoreOptions)— exactly what Marten'sAddMarten(Action<StoreOptions>)does. This is the one behavior change in the PR: the configure lambda now runs at registration time rather than on firstIDocumentStoreresolution. The lambda takes noIServiceProvider, so nothing it can legally reach has changed; theIConfigurePolecatchain andReadJasperFxOptionsstill run later, at store construction, unchanged.AddPolecat(Func<IServiceProvider, StoreOptions>)cannot probe (the options do not exist until the container is built) and is documented as such, with the one-liner for registering it manually.Tests
src/Polecat.Tests/MultiTenancy/dynamic_tenant_source_tests.cs— 7 tests driving the tenancy only through theIDynamicTenantSource<string>reference against real SQL Server databases: cardinality, add→find, unknown-tenant throw, disable/enable round-trip, remove, refresh/active-set, and theNotSupportedExceptionon auto-assign.src/Polecat.Tests/DependencyInjection/dynamic_tenant_source_registration_tests.cs— 4 tests: registered (and same instance asOptions.Tenancy) for master-table tenancy via both the lambda and pre-built-options overloads; empty for single-database and staticMultiTenantedDatabases()stores.Docs
New
### Store-Agnostic Runtime Tenant Managementsection indocs/configuration/multitenancy.md(badged 5.8), based on Marten's master-table docs: the full lifecycle through the abstraction, the DI registration, why the registration is conditional, and both caveats (auto-assignNotSupportedException; the SP-factory overload). Also notes that hard-deleting the physical database is out of scope — SQL Server needsALTER DATABASE ... SET SINGLE_USER WITH ROLLBACK IMMEDIATEbeforeDROP DATABASE, and that is the consumer's job (tracked on the CritterWatch side).🤖 Generated with Claude Code