From b37c11572d9792566b538fc9ec3510511db9facc Mon Sep 17 00:00:00 2001 From: "Roel Bergen, van" Date: Wed, 30 Apr 2025 16:41:45 +0200 Subject: [PATCH 1/2] Added do while task builder --- .../Builders/DoWhileTaskBuilder.cs | 97 +++++++++++++++++++ .../Model/DoWhileTaskModel.cs | 23 +++++ .../ConductorSharp.Engine.Tests.csproj | 1 + .../Integration/WorkflowBuilderTests.cs | 9 ++ .../Samples/Workflows/DoWhileTask.cs | 27 ++++++ .../Samples/Workflows/DoWhileTask.json | 30 ++++++ .../Util/EmbeddedFileHelper.cs | 6 +- 7 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 src/ConductorSharp.Engine/Builders/DoWhileTaskBuilder.cs create mode 100644 src/ConductorSharp.Engine/Model/DoWhileTaskModel.cs create mode 100644 test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.cs create mode 100644 test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.json diff --git a/src/ConductorSharp.Engine/Builders/DoWhileTaskBuilder.cs b/src/ConductorSharp.Engine/Builders/DoWhileTaskBuilder.cs new file mode 100644 index 00000000..ad0b8061 --- /dev/null +++ b/src/ConductorSharp.Engine/Builders/DoWhileTaskBuilder.cs @@ -0,0 +1,97 @@ +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 + /// Delegate to add tasks inside the loop + public static ITaskOptionsBuilder AddTask( + this ITaskSequenceBuilder builder, + Expression> reference, + Expression> input, + Action> configureBody + ) + where TWorkflow : ITypedWorkflow + { + // Extract expressions + var taskBuilder = new DoWhileTaskBuilder(reference.Body, input.Body, 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 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 loopConditionExpression, BuildConfiguration buildConfiguration) + : base(taskExpression, loopConditionExpression, buildConfiguration) + { + BuildConfiguration = buildConfiguration; + // Compile the JS condition string once + _doWhileInput = Expression.Lambda>(loopConditionExpression).Compile().Invoke(); + } + + /// + 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 = new Dictionary(), + LoopCondition = _doWhileInput.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..4fd64a22 --- /dev/null +++ b/src/ConductorSharp.Engine/Model/DoWhileTaskModel.cs @@ -0,0 +1,23 @@ +using ConductorSharp.Engine.Builders; +using MediatR; + +namespace ConductorSharp.Engine.Model +{ + /// + /// Input for configuration of the DO_WHILE task. + /// + public class DoWhileInput : IRequest, IWorkflowInput + { + /// + /// Condition of the loop in JavaScript format. + /// Example: "$.do_while_ref['' + $.do_while_ref.iteration].number_adder_sub_workflow.success == false" + /// To access the latest iteration, use the following syntax: TaskRef.iteration + /// + public string LoopCondition { get; set; } + } + + /// + /// 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..e0cb21ac --- /dev/null +++ b/test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.cs @@ -0,0 +1,27 @@ +namespace ConductorSharp.Engine.Tests.Samples.Workflows +{ + public sealed class DoWhileTaskInput : WorkflowInput { } + + 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() { LoopCondition = "$.do_while.iteration < 3" }, + 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..f480597d --- /dev/null +++ b/test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.json @@ -0,0 +1,30 @@ +{ + "name": "do_while_task", + "version": 1, + "tasks": [ + { + "name": "do_while", + "taskReferenceName": "do_while", + "inputParameters": {}, + "type": "DO_WHILE", + "loopCondition": "$.do_while.iteration < 3", + "loopOver": [ + { + "name": "CUSTOMER_get", + "taskReferenceName": "get_customer", + "inputParameters": { + "customer_id": "CUSTOMER-1" + }, + "type": "SIMPLE", + "optional": false, + "workflowTaskType": "SIMPLE" + } + ], + "workflowTaskType": "DO_WHILE" + } + ], + "inputParameters": [], + "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..20bfcc1a 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 { @@ -43,7 +43,7 @@ public static string GetLinesFromEmbeddedFile(string fileName) var contents = ReadAssemblyFile(typeof(EmbeddedFileHelper).Assembly, fileName); - return contents; + return contents.Replace("\r\n", "\n"); } public static Task GetObjectFromEmbeddedFileAsync(string fileName, params (string Key, object Value)[] templateParams) => From b153f3464045a955e4bc39b2eb3b29bbf5c6415b Mon Sep 17 00:00:00 2001 From: "Roel Bergen, van" Date: Fri, 9 May 2025 10:06:41 +0200 Subject: [PATCH 2/2] - Changed line endings to LF - Updated Do while task to include value input - Moved loop condition from input to parameter in function --- .../Builders/DoWhileTaskBuilder.cs | 16 +++++++++------- .../Model/DoWhileTaskModel.cs | 7 +------ .../Samples/Workflows/DoWhileTask.cs | 8 ++++++-- .../Samples/Workflows/DoWhileTask.json | 10 +++++++--- .../Util/EmbeddedFileHelper.cs | 2 +- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/ConductorSharp.Engine/Builders/DoWhileTaskBuilder.cs b/src/ConductorSharp.Engine/Builders/DoWhileTaskBuilder.cs index ad0b8061..4b8c6414 100644 --- a/src/ConductorSharp.Engine/Builders/DoWhileTaskBuilder.cs +++ b/src/ConductorSharp.Engine/Builders/DoWhileTaskBuilder.cs @@ -22,17 +22,19 @@ public static class DoWhileTaskExtensions /// 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, builder.BuildConfiguration); + 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 @@ -47,6 +49,7 @@ Action> configureBody 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(); @@ -55,12 +58,11 @@ internal sealed class DoWhileTaskBuilder : BaseTaskBuilder ConfigurationProperties { get; } = new List(); /// - public DoWhileTaskBuilder(Expression taskExpression, Expression loopConditionExpression, BuildConfiguration buildConfiguration) - : base(taskExpression, loopConditionExpression, buildConfiguration) + public DoWhileTaskBuilder(Expression taskExpression, Expression inputExpression, string loopCondition, BuildConfiguration buildConfiguration) + : base(taskExpression, inputExpression, buildConfiguration) { + _loopCondition = loopCondition; BuildConfiguration = buildConfiguration; - // Compile the JS condition string once - _doWhileInput = Expression.Lambda>(loopConditionExpression).Compile().Invoke(); } /// @@ -79,8 +81,8 @@ public override WorkflowTask[] Build() TaskReferenceName = refName, WorkflowTaskType = WorkflowTaskType.DO_WHILE, Type = nameof(WorkflowTaskType.DO_WHILE), - InputParameters = new Dictionary(), - LoopCondition = _doWhileInput.LoopCondition, + InputParameters = _inputParameters.ToObject>(), + LoopCondition = _loopCondition, LoopOver = _innerTasks, }; return [loopTask]; diff --git a/src/ConductorSharp.Engine/Model/DoWhileTaskModel.cs b/src/ConductorSharp.Engine/Model/DoWhileTaskModel.cs index 4fd64a22..f635ae2b 100644 --- a/src/ConductorSharp.Engine/Model/DoWhileTaskModel.cs +++ b/src/ConductorSharp.Engine/Model/DoWhileTaskModel.cs @@ -8,12 +8,7 @@ namespace ConductorSharp.Engine.Model /// public class DoWhileInput : IRequest, IWorkflowInput { - /// - /// Condition of the loop in JavaScript format. - /// Example: "$.do_while_ref['' + $.do_while_ref.iteration].number_adder_sub_workflow.success == false" - /// To access the latest iteration, use the following syntax: TaskRef.iteration - /// - public string LoopCondition { get; set; } + public object Value { get; set; } = null; } /// diff --git a/test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.cs b/test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.cs index e0cb21ac..df1b5e2e 100644 --- a/test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.cs +++ b/test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.cs @@ -1,6 +1,9 @@ namespace ConductorSharp.Engine.Tests.Samples.Workflows { - public sealed class DoWhileTaskInput : WorkflowInput { } + public sealed class DoWhileTaskInput : WorkflowInput + { + public int Loops { get; set; } + } public sealed class DoWhileTaskOutput : WorkflowOutput { } @@ -16,7 +19,8 @@ public override void BuildDefinition() { _builder.AddTask( wf => wf.DoWhile, - wf => new() { LoopCondition = "$.do_while.iteration < 3" }, + 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 index f480597d..74bb657a 100644 --- a/test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.json +++ b/test/ConductorSharp.Engine.Tests/Samples/Workflows/DoWhileTask.json @@ -5,9 +5,11 @@ { "name": "do_while", "taskReferenceName": "do_while", - "inputParameters": {}, + "inputParameters": { + "value": "${workflow.input.loops}" + }, "type": "DO_WHILE", - "loopCondition": "$.do_while.iteration < 3", + "loopCondition": "$.do_while.iteration < $.value", "loopOver": [ { "name": "CUSTOMER_get", @@ -23,7 +25,9 @@ "workflowTaskType": "DO_WHILE" } ], - "inputParameters": [], + "inputParameters": [ + "loops" + ], "outputParameters": {}, "schemaVersion": 2, "timeoutSeconds": 0 diff --git a/test/ConductorSharp.Engine.Tests/Util/EmbeddedFileHelper.cs b/test/ConductorSharp.Engine.Tests/Util/EmbeddedFileHelper.cs index 20bfcc1a..54becee2 100644 --- a/test/ConductorSharp.Engine.Tests/Util/EmbeddedFileHelper.cs +++ b/test/ConductorSharp.Engine.Tests/Util/EmbeddedFileHelper.cs @@ -43,7 +43,7 @@ public static string GetLinesFromEmbeddedFile(string fileName) var contents = ReadAssemblyFile(typeof(EmbeddedFileHelper).Assembly, fileName); - return contents.Replace("\r\n", "\n"); + return contents; } public static Task GetObjectFromEmbeddedFileAsync(string fileName, params (string Key, object Value)[] templateParams) =>