< Summary

Information
Class: Elsa.Workflows.ActivityRegistry
Assembly: Elsa.Workflows.Core
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs
Line coverage
93%
Covered lines: 150
Uncovered lines: 11
Coverable lines: 161
Total lines: 390
Line coverage: 93.1%
Branch coverage
94%
Covered branches: 83
Total branches: 88
Branch coverage: 94.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Add(...)100%11100%
Remove(...)100%22100%
ListAll()100%22100%
ListByProvider(...)83.33%66100%
Find(...)100%66100%
Find(...)100%66100%
Find(...)100%44100%
FindMany(...)0%620%
Register(...)100%11100%
RegisterAsync()100%22100%
RegisterAsync()100%22100%
GetDescriptorsAsync(...)100%11100%
RefreshDescriptorsAsync()100%22100%
RefreshDescriptorsAsync()100%88100%
EnsureDescriptorsAsync()100%66100%
Add(...)83.33%6685.71%
Clear()100%11100%
ClearProvider(...)100%1010100%
ClearTenant(...)100%210%
GetOrCreateRegistry(...)100%66100%
GetOrCreateProviderDescriptors(...)100%11100%
UpdateLatestDescriptor(...)100%22100%
RemoveDescriptor(...)83.33%6680%
RecomputeLatestDescriptor(...)100%1010100%
NormalizeTenantIdForGrouping(...)100%22100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.Diagnostics.CodeAnalysis;
 3using Elsa.Common.Multitenancy;
 4using Elsa.Workflows.Helpers;
 5using Elsa.Workflows.Models;
 6using Microsoft.Extensions.Logging;
 7
 8namespace Elsa.Workflows;
 9
 10/// <inheritdoc />
 65111public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<IActivityDescriptorModifier> modifiers, 
 12{
 13    // Legacy support for manually registered activities
 65114    private readonly ISet<ActivityDescriptor> _manualActivityDescriptors = new HashSet<ActivityDescriptor>();
 15
 16    // Per-tenant activity descriptors (workflow-as-activities, tenant-specific providers, etc.)
 65117    private readonly ConcurrentDictionary<string, TenantRegistryData> _tenantRegistries = new();
 18
 19    // Tenant-agnostic activity descriptors (built-in activities, manually registered, etc.)
 65120    private readonly TenantRegistryData _agnosticRegistry = new();
 21
 22    // Tracks tenant-agnostic providers initialized for this registry instance.
 65123    private readonly ConcurrentDictionary<Type, byte> _initializedProviders = new();
 65124    private readonly ConcurrentDictionary<Type, SemaphoreSlim> _providerInitializationLocks = new();
 25
 26    /// <inheritdoc />
 27    public void Add(Type providerType, ActivityDescriptor descriptor)
 28    {
 13129        var registry = GetOrCreateRegistry(descriptor.TenantId);
 13130        var providerDescriptors = GetOrCreateProviderDescriptors(registry, providerType);
 13131        Add(descriptor, registry, providerDescriptors);
 13132    }
 33
 34    /// <inheritdoc />
 35    public void Remove(Type providerType, ActivityDescriptor descriptor)
 36    {
 837        var registry = GetOrCreateRegistry(descriptor.TenantId);
 838        if (registry.ProvidedActivityDescriptors.TryGetValue(providerType, out var providerDescriptors))
 39        {
 840            providerDescriptors.Remove(descriptor);
 841            RemoveDescriptor(registry, descriptor);
 42        }
 843    }
 44
 45    /// <inheritdoc />
 46    public IEnumerable<ActivityDescriptor> ListAll()
 47    {
 1805148        var currentTenantId = tenantAccessor.TenantId;
 49
 50        // Get descriptors from current tenant's registry
 1805151        var tenantDescriptors = _tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry)
 1805152            ? tenantRegistry.ActivityDescriptors.Values
 1805153            : Enumerable.Empty<ActivityDescriptor>();
 54
 55        // Get descriptors from agnostic registry
 1805156        var agnosticDescriptors = _agnosticRegistry.ActivityDescriptors.Values;
 57
 1805158        return tenantDescriptors.Concat(agnosticDescriptors);
 59    }
 60
 61    /// <inheritdoc />
 62    public IEnumerable<ActivityDescriptor> ListByProvider(Type providerType)
 63    {
 128264        var currentTenantId = tenantAccessor.TenantId;
 65
 66        // Get descriptors from current tenant's registry
 128267        var tenantDescriptors = _tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry) &&
 128268                                tenantRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var tenantProvi
 128269            ? tenantProviderDescriptors
 128270            : Enumerable.Empty<ActivityDescriptor>();
 71
 72        // Get descriptors from agnostic registry
 128273        var agnosticDescriptors = _agnosticRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var agnost
 128274            ? agnosticProviderDescriptors
 128275            : Enumerable.Empty<ActivityDescriptor>();
 76
 128277        return tenantDescriptors.Concat(agnosticDescriptors);
 78    }
 79
 80    /// <inheritdoc />
 81    public ActivityDescriptor? Find(string type)
 82    {
 51383        var currentTenantId = tenantAccessor.TenantId;
 84
 85        // Always prefer tenant-specific descriptors over tenant-agnostic ones
 86        // Get highest version from current tenant's registry
 51387        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry))
 88        {
 9689            if (tenantRegistry.LatestActivityDescriptors.TryGetValue(type, out var tenantDescriptor))
 1390                return tenantDescriptor;
 91        }
 92
 93        // Fall back to agnostic registry only if no tenant-specific descriptor exists
 50094        return _agnosticRegistry.LatestActivityDescriptors.TryGetValue(type, out var agnosticDescriptor)
 50095            ? agnosticDescriptor
 50096            : null;
 97    }
 98
 99    /// <inheritdoc />
 100    public ActivityDescriptor? Find(string type, int version)
 101    {
 62096102        var currentTenantId = tenantAccessor.TenantId;
 103
 104        // Check current tenant's registry first
 62096105        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry) &&
 62096106            tenantRegistry.ActivityDescriptors.TryGetValue((type, version), out var tenantDescriptor))
 107        {
 3011108            return tenantDescriptor;
 109        }
 110
 111        // Fall back to agnostic registry
 59085112        return _agnosticRegistry.ActivityDescriptors.TryGetValue((type, version), out var agnosticDescriptor)
 59085113            ? agnosticDescriptor
 59085114            : null;
 115    }
 116
 117    /// <inheritdoc />
 118    public ActivityDescriptor? Find(Func<ActivityDescriptor, bool> predicate)
 119    {
 745120        var currentTenantId = tenantAccessor.TenantId;
 121
 122        // Check current tenant's registry first
 745123        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry))
 124        {
 574125            var tenantMatch = tenantRegistry.ActivityDescriptors.Values.FirstOrDefault(predicate);
 1120126            if (tenantMatch != null) return tenantMatch;
 127        }
 128
 129        // Fall back to agnostic registry
 199130        return _agnosticRegistry.ActivityDescriptors.Values.FirstOrDefault(predicate);
 131    }
 132
 133    /// <inheritdoc />
 134    public IEnumerable<ActivityDescriptor> FindMany(Func<ActivityDescriptor, bool> predicate)
 135    {
 0136        var currentTenantId = tenantAccessor.TenantId;
 137
 138        // Get descriptors from current tenant's registry
 0139        var tenantDescriptors = _tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry)
 0140            ? tenantRegistry.ActivityDescriptors.Values.Where(predicate)
 0141            : Enumerable.Empty<ActivityDescriptor>();
 142
 143        // Get descriptors from agnostic registry
 0144        var agnosticDescriptors = _agnosticRegistry.ActivityDescriptors.Values.Where(predicate);
 145
 0146        return tenantDescriptors.Concat(agnosticDescriptors);
 147    }
 148
 149    /// <inheritdoc />
 150    public void Register(ActivityDescriptor descriptor)
 151    {
 33152        var registry = GetOrCreateRegistry(descriptor.TenantId);
 33153        var providerDescriptors = GetOrCreateProviderDescriptors(registry, GetType());
 33154        Add(descriptor, registry, providerDescriptors);
 33155    }
 156
 157    /// <inheritdoc />
 158    public async Task RegisterAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type a
 159    {
 18047160        var activityTypeName = ActivityTypeNameHelper.GenerateTypeName(activityType);
 161
 162        // Check if already registered in any registry
 971831163        if (ListAll().Any(x => x.TypeName == activityTypeName))
 16806164            return;
 165
 1241166        var activityDescriptor = await activityDescriber.DescribeActivityAsync(activityType, cancellationToken);
 167
 1241168        var registry = GetOrCreateRegistry(activityDescriptor.TenantId);
 1241169        Add(activityDescriptor, registry, _manualActivityDescriptors);
 18047170    }
 171
 172    /// <inheritdoc />
 173    public async Task RegisterAsync(IEnumerable<Type> activityTypes, CancellationToken cancellationToken = default)
 174    {
 44650175        foreach (var activityType in activityTypes)
 17550176            await RegisterAsync(activityType, cancellationToken);
 4775177    }
 178
 179    /// <inheritdoc />
 1180    public ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
 181
 182    /// <inheritdoc />
 183    public async Task RefreshDescriptorsAsync(IEnumerable<IActivityProvider> activityProviders, CancellationToken cancel
 184    {
 1890185        foreach (var activityProvider in activityProviders)
 62186            await RefreshDescriptorsAsync(activityProvider, cancellationToken);
 883187    }
 188
 189    public async Task RefreshDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = 
 190    {
 617191        var providerType = activityProvider.GetType();
 192
 193        // Get new descriptors from provider
 617194        var descriptors = (await activityProvider.GetDescriptorsAsync(cancellationToken)).ToList();
 195
 196        // Group descriptors by normalized tenant ID
 197        // Normalize null to "*" so both map to the same agnostic group, avoiding redundant processing
 7771198        var descriptorsByTenant = descriptors.GroupBy(d => NormalizeTenantIdForGrouping(d.TenantId));
 199
 1672200        foreach (var group in descriptorsByTenant)
 201        {
 221202            var tenantId = group.Key;
 221203            var registry = GetOrCreateRegistry(tenantId);
 204
 205            // Remove old descriptors for this provider from this tenant's registry
 221206            if (registry.ProvidedActivityDescriptors.TryGetValue(providerType, out var oldDescriptors))
 207            {
 2352208                foreach (var oldDescriptor in oldDescriptors.ToList())
 209                {
 1106210                    RemoveDescriptor(registry, oldDescriptor);
 211                }
 212            }
 213
 214            // Add new descriptors for this tenant
 221215            var providerDescriptors = new List<ActivityDescriptor>();
 14754216            foreach (var descriptor in group)
 217            {
 7156218                Add(descriptor, registry, providerDescriptors);
 219            }
 220
 221            // Update the provider's descriptor list in this registry
 221222            registry.ProvidedActivityDescriptors[providerType] = providerDescriptors;
 223        }
 615224    }
 225
 226    /// <inheritdoc />
 227    public async Task EnsureDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = d
 228    {
 872229        if (activityProvider is not ITenantAgnosticActivityProvider)
 230        {
 286231            await RefreshDescriptorsAsync(activityProvider, cancellationToken);
 286232            return;
 233        }
 234
 586235        var providerType = activityProvider.GetType();
 586236        if (_initializedProviders.ContainsKey(providerType))
 315237            return;
 238
 531239        var initializationLock = _providerInitializationLocks.GetOrAdd(providerType, _ => new(1, 1));
 271240        await initializationLock.WaitAsync(cancellationToken);
 241
 242        try
 243        {
 271244            if (_initializedProviders.ContainsKey(providerType))
 7245                return;
 246
 264247            await RefreshDescriptorsAsync(activityProvider, cancellationToken);
 262248            _initializedProviders.TryAdd(providerType, 0);
 262249        }
 250        finally
 251        {
 271252            initializationLock.Release();
 253        }
 870254    }
 255
 256    private void Add(ActivityDescriptor? descriptor, TenantRegistryData registry, ICollection<ActivityDescriptor> provid
 257    {
 8561258        if (descriptor is null)
 259        {
 0260            logger.LogError("Unable to add a null descriptor");
 0261            return;
 262        }
 263
 50070264        foreach (var modifier in modifiers)
 16474265            modifier.Modify(descriptor);
 266
 8561267        var activityDescriptors = registry.ActivityDescriptors;
 8561268        var descriptorKey = (descriptor.TypeName, descriptor.Version);
 269
 270        // If the descriptor already exists, replace it. But log a warning.
 8561271        if (activityDescriptors.TryGetValue(descriptorKey, out var existingDescriptor))
 272        {
 273            // Remove the existing descriptor from the providerDescriptors collection.
 130274            providerDescriptors.Remove(existingDescriptor);
 275
 276            // Log a warning.
 130277            logger.LogWarning("Activity descriptor {ActivityType} v{ActivityVersion} was already registered for tenant {
 278        }
 279
 8561280        activityDescriptors[descriptorKey] = descriptor;
 8561281        UpdateLatestDescriptor(registry, descriptor);
 8561282        providerDescriptors.Add(descriptor);
 8561283    }
 284
 285    /// <inheritdoc />
 286    public void Clear()
 287    {
 3288        _manualActivityDescriptors.Clear();
 3289        _tenantRegistries.Clear();
 3290        _agnosticRegistry.ActivityDescriptors.Clear();
 3291        _agnosticRegistry.LatestActivityDescriptors.Clear();
 3292        _agnosticRegistry.ProvidedActivityDescriptors.Clear();
 3293        _initializedProviders.Clear();
 3294    }
 295
 296    /// <inheritdoc />
 297    public void ClearProvider(Type providerType)
 298    {
 3299        _initializedProviders.TryRemove(providerType, out _);
 3300        var currentTenantId = tenantAccessor.TenantId;
 301
 302        // Clear from current tenant's registry
 3303        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry)
 3304            && tenantRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var descriptors))
 305        {
 10306            foreach (var descriptor in descriptors.ToList())
 3307                RemoveDescriptor(tenantRegistry, descriptor);
 308
 2309            tenantRegistry.ProvidedActivityDescriptors.TryRemove(providerType, out _);
 310        }
 311
 312        // Clear from agnostic registry
 3313        if (_agnosticRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var agnosticDescriptors))
 314        {
 4315            foreach (var descriptor in agnosticDescriptors.ToList())
 1316                RemoveDescriptor(_agnosticRegistry, descriptor);
 317
 1318            _agnosticRegistry.ProvidedActivityDescriptors.TryRemove(providerType, out _);
 319        }
 3320    }
 321
 322    /// <summary>
 323    /// Clears all activity descriptors for a specific tenant. Useful when a tenant is deactivated.
 324    /// </summary>
 325    internal void ClearTenant(string tenantId)
 326    {
 0327        _tenantRegistries.TryRemove(tenantId, out _);
 0328    }
 329
 330    private TenantRegistryData GetOrCreateRegistry(string? tenantId)
 331    {
 332        // Null or agnostic tenant ID goes to agnostic registry
 1634333        if (tenantId is null or Tenant.AgnosticTenantId)
 1488334            return _agnosticRegistry;
 335
 336        // Get or create tenant-specific registry
 165337        return _tenantRegistries.GetOrAdd(tenantId, _ => new());
 338    }
 339
 340    private ICollection<ActivityDescriptor> GetOrCreateProviderDescriptors(TenantRegistryData registry, Type providerTyp
 341    {
 184342        return registry.ProvidedActivityDescriptors.GetOrAdd(providerType, _ => new List<ActivityDescriptor>());
 343    }
 344
 345    private static void UpdateLatestDescriptor(TenantRegistryData registry, ActivityDescriptor descriptor)
 346    {
 8561347        registry.LatestActivityDescriptors.AddOrUpdate(
 8561348            descriptor.TypeName,
 8561349            descriptor,
 8726350            (_, latestDescriptor) => descriptor.Version >= latestDescriptor.Version ? descriptor : latestDescriptor);
 8561351    }
 352
 353    private static void RemoveDescriptor(TenantRegistryData registry, ActivityDescriptor descriptor)
 354    {
 1118355        if (!registry.ActivityDescriptors.TryRemove((descriptor.TypeName, descriptor.Version), out var removedDescriptor
 0356            return;
 357
 1118358        if (registry.LatestActivityDescriptors.TryGetValue(removedDescriptor.TypeName, out var latestDescriptor) && late
 1104359            RecomputeLatestDescriptor(registry, removedDescriptor.TypeName);
 1118360    }
 361
 362    private static void RecomputeLatestDescriptor(TenantRegistryData registry, string typeName)
 363    {
 1104364        ActivityDescriptor? latestDescriptor = null;
 53798365        foreach (var descriptor in registry.ActivityDescriptors.Values)
 366        {
 25795367            if (descriptor.TypeName != typeName)
 368                continue;
 369
 9370            if (latestDescriptor == null || descriptor.Version > latestDescriptor.Version)
 9371                latestDescriptor = descriptor;
 372        }
 373
 1104374        if (latestDescriptor == null)
 1096375            registry.LatestActivityDescriptors.TryRemove(typeName, out _);
 376        else
 8377            registry.LatestActivityDescriptors[typeName] = latestDescriptor;
 8378    }
 379
 380    /// <summary>
 381    /// Normalizes tenant ID for grouping purposes.
 382    /// Converts null to "*" so that both null and "*" descriptors are grouped together,
 383    /// avoiding redundant processing of the agnostic registry.
 384    /// </summary>
 385    private static string? NormalizeTenantIdForGrouping(string? tenantId)
 386    {
 387        // Normalize null to "*" so both map to the same group
 7156388        return tenantId ?? Tenant.AgnosticTenantId;
 389    }
 390}

Methods/Properties

.ctor(Elsa.Workflows.IActivityDescriber,System.Collections.Generic.IEnumerable`1<Elsa.Workflows.IActivityDescriptorModifier>,Elsa.Common.Multitenancy.ITenantAccessor,Microsoft.Extensions.Logging.ILogger`1<Elsa.Workflows.ActivityRegistry>)
Add(System.Type,Elsa.Workflows.Models.ActivityDescriptor)
Remove(System.Type,Elsa.Workflows.Models.ActivityDescriptor)
ListAll()
ListByProvider(System.Type)
Find(System.String)
Find(System.String,System.Int32)
Find(System.Func`2<Elsa.Workflows.Models.ActivityDescriptor,System.Boolean>)
FindMany(System.Func`2<Elsa.Workflows.Models.ActivityDescriptor,System.Boolean>)
Register(Elsa.Workflows.Models.ActivityDescriptor)
RegisterAsync()
RegisterAsync()
GetDescriptorsAsync(System.Threading.CancellationToken)
RefreshDescriptorsAsync()
RefreshDescriptorsAsync()
EnsureDescriptorsAsync()
Add(Elsa.Workflows.Models.ActivityDescriptor,Elsa.Workflows.Models.TenantRegistryData,System.Collections.Generic.ICollection`1<Elsa.Workflows.Models.ActivityDescriptor>)
Clear()
ClearProvider(System.Type)
ClearTenant(System.String)
GetOrCreateRegistry(System.String)
GetOrCreateProviderDescriptors(Elsa.Workflows.Models.TenantRegistryData,System.Type)
UpdateLatestDescriptor(Elsa.Workflows.Models.TenantRegistryData,Elsa.Workflows.Models.ActivityDescriptor)
RemoveDescriptor(Elsa.Workflows.Models.TenantRegistryData,Elsa.Workflows.Models.ActivityDescriptor)
RecomputeLatestDescriptor(Elsa.Workflows.Models.TenantRegistryData,System.String)
NormalizeTenantIdForGrouping(System.String)