diff --git a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs index 9db13dc361..fca84ebc97 100644 --- a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs +++ b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs @@ -5,12 +5,15 @@ namespace Elsa.Common.Multitenancy; public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScopeFactory tenantScopeFactory, TenantEventsManager tenantEvents, ITenantAccessor tenantAccessor) : ITenantService, IAsyncDisposable { private readonly AsyncServiceScope _serviceScope = scopeFactory.CreateAsyncScope(); + private readonly SemaphoreSlim _initializationLock = new(1, 1); + private readonly SemaphoreSlim _refreshLock = new(1, 1); private IDictionary? _tenantsDictionary; private IDictionary? _tenantScopesDictionary; public async ValueTask DisposeAsync() { await _serviceScope.DisposeAsync(); + _initializationLock.Dispose(); } public async Task FindAsync(string id, CancellationToken cancellationToken = default) @@ -56,7 +59,8 @@ public async Task ActivateTenantsAsync(CancellationToken cancellationToken = def public async Task DeactivateTenantsAsync(CancellationToken cancellationToken = default) { - var tenants = _tenantsDictionary!.Values.ToArray(); + var dictionary = await GetTenantsDictionaryAsync(cancellationToken); + var tenants = dictionary.Values.ToArray(); foreach (var tenant in tenants) await UnregisterTenantAsync(tenant, cancellationToken); @@ -64,25 +68,34 @@ public async Task DeactivateTenantsAsync(CancellationToken cancellationToken = d public async Task RefreshAsync(CancellationToken cancellationToken = default) { - await using var scope = scopeFactory.CreateAsyncScope(); - var tenantsProvider = scope.ServiceProvider.GetRequiredService(); - var currentTenants = await GetTenantsDictionaryAsync(cancellationToken); - var currentTenantIds = currentTenants.Keys; - var newTenants = (await tenantsProvider.ListAsync(cancellationToken)).ToDictionary(x => x.Id); - var newTenantIds = newTenants.Keys; - var removedTenantIds = currentTenantIds.Except(newTenantIds).ToArray(); - var addedTenantIds = newTenantIds.Except(currentTenantIds).ToArray(); - - foreach (var removedTenantId in removedTenantIds) + await _refreshLock.WaitAsync(cancellationToken); + + try { - var removedTenant = currentTenants[removedTenantId]; - await UnregisterTenantAsync(removedTenant, cancellationToken); + await using var scope = scopeFactory.CreateAsyncScope(); + var tenantsProvider = scope.ServiceProvider.GetRequiredService(); + var currentTenants = await GetTenantsDictionaryAsync(cancellationToken); + var currentTenantIds = currentTenants.Keys; + var newTenants = (await tenantsProvider.ListAsync(cancellationToken)).ToDictionary(x => x.Id); + var newTenantIds = newTenants.Keys; + var removedTenantIds = currentTenantIds.Except(newTenantIds).ToArray(); + var addedTenantIds = newTenantIds.Except(currentTenantIds).ToArray(); + + foreach (var removedTenantId in removedTenantIds) + { + var removedTenant = currentTenants[removedTenantId]; + await UnregisterTenantAsync(removedTenant, cancellationToken); + } + + foreach (var addedTenantId in addedTenantIds) + { + var addedTenant = newTenants[addedTenantId]; + await RegisterTenantAsync(addedTenant, cancellationToken); + } } - - foreach (var addedTenantId in addedTenantIds) + finally { - var addedTenant = newTenants[addedTenantId]; - await RegisterTenantAsync(addedTenant, cancellationToken); + _refreshLock.Release(); } } @@ -90,13 +103,24 @@ private async Task> GetTenantsDictionaryAsync(Cancel { if (_tenantsDictionary == null) { - _tenantsDictionary = new Dictionary(); - _tenantScopesDictionary = new Dictionary(); - var tenantsProvider = _serviceScope.ServiceProvider.GetRequiredService(); - var tenants = await tenantsProvider.ListAsync(cancellationToken); - - foreach (var tenant in tenants) - await RegisterTenantAsync(tenant, cancellationToken); + await _initializationLock.WaitAsync(cancellationToken); // Lock to ensure single-threaded initialization + try + { + if (_tenantsDictionary == null) // Double-check locking + { + _tenantsDictionary = new Dictionary(); + _tenantScopesDictionary = new Dictionary(); + var tenantsProvider = _serviceScope.ServiceProvider.GetRequiredService(); + var tenants = await tenantsProvider.ListAsync(cancellationToken); + + foreach (var tenant in tenants) + await RegisterTenantAsync(tenant, cancellationToken); + } + } + finally + { + _initializationLock.Release(); + } } return _tenantsDictionary; @@ -114,11 +138,12 @@ private async Task RegisterTenantAsync(Tenant tenant, CancellationToken cancella private async Task UnregisterTenantAsync(Tenant tenant, CancellationToken cancellationToken = default) { - var scope = _tenantScopesDictionary![tenant]; - _tenantsDictionary!.Remove(tenant.Id); - _tenantScopesDictionary!.Remove(tenant); + if (_tenantScopesDictionary!.Remove(tenant, out var scope)) + { + _tenantsDictionary!.Remove(tenant.Id, out _); - using (tenantAccessor.PushContext(tenant)) - await tenantEvents.TenantDeactivatedAsync(new TenantDeactivatedEventArgs(tenant, scope, cancellationToken)); + using (tenantAccessor.PushContext(tenant)) + await tenantEvents.TenantDeactivatedAsync(new TenantDeactivatedEventArgs(tenant, scope, cancellationToken)); + } } } \ No newline at end of file diff --git a/src/modules/Elsa.Tenants.AspNetCore/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Tenants.AspNetCore/Extensions/ModuleExtensions.cs index a098277e0c..45b54a672e 100644 --- a/src/modules/Elsa.Tenants.AspNetCore/Extensions/ModuleExtensions.cs +++ b/src/modules/Elsa.Tenants.AspNetCore/Extensions/ModuleExtensions.cs @@ -13,7 +13,7 @@ public static class ModuleExtensions /// /// Installs and configures the feature. /// - public static IModule UseTenantHttpRouting(this IModule module, Action? configure = default) + public static IModule UseTenantHttpRouting(this IModule module, Action? configure = null) { module.Configure(configure); return module;