< 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
77%
Covered branches: 48
Total branches: 62
Branch coverage: 77.4%
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%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 />
 55111public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<IActivityDescriptorModifier> modifiers, 
 12{
 13    // Legacy support for manually registered activities
 55114    private readonly ISet<ActivityDescriptor> _manualActivityDescriptors = new HashSet<ActivityDescriptor>();
 15
 16    // Per-tenant activity descriptors (workflow-as-activities, tenant-specific providers, etc.)
 55117    private readonly ConcurrentDictionary<string, TenantRegistryData> _tenantRegistries = new();
 18
 19    // Tenant-agnostic activity descriptors (built-in activities, manually registered, etc.)
 55120    private readonly TenantRegistryData _agnosticRegistry = new();
 21
 22    /// <inheritdoc />
 23    public void Add(Type providerType, ActivityDescriptor descriptor)
 24    {
 12725        var registry = GetOrCreateRegistry(descriptor.TenantId);
 12726        var providerDescriptors = GetOrCreateProviderDescriptors(registry, providerType);
 12727        Add(descriptor, registry.ActivityDescriptors, providerDescriptors);
 12728    }
 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    {
 1787744        var currentTenantId = tenantAccessor.TenantId;
 45
 46        // Get descriptors from current tenant's registry
 1787747        var tenantDescriptors = _tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry)
 1787748            ? tenantRegistry.ActivityDescriptors.Values
 1787749            : Enumerable.Empty<ActivityDescriptor>();
 50
 51        // Get descriptors from agnostic registry
 1787752        var agnosticDescriptors = _agnosticRegistry.ActivityDescriptors.Values;
 53
 1787754        return tenantDescriptors.Concat(agnosticDescriptors);
 55    }
 56
 57    /// <inheritdoc />
 58    public IEnumerable<ActivityDescriptor> ListByProvider(Type providerType)
 59    {
 126460        var currentTenantId = tenantAccessor.TenantId;
 61
 62        // Get descriptors from current tenant's registry
 126463        var tenantDescriptors = _tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry) &&
 126464                                tenantRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var tenantProvi
 126465            ? tenantProviderDescriptors
 126466            : Enumerable.Empty<ActivityDescriptor>();
 67
 68        // Get descriptors from agnostic registry
 126469        var agnosticDescriptors = _agnosticRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var agnost
 126470            ? agnosticProviderDescriptors
 126471            : Enumerable.Empty<ActivityDescriptor>();
 72
 126473        return tenantDescriptors.Concat(agnosticDescriptors);
 74    }
 75
 76    /// <inheritdoc />
 77    public ActivityDescriptor? Find(string type)
 78    {
 50679        var currentTenantId = tenantAccessor.TenantId;
 80
 81        // Always prefer tenant-specific descriptors over tenant-agnostic ones
 82        // Get highest version from current tenant's registry
 50683        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry))
 84        {
 9185            var tenantDescriptor = tenantRegistry.ActivityDescriptors.Values
 83986                .Where(x => x.TypeName == type)
 10487                .MaxBy(x => x.Version);
 88
 9189            if (tenantDescriptor != null)
 890                return tenantDescriptor;
 91        }
 92
 93        // Fall back to agnostic registry only if no tenant-specific descriptor exists
 49894        return _agnosticRegistry.ActivityDescriptors.Values
 3705095            .Where(x => x.TypeName == type)
 79096            .MaxBy(x => x.Version);
 97    }
 98
 99    /// <inheritdoc />
 100    public ActivityDescriptor? Find(string type, int version)
 101    {
 63466102        var currentTenantId = tenantAccessor.TenantId;
 103
 104        // Check current tenant's registry first
 63466105        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry) &&
 63466106            tenantRegistry.ActivityDescriptors.TryGetValue((type, version), out var tenantDescriptor))
 107        {
 3483108            return tenantDescriptor;
 109        }
 110
 111        // Fall back to agnostic registry
 59983112        return _agnosticRegistry.ActivityDescriptors.TryGetValue((type, version), out var agnosticDescriptor)
 59983113            ? agnosticDescriptor
 59983114            : null;
 115    }
 116
 117    /// <inheritdoc />
 118    public ActivityDescriptor? Find(Func<ActivityDescriptor, bool> predicate)
 119    {
 881120        var currentTenantId = tenantAccessor.TenantId;
 121
 122        // Check current tenant's registry first
 881123        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry))
 124        {
 710125            var tenantMatch = tenantRegistry.ActivityDescriptors.Values.FirstOrDefault(predicate);
 1392126            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    {
 25152        var registry = GetOrCreateRegistry(descriptor.TenantId);
 25153        var providerDescriptors = GetOrCreateProviderDescriptors(registry, GetType());
 25154        Add(descriptor, registry.ActivityDescriptors, providerDescriptors);
 25155    }
 156
 157    /// <inheritdoc />
 158    public async Task RegisterAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type a
 159    {
 17876160        var activityTypeName = ActivityTypeNameHelper.GenerateTypeName(activityType);
 161
 162        // Check if already registered in any registry
 808464163        if (ListAll().Any(x => x.TypeName == activityTypeName))
 16849164            return;
 165
 1027166        var activityDescriptor = await activityDescriber.DescribeActivityAsync(activityType, cancellationToken);
 167
 1027168        var registry = GetOrCreateRegistry(activityDescriptor.TenantId);
 1027169        Add(activityDescriptor, registry.ActivityDescriptors, _manualActivityDescriptors);
 1027170        _manualActivityDescriptors.Add(activityDescriptor);
 17876171    }
 172
 173    /// <inheritdoc />
 174    public async Task RegisterAsync(IEnumerable<Type> activityTypes, CancellationToken cancellationToken = default)
 175    {
 44370176        foreach (var activityType in activityTypes)
 17461177            await RegisterAsync(activityType, cancellationToken);
 4724178    }
 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    {
 3658186        foreach (var activityProvider in activityProviders)
 903187            await RefreshDescriptorsAsync(activityProvider, cancellationToken);
 926188    }
 189
 190    public async Task RefreshDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = 
 191    {
 904192        var providerType = activityProvider.GetType();
 193
 194        // Get new descriptors from provider
 904195        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
 15879199        var descriptorsByTenant = descriptors.GroupBy(d => NormalizeTenantIdForGrouping(d.TenantId));
 200
 2590201        foreach (var group in descriptorsByTenant)
 202        {
 391203            var tenantId = group.Key;
 391204            var registry = GetOrCreateRegistry(tenantId);
 205
 206            // Remove old descriptors for this provider from this tenant's registry
 391207            if (registry.ProvidedActivityDescriptors.TryGetValue(providerType, out var oldDescriptors))
 208            {
 18750209                foreach (var oldDescriptor in oldDescriptors.ToList())
 210                {
 9119211                    registry.ActivityDescriptors.TryRemove((oldDescriptor.TypeName, oldDescriptor.Version), out _);
 212                }
 213            }
 214
 215            // Add new descriptors for this tenant
 391216            var providerDescriptors = new List<ActivityDescriptor>();
 30732217            foreach (var descriptor in group)
 218            {
 14975219                Add(descriptor, registry.ActivityDescriptors, providerDescriptors);
 220            }
 221
 222            // Update the provider's descriptor list in this registry
 391223            registry.ProvidedActivityDescriptors[providerType] = providerDescriptors;
 224        }
 904225    }
 226
 227    private void Add(ActivityDescriptor? descriptor, ConcurrentDictionary<(string Type, int Version), ActivityDescriptor
 228    {
 16154229        if (descriptor is null)
 230        {
 0231            logger.LogError("Unable to add a null descriptor");
 0232            return;
 233        }
 234
 70800235        foreach (var modifier in modifiers)
 19246236            modifier.Modify(descriptor);
 237
 238        // If the descriptor already exists, replace it. But log a warning.
 16154239        if (activityDescriptors.TryGetValue((descriptor.TypeName, descriptor.Version), out var existingDescriptor))
 240        {
 241            // Remove the existing descriptor from the providerDescriptors collection.
 129242            providerDescriptors.Remove(existingDescriptor);
 243
 244            // Log a warning.
 129245            logger.LogWarning("Activity descriptor {ActivityType} v{ActivityVersion} was already registered for tenant {
 246        }
 247
 16154248        activityDescriptors[(descriptor.TypeName, descriptor.Version)] = descriptor;
 16154249        providerDescriptors.Add(descriptor);
 16154250    }
 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
 1576296        if (tenantId is null or Tenant.AgnosticTenantId)
 1449297            return _agnosticRegistry;
 298
 299        // Get or create tenant-specific registry
 137300        return _tenantRegistries.GetOrAdd(tenantId, _ => new());
 301    }
 302
 303    private ICollection<ActivityDescriptor> GetOrCreateProviderDescriptors(TenantRegistryData registry, Type providerTyp
 304    {
 165305        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
 14975316        return tenantId ?? Tenant.AgnosticTenantId;
 317    }
 318}