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