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.
This commit is contained in:
Avinesh Singh 2026-04-15 17:50:44 +05:30 committed by GitHub
parent a459904e1f
commit 8d7d1a9862
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 109 additions and 1 deletions

View file

@ -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<string, object>();
var baseInput = Input.GetOrDefault(context);
var input = baseInput != null ? new Dictionary<string, object>(baseInput) : new Dictionary<string, object>();
var channelName = ChannelName.GetOrDefault(context);
var defaultInputItemKey = DefaultItemInputKey.GetOrDefault(context, () => "Item")!;
var properties = new Dictionary<string, object>

View file

@ -0,0 +1,17 @@
using Elsa.Workflows.Runtime.Requests;
namespace Elsa.Workflows.IntegrationTests.Scenarios.BulkDispatchWithInput;
public class Spy
{
public List<IDictionary<string, object>?> CapturedInputReferences { get; } = [];
public List<IDictionary<string, object>?> CapturedInputSnapshots { get; } = [];
public void CaptureDispatch(DispatchWorkflowDefinitionRequest request)
{
CapturedInputReferences.Add(request.Input);
CapturedInputSnapshots.Add(request.Input != null
? new Dictionary<string, object>(request.Input)
: null);
}
}

View file

@ -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<WorkflowDefinitionDispatching>
{
public Task HandleAsync(WorkflowDefinitionDispatching notification, CancellationToken cancellationToken)
{
spy.CaptureDispatch(notification.Request);
return Task.CompletedTask;
}
}

View file

@ -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<ParentWorkflow>()
.AddWorkflow<ChildWorkflow>()
.ConfigureServices(services =>
{
services.AddSingleton<Spy>();
services.AddNotificationHandler<TestHandler, WorkflowDefinitionDispatching>();
})
.Build();
_spy = _services.GetRequiredService<Spy>();
}
[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<string>("Item"))
.ToList();
Assert.Contains("Apple", items);
Assert.Contains("Banana", items);
Assert.Contains("Cherry", items);
}
}

View file

@ -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<string, object> { ["ExtraData"] = "SharedValue" }),
WaitForCompletion = new(false)
};
}
}
public class ChildWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
builder.Root = new WriteLine("Child executed");
}
}