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"); + } +}