diff --git a/src/bundles/Elsa.ServerAndStudio.Web/Program.cs b/src/bundles/Elsa.ServerAndStudio.Web/Program.cs
index c3e092e56..8e3252f92 100644
--- a/src/bundles/Elsa.ServerAndStudio.Web/Program.cs
+++ b/src/bundles/Elsa.ServerAndStudio.Web/Program.cs
@@ -1,4 +1,3 @@
-using Elsa.Common.DistributedLocks.Noop;
using Elsa.EntityFrameworkCore.Extensions;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
@@ -7,6 +6,7 @@ using Elsa.Extensions;
using Elsa.ServerAndStudio.Web.Extensions;
using Elsa.MassTransit.Extensions;
using Elsa.ServerAndStudio.Web.Enums;
+using Medallion.Threading.FileSystem;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Proto.Persistence.Sqlite;
@@ -73,7 +73,7 @@ services
});
}
- runtime.DistributedLockProvider = _ => new NoopDistributedSynchronizationProvider();
+ runtime.DistributedLockProvider = _ => new FileDistributedSynchronizationProvider(new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "locks")));
runtime.WorkflowInboxCleanupOptions = options => configuration.GetSection("Runtime:WorkflowInboxCleanup").Bind(options);
runtime.WorkflowDispatcherOptions = options => configuration.GetSection("Runtime:WorkflowDispatcher").Bind(options);
})
diff --git a/src/modules/Elsa.MassTransit/Consumers/WorkflowDefinitionEventsConsumer.cs b/src/modules/Elsa.MassTransit/Consumers/WorkflowDefinitionEventsConsumer.cs
index 0d1b52a75..3b5faab42 100644
--- a/src/modules/Elsa.MassTransit/Consumers/WorkflowDefinitionEventsConsumer.cs
+++ b/src/modules/Elsa.MassTransit/Consumers/WorkflowDefinitionEventsConsumer.cs
@@ -41,7 +41,7 @@ public class WorkflowDefinitionEventsConsumer(IActivityRegistryPopulator activit
///
public Task Consume(ConsumeContext context)
{
- activityRegistryPopulator.RemoveDefinitionFromRegistry(typeof(WorkflowDefinitionActivityProvider), context.Message.Id);
+ activityRegistryPopulator.RemoveDefinitionVersionFromRegistry(typeof(WorkflowDefinitionActivityProvider), context.Message.Id);
return Task.CompletedTask;
}
@@ -79,7 +79,7 @@ public class WorkflowDefinitionEventsConsumer(IActivityRegistryPopulator activit
if (usableAsActivity)
return activityRegistryPopulator.AddToRegistry(typeof(WorkflowDefinitionActivityProvider), id);
- activityRegistryPopulator.RemoveDefinitionFromRegistry(typeof(WorkflowDefinitionActivityProvider), id);
+ activityRegistryPopulator.RemoveDefinitionVersionFromRegistry(typeof(WorkflowDefinitionActivityProvider), id);
return Task.CompletedTask;
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Management/Handlers/RefreshActivityRegistry.cs b/src/modules/Elsa.Workflows.Management/Handlers/RefreshActivityRegistry.cs
index 471377c34..434fc2672 100644
--- a/src/modules/Elsa.Workflows.Management/Handlers/RefreshActivityRegistry.cs
+++ b/src/modules/Elsa.Workflows.Management/Handlers/RefreshActivityRegistry.cs
@@ -30,7 +30,7 @@ public class RefreshActivityRegistry(IActivityRegistryPopulator activityRegistry
///
public Task HandleAsync(WorkflowDefinitionRetracted notification, CancellationToken cancellationToken)
{
- activityRegistryPopulator.RemoveDefinitionFromRegistry(typeof(WorkflowDefinitionActivityProvider), notification.WorkflowDefinition.Id, cancellationToken);
+ activityRegistryPopulator.RemoveDefinitionVersionFromRegistry(typeof(WorkflowDefinitionActivityProvider), notification.WorkflowDefinition.Id, cancellationToken);
return Task.CompletedTask;
}
@@ -61,7 +61,7 @@ public class RefreshActivityRegistry(IActivityRegistryPopulator activityRegistry
///
public Task HandleAsync(WorkflowDefinitionVersionDeleted notification, CancellationToken cancellationToken)
{
- activityRegistryPopulator.RemoveDefinitionFromRegistry(typeof(WorkflowDefinitionActivityProvider), notification.WorkflowDefinition.Id, cancellationToken);
+ activityRegistryPopulator.RemoveDefinitionVersionFromRegistry(typeof(WorkflowDefinitionActivityProvider), notification.WorkflowDefinition.Id, cancellationToken);
return Task.CompletedTask;
}
@@ -81,7 +81,7 @@ public class RefreshActivityRegistry(IActivityRegistryPopulator activityRegistry
if (usableAsActivity.GetValueOrDefault())
return activityRegistryPopulator.AddToRegistry(typeof(WorkflowDefinitionActivityProvider), id);
- activityRegistryPopulator.RemoveDefinitionFromRegistry(typeof(WorkflowDefinitionActivityProvider), id);
+ activityRegistryPopulator.RemoveDefinitionVersionFromRegistry(typeof(WorkflowDefinitionActivityProvider), id);
return Task.CompletedTask;
}
}
\ 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 a7836f5ac..c69c3bf99 100644
--- a/src/modules/Elsa.Workflows.Management/Services/ActivityRegistryPopulator.cs
+++ b/src/modules/Elsa.Workflows.Management/Services/ActivityRegistryPopulator.cs
@@ -37,7 +37,7 @@ public class ActivityRegistryPopulator(IEnumerable providers,
var descriptorsToRemove = providerDescriptors
.Where(d =>
d.CustomProperties.TryGetValue("WorkflowDefinitionId", out var val) &&
- val.ToString() == workflowDefinitionId);
+ val.ToString() == workflowDefinitionId).ToList();
foreach (ActivityDescriptor activityDescriptor in descriptorsToRemove)
{
diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Consumers/WorkflowDefinitionEventHandlers.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Consumers/WorkflowDefinitionEventHandlers.cs
new file mode 100644
index 000000000..790606986
--- /dev/null
+++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/Consumers/WorkflowDefinitionEventHandlers.cs
@@ -0,0 +1,15 @@
+using Elsa.MassTransit.Messages;
+using Hangfire.Annotations;
+using MassTransit;
+
+namespace Elsa.Workflows.ComponentTests.Consumers;
+
+[UsedImplicitly]
+public class WorkflowDefinitionEventHandlers(IWorkflowDefinitionEvents workflowDefinitionEvents) : IConsumer
+{
+ public Task Consume(ConsumeContext context)
+ {
+ workflowDefinitionEvents.OnWorkflowDefinitionDeleted(new WorkflowDefinitionDeletedEventArgs(context.Message.Id));
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Contracts/IWorkflowDefinitionEvents.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Contracts/IWorkflowDefinitionEvents.cs
new file mode 100644
index 000000000..dc98ee3e4
--- /dev/null
+++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/Contracts/IWorkflowDefinitionEvents.cs
@@ -0,0 +1,8 @@
+namespace Elsa.Workflows.ComponentTests;
+
+public interface IWorkflowDefinitionEvents
+{
+ event EventHandler WorkflowDefinitionDeleted;
+
+ void OnWorkflowDefinitionDeleted(WorkflowDefinitionDeletedEventArgs args);
+}
\ No newline at end of file
diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/EventArgs/WorkflowDefinitionDeletedEventArgs.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/EventArgs/WorkflowDefinitionDeletedEventArgs.cs
new file mode 100644
index 000000000..93324767c
--- /dev/null
+++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/EventArgs/WorkflowDefinitionDeletedEventArgs.cs
@@ -0,0 +1,6 @@
+namespace Elsa.Workflows.ComponentTests;
+
+public class WorkflowDefinitionDeletedEventArgs(string definitionId) : EventArgs
+{
+ public string DefinitionId { get; } = definitionId;
+}
\ No newline at end of file
diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs
index df2afc214..75e5301b9 100644
--- a/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs
+++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs
@@ -1,9 +1,11 @@
using System.Net.Http.Headers;
+using System.Reflection;
using Elsa.EntityFrameworkCore.Extensions;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.Extensions;
using Elsa.Identity.Providers;
using Elsa.MassTransit.Extensions;
+using Elsa.Workflows.ComponentTests.Consumers;
using Elsa.Workflows.ComponentTests.Services;
using FluentStorage;
using Hangfire.Annotations;
@@ -52,7 +54,7 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl
elsa.UseDefaultAuthentication(defaultAuthentication => defaultAuthentication.UseAdminApiKey());
elsa.UseFluentStorageProvider(sp =>
{
- var assemblyLocation = System.Reflection.Assembly.GetExecutingAssembly().Location;
+ var assemblyLocation = Assembly.GetExecutingAssembly().Location;
var assemblyDirectory = Path.GetDirectoryName(assemblyLocation)!;
var workflowsDirectorySegments = new[]
{
@@ -64,6 +66,7 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl
elsa.UseMassTransit(massTransit =>
{
massTransit.UseRabbitMq(rabbitMqConnectionString);
+ massTransit.AddConsumer("elsa-test-workflow-definition-updates", true);
});
elsa.UseWorkflowManagement(management =>
{
@@ -82,6 +85,7 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl
{
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddNotificationHandlersFrom();
});
}
diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Services/WorkflowDefinitionEvents.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Services/WorkflowDefinitionEvents.cs
new file mode 100644
index 000000000..514c1a19c
--- /dev/null
+++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/Services/WorkflowDefinitionEvents.cs
@@ -0,0 +1,7 @@
+namespace Elsa.Workflows.ComponentTests.Services;
+
+public class WorkflowDefinitionEvents : IWorkflowDefinitionEvents
+{
+ public event EventHandler? WorkflowDefinitionDeleted;
+ public void OnWorkflowDefinitionDeleted(WorkflowDefinitionDeletedEventArgs args) => WorkflowDefinitionDeleted?.Invoke(this, args);
+}
\ No newline at end of file
diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/DeleteWorkflowTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/DeleteWorkflowTests.cs
new file mode 100644
index 000000000..63df0cb74
--- /dev/null
+++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/DeleteWorkflowTests.cs
@@ -0,0 +1,86 @@
+using Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities.Workflows;
+using Elsa.Workflows.Contracts;
+using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
+using Elsa.Workflows.Management.Contracts;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities;
+
+public class DeleteWorkflowTests : AppComponentTest
+{
+ private readonly ISignalManager _signalManager;
+ private readonly IWorkflowDefinitionEvents _workflowDefinitionEvents;
+ private readonly IServiceScope _scope1;
+ private readonly IServiceScope _scope2;
+ private readonly IServiceScope _scope3;
+ private static readonly object WorkflowDeletedSignal = new();
+
+ public DeleteWorkflowTests(App app) : base(app)
+ {
+ _scope1 = app.Cluster.Pod1.Services.CreateScope();
+ _scope2 = app.Cluster.Pod2.Services.CreateScope();
+ _scope3 = app.Cluster.Pod3.Services.CreateScope();
+ _signalManager = Scope.ServiceProvider.GetRequiredService();
+ _workflowDefinitionEvents = Scope.ServiceProvider.GetRequiredService();
+ _workflowDefinitionEvents.WorkflowDefinitionDeleted += OnWorkflowDefinionDeleted;
+ }
+
+ [Fact]
+ public async Task DeleteWorkflow()
+ {
+ EnsureWorkflowInRegistry(_scope1);
+
+ var workflowDefinitionManager = _scope1.ServiceProvider.GetRequiredService();
+ await workflowDefinitionManager.DeleteByDefinitionIdAsync(Workflows.DeleteWorkflow.DefinitionId);
+
+ WorkflowTypeDeletedFromRegistry(_scope1);
+ }
+
+ [Fact]
+ public async Task DeleteWorkflow_Clustered()
+ {
+ EnsureWorkflowInRegistry(_scope1);
+ EnsureWorkflowInRegistry(_scope2);
+ EnsureWorkflowInRegistry(_scope3);
+
+ var workflowDefinitionManager = _scope3.ServiceProvider.GetRequiredService();
+ await workflowDefinitionManager.DeleteByDefinitionIdAsync(Workflows.DeleteWorkflow.DefinitionId);
+
+ WorkflowTypeDeletedFromRegistry(_scope1);
+
+ await _signalManager.WaitAsync(WorkflowDeletedSignal);
+ WorkflowTypeDeletedFromRegistry(_scope2);
+ WorkflowTypeDeletedFromRegistry(_scope3);
+ }
+
+ private void EnsureWorkflowInRegistry(IServiceScope scope)
+ {
+ var activityRegistry = scope.ServiceProvider.GetRequiredService();
+ var descriptor = activityRegistry.Find(Workflows.DeleteWorkflow.Type);
+ if (descriptor is null)
+ activityRegistry.Add(typeof(WorkflowDefinitionActivityProvider), descriptor);
+ }
+
+ private void WorkflowTypeDeletedFromRegistry(IServiceScope scope)
+ {
+ var activityRegistry = scope.ServiceProvider.GetRequiredService();
+ var descriptor = activityRegistry.Find(Workflows.DeleteWorkflow.Type);
+
+ Assert.Null(descriptor);
+ }
+
+ private void OnWorkflowDefinionDeleted(object? sender, WorkflowDefinitionDeletedEventArgs args)
+ {
+ if (args.DefinitionId == Workflows.DeleteWorkflow.DefinitionId)
+ {
+ _signalManager.Trigger(WorkflowDeletedSignal, args);
+ }
+ }
+
+ protected override void OnDispose()
+ {
+ _scope1.Dispose();
+ _scope2.Dispose();
+ _scope3.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/SaveWorkflowTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/SaveWorkflowTests.cs
new file mode 100644
index 000000000..c5d869069
--- /dev/null
+++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/SaveWorkflowTests.cs
@@ -0,0 +1,61 @@
+using Elsa.Workflows.Contracts;
+using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
+using Elsa.Workflows.Management.Contracts;
+using Elsa.Workflows.Management.Models;
+using Elsa.Workflows.Models;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities;
+
+public class SaveWorkflowTests(App app) : AppComponentTest(app)
+{
+ private readonly IServiceScope _scope = app.Cluster.Pod1.Services.CreateScope();
+
+ [Theory]
+ [InlineData("Save1", true, true, true, true)]
+ [InlineData("Save2", true, false, true, false)]
+ [InlineData("Save3", false, true, false, false)]
+ [InlineData("Save4", false, false, false, false)]
+ private async Task SaveWorkflow(string name, bool usableAsActivity, bool publish, bool expectedInRegistry, bool isBrowsable)
+ {
+ var activityRegistry = _scope.ServiceProvider.GetRequiredService();
+
+ var descriptor = activityRegistry.Find(name);
+ if (descriptor is not null)
+ activityRegistry.Remove(typeof(WorkflowDefinitionActivityProvider), descriptor);
+
+ var importer = _scope.ServiceProvider.GetRequiredService();
+ var request = new SaveWorkflowDefinitionRequest
+ {
+ Model = new WorkflowDefinitionModel
+ {
+ Name = name,
+ DefinitionId = name,
+ Options = new WorkflowOptions
+ {
+ UsableAsActivity = usableAsActivity,
+ AutoUpdateConsumingWorkflows = true
+ }
+ },
+ Publish = publish
+ };
+ await importer.ImportAsync(request);
+
+ descriptor = activityRegistry.Find(name);
+
+ if (expectedInRegistry)
+ {
+ Assert.NotNull(descriptor);
+ Assert.Equal(isBrowsable, descriptor.IsBrowsable);
+ }
+ else
+ {
+ Assert.Null(descriptor);
+ }
+ }
+
+ protected override void OnDispose()
+ {
+ _scope.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/Workflows/DeleteWorkflow.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/Workflows/DeleteWorkflow.cs
new file mode 100644
index 000000000..1e3ba478e
--- /dev/null
+++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/Workflows/DeleteWorkflow.cs
@@ -0,0 +1,25 @@
+using Elsa.Scheduling.Activities;
+using Elsa.Workflows.Activities;
+using Elsa.Workflows.Contracts;
+
+namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities.Workflows;
+
+public class DeleteWorkflow : WorkflowBase
+{
+ public static readonly string DefinitionId = Guid.NewGuid().ToString();
+ public static readonly string Type = nameof(DeleteWorkflow);
+ protected override void Build(IWorkflowBuilder builder)
+ {
+ builder.Name = Type;
+ builder.WithDefinitionId(DefinitionId);
+ builder.WorkflowOptions.UsableAsActivity = true;
+ builder.Root = new Sequence
+ {
+ Activities =
+ {
+ new Delay(TimeSpan.FromMilliseconds(250)),
+ new WriteLine("This workflow will be deleted!")
+ }
+ };
+ }
+}
\ No newline at end of file