diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityRegistry.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityRegistry.cs
index 03ef41876..ec9bde24d 100644
--- a/src/modules/Elsa.Workflows.Core/Contracts/IActivityRegistry.cs
+++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivityRegistry.cs
@@ -95,10 +95,21 @@ public interface IActivityRegistry : IActivityProvider
///
/// Refreshes the activity descriptors in the registry by querying the specified activity provider.
///
- Task RefreshDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = default);
-
- ///
- /// Clears all activity descriptors from the registry.
+ Task RefreshDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = default);
+
+ ///
+ /// Ensures that descriptors from a tenant-agnostic activity provider have been initialized.
+ ///
+ /// The activity provider used to retrieve the descriptors.
+ /// The cancellation token.
+ ///
+ /// Tenant-sensitive providers are refreshed on every call. The default implementation refreshes every provider to preserve compatibility with custom registry implementations.
+ ///
+ Task EnsureDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = default) =>
+ RefreshDescriptorsAsync(activityProvider, cancellationToken);
+
+ ///
+ /// Clears all activity descriptors from the registry.
///
void Clear();
@@ -107,4 +118,4 @@ public interface IActivityRegistry : IActivityProvider
///
/// The type of the activity provider.
void ClearProvider(Type providerType);
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Workflows.Core/Contracts/ITenantAgnosticActivityProvider.cs b/src/modules/Elsa.Workflows.Core/Contracts/ITenantAgnosticActivityProvider.cs
new file mode 100644
index 000000000..e0b08d4a7
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/Contracts/ITenantAgnosticActivityProvider.cs
@@ -0,0 +1,10 @@
+namespace Elsa.Workflows;
+
+///
+/// Marks an activity provider whose descriptors do not depend on the current tenant.
+///
+///
+/// Descriptors from a tenant-agnostic provider can be initialized once for each
+/// instance.
+///
+public interface ITenantAgnosticActivityProvider : IActivityProvider;
diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs
index d1b334b51..62bc72b0e 100644
--- a/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs
+++ b/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs
@@ -16,9 +16,13 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
// Per-tenant activity descriptors (workflow-as-activities, tenant-specific providers, etc.)
private readonly ConcurrentDictionary _tenantRegistries = new();
- // Tenant-agnostic activity descriptors (built-in activities, manually registered, etc.)
- private readonly TenantRegistryData _agnosticRegistry = new();
-
+ // Tenant-agnostic activity descriptors (built-in activities, manually registered, etc.)
+ private readonly TenantRegistryData _agnosticRegistry = new();
+
+ // Tracks tenant-agnostic providers initialized for this registry instance.
+ private readonly ConcurrentDictionary _initializedProviders = new();
+ private readonly ConcurrentDictionary _providerInitializationLocks = new();
+
///
public void Add(Type providerType, ActivityDescriptor descriptor)
{
@@ -182,7 +186,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
await RefreshDescriptorsAsync(activityProvider, cancellationToken);
}
- public async Task RefreshDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = default)
+ public async Task RefreshDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = default)
{
var providerType = activityProvider.GetType();
@@ -216,9 +220,39 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
// Update the provider's descriptor list in this registry
registry.ProvidedActivityDescriptors[providerType] = providerDescriptors;
- }
- }
-
+ }
+ }
+
+ ///
+ public async Task EnsureDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = default)
+ {
+ if (activityProvider is not ITenantAgnosticActivityProvider)
+ {
+ await RefreshDescriptorsAsync(activityProvider, cancellationToken);
+ return;
+ }
+
+ var providerType = activityProvider.GetType();
+ if (_initializedProviders.ContainsKey(providerType))
+ return;
+
+ var initializationLock = _providerInitializationLocks.GetOrAdd(providerType, _ => new(1, 1));
+ await initializationLock.WaitAsync(cancellationToken);
+
+ try
+ {
+ if (_initializedProviders.ContainsKey(providerType))
+ return;
+
+ await RefreshDescriptorsAsync(activityProvider, cancellationToken);
+ _initializedProviders.TryAdd(providerType, 0);
+ }
+ finally
+ {
+ initializationLock.Release();
+ }
+ }
+
private void Add(ActivityDescriptor? descriptor, TenantRegistryData registry, ICollection providerDescriptors)
{
if (descriptor is null)
@@ -253,15 +287,17 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
{
_manualActivityDescriptors.Clear();
_tenantRegistries.Clear();
- _agnosticRegistry.ActivityDescriptors.Clear();
- _agnosticRegistry.LatestActivityDescriptors.Clear();
- _agnosticRegistry.ProvidedActivityDescriptors.Clear();
- }
+ _agnosticRegistry.ActivityDescriptors.Clear();
+ _agnosticRegistry.LatestActivityDescriptors.Clear();
+ _agnosticRegistry.ProvidedActivityDescriptors.Clear();
+ _initializedProviders.Clear();
+ }
///
- public void ClearProvider(Type providerType)
- {
- var currentTenantId = tenantAccessor.TenantId;
+ public void ClearProvider(Type providerType)
+ {
+ _initializedProviders.TryRemove(providerType, out _);
+ var currentTenantId = tenantAccessor.TenantId;
// Clear from current tenant's registry
if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry)
diff --git a/src/modules/Elsa.Workflows.Management/Activities/HostMethod/HostMethodActivityProvider.cs b/src/modules/Elsa.Workflows.Management/Activities/HostMethod/HostMethodActivityProvider.cs
index 34641f283..643c25df2 100644
--- a/src/modules/Elsa.Workflows.Management/Activities/HostMethod/HostMethodActivityProvider.cs
+++ b/src/modules/Elsa.Workflows.Management/Activities/HostMethod/HostMethodActivityProvider.cs
@@ -11,7 +11,7 @@ namespace Elsa.Workflows.Management.Activities.HostMethod;
/// Inputs come from public properties and method parameters.
///
[UsedImplicitly]
-public class HostMethodActivityProvider(IOptions options, IHostMethodActivityDescriber hostMethodActivityDescriber) : IActivityProvider
+public class HostMethodActivityProvider(IOptions options, IHostMethodActivityDescriber hostMethodActivityDescriber) : ITenantAgnosticActivityProvider
{
public async ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default)
{
diff --git a/src/modules/Elsa.Workflows.Management/Contracts/IActivityRegistryPopulator.cs b/src/modules/Elsa.Workflows.Management/Contracts/IActivityRegistryPopulator.cs
index ba841171c..75964aa62 100644
--- a/src/modules/Elsa.Workflows.Management/Contracts/IActivityRegistryPopulator.cs
+++ b/src/modules/Elsa.Workflows.Management/Contracts/IActivityRegistryPopulator.cs
@@ -10,4 +10,14 @@ public interface IActivityRegistryPopulator
///
/// The cancellation token.
Task PopulateRegistryAsync(CancellationToken cancellationToken = default);
-}
\ No newline at end of file
+
+ ///
+ /// Ensures that activity descriptors have been populated, initializing tenant-agnostic providers once.
+ ///
+ /// The cancellation token.
+ ///
+ /// The default implementation preserves the existing behavior for custom populators.
+ ///
+ Task EnsureRegistryPopulatedAsync(CancellationToken cancellationToken = default) =>
+ PopulateRegistryAsync(cancellationToken);
+}
diff --git a/src/modules/Elsa.Workflows.Management/Providers/TypedActivityProvider.cs b/src/modules/Elsa.Workflows.Management/Providers/TypedActivityProvider.cs
index 3ebba7b16..dd0ece8d7 100644
--- a/src/modules/Elsa.Workflows.Management/Providers/TypedActivityProvider.cs
+++ b/src/modules/Elsa.Workflows.Management/Providers/TypedActivityProvider.cs
@@ -10,7 +10,7 @@ namespace Elsa.Workflows.Management.Providers;
/// Provides activity descriptors based on a list of activity types registered in the .
///
[UsedImplicitly]
-public class TypedActivityProvider : IActivityProvider
+public class TypedActivityProvider : ITenantAgnosticActivityProvider
{
private readonly IActivityDescriber _activityDescriber;
private readonly ManagementOptions _options;
@@ -40,4 +40,4 @@ public class TypedActivityProvider : IActivityProvider
yield return descriptor;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Workflows.Management/Services/ActivityRegistryPopulator.cs b/src/modules/Elsa.Workflows.Management/Services/ActivityRegistryPopulator.cs
index a00cb8559..5310f95a3 100644
--- a/src/modules/Elsa.Workflows.Management/Services/ActivityRegistryPopulator.cs
+++ b/src/modules/Elsa.Workflows.Management/Services/ActivityRegistryPopulator.cs
@@ -10,4 +10,11 @@ public class ActivityRegistryPopulator(IEnumerable providers,
{
await registry.RefreshDescriptorsAsync(providers, cancellationToken);
}
-}
\ No newline at end of file
+
+ ///
+ public async Task EnsureRegistryPopulatedAsync(CancellationToken cancellationToken)
+ {
+ foreach (var provider in providers)
+ await registry.EnsureDescriptorsAsync(provider, cancellationToken);
+ }
+}
diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultRegistriesPopulator.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultRegistriesPopulator.cs
index 1ec647cb5..4db677c0d 100644
--- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultRegistriesPopulator.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultRegistriesPopulator.cs
@@ -14,16 +14,16 @@ public class DefaultRegistriesPopulator(
///
public async Task PopulateAsync(CancellationToken cancellationToken = default)
{
- // Stage 1: Populate the activity registry.
- // Because workflow definitions can be used as activities, we need to make sure that the activity registry is populated before we populate the workflow definition store.
- await activityRegistryPopulator.PopulateRegistryAsync(cancellationToken);
+ // Stage 1: Ensure the activity registry is populated.
+ // Tenant-agnostic providers are initialized once, while tenant-sensitive providers are refreshed before the workflow definition store is populated.
+ await activityRegistryPopulator.EnsureRegistryPopulatedAsync(cancellationToken);
// Stage 2: Populate the workflow definition store.
await workflowDefinitionStorePopulator.PopulateStoreAsync(false, cancellationToken);
- // Stage 3: Re-populate the activity registry.
- // After the workflow definition store has been populated, we need to re-populate the activity registry to make sure that the activity descriptors are up-to-date.
- await activityRegistryPopulator.PopulateRegistryAsync(cancellationToken);
+ // Stage 3: Refresh tenant-sensitive activity providers.
+ // After the workflow definition store has been populated, refresh tenant-specific descriptors such as workflow-as-activity definitions.
+ await activityRegistryPopulator.EnsureRegistryPopulatedAsync(cancellationToken);
// Stage 4. Re-update the workflow definition store with the current set of activities.
// Finally, we need to re-populate the workflow definition store to make sure that the workflow definitions are up-to-date.
@@ -34,4 +34,4 @@ public class DefaultRegistriesPopulator(
var notification = new WorkflowDefinitionsReloaded(reloadedWorkflowDefinitions);
await notificationSender.SendAsync(notification, cancellationToken);
}
-}
\ No newline at end of file
+}
diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/Services/ActivityRegistryTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Services/ActivityRegistryTests.cs
index 916971d1b..f51f5022c 100644
--- a/test/unit/Elsa.Workflows.Core.UnitTests/Services/ActivityRegistryTests.cs
+++ b/test/unit/Elsa.Workflows.Core.UnitTests/Services/ActivityRegistryTests.cs
@@ -425,6 +425,112 @@ public class ActivityRegistryTests
AssertDescriptor(result, CurrentTenant, 2);
}
+ [Fact]
+ public async Task EnsureDescriptorsAsync_InitializesTenantAgnosticProviderOnlyOnce_WhenCalledRepeatedly()
+ {
+ // Arrange
+ var provider = new CountingTenantAgnosticProvider();
+
+ // Act
+ await _registry.EnsureDescriptorsAsync(provider);
+ await _registry.EnsureDescriptorsAsync(provider);
+
+ // Assert
+ Assert.Equal(1, provider.CallCount);
+ }
+
+ [Fact]
+ public async Task EnsureDescriptorsAsync_InitializesTenantAgnosticProviderOnlyOnce_WhenCalledConcurrently()
+ {
+ // Arrange
+ var provider = new BlockingTenantAgnosticProvider();
+ var initializationTasks = Enumerable.Range(0, 8)
+ .Select(_ => _registry.EnsureDescriptorsAsync(provider))
+ .ToArray();
+
+ await provider.FirstCallStarted.Task.WaitAsync(TimeSpan.FromSeconds(1));
+ await Task.Yield();
+ provider.Release();
+
+ // Act
+ await Task.WhenAll(initializationTasks);
+
+ // Assert
+ Assert.Equal(1, provider.CallCount);
+ }
+
+ [Fact]
+ public async Task EnsureDescriptorsAsync_RetriesTenantAgnosticProvider_AfterFailure()
+ {
+ // Arrange
+ var provider = new CountingTenantAgnosticProvider([new InvalidOperationException("Expected failure")]);
+
+ // Act
+ await Assert.ThrowsAsync(() => _registry.EnsureDescriptorsAsync(provider));
+ await _registry.EnsureDescriptorsAsync(provider);
+
+ // Assert
+ Assert.Equal(2, provider.CallCount);
+ }
+
+ [Fact]
+ public async Task EnsureDescriptorsAsync_RetriesTenantAgnosticProvider_AfterCancellation()
+ {
+ // Arrange
+ var provider = new CountingTenantAgnosticProvider([new OperationCanceledException()]);
+
+ // Act
+ await Assert.ThrowsAnyAsync(() => _registry.EnsureDescriptorsAsync(provider));
+ await _registry.EnsureDescriptorsAsync(provider);
+
+ // Assert
+ Assert.Equal(2, provider.CallCount);
+ }
+
+ [Fact]
+ public async Task EnsureDescriptorsAsync_ReinitializesTenantAgnosticProvider_AfterRegistryIsCleared()
+ {
+ // Arrange
+ var provider = new CountingTenantAgnosticProvider();
+ await _registry.EnsureDescriptorsAsync(provider);
+
+ // Act
+ _registry.Clear();
+ await _registry.EnsureDescriptorsAsync(provider);
+
+ // Assert
+ Assert.Equal(2, provider.CallCount);
+ }
+
+ [Fact]
+ public async Task EnsureDescriptorsAsync_ReinitializesTenantAgnosticProvider_AfterProviderIsCleared()
+ {
+ // Arrange
+ var provider = new CountingTenantAgnosticProvider();
+ await _registry.EnsureDescriptorsAsync(provider);
+
+ // Act
+ _registry.ClearProvider(provider.GetType());
+ await _registry.EnsureDescriptorsAsync(provider);
+
+ // Assert
+ Assert.Equal(2, provider.CallCount);
+ }
+
+ [Fact]
+ public async Task EnsureDescriptorsAsync_RefreshesTenantSensitiveProvider_OnEveryCall()
+ {
+ // Arrange
+ var provider = new CountingTenantSensitiveProvider();
+
+ // Act
+ await _registry.EnsureDescriptorsAsync(provider);
+ await _registry.EnsureDescriptorsAsync(provider);
+
+ // Assert
+ Assert.Equal(2, provider.CallCount);
+ }
+
private ActivityDescriptor CreateDescriptor(string typeName, int version, string? tenantId) =>
new()
@@ -464,4 +570,73 @@ public class ActivityRegistryTests
public ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) => new(Descriptors);
}
+ private sealed class CountingTenantAgnosticProvider(IEnumerable? failures = null) : ITenantAgnosticActivityProvider
+ {
+ private readonly Queue _failures = new(failures ?? []);
+ private int _callCount;
+
+ public int CallCount => Volatile.Read(ref _callCount);
+
+ public ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default)
+ {
+ Interlocked.Increment(ref _callCount);
+
+ if (_failures.TryDequeue(out var failure))
+ return ValueTask.FromException>(failure);
+
+ return new([new ActivityDescriptor
+ {
+ TypeName = nameof(CountingTenantAgnosticProvider),
+ Version = 1,
+ TenantId = Tenant.AgnosticTenantId,
+ Kind = ActivityKind.Action
+ }]);
+ }
+ }
+
+ private sealed class BlockingTenantAgnosticProvider : ITenantAgnosticActivityProvider
+ {
+ private readonly TaskCompletionSource _release = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ private int _callCount;
+
+ public TaskCompletionSource FirstCallStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ public int CallCount => Volatile.Read(ref _callCount);
+
+ public async ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default)
+ {
+ Interlocked.Increment(ref _callCount);
+ FirstCallStarted.TrySetResult();
+ await _release.Task.WaitAsync(cancellationToken);
+
+ return [new ActivityDescriptor
+ {
+ TypeName = nameof(BlockingTenantAgnosticProvider),
+ Version = 1,
+ TenantId = Tenant.AgnosticTenantId,
+ Kind = ActivityKind.Action
+ }];
+ }
+
+ public void Release() => _release.TrySetResult();
+ }
+
+ private sealed class CountingTenantSensitiveProvider : IActivityProvider
+ {
+ private int _callCount;
+
+ public int CallCount => Volatile.Read(ref _callCount);
+
+ public ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default)
+ {
+ Interlocked.Increment(ref _callCount);
+ return new([new ActivityDescriptor
+ {
+ TypeName = nameof(CountingTenantSensitiveProvider),
+ Version = 1,
+ TenantId = CurrentTenant,
+ Kind = ActivityKind.Action
+ }]);
+ }
+ }
+
}
diff --git a/test/unit/Elsa.Workflows.Management.UnitTests/Services/ActivityRegistryPopulatorTests.cs b/test/unit/Elsa.Workflows.Management.UnitTests/Services/ActivityRegistryPopulatorTests.cs
new file mode 100644
index 000000000..5256762bb
--- /dev/null
+++ b/test/unit/Elsa.Workflows.Management.UnitTests/Services/ActivityRegistryPopulatorTests.cs
@@ -0,0 +1,111 @@
+using Elsa.Common.Multitenancy;
+using Elsa.Workflows.Management.Activities.HostMethod;
+using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
+using Elsa.Workflows.Management.Providers;
+using Elsa.Workflows.Management.Services;
+using Elsa.Workflows.Models;
+using Microsoft.Extensions.Logging;
+using NSubstitute;
+
+namespace Elsa.Workflows.Management.UnitTests.Services;
+
+public class ActivityRegistryPopulatorTests
+{
+ [Fact]
+ public async Task EnsureRegistryPopulatedAsync_InitializesAgnosticProvidersOnceAcrossScopedPopulators_AndRefreshesTenantSensitiveProvidersPerPass()
+ {
+ // Arrange
+ var agnosticProviderCallCount = new CallCounter();
+ var tenantSensitiveProviderCallCount = new CallCounter();
+ var registry = CreateRegistry();
+ IActivityRegistryPopulator firstScopePopulator = new ActivityRegistryPopulator(
+ [new CountingTenantAgnosticProvider(agnosticProviderCallCount), new CountingTenantSensitiveProvider(tenantSensitiveProviderCallCount)], registry);
+ IActivityRegistryPopulator secondScopePopulator = new ActivityRegistryPopulator(
+ [new CountingTenantAgnosticProvider(agnosticProviderCallCount), new CountingTenantSensitiveProvider(tenantSensitiveProviderCallCount)], registry);
+
+ // Act
+ await firstScopePopulator.EnsureRegistryPopulatedAsync();
+ await secondScopePopulator.EnsureRegistryPopulatedAsync();
+
+ // Assert
+ Assert.Equal(1, agnosticProviderCallCount.Value);
+ Assert.Equal(2, tenantSensitiveProviderCallCount.Value);
+ }
+
+ [Fact]
+ public async Task PopulateRegistryAsync_ForceRefreshesAllProvidersOnEveryPass()
+ {
+ // Arrange
+ var agnosticProviderCallCount = new CallCounter();
+ var tenantSensitiveProviderCallCount = new CallCounter();
+ var agnosticProvider = new CountingTenantAgnosticProvider(agnosticProviderCallCount);
+ var tenantSensitiveProvider = new CountingTenantSensitiveProvider(tenantSensitiveProviderCallCount);
+ IActivityRegistryPopulator populator = new ActivityRegistryPopulator([agnosticProvider, tenantSensitiveProvider], CreateRegistry());
+
+ // Act
+ await populator.PopulateRegistryAsync();
+ await populator.PopulateRegistryAsync();
+
+ // Assert
+ Assert.Equal(2, agnosticProviderCallCount.Value);
+ Assert.Equal(2, tenantSensitiveProviderCallCount.Value);
+ }
+
+ [Fact]
+ public void BuiltInTenantAgnosticProviders_OptIntoOncePerRegistryPopulation()
+ {
+ Assert.True(typeof(ITenantAgnosticActivityProvider).IsAssignableFrom(typeof(TypedActivityProvider)));
+ Assert.True(typeof(ITenantAgnosticActivityProvider).IsAssignableFrom(typeof(HostMethodActivityProvider)));
+ Assert.False(typeof(ITenantAgnosticActivityProvider).IsAssignableFrom(typeof(WorkflowDefinitionActivityProvider)));
+ }
+
+ private static ActivityRegistry CreateRegistry()
+ {
+ var tenantAccessor = Substitute.For();
+ tenantAccessor.TenantId.Returns("tenant-1");
+ return new ActivityRegistry(
+ Substitute.For(),
+ [],
+ tenantAccessor,
+ Substitute.For>());
+ }
+
+ private sealed class CountingTenantAgnosticProvider(CallCounter callCounter) : ITenantAgnosticActivityProvider
+ {
+ public ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default)
+ {
+ callCounter.Increment();
+ return new([new ActivityDescriptor
+ {
+ TypeName = nameof(CountingTenantAgnosticProvider),
+ Version = 1,
+ TenantId = Tenant.AgnosticTenantId,
+ Kind = ActivityKind.Action
+ }]);
+ }
+ }
+
+ private sealed class CountingTenantSensitiveProvider(CallCounter callCounter) : IActivityProvider
+ {
+ public ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default)
+ {
+ callCounter.Increment();
+ return new([new ActivityDescriptor
+ {
+ TypeName = nameof(CountingTenantSensitiveProvider),
+ Version = 1,
+ TenantId = "tenant-1",
+ Kind = ActivityKind.Action
+ }]);
+ }
+ }
+
+ private sealed class CallCounter
+ {
+ private int _value;
+
+ public int Value => Volatile.Read(ref _value);
+
+ public void Increment() => Interlocked.Increment(ref _value);
+ }
+}
diff --git a/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/DefaultRegistriesPopulatorTests.cs b/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/DefaultRegistriesPopulatorTests.cs
index 938c2e3c4..17b52bac3 100644
--- a/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/DefaultRegistriesPopulatorTests.cs
+++ b/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/DefaultRegistriesPopulatorTests.cs
@@ -9,18 +9,19 @@ namespace Elsa.Workflows.Runtime.UnitTests.Services;
public class DefaultRegistriesPopulatorTests
{
private readonly IWorkflowDefinitionStorePopulator _workflowDefinitionStorePopulatorMock;
+ private readonly IActivityRegistryPopulator _activityRegistryPopulatorMock;
private readonly INotificationSender _notificationSenderMock;
private readonly DefaultRegistriesPopulator _populator;
public DefaultRegistriesPopulatorTests()
{
_workflowDefinitionStorePopulatorMock = Substitute.For();
- var activityRegistryPopulatorMock = Substitute.For();
+ _activityRegistryPopulatorMock = Substitute.For();
_notificationSenderMock = Substitute.For();
_populator = new(
_workflowDefinitionStorePopulatorMock,
- activityRegistryPopulatorMock,
+ _activityRegistryPopulatorMock,
_notificationSenderMock);
}
@@ -56,4 +57,28 @@ public class DefaultRegistriesPopulatorTests
Arg.Is(n => n.ReloadedWorkflowDefinitions.Count == 2),
Arg.Any());
}
-}
\ No newline at end of file
+
+ [Fact]
+ public async Task PopulateAsync_EnsuresActivityRegistryBeforeEachWorkflowDefinitionStorePopulation()
+ {
+ // Arrange
+ _workflowDefinitionStorePopulatorMock
+ .PopulateStoreAsync(Arg.Any(), Arg.Any())
+ .Returns(Array.Empty());
+
+ // Act
+ await _populator.PopulateAsync();
+
+ // Assert
+ Received.InOrder(() =>
+ {
+ _activityRegistryPopulatorMock.EnsureRegistryPopulatedAsync(Arg.Any());
+ _workflowDefinitionStorePopulatorMock.PopulateStoreAsync(false, Arg.Any());
+ _activityRegistryPopulatorMock.EnsureRegistryPopulatedAsync(Arg.Any());
+ _workflowDefinitionStorePopulatorMock.PopulateStoreAsync(true, Arg.Any());
+ _notificationSenderMock.SendAsync(Arg.Any(), Arg.Any());
+ });
+
+ await _activityRegistryPopulatorMock.Received(2).EnsureRegistryPopulatedAsync(Arg.Any());
+ }
+}