From 8d7d1a986294dc9d603bc288fce31db8ad6464df Mon Sep 17 00:00:00 2001 From: Avinesh Singh Date: Wed, 15 Apr 2026 17:50:44 +0530 Subject: [PATCH] Fix BulkDispatchWorkflows sharing input dictionary across dispatches (#7284) When using BulkDispatchWorkflows with the Input property set, all dispatched child workflows received the same dictionary reference. This caused the input to be mutated across iterations, resulting in all child workflows seeing the last item's value instead of their own distinct values. The fix creates a copy of the base input dictionary for each dispatch iteration, ensuring each child workflow receives its own isolated input. --- .../Activities/BulkDispatchWorkflows.cs | 3 +- .../Scenarios/BulkDispatchWithInput/Spy.cs | 17 +++++++ .../BulkDispatchWithInput/TestHandler.cs | 13 +++++ .../Scenarios/BulkDispatchWithInput/Tests.cs | 51 +++++++++++++++++++ .../BulkDispatchWithInput/Workflows.cs | 26 ++++++++++ 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/Spy.cs create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/TestHandler.cs create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/Tests.cs create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/Workflows.cs diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs index 767ee8995..9430c0bf1 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs @@ -156,7 +156,8 @@ public class BulkDispatchWorkflows : Activity throw new($"No published version of workflow definition with ID {workflowDefinitionId} found."); var parentInstanceId = context.WorkflowExecutionContext.Id; - var input = Input.GetOrDefault(context) ?? new Dictionary(); + var baseInput = Input.GetOrDefault(context); + var input = baseInput != null ? new Dictionary(baseInput) : new Dictionary(); var channelName = ChannelName.GetOrDefault(context); var defaultInputItemKey = DefaultItemInputKey.GetOrDefault(context, () => "Item")!; var properties = new Dictionary diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/Spy.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/Spy.cs new file mode 100644 index 000000000..77ca6a3ba --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/Spy.cs @@ -0,0 +1,17 @@ +using Elsa.Workflows.Runtime.Requests; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.BulkDispatchWithInput; + +public class Spy +{ + public List?> CapturedInputReferences { get; } = []; + public List?> CapturedInputSnapshots { get; } = []; + + public void CaptureDispatch(DispatchWorkflowDefinitionRequest request) + { + CapturedInputReferences.Add(request.Input); + CapturedInputSnapshots.Add(request.Input != null + ? new Dictionary(request.Input) + : null); + } +} diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/TestHandler.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/TestHandler.cs new file mode 100644 index 000000000..f69b1def6 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/TestHandler.cs @@ -0,0 +1,13 @@ +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Runtime.Notifications; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.BulkDispatchWithInput; + +public class TestHandler(Spy spy) : INotificationHandler +{ + public Task HandleAsync(WorkflowDefinitionDispatching notification, CancellationToken cancellationToken) + { + spy.CaptureDispatch(notification.Request); + return Task.CompletedTask; + } +} diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/Tests.cs new file mode 100644 index 000000000..bb412e09b --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/Tests.cs @@ -0,0 +1,51 @@ +using Elsa.Extensions; +using Elsa.Testing.Shared; +using Elsa.Workflows.Runtime.Notifications; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.BulkDispatchWithInput; + +public class Tests +{ + private readonly IServiceProvider _services; + private readonly Spy _spy; + + public Tests(ITestOutputHelper testOutputHelper) + { + _services = new TestApplicationBuilder(testOutputHelper) + .AddWorkflow() + .AddWorkflow() + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddNotificationHandler(); + }) + .Build(); + + _spy = _services.GetRequiredService(); + } + + [Fact(DisplayName = "Each dispatched child workflow receives its own input dictionary")] + public async Task BulkDispatch_EachChildReceivesDistinctInputDictionary() + { + // Arrange + await _services.PopulateRegistriesAsync(); + + // Act + await _services.RunWorkflowUntilEndAsync(nameof(ParentWorkflow)); + + // Assert - each dispatch should receive a distinct dictionary instance + Assert.Equal(3, _spy.CapturedInputReferences.Count); + Assert.Equal(3, _spy.CapturedInputReferences.Distinct().Count()); + + // Assert - each dispatch should have its corresponding item value + var items = _spy.CapturedInputSnapshots + .Select(s => s?.GetValueOrDefault("Item")) + .ToList(); + + Assert.Contains("Apple", items); + Assert.Contains("Banana", items); + Assert.Contains("Cherry", items); + } +} diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/Workflows.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/Workflows.cs new file mode 100644 index 000000000..5631926e4 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/BulkDispatchWithInput/Workflows.cs @@ -0,0 +1,26 @@ +using Elsa.Workflows.Activities; +using Elsa.Workflows.Runtime.Activities; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.BulkDispatchWithInput; + +public class ParentWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + builder.Root = new BulkDispatchWorkflows + { + WorkflowDefinitionId = new(nameof(ChildWorkflow)), + Items = new(new[] { "Apple", "Banana", "Cherry" }), + Input = new(new Dictionary { ["ExtraData"] = "SharedValue" }), + WaitForCompletion = new(false) + }; + } +} + +public class ChildWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + builder.Root = new WriteLine("Child executed"); + } +}