DefaultTenantService.RefreshAsync ignores in-place tenant mutations (Update API + ConfigurationTenantsProvider reload)
Summary
ITenantService.RefreshAsync only reconciles added and removed tenant IDs. It never replaces or re-activates tenants whose Id is unchanged but whose Name / Configuration (or other fields) changed. Call sites that mutate tenants and then call RefreshAsync therefore leave the live tenant cache and TenantScope on stale instances.
Evidence
Refresh only diffs ID sets
DefaultTenantService.RefreshAsync (src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs) loads tenantsProvider.ListAsync(), then:
- unregisters IDs present in the cache but missing from the provider
- registers IDs present in the provider but missing from the cache
- does nothing for IDs present in both, even when the provider returned a different
Tenantinstance or updated fields
There is no ITenantUpdatedEvent — only Activated / Deactivated / Deleted — so updates have no lifecycle path.
Unit tests (DefaultTenantServiceTests) cover empty-fallback and add/remove-style refresh, not in-place field updates.
Call sites that expect refresh to pick up mutations
Tenant Update API —
src/modules/Elsa.Tenants/Endpoints/Tenants/Update/Endpoint.csloads the tenant fromITenantStore, mutatesName/Configuration/TenantId,UpdateAsyncs the store, thenawait tenantService.RefreshAsync(ct). For EF (EFCoreTenantStore),FindAsyncreturns a new instance distinct from the object held in_tenantsDictionary; after save, Refresh leaves the cache on the old instance (staleName/Configuration, staleTenantScopeconfiguration). Memory can mask this when Find returns the same reference already registered in the cache (in-place mutation accidentally updates the live object).ConfigurationTenantsProvider —
OnOptionsChangedrebuilds_tenantsfrom options (new list / reboundConfigurationsections) and calls_tenantService.RefreshAsync(). Unchanged IDs never get their newConfigurationapplied to the live service.
Expected
For tenants whose ID is unchanged but whose provider/store representation changed, Refresh should either:
- replace the cached
Tenantand refresh the associatedTenantScope(typically deactivate + activate), or - document that Refresh is ID-set-only and provide an explicit update path the Update API / configuration provider must use.
Prefer the first: Update API and configuration reload already call Refresh intending “apply latest tenant definitions.”
Not duplicates
- #7173 —
ITenantsProvider.ListAsyncscalability / loading all tenants (orthogonal; this is about mutation after load). - #6764 —
ApplyTenantIdstamping onTenantentities so EF List cannot see them (related multitenancy pain; different mechanism). - #8172 / #8173 — Memory leftover workflow/runtime store tenant filters after #8087 (different stores).
- Closed #7771 — Refresh/
DeactivateTenantsAsynclocking (thread-safety), not update semantics.
Suggested direction (non-binding)
On Refresh, for intersecting IDs, detect material changes (at least Name / Configuration) and unregister + register (or add ITenantUpdatedEvent and coordinate task lifecycle). Add a unit test: provider returns same ID with changed Name/Configuration → ITenantService.FindAsync reflects the new values after RefreshAsync.
Milestone: unset — Elsa 3 Issue Triage / Crew Lead place as needed. Soft proposal: Backlog near other multitenancy correctness work; not a reason to block #7173.
Source: elsa-workflows/elsa-core