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