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:
parent
a459904e1f
commit
8d7d1a9862
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue