#8178·elsa-core

DefaultTenantService.RefreshAsync ignores in-place tenant mutations (Update API + ConfigurationTenantsProvider reload)

Author: sfmskywalkerCreated Sep 15, 2026Updated Sep 15, 2026
Labelsbugcoreprio lowelsa 3testtriaged

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

  1. Tenant Update APIsrc/modules/Elsa.Tenants/Endpoints/Tenants/Update/Endpoint.cs loads the tenant from ITenantStore, mutates Name / Configuration / TenantId, UpdateAsyncs the store, then await tenantService.RefreshAsync(ct). For EF (EFCoreTenantStore), FindAsync returns a new instance distinct from the object held in _tenantsDictionary; after save, Refresh leaves the cache on the old instance (stale Name/Configuration, stale TenantScope configuration). Memory can mask this when Find returns the same reference already registered in the cache (in-place mutation accidentally updates the live object).

  2. ConfigurationTenantsProviderOnOptionsChanged rebuilds _tenants from options (new list / rebound Configuration sections) and calls _tenantService.RefreshAsync(). Unchanged IDs never get their new Configuration applied to the live service.

Expected

For tenants whose ID is unchanged but whose provider/store representation changed, Refresh should either:

  • replace the cached Tenant and refresh the associated TenantScope (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

  • #7173ITenantsProvider.ListAsync scalability / loading all tenants (orthogonal; this is about mutation after load).
  • #6764ApplyTenantId stamping on Tenant entities 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/DeactivateTenantsAsync locking (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/ConfigurationITenantService.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