< Summary

Information
Class: Elsa.Common.Multitenancy.DefaultTenantService
Assembly: Elsa.Common
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs
Line coverage
86%
Covered lines: 77
Uncovered lines: 12
Coverable lines: 89
Total lines: 182
Line coverage: 86.5%
Branch coverage
96%
Covered branches: 25
Total branches: 26
Branch coverage: 96.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
DisposeAsync()100%11100%
FindAsync()50%22100%
FindAsync()100%210%
GetAsync()100%210%
GetAsync()100%210%
ListAsync()100%11100%
ListAsync()100%210%
ActivateTenantsAsync()100%11100%
DeactivateTenantsAsync()100%22100%
RefreshAsync()100%88100%
GetTenantsDictionaryForMutationAsync()100%11100%
GetTenantsDictionaryAsync()100%1010100%
RegisterTenantAsync()100%11100%
UnregisterTenantAsync()100%44100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantService.cs

#LineLine coverage
 1using Elsa.Extensions;
 2using Microsoft.Extensions.DependencyInjection;
 3
 4namespace Elsa.Common.Multitenancy;
 5
 106public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScopeFactory tenantScopeFactory, TenantEvent
 7{
 108    private readonly AsyncServiceScope _serviceScope = scopeFactory.CreateAsyncScope();
 109    private readonly SemaphoreSlim _initializationLock = new(1, 1);
 1010    private readonly SemaphoreSlim _tenantMutationLock = new(1, 1);
 11    private IDictionary<string, Tenant>? _tenantsDictionary;
 12    private IDictionary<Tenant, TenantScope>? _tenantScopesDictionary;
 13
 14    public async ValueTask DisposeAsync()
 15    {
 1716        await _serviceScope.DisposeAsync();
 1717    }
 18
 19    public async Task<Tenant?> FindAsync(string id, CancellationToken cancellationToken = default)
 20    {
 5021        var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
 5022        return dictionary.TryGetValue(id.EmptyIfNull(), out var tenant) ? tenant : null;
 5023    }
 24
 25    public async Task<Tenant?> FindAsync(TenantFilter filter, CancellationToken cancellationToken = default)
 26    {
 027        var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
 028        return filter.Apply(dictionary.Values.AsQueryable()).FirstOrDefault();
 029    }
 30
 31    public async Task<Tenant> GetAsync(string id, CancellationToken cancellationToken = default)
 32    {
 033        var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
 034        return dictionary[id.EmptyIfNull()];
 035    }
 36
 37    public async Task<Tenant> GetAsync(TenantFilter filter, CancellationToken cancellationToken = default)
 38    {
 039        var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
 040        return filter.Apply(dictionary.Values.AsQueryable()).First();
 041    }
 42
 43    public async Task<IEnumerable<Tenant>> ListAsync(CancellationToken cancellationToken = default)
 44    {
 1145        var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
 1146        return dictionary.Values;
 1147    }
 48
 49    public async Task<IEnumerable<Tenant>> ListAsync(TenantFilter filter, CancellationToken cancellationToken = default)
 50    {
 051        var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
 052        return filter.Apply(dictionary.Values.AsQueryable());
 053    }
 54
 55    public async Task ActivateTenantsAsync(CancellationToken cancellationToken = default)
 56    {
 657        await RefreshAsync(cancellationToken);
 658    }
 59
 60    public async Task DeactivateTenantsAsync(CancellationToken cancellationToken = default)
 61    {
 862        var dictionary = await GetTenantsDictionaryForMutationAsync(cancellationToken);
 863        await _tenantMutationLock.WaitAsync(cancellationToken);
 64
 65        try
 66        {
 867            var tenants = dictionary.Values.ToArray();
 68
 4669            foreach (var tenant in tenants)
 70            {
 1571                await UnregisterTenantAsync(tenant, false, cancellationToken);
 72            }
 873        }
 74        finally
 75        {
 876            _tenantMutationLock.Release();
 77        }
 878    }
 79
 80    public async Task RefreshAsync(CancellationToken cancellationToken = default)
 81    {
 882        var currentTenants = await GetTenantsDictionaryForMutationAsync(cancellationToken);
 883        await _tenantMutationLock.WaitAsync(cancellationToken);
 84
 85        try
 86        {
 887            await using var scope = scopeFactory.CreateAsyncScope();
 888            var tenantsProvider = scope.ServiceProvider.GetRequiredService<ITenantsProvider>();
 889            var currentTenantIds = currentTenants.Keys;
 890            var tenantsFromProvider = (await tenantsProvider.ListAsync(cancellationToken)).ToList();
 891            var newTenants = tenantsFromProvider.Count == 0
 892                ? new Dictionary<string, Tenant> { [Tenant.DefaultTenantId] = Tenant.Default }
 2493                : tenantsFromProvider.ToDictionary(x => x.Id.EmptyIfNull());
 894            var newTenantIds = newTenants.Keys;
 895            var removedTenantIds = currentTenantIds.Except(newTenantIds).ToArray();
 896            var addedTenantIds = newTenantIds.Except(currentTenantIds).ToArray();
 97
 1898            foreach (var removedTenantId in removedTenantIds)
 99            {
 1100                var removedTenant = currentTenants[removedTenantId];
 1101                await UnregisterTenantAsync(removedTenant, true, cancellationToken);
 102            }
 103
 18104            foreach (var addedTenantId in addedTenantIds)
 105            {
 1106                var addedTenant = newTenants[addedTenantId];
 1107                await RegisterTenantAsync(addedTenant, cancellationToken);
 108            }
 8109        }
 110        finally
 111        {
 8112            _tenantMutationLock.Release();
 113        }
 8114    }
 115
 116    private async Task<IDictionary<string, Tenant>> GetTenantsDictionaryForMutationAsync(CancellationToken cancellationT
 117    {
 16118        var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
 119
 120        // The dictionary is published before initialization completes so lifecycle event handlers can read it.
 121        // Wait for any concurrent initializer before allowing a mutation to proceed.
 16122        await _initializationLock.WaitAsync(cancellationToken);
 16123        _initializationLock.Release();
 124
 16125        return dictionary;
 16126    }
 127
 128    private async Task<IDictionary<string, Tenant>> GetTenantsDictionaryAsync(CancellationToken cancellationToken)
 129    {
 77130        if (_tenantsDictionary == null)
 131        {
 10132            await _initializationLock.WaitAsync(cancellationToken); // Lock to ensure single-threaded initialization
 133            try
 134            {
 10135                if (_tenantsDictionary == null) // Double-check locking
 136                {
 10137                    _tenantsDictionary = new Dictionary<string, Tenant>();
 10138                    _tenantScopesDictionary = new Dictionary<Tenant, TenantScope>();
 10139                    var tenantsProvider = _serviceScope.ServiceProvider.GetRequiredService<ITenantsProvider>();
 10140                    var tenants = (await tenantsProvider.ListAsync(cancellationToken)).ToList();
 10141                    if (tenants.Count == 0)
 2142                        tenants = [Tenant.Default];
 143
 60144                    foreach (var tenant in tenants)
 20145                        await RegisterTenantAsync(tenant, cancellationToken);
 146                }
 10147            }
 148            finally
 149            {
 10150                _initializationLock.Release();
 151            }
 152        }
 153
 77154        return _tenantsDictionary;
 77155    }
 156
 157    private async Task RegisterTenantAsync(Tenant tenant, CancellationToken cancellationToken = default)
 158    {
 21159        var scope = tenantScopeFactory.CreateScope(tenant);
 21160        _tenantsDictionary![tenant.Id.EmptyIfNull()] = tenant;
 21161        _tenantScopesDictionary![tenant] = scope;
 162
 21163        using (tenantAccessor.PushContext(tenant))
 21164            await tenantEvents.TenantActivatedAsync(new(tenant, scope, cancellationToken));
 21165    }
 166
 167    private async Task UnregisterTenantAsync(Tenant tenant, bool isDeleted, CancellationToken cancellationToken = defaul
 168    {
 16169        if (_tenantScopesDictionary!.Remove(tenant, out var scope))
 170        {
 16171            _tenantsDictionary!.Remove(tenant.Id.EmptyIfNull(), out _);
 172
 16173            using (tenantAccessor.PushContext(tenant))
 174            {
 16175                await tenantEvents.TenantDeactivatedAsync(new(tenant, scope, cancellationToken));
 176
 16177                if (isDeleted)
 1178                    await tenantEvents.TenantDeletedAsync(new(tenant, scope, cancellationToken));
 16179            }
 180        }
 16181    }
 182}