< 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
90%
Covered lines: 128
Uncovered lines: 14
Coverable lines: 142
Total lines: 354
Line coverage: 90.1%
Branch coverage
90%
Covered branches: 74
Total branches: 82
Branch coverage: 90.2%
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%
Add(...)83.33%6685.71%
Clear()100%11100%
ClearProvider(...)70%121072.72%
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 />
 59311public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<IActivityDescriptorModifier> modifiers, 
 12{
 13    // Legacy support for manually registered activities
 59314    private readonly ISet<ActivityDescriptor> _manualActivityDescriptors = new HashSet<ActivityDescriptor>();
 15
 16    // Per-tenant activity descriptors (workflow-as-activities, tenant-specific providers, etc.)
 59317    private readonly ConcurrentDictionary<string, TenantRegistryData> _tenantRegistries = new();
 18
 19    // Tenant-agnostic activity descriptors (built-in activities, manually registered, etc.)
 59320    private readonly TenantRegistryData _agnosticRegistry = new();
 21
 22    /// <inheritdoc />
 23    public void Add(Type providerType, ActivityDescriptor descriptor)
 24    {
 13125        var registry = GetOrCreateRegistry(descriptor.TenantId);
 13126        var providerDescriptors = GetOrCreateProviderDescriptors(registry, providerType);
 13127        Add(descriptor, registry, providerDescriptors);
 13128    }
 29
 30    /// <inheritdoc />
 31    public void Remove(Type providerType, ActivityDescriptor descriptor)
 32    {
 833        var registry = GetOrCreateRegistry(descriptor.TenantId);
 834        if (registry.ProvidedActivityDescriptors.TryGetValue(providerType, out var providerDescriptors))
 35        {
 836            providerDescriptors.Remove(descriptor);
 837            RemoveDescriptor(registry, descriptor);
 38        }
 839    }
 40
 41    /// <inheritdoc />
 42    public IEnumerable<ActivityDescriptor> ListAll()
 43    {
 1783344        var currentTenantId = tenantAccessor.TenantId;
 45
 46        // Get descriptors from current tenant's registry
 1783347        var tenantDescriptors = _tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry)
 1783348            ? tenantRegistry.ActivityDescriptors.Values
 1783349            : Enumerable.Empty<ActivityDescriptor>();
 50
 51        // Get descriptors from agnostic registry
 1783352        var agnosticDescriptors = _agnosticRegistry.ActivityDescriptors.Values;
 53
 1783354        return tenantDescriptors.Concat(agnosticDescriptors);
 55    }
 56
 57    /// <inheritdoc />
 58    public IEnumerable<ActivityDescriptor> ListByProvider(Type providerType)
 59    {
 128260        var currentTenantId = tenantAccessor.TenantId;
 61
 62        // Get descriptors from current tenant's registry
 128263        var tenantDescriptors = _tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry) &&
 128264                                tenantRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var tenantProvi
 128265            ? tenantProviderDescriptors
 128266            : Enumerable.Empty<ActivityDescriptor>();
 67
 68        // Get descriptors from agnostic registry
 128269        var agnosticDescriptors = _agnosticRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var agnost
 128270            ? agnosticProviderDescriptors
 128271            : Enumerable.Empty<ActivityDescriptor>();
 72
 128273        return tenantDescriptors.Concat(agnosticDescriptors);
 74    }
 75
 76    /// <inheritdoc />
 77    public ActivityDescriptor? Find(string type)
 78    {
 51379        var currentTenantId = tenantAccessor.TenantId;
 80
 81        // Always prefer tenant-specific descriptors over tenant-agnostic ones
 82        // Get highest version from current tenant's registry
 51383        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry))
 84        {
 9685            if (tenantRegistry.LatestActivityDescriptors.TryGetValue(type, out var tenantDescriptor))
 1386                return tenantDescriptor;
 87        }
 88
 89        // Fall back to agnostic registry only if no tenant-specific descriptor exists
 50090        return _agnosticRegistry.LatestActivityDescriptors.TryGetValue(type, out var agnosticDescriptor)
 50091            ? agnosticDescriptor
 50092            : null;
 93    }
 94
 95    /// <inheritdoc />
 96    public ActivityDescriptor? Find(string type, int version)
 97    {
 6230598        var currentTenantId = tenantAccessor.TenantId;
 99
 100        // Check current tenant's registry first
 62305101        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry) &&
 62305102            tenantRegistry.ActivityDescriptors.TryGetValue((type, version), out var tenantDescriptor))
 103        {
 3184104            return tenantDescriptor;
 105        }
 106
 107        // Fall back to agnostic registry
 59121108        return _agnosticRegistry.ActivityDescriptors.TryGetValue((type, version), out var agnosticDescriptor)
 59121109            ? agnosticDescriptor
 59121110            : null;
 111    }
 112
 113    /// <inheritdoc />
 114    public ActivityDescriptor? Find(Func<ActivityDescriptor, bool> predicate)
 115    {
 795116        var currentTenantId = tenantAccessor.TenantId;
 117
 118        // Check current tenant's registry first
 795119        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry))
 120        {
 624121            var tenantMatch = tenantRegistry.ActivityDescriptors.Values.FirstOrDefault(predicate);
 1220122            if (tenantMatch != null) return tenantMatch;
 123        }
 124
 125        // Fall back to agnostic registry
 199126        return _agnosticRegistry.ActivityDescriptors.Values.FirstOrDefault(predicate);
 127    }
 128
 129    /// <inheritdoc />
 130    public IEnumerable<ActivityDescriptor> FindMany(Func<ActivityDescriptor, bool> predicate)
 131    {
 0132        var currentTenantId = tenantAccessor.TenantId;
 133
 134        // Get descriptors from current tenant's registry
 0135        var tenantDescriptors = _tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry)
 0136            ? tenantRegistry.ActivityDescriptors.Values.Where(predicate)
 0137            : Enumerable.Empty<ActivityDescriptor>();
 138
 139        // Get descriptors from agnostic registry
 0140        var agnosticDescriptors = _agnosticRegistry.ActivityDescriptors.Values.Where(predicate);
 141
 0142        return tenantDescriptors.Concat(agnosticDescriptors);
 143    }
 144
 145    /// <inheritdoc />
 146    public void Register(ActivityDescriptor descriptor)
 147    {
 33148        var registry = GetOrCreateRegistry(descriptor.TenantId);
 33149        var providerDescriptors = GetOrCreateProviderDescriptors(registry, GetType());
 33150        Add(descriptor, registry, providerDescriptors);
 33151    }
 152
 153    /// <inheritdoc />
 154    public async Task RegisterAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type a
 155    {
 17829156        var activityTypeName = ActivityTypeNameHelper.GenerateTypeName(activityType);
 157
 158        // Check if already registered in any registry
 678108159        if (ListAll().Any(x => x.TypeName == activityTypeName))
 16727160            return;
 161
 1102162        var activityDescriptor = await activityDescriber.DescribeActivityAsync(activityType, cancellationToken);
 163
 1102164        var registry = GetOrCreateRegistry(activityDescriptor.TenantId);
 1102165        Add(activityDescriptor, registry, _manualActivityDescriptors);
 17829166    }
 167
 168    /// <inheritdoc />
 169    public async Task RegisterAsync(IEnumerable<Type> activityTypes, CancellationToken cancellationToken = default)
 170    {
 44190171        foreach (var activityType in activityTypes)
 17382172            await RegisterAsync(activityType, cancellationToken);
 4713173    }
 174
 175    /// <inheritdoc />
 1176    public ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
 177
 178    /// <inheritdoc />
 179    public async Task RefreshDescriptorsAsync(IEnumerable<IActivityProvider> activityProviders, CancellationToken cancel
 180    {
 3870181        foreach (var activityProvider in activityProviders)
 908182            await RefreshDescriptorsAsync(activityProvider, cancellationToken);
 1027183    }
 184
 185    public async Task RefreshDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = 
 186    {
 913187        var providerType = activityProvider.GetType();
 188
 189        // Get new descriptors from provider
 913190        var descriptors = (await activityProvider.GetDescriptorsAsync(cancellationToken)).ToList();
 191
 192        // Group descriptors by normalized tenant ID
 193        // Normalize null to "*" so both map to the same agnostic group, avoiding redundant processing
 16200194        var descriptorsByTenant = descriptors.GroupBy(d => NormalizeTenantIdForGrouping(d.TenantId));
 195
 2624196        foreach (var group in descriptorsByTenant)
 197        {
 399198            var tenantId = group.Key;
 399199            var registry = GetOrCreateRegistry(tenantId);
 200
 201            // Remove old descriptors for this provider from this tenant's registry
 399202            if (registry.ProvidedActivityDescriptors.TryGetValue(providerType, out var oldDescriptors))
 203            {
 19112204                foreach (var oldDescriptor in oldDescriptors.ToList())
 205                {
 9298206                    RemoveDescriptor(registry, oldDescriptor);
 207                }
 208            }
 209
 210            // Add new descriptors for this tenant
 399211            var providerDescriptors = new List<ActivityDescriptor>();
 31372212            foreach (var descriptor in group)
 213            {
 15287214                Add(descriptor, registry, providerDescriptors);
 215            }
 216
 217            // Update the provider's descriptor list in this registry
 399218            registry.ProvidedActivityDescriptors[providerType] = providerDescriptors;
 219        }
 913220    }
 221
 222    private void Add(ActivityDescriptor? descriptor, TenantRegistryData registry, ICollection<ActivityDescriptor> provid
 223    {
 16553224        if (descriptor is null)
 225        {
 0226            logger.LogError("Unable to add a null descriptor");
 0227            return;
 228        }
 229
 109438230        foreach (var modifier in modifiers)
 38166231            modifier.Modify(descriptor);
 232
 16553233        var activityDescriptors = registry.ActivityDescriptors;
 16553234        var descriptorKey = (descriptor.TypeName, descriptor.Version);
 235
 236        // If the descriptor already exists, replace it. But log a warning.
 16553237        if (activityDescriptors.TryGetValue(descriptorKey, out var existingDescriptor))
 238        {
 239            // Remove the existing descriptor from the providerDescriptors collection.
 130240            providerDescriptors.Remove(existingDescriptor);
 241
 242            // Log a warning.
 130243            logger.LogWarning("Activity descriptor {ActivityType} v{ActivityVersion} was already registered for tenant {
 244        }
 245
 16553246        activityDescriptors[descriptorKey] = descriptor;
 16553247        UpdateLatestDescriptor(registry, descriptor);
 16553248        providerDescriptors.Add(descriptor);
 16553249    }
 250
 251    /// <inheritdoc />
 252    public void Clear()
 253    {
 2254        _manualActivityDescriptors.Clear();
 2255        _tenantRegistries.Clear();
 2256        _agnosticRegistry.ActivityDescriptors.Clear();
 2257        _agnosticRegistry.LatestActivityDescriptors.Clear();
 2258        _agnosticRegistry.ProvidedActivityDescriptors.Clear();
 2259    }
 260
 261    /// <inheritdoc />
 262    public void ClearProvider(Type providerType)
 263    {
 2264        var currentTenantId = tenantAccessor.TenantId;
 265
 266        // Clear from current tenant's registry
 2267        if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry)
 2268            && tenantRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var descriptors))
 269        {
 10270            foreach (var descriptor in descriptors.ToList())
 3271                RemoveDescriptor(tenantRegistry, descriptor);
 272
 2273            tenantRegistry.ProvidedActivityDescriptors.TryRemove(providerType, out _);
 274        }
 275
 276        // Clear from agnostic registry
 2277        if (_agnosticRegistry.ProvidedActivityDescriptors.TryGetValue(providerType, out var agnosticDescriptors))
 278        {
 0279            foreach (var descriptor in agnosticDescriptors.ToList())
 0280                RemoveDescriptor(_agnosticRegistry, descriptor);
 281
 0282            _agnosticRegistry.ProvidedActivityDescriptors.TryRemove(providerType, out _);
 283        }
 2284    }
 285
 286    /// <summary>
 287    /// Clears all activity descriptors for a specific tenant. Useful when a tenant is deactivated.
 288    /// </summary>
 289    internal void ClearTenant(string tenantId)
 290    {
 0291        _tenantRegistries.TryRemove(tenantId, out _);
 0292    }
 293
 294    private TenantRegistryData GetOrCreateRegistry(string? tenantId)
 295    {
 296        // Null or agnostic tenant ID goes to agnostic registry
 1673297        if (tenantId is null or Tenant.AgnosticTenantId)
 1529298            return _agnosticRegistry;
 299
 300        // Get or create tenant-specific registry
 162301        return _tenantRegistries.GetOrAdd(tenantId, _ => new());
 302    }
 303
 304    private ICollection<ActivityDescriptor> GetOrCreateProviderDescriptors(TenantRegistryData registry, Type providerTyp
 305    {
 184306        return registry.ProvidedActivityDescriptors.GetOrAdd(providerType, _ => new List<ActivityDescriptor>());
 307    }
 308
 309    private static void UpdateLatestDescriptor(TenantRegistryData registry, ActivityDescriptor descriptor)
 310    {
 16553311        registry.LatestActivityDescriptors.AddOrUpdate(
 16553312            descriptor.TypeName,
 16553313            descriptor,
 16718314            (_, latestDescriptor) => descriptor.Version >= latestDescriptor.Version ? descriptor : latestDescriptor);
 16553315    }
 316
 317    private static void RemoveDescriptor(TenantRegistryData registry, ActivityDescriptor descriptor)
 318    {
 9309319        if (!registry.ActivityDescriptors.TryRemove((descriptor.TypeName, descriptor.Version), out var removedDescriptor
 0320            return;
 321
 9309322        if (registry.LatestActivityDescriptors.TryGetValue(removedDescriptor.TypeName, out var latestDescriptor) && late
 9289323            RecomputeLatestDescriptor(registry, removedDescriptor.TypeName);
 9309324    }
 325
 326    private static void RecomputeLatestDescriptor(TenantRegistryData registry, string typeName)
 327    {
 9289328        ActivityDescriptor? latestDescriptor = null;
 625900329        foreach (var descriptor in registry.ActivityDescriptors.Values)
 330        {
 303661331            if (descriptor.TypeName != typeName)
 332                continue;
 333
 3334            if (latestDescriptor == null || descriptor.Version > latestDescriptor.Version)
 3335                latestDescriptor = descriptor;
 336        }
 337
 9289338        if (latestDescriptor == null)
 9287339            registry.LatestActivityDescriptors.TryRemove(typeName, out _);
 340        else
 2341            registry.LatestActivityDescriptors[typeName] = latestDescriptor;
 2342    }
 343
 344    /// <summary>
 345    /// Normalizes tenant ID for grouping purposes.
 346    /// Converts null to "*" so that both null and "*" descriptors are grouped together,
 347    /// avoiding redundant processing of the agnostic registry.
 348    /// </summary>
 349    private static string? NormalizeTenantIdForGrouping(string? tenantId)
 350    {
 351        // Normalize null to "*" so both map to the same group
 15287352        return tenantId ?? Tenant.AgnosticTenantId;
 353    }
 354}

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()
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)