From f834b040f9a4d18d0feea61823d178d943219025 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 20 Dec 2025 22:30:43 +0000
Subject: [PATCH] Add workflow dispatch notifications
- Created WorkflowDefinitionDispatching and WorkflowDefinitionDispatched notifications
- Created WorkflowInstanceDispatching and WorkflowInstanceDispatched notifications
- Updated BackgroundWorkflowDispatcher to emit notifications before and after dispatch
- Added integration tests to verify notifications are emitted correctly
Co-authored-by: KnibbsyMan <23156317+KnibbsyMan@users.noreply.github.com>
---
.../WorkflowDefinitionDispatched.cs | 10 +++
.../WorkflowDefinitionDispatching.cs | 9 ++
.../WorkflowInstanceDispatched.cs | 10 +++
.../WorkflowInstanceDispatching.cs | 9 ++
.../Services/BackgroundWorkflowDispatcher.cs | 23 ++++-
.../WorkflowDispatchNotifications/Spy.cs | 16 ++++
.../TestHandler.cs | 46 ++++++++++
.../WorkflowDispatchNotifications/Tests.cs | 85 +++++++++++++++++++
.../Workflows.cs | 14 +++
9 files changed, 219 insertions(+), 3 deletions(-)
create mode 100644 src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowDefinitionDispatched.cs
create mode 100644 src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowDefinitionDispatching.cs
create mode 100644 src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowInstanceDispatched.cs
create mode 100644 src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowInstanceDispatching.cs
create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/Spy.cs
create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/TestHandler.cs
create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/Tests.cs
create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/Workflows.cs
diff --git a/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowDefinitionDispatched.cs b/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowDefinitionDispatched.cs
new file mode 100644
index 000000000..d1258268c
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowDefinitionDispatched.cs
@@ -0,0 +1,10 @@
+using Elsa.Mediator.Contracts;
+using Elsa.Workflows.Runtime.Requests;
+using Elsa.Workflows.Runtime.Responses;
+
+namespace Elsa.Workflows.Runtime.Notifications;
+
+///
+/// A notification that is published when a workflow definition has been dispatched.
+///
+public record WorkflowDefinitionDispatched(DispatchWorkflowDefinitionRequest Request, DispatchWorkflowResponse Response) : INotification;
diff --git a/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowDefinitionDispatching.cs b/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowDefinitionDispatching.cs
new file mode 100644
index 000000000..f6cdb248e
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowDefinitionDispatching.cs
@@ -0,0 +1,9 @@
+using Elsa.Mediator.Contracts;
+using Elsa.Workflows.Runtime.Requests;
+
+namespace Elsa.Workflows.Runtime.Notifications;
+
+///
+/// A notification that is published when a workflow definition is being dispatched.
+///
+public record WorkflowDefinitionDispatching(DispatchWorkflowDefinitionRequest Request) : INotification;
diff --git a/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowInstanceDispatched.cs b/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowInstanceDispatched.cs
new file mode 100644
index 000000000..64690605c
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowInstanceDispatched.cs
@@ -0,0 +1,10 @@
+using Elsa.Mediator.Contracts;
+using Elsa.Workflows.Runtime.Requests;
+using Elsa.Workflows.Runtime.Responses;
+
+namespace Elsa.Workflows.Runtime.Notifications;
+
+///
+/// A notification that is published when a workflow instance has been dispatched.
+///
+public record WorkflowInstanceDispatched(DispatchWorkflowInstanceRequest Request, DispatchWorkflowResponse Response) : INotification;
diff --git a/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowInstanceDispatching.cs b/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowInstanceDispatching.cs
new file mode 100644
index 000000000..0e4a6b3e7
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowInstanceDispatching.cs
@@ -0,0 +1,9 @@
+using Elsa.Mediator.Contracts;
+using Elsa.Workflows.Runtime.Requests;
+
+namespace Elsa.Workflows.Runtime.Notifications;
+
+///
+/// A notification that is published when a workflow instance is being dispatched.
+///
+public record WorkflowInstanceDispatching(DispatchWorkflowInstanceRequest Request) : INotification;
diff --git a/src/modules/Elsa.Workflows.Runtime/Services/BackgroundWorkflowDispatcher.cs b/src/modules/Elsa.Workflows.Runtime/Services/BackgroundWorkflowDispatcher.cs
index 0af4a9278..b5b99007f 100644
--- a/src/modules/Elsa.Workflows.Runtime/Services/BackgroundWorkflowDispatcher.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Services/BackgroundWorkflowDispatcher.cs
@@ -3,6 +3,7 @@ using Elsa.Mediator;
using Elsa.Mediator.Contracts;
using Elsa.Tenants.Mediator;
using Elsa.Workflows.Runtime.Commands;
+using Elsa.Workflows.Runtime.Notifications;
using Elsa.Workflows.Runtime.Requests;
using Elsa.Workflows.Runtime.Responses;
@@ -11,11 +12,14 @@ namespace Elsa.Workflows.Runtime;
///
/// A simple implementation that queues the specified request for workflow execution on a non-durable background worker.
///
-public class BackgroundWorkflowDispatcher(ICommandSender commandSender, ITenantAccessor tenantAccessor) : IWorkflowDispatcher
+public class BackgroundWorkflowDispatcher(ICommandSender commandSender, INotificationSender notificationSender, ITenantAccessor tenantAccessor) : IWorkflowDispatcher
{
///
public async Task DispatchAsync(DispatchWorkflowDefinitionRequest request, DispatchWorkflowOptions? options = null, CancellationToken cancellationToken = default)
{
+ // Emit dispatching notification
+ await notificationSender.SendAsync(new WorkflowDefinitionDispatching(request), cancellationToken);
+
var command = new DispatchWorkflowDefinitionCommand(request.DefinitionVersionId)
{
Input = request.Input,
@@ -27,12 +31,20 @@ public class BackgroundWorkflowDispatcher(ICommandSender commandSender, ITenantA
};
await commandSender.SendAsync(command, CommandStrategy.Background, CreateHeaders(), cancellationToken);
- return DispatchWorkflowResponse.Success();
+ var response = DispatchWorkflowResponse.Success();
+
+ // Emit dispatched notification
+ await notificationSender.SendAsync(new WorkflowDefinitionDispatched(request, response), cancellationToken);
+
+ return response;
}
///
public async Task DispatchAsync(DispatchWorkflowInstanceRequest request, DispatchWorkflowOptions? options = null, CancellationToken cancellationToken = default)
{
+ // Emit dispatching notification
+ await notificationSender.SendAsync(new WorkflowInstanceDispatching(request), cancellationToken);
+
var command = new DispatchWorkflowInstanceCommand(request.InstanceId){
BookmarkId = request.BookmarkId,
ActivityHandle = request.ActivityHandle,
@@ -41,7 +53,12 @@ public class BackgroundWorkflowDispatcher(ICommandSender commandSender, ITenantA
CorrelationId = request.CorrelationId};
await commandSender.SendAsync(command, CommandStrategy.Background, CreateHeaders(), cancellationToken);
- return DispatchWorkflowResponse.Success();
+ var response = DispatchWorkflowResponse.Success();
+
+ // Emit dispatched notification
+ await notificationSender.SendAsync(new WorkflowInstanceDispatched(request, response), cancellationToken);
+
+ return response;
}
///
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/Spy.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/Spy.cs
new file mode 100644
index 000000000..fbe7c2b99
--- /dev/null
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/Spy.cs
@@ -0,0 +1,16 @@
+using Elsa.Workflows.Runtime.Requests;
+using Elsa.Workflows.Runtime.Responses;
+
+namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowDispatchNotifications;
+
+public class Spy
+{
+ public bool WorkflowDefinitionDispatchingWasCalled { get; set; }
+ public bool WorkflowDefinitionDispatchedWasCalled { get; set; }
+ public bool WorkflowInstanceDispatchingWasCalled { get; set; }
+ public bool WorkflowInstanceDispatchedWasCalled { get; set; }
+
+ public DispatchWorkflowDefinitionRequest? CapturedDefinitionRequest { get; set; }
+ public DispatchWorkflowInstanceRequest? CapturedInstanceRequest { get; set; }
+ public DispatchWorkflowResponse? CapturedResponse { get; set; }
+}
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/TestHandler.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/TestHandler.cs
new file mode 100644
index 000000000..f2ede80cd
--- /dev/null
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/TestHandler.cs
@@ -0,0 +1,46 @@
+using Elsa.Mediator.Contracts;
+using Elsa.Workflows.Runtime.Notifications;
+
+namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowDispatchNotifications;
+
+public class TestHandler :
+ INotificationHandler,
+ INotificationHandler,
+ INotificationHandler,
+ INotificationHandler
+{
+ private readonly Spy _spy;
+
+ public TestHandler(Spy spy)
+ {
+ _spy = spy;
+ }
+
+ public Task HandleAsync(WorkflowDefinitionDispatching notification, CancellationToken cancellationToken)
+ {
+ _spy.WorkflowDefinitionDispatchingWasCalled = true;
+ _spy.CapturedDefinitionRequest = notification.Request;
+ return Task.CompletedTask;
+ }
+
+ public Task HandleAsync(WorkflowDefinitionDispatched notification, CancellationToken cancellationToken)
+ {
+ _spy.WorkflowDefinitionDispatchedWasCalled = true;
+ _spy.CapturedResponse = notification.Response;
+ return Task.CompletedTask;
+ }
+
+ public Task HandleAsync(WorkflowInstanceDispatching notification, CancellationToken cancellationToken)
+ {
+ _spy.WorkflowInstanceDispatchingWasCalled = true;
+ _spy.CapturedInstanceRequest = notification.Request;
+ return Task.CompletedTask;
+ }
+
+ public Task HandleAsync(WorkflowInstanceDispatched notification, CancellationToken cancellationToken)
+ {
+ _spy.WorkflowInstanceDispatchedWasCalled = true;
+ _spy.CapturedResponse = notification.Response;
+ return Task.CompletedTask;
+ }
+}
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/Tests.cs
new file mode 100644
index 000000000..d05f3b656
--- /dev/null
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/Tests.cs
@@ -0,0 +1,85 @@
+using Elsa.Testing.Shared;
+using Elsa.Workflows.Runtime;
+using Elsa.Workflows.Runtime.Notifications;
+using Elsa.Workflows.Runtime.Requests;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit.Abstractions;
+
+namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowDispatchNotifications;
+
+public class Tests
+{
+ private readonly IWorkflowDispatcher _workflowDispatcher;
+ private readonly Spy _spy;
+
+ public Tests(ITestOutputHelper testOutputHelper)
+ {
+ var services = new TestApplicationBuilder(testOutputHelper)
+ .ConfigureServices(s =>
+ {
+ s.AddSingleton();
+ s.AddNotificationHandler();
+ s.AddNotificationHandler();
+ s.AddNotificationHandler();
+ s.AddNotificationHandler();
+ })
+ .Build();
+
+ _workflowDispatcher = services.GetRequiredService();
+ _spy = services.GetRequiredService();
+ }
+
+ [Fact(DisplayName = "Dispatching workflow definition should emit notifications")]
+ public async Task DispatchWorkflowDefinition_ShouldEmitNotifications()
+ {
+ // Arrange
+ var definitionVersionId = "test-definition-version-id";
+ var request = new DispatchWorkflowDefinitionRequest(definitionVersionId)
+ {
+ CorrelationId = "test-correlation-id",
+ Input = new Dictionary { { "TestKey", "TestValue" } }
+ };
+
+ // Act
+ await _workflowDispatcher.DispatchAsync(request, null);
+
+ // Allow async notification handlers to complete
+ await Task.Delay(100);
+
+ // Assert
+ Assert.True(_spy.WorkflowDefinitionDispatchingWasCalled, "WorkflowDefinitionDispatching notification should be called");
+ Assert.True(_spy.WorkflowDefinitionDispatchedWasCalled, "WorkflowDefinitionDispatched notification should be called");
+ Assert.NotNull(_spy.CapturedDefinitionRequest);
+ Assert.Equal(definitionVersionId, _spy.CapturedDefinitionRequest.DefinitionVersionId);
+ Assert.Equal("test-correlation-id", _spy.CapturedDefinitionRequest.CorrelationId);
+ Assert.NotNull(_spy.CapturedResponse);
+ Assert.True(_spy.CapturedResponse.Succeeded);
+ }
+
+ [Fact(DisplayName = "Dispatching workflow instance should emit notifications")]
+ public async Task DispatchWorkflowInstance_ShouldEmitNotifications()
+ {
+ // Arrange
+ var instanceId = "test-instance-id";
+ var request = new DispatchWorkflowInstanceRequest(instanceId)
+ {
+ CorrelationId = "test-correlation-id",
+ Input = new Dictionary { { "TestKey", "TestValue" } }
+ };
+
+ // Act
+ await _workflowDispatcher.DispatchAsync(request, null);
+
+ // Allow async notification handlers to complete
+ await Task.Delay(100);
+
+ // Assert
+ Assert.True(_spy.WorkflowInstanceDispatchingWasCalled, "WorkflowInstanceDispatching notification should be called");
+ Assert.True(_spy.WorkflowInstanceDispatchedWasCalled, "WorkflowInstanceDispatched notification should be called");
+ Assert.NotNull(_spy.CapturedInstanceRequest);
+ Assert.Equal(instanceId, _spy.CapturedInstanceRequest.InstanceId);
+ Assert.Equal("test-correlation-id", _spy.CapturedInstanceRequest.CorrelationId);
+ Assert.NotNull(_spy.CapturedResponse);
+ Assert.True(_spy.CapturedResponse.Succeeded);
+ }
+}
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/Workflows.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/Workflows.cs
new file mode 100644
index 000000000..8d8ebddfe
--- /dev/null
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDispatchNotifications/Workflows.cs
@@ -0,0 +1,14 @@
+using Elsa.Workflows.Activities;
+
+namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowDispatchNotifications;
+
+public class SimpleWorkflow : WorkflowBase
+{
+ public static string DefinitionId = "SimpleWorkflow";
+
+ protected override void Build(IWorkflowBuilder builder)
+ {
+ builder.DefinitionId = DefinitionId;
+ builder.Root = new WriteLine("Hello from SimpleWorkflow");
+ }
+}