< 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
78%
Covered lines: 96
Uncovered lines: 26
Coverable lines: 122
Total lines: 318
Line coverage: 78.6%
Branch coverage
74%
Covered branches: 46
Total branches: 62
Branch coverage: 74.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%
Add(...)100%11100%
Remove(...)100%22100%
ListAll()100%22100%
ListByProvider(...)50%66100%
Find(...)100%44100%
Find(...)100%66100%
Find(...)100%44100%
FindMany(...)0%620%
Register(...)100%11100%
RegisterAsync()100%22100%
RegisterAsync()100%22100%
GetDescriptorsAsync(...)100%210%
RefreshDescriptorsAsync()100%22100%
RefreshDescriptorsAsync()100%88100%
Add(...)83.33%6681.81%
Clear()100%210%
ClearProvider(...)0%110100%
ClearTenant(...)100%210%
GetOrCreateRegistry(...)100%66100%
GetOrCreateProviderDescriptors(...)100%11100%
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 />
 55511public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<IActivityDescriptorModifier> modifiers, 
 12{
 13    // Legacy support for manually registered activities
 55514    private readonly ISet<ActivityDescriptor> _manualActivityDescriptors = new HashSet<ActivityDescriptor>();
 15
 16    // Per-tenant activity descriptors (workflow-as-activities, tenant-specific providers, etc.)
 55517    private readonly ConcurrentDictionary<string, TenantRegistryData> _tenantRegistries = new();
 18
 19    // Tenant-agnostic activity descriptors (built-in activities, manually registered, etc.)
 55520    private readonly TenantRegistryData _agnosticRegistry = new();
 21
 22    /// <inheritdoc />
 23    public void Add(Type providerType, ActivityDescriptor descriptor)
 24    {
 6425        var registry = GetOrCreateRegistry(descriptor.TenantId);
 6426        var providerDescriptors = GetOrCreateProviderDescriptors(registry, providerType);
 6427        Add(descriptor, registry.ActivityDescriptors, providerDescriptors);
 6428    }
 29
 30    /// <inheritdoc />
 31    public void Remove(Type providerType, ActivityDescriptor descriptor)
 32    {
 633        var registry = GetOrCreateRegistry(descriptor.TenantId);
 634        if (registry.ProvidedActivityDescriptors.TryGetValue(providerType, out var providerDescriptors))
 35        {
 636            providerDescriptors.Remove(descriptor);
 637            registry.ActivityDescriptors.TryRemove((descriptor.TypeName, descriptor.Version), out _);
 38        }
 639    }
 40
 41    /// <inheritdoc />
 42    public IEnumerable<ActivityDescriptor> ListAll()
 43    {
 2273244        var currentTenantId = tenantAccessor.TenantId;
 45
 46        // Get descriptors from current tenant's registry
 2273247        var tenantDescriptors = _tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry)
 2273248            ? tenantRegistry.ActivityDescriptors.Values
 2273249            : Enumerable.Empty<ActivityDescriptor>();
 50
 51        // Get descriptors from agnostic registry
 2273252        var agnosticDescriptors = _agnosticRegistry.ActivityDescriptors.Values;
 53
 2273254        return tenantDescriptors.Concat(agnosticDescriptors);
 55    }
 56
 57    /// <inheritdoc />
 58    public IEnumerable<ActivityDescriptor> ListByProvider(Type providerType)
 59    {
 45360        var currentTenantId = tenantAccessor.TenantId;
 61
 62        // Get descriptors from current tenant's registry
 45363        var tenantDescriptors = _tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry) &&
 45364                                tenantRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var tenantProvi
 45365            ? tenantProviderDescriptors
 45366            : Enumerable.Empty<ActivityDescriptor>();
 67
 68        // Get descriptors from agnostic registry
 45369        var agnosticDescriptors = _agnosticRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var agnost
 45370            ? agnosticProviderDescriptors
 45371            : Enumerable.Empty<ActivityDescriptor>();
 72
 45373        return tenantDescriptors.Concat(agnosticDescriptors);
 74    }
 75
 76    /// <inheritdoc />
 77    public ActivityDescriptor? Find(string type)
 78    {
 76979        var currentTenantId = tenantAccessor.TenantId;
 80
 81        // Always prefer tenant-specific descriptors over tenant-agnostic ones
 82        // Get highest version from current tenant's registry
 76983        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry))
 84        {
 9685            var tenantDescriptor = tenantRegistry.ActivityDescriptors.Values
 85786                .Where(x => x.TypeName == type)
 11087                .MaxBy(x => x.Version);
 88
 9689            if (tenantDescriptor != null)
 990                return tenantDescriptor;
 91        }
 92
 93        // Fall back to agnostic registry only if no tenant-specific descriptor exists
 76094        return _agnosticRegistry.ActivityDescriptors.Values
 5753295            .Where(x => x.TypeName == type)
 117996            .MaxBy(x => x.Version);
 97    }
 98
 99    /// <inheritdoc />
 100    public ActivityDescriptor? Find(string type, int version)
 101    {
 56770102        var currentTenantId = tenantAccessor.TenantId;
 103
 104        // Check current tenant's registry first
 56770105        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry) &&
 56770106            tenantRegistry.ActivityDescriptors.TryGetValue((type, version), out var tenantDescriptor))
 107        {
 2455108            return tenantDescriptor;
 109        }
 110
 111        // Fall back to agnostic registry
 54315112        return _agnosticRegistry.ActivityDescriptors.TryGetValue((type, version), out var agnosticDescriptor)
 54315113            ? agnosticDescriptor
 54315114            : null;
 115    }
 116
 117    /// <inheritdoc />
 118    public ActivityDescriptor? Find(Func<ActivityDescriptor, bool> predicate)
 119    {
 733120        var currentTenantId = tenantAccessor.TenantId;
 121
 122        // Check current tenant's registry first
 733123        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry))
 124        {
 432125            var tenantMatch = tenantRegistry.ActivityDescriptors.Values.FirstOrDefault(predicate);
 831126            if (tenantMatch != null) return tenantMatch;
 127        }
 128
 129        // Fall back to agnostic registry
 334130        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    {
 26152        var registry = GetOrCreateRegistry(descriptor.TenantId);
 26153        var providerDescriptors = GetOrCreateProviderDescriptors(registry, GetType());
 26154        Add(descriptor, registry.ActivityDescriptors, providerDescriptors);
 26155    }
 156
 157    /// <inheritdoc />
 158    public async Task RegisterAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type a
 159    {
 22729160        var activityTypeName = ActivityTypeNameHelper.GenerateTypeName(activityType);
 161
 162        // Check if already registered in any registry
 849204163        if (ListAll().Any(x => x.TypeName == activityTypeName))
 21679164            return;
 165
 1050166        var activityDescriptor = await activityDescriber.DescribeActivityAsync(activityType, cancellationToken);
 167
 1050168        var registry = GetOrCreateRegistry(activityDescriptor.TenantId);
 1050169        Add(activityDescriptor, registry.ActivityDescriptors, _manualActivityDescriptors);
 1050170        _manualActivityDescriptors.Add(activityDescriptor);
 22729171    }
 172
 173    /// <inheritdoc />
 174    public async Task RegisterAsync(IEnumerable<Type> activityTypes, CancellationToken cancellationToken = default)
 175    {
 56676176        foreach (var activityType in activityTypes)
 22316177            await RegisterAsync(activityType, cancellationToken);
 6022178    }
 179
 180    /// <inheritdoc />
 0181    public ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
 182
 183    /// <inheritdoc />
 184    public async Task RefreshDescriptorsAsync(IEnumerable<IActivityProvider> activityProviders, CancellationToken cancel
 185    {
 3808186        foreach (var activityProvider in activityProviders)
 959187            await RefreshDescriptorsAsync(activityProvider, cancellationToken);
 945188    }
 189
 190    public async Task RefreshDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = 
 191    {
 960192        var providerType = activityProvider.GetType();
 193
 194        // Get new descriptors from provider
 960195        var descriptors = (await activityProvider.GetDescriptorsAsync(cancellationToken)).ToList();
 196
 197        // Group descriptors by normalized tenant ID
 198        // Normalize null to "*" so both map to the same agnostic group, avoiding redundant processing
 17148199        var descriptorsByTenant = descriptors.GroupBy(d => NormalizeTenantIdForGrouping(d.TenantId));
 200
 2706201        foreach (var group in descriptorsByTenant)
 202        {
 393203            var tenantId = group.Key;
 393204            var registry = GetOrCreateRegistry(tenantId);
 205
 206            // Remove old descriptors for this provider from this tenant's registry
 393207            if (registry.ProvidedActivityDescriptors.TryGetValue(providerType, out var oldDescriptors))
 208            {
 20594209                foreach (var oldDescriptor in oldDescriptors.ToList())
 210                {
 10051211                    registry.ActivityDescriptors.TryRemove((oldDescriptor.TypeName, oldDescriptor.Version), out _);
 212                }
 213            }
 214
 215            // Add new descriptors for this tenant
 393216            var providerDescriptors = new List<ActivityDescriptor>();
 33162217            foreach (var descriptor in group)
 218            {
 16188219                Add(descriptor, registry.ActivityDescriptors, providerDescriptors);
 220            }
 221
 222            // Update the provider's descriptor list in this registry
 393223            registry.ProvidedActivityDescriptors[providerType] = providerDescriptors;
 224        }
 960225    }
 226
 227    private void Add(ActivityDescriptor? descriptor, ConcurrentDictionary<(string Type, int Version), ActivityDescriptor
 228    {
 17328229        if (descriptor is null)
 230        {
 0231            logger.LogError("Unable to add a null descriptor");
 0232            return;
 233        }
 234
 78100235        foreach (var modifier in modifiers)
 21722236            modifier.Modify(descriptor);
 237
 238        // If the descriptor already exists, replace it. But log a warning.
 17328239        if (activityDescriptors.TryGetValue((descriptor.TypeName, descriptor.Version), out var existingDescriptor))
 240        {
 241            // Remove the existing descriptor from the providerDescriptors collection.
 62242            providerDescriptors.Remove(existingDescriptor);
 243
 244            // Log a warning.
 62245            logger.LogWarning("Activity descriptor {ActivityType} v{ActivityVersion} was already registered for tenant {
 246        }
 247
 17328248        activityDescriptors[(descriptor.TypeName, descriptor.Version)] = descriptor;
 17328249        providerDescriptors.Add(descriptor);
 17328250    }
 251
 252    /// <inheritdoc />
 253    public void Clear()
 254    {
 0255        _tenantRegistries.Clear();
 0256        _agnosticRegistry.ActivityDescriptors.Clear();
 0257        _agnosticRegistry.ProvidedActivityDescriptors.Clear();
 0258    }
 259
 260    /// <inheritdoc />
 261    public void ClearProvider(Type providerType)
 262    {
 0263        var currentTenantId = tenantAccessor.TenantId;
 264
 265        // Clear from current tenant's registry
 0266        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry)
 0267            && tenantRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var descriptors))
 268        {
 0269            foreach (var descriptor in descriptors.ToList())
 0270                tenantRegistry.ActivityDescriptors.TryRemove((descriptor.TypeName, descriptor.Version), out _);
 271
 0272            tenantRegistry.ProvidedActivityDescriptors.TryRemove(providerType, out _);
 273        }
 274
 275        // Clear from agnostic registry
 0276        if (_agnosticRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var agnosticDescriptors))
 277        {
 0278            foreach (var descriptor in agnosticDescriptors.ToList())
 0279                _agnosticRegistry.ActivityDescriptors.TryRemove((descriptor.TypeName, descriptor.Version), out _);
 280
 0281            _agnosticRegistry.ProvidedActivityDescriptors.TryRemove(providerType, out _);
 282        }
 0283    }
 284
 285    /// <summary>
 286    /// Clears all activity descriptors for a specific tenant. Useful when a tenant is deactivated.
 287    /// </summary>
 288    internal void ClearTenant(string tenantId)
 289    {
 0290        _tenantRegistries.TryRemove(tenantId, out _);
 0291    }
 292
 293    private TenantRegistryData GetOrCreateRegistry(string? tenantId)
 294    {
 295        // Null or agnostic tenant ID goes to agnostic registry
 1539296        if (tenantId is null or Tenant.AgnosticTenantId)
 1442297            return _agnosticRegistry;
 298
 299        // Get or create tenant-specific registry
 111300        return _tenantRegistries.GetOrAdd(tenantId, _ => new());
 301    }
 302
 303    private ICollection<ActivityDescriptor> GetOrCreateProviderDescriptors(TenantRegistryData registry, Type providerTyp
 304    {
 104305        return registry.ProvidedActivityDescriptors.GetOrAdd(providerType, _ => new List<ActivityDescriptor>());
 306    }
 307
 308    /// <summary>
 309    /// Normalizes tenant ID for grouping purposes.
 310    /// Converts null to "*" so that both null and "*" descriptors are grouped together,
 311    /// avoiding redundant processing of the agnostic registry.
 312    /// </summary>
 313    private static string? NormalizeTenantIdForGrouping(string? tenantId)
 314    {
 315        // Normalize null to "*" so both map to the same group
 16188316        return tenantId ?? Tenant.AgnosticTenantId;
 317    }
 318}