diff --git a/src/ConductorSharp.Engine/Builders/DoWhileTaskBuilder.cs b/src/ConductorSharp.Engine/Builders/DoWhileTaskBuilder.cs new file mode 100644 index 00000000..4b8c6414 --- /dev/null +++ b/src/ConductorSharp.Engine/Builders/DoWhileTaskBuilder.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using ConductorSharp.Client.Generated; +using ConductorSharp.Engine.Interface; +using ConductorSharp.Engine.Model; +using ConductorSharp.Engine.Util; +using ConductorSharp.Engine.Util.Builders; + +namespace ConductorSharp.Engine.Builders +{ + /// + /// Extension methods to add a DO_WHILE loop with multiple inner tasks, based on BaseTaskBuilder. + /// + public static class DoWhileTaskExtensions + { + /// + /// Adds a DO_WHILE loop to the workflow definition. + /// + /// Workflow type + /// Parent sequence builder + /// Expression selecting the workflow property holding the loop's reference name + /// Expression creating the input + /// Loop condition + /// Delegate to add tasks inside the loop + public static ITaskOptionsBuilder AddTask( + this ITaskSequenceBuilder builder, + Expression> reference, + Expression> input, + string loopCondition, + Action> configureBody + ) + where TWorkflow : ITypedWorkflow + { + // Extract expressions + var taskBuilder = new DoWhileTaskBuilder(reference.Body, input.Body, loopCondition, builder.BuildConfiguration); + // Register it with the outer sequence + builder.AddTaskBuilderToSequence(taskBuilder); + // Allow the caller to configure inner loop body tasks + configureBody(taskBuilder); + return taskBuilder; + } + } + + /// + /// Builder for the DO_WHILE task, extending BaseTaskBuilder for consistent patterns. + /// + internal sealed class DoWhileTaskBuilder : BaseTaskBuilder, ITaskSequenceBuilder + where TWorkflow : ITypedWorkflow + { + private readonly string _loopCondition; + private readonly DoWhileInput _doWhileInput; + private readonly List _innerTasks = new(); + public BuildContext BuildContext { get; } = new(); + public BuildConfiguration BuildConfiguration { get; } + public WorkflowBuildItemRegistry WorkflowBuildRegistry { get; } = new(); + public IEnumerable ConfigurationProperties { get; } = new List(); + + /// + public DoWhileTaskBuilder(Expression taskExpression, Expression inputExpression, string loopCondition, BuildConfiguration buildConfiguration) + : base(taskExpression, inputExpression, buildConfiguration) + { + _loopCondition = loopCondition; + BuildConfiguration = buildConfiguration; + } + + /// + public override WorkflowTask[] Build() + { + var refName = _taskRefferenceName; + + if (_innerTasks.Any(t => t.WorkflowTaskType == WorkflowTaskType.DO_WHILE)) + { + throw new InvalidOperationException("Nested DO_WHILE tasks are not allowed."); + } + + var loopTask = new WorkflowTask + { + Name = refName, + TaskReferenceName = refName, + WorkflowTaskType = WorkflowTaskType.DO_WHILE, + Type = nameof(WorkflowTaskType.DO_WHILE), + InputParameters = _inputParameters.ToObject>(), + LoopCondition = _loopCondition, + LoopOver = _innerTasks, + }; + return [loopTask]; + } + + public void AddTaskBuilderToSequence(ITaskBuilder builder) + { + foreach (var task in builder.Build()) + { + _innerTasks.Add(task); + } + } + } +} diff --git a/src/ConductorSharp.Engine/Model/DoWhileTaskModel.cs b/src/ConductorSharp.Engine/Model/DoWhileTaskModel.cs new file mode 100644 index 00000000..f635ae2b --- /dev/null +++ b/src/ConductorSharp.Engine/Model/DoWhileTaskModel.cs @@ -0,0 +1,18 @@ +using ConductorSharp.Engine.Builders; +using MediatR; + +namespace ConductorSharp.Engine.Model +{ + /// + /// Input for configuration of the DO_WHILE task. + /// + public class DoWhileInput : IRequest, IWorkflowInput + { + public object Value { get; set; } = null; + } + + /// + /// Task Model to reference the do while task in the workflow builders + /// + public class DoWhileTaskModel : TaskModel { } +} diff --git a/test/ConductorSharp.Engine.Tests/ConductorSharp.Engine.Tests.csproj b/test/ConductorSharp.Engine.Tests/ConductorSharp.Engine.Tests.csproj index 182c8412..a67cc2a7 100644 --- a/test/ConductorSharp.Engine.Tests/ConductorSharp.Engine.Tests.csproj +++ b/test/ConductorSharp.Engine.Tests/ConductorSharp.Engine.Tests.csproj @@ -42,6 +42,7 @@ + diff --git a/test/ConductorSharp.Engine.Tests/Integration/WorkflowBuilderTests.cs b/test/ConductorSharp.Engine.Tests/Integration/WorkflowBuilderTests.cs index 3c914816..7c1dcc9d 100644 --- a/test/ConductorSharp.Engine.Tests/Integration/WorkflowBuilderTests.cs +++ b/test/ConductorSharp.Engine.Tests/Integration/WorkflowBuilderTests.cs @@ -166,6 +166,15 @@ public void BuilderReturnsCorrectDefinitionDecisionTask() Assert.Equal(expectedDefinition, definition); } + [Fact] + public void BuilderReturnsCorrectDefinitionDoWhileTask() + { + var definition = GetDefinitionFromWorkflow(); + var expectedDefinition = EmbeddedFileHelper.GetLinesFromEmbeddedFile("~/Samples/Workflows/DoWhileTask.json"); + + Assert.Equal(expectedDefinition, definition); + } + [Fact] public void BuilderReturnsCorrectDefinitionSwitchTask() { diff --git a/test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.cs b/test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.cs new file mode 100644 index 00000000..df1b5e2e --- /dev/null +++ b/test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.cs @@ -0,0 +1,31 @@ +namespace ConductorSharp.Engine.Tests.Samples.Workflows +{ + public sealed class DoWhileTaskInput : WorkflowInput + { + public int Loops { get; set; } + } + + public sealed class DoWhileTaskOutput : WorkflowOutput { } + + public sealed class DoWhileTask : Workflow + { + public DoWhileTaskModel DoWhile { get; set; } + public CustomerGetV1 GetCustomer { get; set; } + + public DoWhileTask(WorkflowDefinitionBuilder builder) + : base(builder) { } + + public override void BuildDefinition() + { + _builder.AddTask( + wf => wf.DoWhile, + wf => new() { Value = wf.Input.Loops }, + "$.do_while.iteration < $.value", + builder => + { + builder.AddTask(wf => wf.GetCustomer, wf => new CustomerGetV1Input() { CustomerId = "CUSTOMER-1" }); + } + ); + } + } +} diff --git a/test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.json b/test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.json new file mode 100644 index 00000000..74bb657a --- /dev/null +++ b/test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.json @@ -0,0 +1,34 @@ +{ + "name": "do_while_task", + "version": 1, + "tasks": [ + { + "name": "do_while", + "taskReferenceName": "do_while", + "inputParameters": { + "value": "${workflow.input.loops}" + }, + "type": "DO_WHILE", + "loopCondition": "$.do_while.iteration < $.value", + "loopOver": [ + { + "name": "CUSTOMER_get", + "taskReferenceName": "get_customer", + "inputParameters": { + "customer_id": "CUSTOMER-1" + }, + "type": "SIMPLE", + "optional": false, + "workflowTaskType": "SIMPLE" + } + ], + "workflowTaskType": "DO_WHILE" + } + ], + "inputParameters": [ + "loops" + ], + "outputParameters": {}, + "schemaVersion": 2, + "timeoutSeconds": 0 +} \ No newline at end of file diff --git a/test/ConductorSharp.Engine.Tests/Util/EmbeddedFileHelper.cs b/test/ConductorSharp.Engine.Tests/Util/EmbeddedFileHelper.cs index 83c8a821..54becee2 100644 --- a/test/ConductorSharp.Engine.Tests/Util/EmbeddedFileHelper.cs +++ b/test/ConductorSharp.Engine.Tests/Util/EmbeddedFileHelper.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json; -using System; +using System; using System.IO; using System.Reflection; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json; namespace ConductorSharp.Engine.Tests.Util {