From 52516aa0ff0367232f15ccd11efb9ae458794ea6 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 23 Oct 2024 16:06:28 +0200 Subject: [PATCH] Fix Bulk Dispatch Missing Input (#6052) * Remove outdated sample workflows and activities Deleted CompositeExample, SampleWorkflow, and SlowActivity classes as they are no longer needed. These deletions help clean up the codebase and maintain relevance in the code structure. * Handle null WorkflowBuilderType in ClrWorkflowMaterializer Introduce a fallback NotFoundWorkflowbuilder class when WorkflowBuilderType is null. This ensures the MaterializeAsync method functions even if the provided context lacks a specific workflow builder type. * Add merging of itemDictionary to input This change ensures that the itemDictionary's contents are also merged into the input dictionary, preventing possible data loss. It builds on existing logic by adding an additional merge operation to incorporate all necessary data. * Add new workflows to BulkDispatchWorkflows and modify tests Introduce `FruitWorkflow` and `MixFruitsWorkflow` for bulk dispatch scenarios. Refactor `BulkDispatchWorkflowsTests` to include new tests and rename signals for clarity. Optimize `BulkDispatchWorkflows` activity by removing redundant dictionary merge. --- .../Activities/CompositeExample.cs | 40 -------------- .../Activities/SlowActivity.cs | 37 ------------- src/bundles/Elsa.Server.Web/SampleWorkflow.cs | 41 -------------- .../Materializers/ClrWorkflowMaterializer.cs | 11 +++- .../Activities/BulkDispatchWorkflows.cs | 2 +- ...orkflows.ComponentTests.csproj.DotSettings | 1 + .../Helpers/Activities/SignalResetEvent.cs | 17 ------ .../Helpers/Activities/TriggerSignal.cs | 55 +++++++++++++++++++ .../Helpers/Services/SignalManager.cs | 7 ++- .../BulkDispatchWorkflowsTests.cs | 18 +++++- .../Workflows/FruitWorkflow.cs | 27 +++++++++ .../Workflows/MixFruitsWorkflow.cs | 31 +++++++++++ 12 files changed, 145 insertions(+), 142 deletions(-) delete mode 100644 src/bundles/Elsa.Server.Web/Activities/CompositeExample.cs delete mode 100644 src/bundles/Elsa.Server.Web/Activities/SlowActivity.cs delete mode 100644 src/bundles/Elsa.Server.Web/SampleWorkflow.cs delete mode 100644 test/component/Elsa.Workflows.ComponentTests/Helpers/Activities/SignalResetEvent.cs create mode 100644 test/component/Elsa.Workflows.ComponentTests/Helpers/Activities/TriggerSignal.cs create mode 100644 test/component/Elsa.Workflows.ComponentTests/Scenarios/BulkDispatchWorkflows/Workflows/FruitWorkflow.cs create mode 100644 test/component/Elsa.Workflows.ComponentTests/Scenarios/BulkDispatchWorkflows/Workflows/MixFruitsWorkflow.cs diff --git a/src/bundles/Elsa.Server.Web/Activities/CompositeExample.cs b/src/bundles/Elsa.Server.Web/Activities/CompositeExample.cs deleted file mode 100644 index 7eb7a335c..000000000 --- a/src/bundles/Elsa.Server.Web/Activities/CompositeExample.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Elsa.AzureServiceBus.Activities; -using Elsa.Extensions; -using Elsa.Workflows.Activities; -using Elsa.Workflows.Attributes; -using Elsa.Workflows.Models; - -namespace Elsa.Server.Web.Activities; - -[Activity("Elsa", "Example")] -public class CompositeExample : Composite -{ - /// - /// The name of the queue or topic to read from. - /// - [Input(Description = "The name of the queue or topic to read from.")] - public Input QueueOrTopic { get; set; } = default!; - - private MessageReceived _messageReceived = default!; - - /// - public override void Setup() - { - _messageReceived = new MessageReceived - { - QueueOrTopic = QueueOrTopic, - CanStartWorkflow = true - }; - - var writeLine = new WriteLine("Hello World!"); - - Root = new Sequence - { - Activities = - { - _messageReceived, - writeLine - } - }; - } -} \ No newline at end of file diff --git a/src/bundles/Elsa.Server.Web/Activities/SlowActivity.cs b/src/bundles/Elsa.Server.Web/Activities/SlowActivity.cs deleted file mode 100644 index 5d17d421c..000000000 --- a/src/bundles/Elsa.Server.Web/Activities/SlowActivity.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Reflection; -using Elsa.Extensions; -using Elsa.Workflows; -using Elsa.Workflows.Attributes; -using Elsa.Workflows.Contracts; -using Elsa.Workflows.Models; - -namespace Elsa.Server.Web.Activities; - -/// -/// Simulate an activity that takes a long time to complete. -/// -[Activity("Testing", "Testing", "Simulate an activity that takes a long time to complete.")] -public class SlowActivity : CodeActivity, IActivityPropertyDefaultValueProvider -{ - /// - /// The delay. - /// - [Input(Description = "The delay.", DefaultValueProvider = typeof(SlowActivity))] - public Input Delay { get; set; } = new(TimeSpan.FromSeconds(1)); - - /// - protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) - { - var delay = Delay.Get(context); - await Task.Delay(delay, context.CancellationToken); - } - - object IActivityPropertyDefaultValueProvider.GetDefaultValue(PropertyInfo property) - { - return property.Name switch - { - nameof(Delay) => TimeSpan.FromSeconds(1), - _ => default - }; - } -} \ No newline at end of file diff --git a/src/bundles/Elsa.Server.Web/SampleWorkflow.cs b/src/bundles/Elsa.Server.Web/SampleWorkflow.cs deleted file mode 100644 index 44c8a15ca..000000000 --- a/src/bundles/Elsa.Server.Web/SampleWorkflow.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Elsa.Expressions.Models; -using Elsa.Extensions; -using Elsa.Scheduling.Activities; -using Elsa.Workflows; -using Elsa.Workflows.Activities; -using Elsa.Workflows.Contracts; - -namespace Elsa.Server.Web; - -public class SampleWorkflow : WorkflowBase -{ - protected override void Build(IWorkflowBuilder workflow) - { - // The WithVariable method ensures that the created variable will be added to the Workflow's Variables collection, which is required for persistent variables. - var variable1 = workflow.WithVariable("Foo").WithWorkflowStorage(); - - workflow.Variables = - [ - variable1 - ]; - - workflow.Root = new Sequence - { - Activities = - { - new StartAt(DateTimeOffset.UtcNow + TimeSpan.FromSeconds(5)) - { - CanStartWorkflow = true - }, - new WriteLine(variable1), - new SetVariable - { - Variable = variable1, - Value = new (Literal.From("Bar")) - }, - new Delay(TimeSpan.FromSeconds(1)), - new WriteLine(variable1) - } - }; - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Materializers/ClrWorkflowMaterializer.cs b/src/modules/Elsa.Workflows.Management/Materializers/ClrWorkflowMaterializer.cs index 68af6efc8..efad71767 100644 --- a/src/modules/Elsa.Workflows.Management/Materializers/ClrWorkflowMaterializer.cs +++ b/src/modules/Elsa.Workflows.Management/Materializers/ClrWorkflowMaterializer.cs @@ -42,7 +42,7 @@ public class ClrWorkflowMaterializer : IWorkflowMaterializer public async ValueTask MaterializeAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default) { var providerContext = _payloadSerializer.Deserialize(definition.MaterializerContext!); - var workflowBuilderType = providerContext.WorkflowBuilderType; + var workflowBuilderType = providerContext.WorkflowBuilderType == null! ? typeof(NotFoundWorkflowbuilder) : providerContext.WorkflowBuilderType; var workflowBuilder = (IWorkflow)ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, workflowBuilderType); var workflowDefinitionBuilder = _workflowBuilderFactory.CreateBuilder(); var workflow = await workflowDefinitionBuilder.BuildWorkflowAsync(workflowBuilder, cancellationToken); @@ -58,4 +58,11 @@ public class ClrWorkflowMaterializer : IWorkflowMaterializer /// Provides context for the CLR workflow materializer. /// /// The type of the workflow builder. -public record ClrWorkflowMaterializerContext(Type WorkflowBuilderType); \ No newline at end of file +public record ClrWorkflowMaterializerContext(Type WorkflowBuilderType); + +/// +/// A workflow builder that is used when the workflow builder type is not found. +/// +public class NotFoundWorkflowbuilder : WorkflowBase +{ +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs index 06eb5306a..19ae2ce1d 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs @@ -195,7 +195,7 @@ public class BulkDispatchWorkflows : Activity Arguments = itemDictionary }; - var inputDictionary = item as IDictionary ?? new Dictionary(); + var inputDictionary = item as IDictionary ?? itemDictionary; input["ParentInstanceId"] = parentInstanceId; input.Merge(inputDictionary); diff --git a/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj.DotSettings b/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj.DotSettings index 883416dd1..9fa3ae6cc 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj.DotSettings +++ b/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj.DotSettings @@ -1,4 +1,5 @@  + True True True \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Activities/SignalResetEvent.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Activities/SignalResetEvent.cs deleted file mode 100644 index be4fce31e..000000000 --- a/test/component/Elsa.Workflows.ComponentTests/Helpers/Activities/SignalResetEvent.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Elsa.Workflows.ComponentTests.Activities; - -public class SignalResetEvent : CodeActivity -{ - public SignalResetEvent(string eventName) - { - EventName = eventName; - } - - public string EventName { get; set; } - - protected override void Execute(ActivityExecutionContext context) - { - var testEventManager = context.GetRequiredService(); - testEventManager.Trigger(EventName); - } -} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Activities/TriggerSignal.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Activities/TriggerSignal.cs new file mode 100644 index 000000000..dc3f69cf4 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/Activities/TriggerSignal.cs @@ -0,0 +1,55 @@ +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; +using Elsa.Expressions.Models; +using Elsa.Extensions; +using Elsa.Workflows.Memory; +using Elsa.Workflows.Models; + +namespace Elsa.Workflows.ComponentTests.Activities; + +public class TriggerSignal : CodeActivity +{ + /// + [JsonConstructor] + private TriggerSignal(string? source = default, int? line = default) : base(source, line) + { + } + + /// + public TriggerSignal(string eventName, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(new Literal(eventName), source, line) + { + } + + /// + public TriggerSignal(Func eventName, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(Expression.DelegateExpression(eventName), source, line) + { + } + + /// + public TriggerSignal(Func eventName, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(Expression.DelegateExpression(eventName), source, line) + { + } + + /// + public TriggerSignal(Variable variable, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => EventName = new Input(variable); + + /// + public TriggerSignal(Literal literal, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => EventName = new Input(literal); + + /// + public TriggerSignal(Expression expression, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => EventName = new Input(expression, new MemoryBlockReference()); + + /// + public TriggerSignal(Input eventName, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => EventName = eventName; + + public Input EventName { get; set; } + + protected override void Execute(ActivityExecutionContext context) + { + var testEventManager = context.GetRequiredService(); + var eventName = EventName.Get(context); + testEventManager.Trigger(eventName); + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Services/SignalManager.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Services/SignalManager.cs index 0bacab794..68ca6bb75 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Helpers/Services/SignalManager.cs +++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/Services/SignalManager.cs @@ -8,7 +8,12 @@ public class SignalManager : ISignalManager public async Task WaitAsync(object signal, int millisecondsTimeout = 5000) { - return await WaitAsync(signal, millisecondsTimeout) is T result ? result : throw new InvalidCastException($"Signal '{signal}' was not of type '{typeof(T).Name}'."); + var result = await WaitAsync(signal, millisecondsTimeout); + + if(result is not T typedResult) + throw new InvalidCastException($"Signal '{signal}' was not of type '{typeof(T).Name}'."); + + return typedResult; } public async Task WaitAsync(object signal, int millisecondsTimeout = 5000) diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/BulkDispatchWorkflows/BulkDispatchWorkflowsTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/BulkDispatchWorkflows/BulkDispatchWorkflowsTests.cs index 30e19e86b..00e3c9093 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Scenarios/BulkDispatchWorkflows/BulkDispatchWorkflowsTests.cs +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/BulkDispatchWorkflows/BulkDispatchWorkflowsTests.cs @@ -9,7 +9,7 @@ public class BulkDispatchWorkflowsTests : AppComponentTest private readonly IWorkflowEvents _workflowEvents; private readonly ISignalManager _signalManager; private readonly IWorkflowRuntime _workflowRuntime; - private static readonly object ParentWorkflowCompletedSignal = new(); + private static readonly object GreetEmployeesWorkflowCompletedSignal = new(); public BulkDispatchWorkflowsTests(App app) : base(app) { @@ -26,10 +26,22 @@ public class BulkDispatchWorkflowsTests : AppComponentTest public async Task DispatchAndWaitWorkflow_ShouldWaitForChildWorkflowToComplete() { await _workflowRuntime.StartWorkflowAsync(GreetEmployeesWorkflow.DefinitionId); - var parentWorkflowInstanceArgs = await _signalManager.WaitAsync(ParentWorkflowCompletedSignal); + var parentWorkflowInstanceArgs = await _signalManager.WaitAsync(GreetEmployeesWorkflowCompletedSignal); Assert.Equal(WorkflowStatus.Finished, parentWorkflowInstanceArgs.WorkflowInstance.Status); } + + /// + /// Individual items are sent as input to child workflows. + /// + [Fact] + public async Task DispatchWorkflows_ChildWorkflowsShouldReceiveCurrentItem() + { + await _workflowRuntime.StartWorkflowAsync(MixFruitsWorkflow.DefinitionId); + await _signalManager.WaitAsync("Apple"); + await _signalManager.WaitAsync("Banana"); + await _signalManager.WaitAsync("Cherry"); + } private void OnWorkflowInstanceSaved(object? sender, WorkflowInstanceSavedEventArgs e) { @@ -37,7 +49,7 @@ public class BulkDispatchWorkflowsTests : AppComponentTest return; if(e.WorkflowInstance.DefinitionId == GreetEmployeesWorkflow.DefinitionId) - _signalManager.Trigger(ParentWorkflowCompletedSignal, e); + _signalManager.Trigger(GreetEmployeesWorkflowCompletedSignal, e); } protected override void OnDispose() diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/BulkDispatchWorkflows/Workflows/FruitWorkflow.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/BulkDispatchWorkflows/Workflows/FruitWorkflow.cs new file mode 100644 index 000000000..58f644d81 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/BulkDispatchWorkflows/Workflows/FruitWorkflow.cs @@ -0,0 +1,27 @@ +using Elsa.Extensions; +using Elsa.Workflows.Activities; +using Elsa.Workflows.ComponentTests.Activities; +using Elsa.Workflows.Contracts; +using Hangfire.Annotations; + +namespace Elsa.Workflows.ComponentTests.Scenarios.BulkDispatchWorkflows.Workflows; + +[UsedImplicitly] +public class FruitWorkflow : WorkflowBase +{ + public static readonly string DefinitionId = Guid.NewGuid().ToString(); + + protected override void Build(IWorkflowBuilder builder) + { + builder.WithDefinitionId(DefinitionId); + var item = builder.WithInput("Item"); + builder.Root = new Sequence + { + Activities = + { + new WriteLine(x => $"Mixing {x.GetInput(item)}"), + new TriggerSignal(x => x.GetInput(item)) + } + }; + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/BulkDispatchWorkflows/Workflows/MixFruitsWorkflow.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/BulkDispatchWorkflows/Workflows/MixFruitsWorkflow.cs new file mode 100644 index 000000000..a6fcc8151 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/BulkDispatchWorkflows/Workflows/MixFruitsWorkflow.cs @@ -0,0 +1,31 @@ +using Elsa.Workflows.Activities; +using Elsa.Workflows.Contracts; + +namespace Elsa.Workflows.ComponentTests.Scenarios.BulkDispatchWorkflows.Workflows; + +public class MixFruitsWorkflow : WorkflowBase +{ + public static readonly string DefinitionId = Guid.NewGuid().ToString(); + + protected override void Build(IWorkflowBuilder builder) + { + var fruits = new[] + { + "Apple", "Banana", "Cherry" + }; + + builder.WithDefinitionId(DefinitionId); + builder.Root = new Sequence + { + Activities = + { + new Runtime.Activities.BulkDispatchWorkflows + { + WorkflowDefinitionId = new(FruitWorkflow.DefinitionId), + Items = new(fruits), + WaitForCompletion = new(true) + } + } + }; + } +} \ No newline at end of file