Skip to content

Commit effdd02

Browse files
authored
Decision Branching (#486)
1 parent 580cd44 commit effdd02

File tree

26 files changed

+580
-89
lines changed

26 files changed

+580
-89
lines changed

ReleaseNotes/3.1.0.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Workflow Core 3.1.0
2+
3+
## Decision Branching
4+
5+
You can define multiple independent branches within your workflow and select one based on an expression value.
6+
7+
For the fluent API, we define our branches with the `CreateBranch()` method on the workflow builder. We can then select a branch using the `Decision` step.
8+
9+
This workflow will select `branch1` if the value of `data.Value1` is `one`, and `branch2` if it is `two`.
10+
```c#
11+
var branch1 = builder.CreateBranch()
12+
.StartWith<PrintMessage>()
13+
.Input(step => step.Message, data => "hi from 1")
14+
.Then<PrintMessage>()
15+
.Input(step => step.Message, data => "bye from 1");
16+
17+
var branch2 = builder.CreateBranch()
18+
.StartWith<PrintMessage>()
19+
.Input(step => step.Message, data => "hi from 2")
20+
.Then<PrintMessage>()
21+
.Input(step => step.Message, data => "bye from 2");
22+
23+
24+
builder
25+
.StartWith<HelloWorld>()
26+
.Decide(data => data.Value1)
27+
.Branch("one", branch1)
28+
.Branch("two", branch2);
29+
```
30+
31+
The JSON representation would look somthing like this.
32+
33+
```json
34+
{
35+
"Id": "DecisionWorkflow",
36+
"Version": 1,
37+
"DataType": "MyApp.MyData, MyApp",
38+
"Steps": [
39+
{
40+
"Id": "decide",
41+
"StepType": "WorkflowCore.Primitives.Decide, WorkflowCore",
42+
"Inputs":
43+
{
44+
"Expression": "data.Value1"
45+
},
46+
"OutcomeSteps":
47+
{
48+
"Print1": "\"one\"",
49+
"Print2": "\"two\""
50+
}
51+
},
52+
{
53+
"Id": "Print1",
54+
"StepType": "MyApp.PrintMessage, MyApp",
55+
"Inputs":
56+
{
57+
"Message": "\"Hello from 1\""
58+
}
59+
},
60+
{
61+
"Id": "Print2",
62+
"StepType": "MyApp.PrintMessage, MyApp",
63+
"Inputs":
64+
{
65+
"Message": "\"Hello from 2\""
66+
}
67+
}
68+
]
69+
}
70+
```
71+
72+
73+
## Outcomes for JSON workflows
74+
75+
You can now specify `OutcomeSteps` for a step in JSON and YAML workflow definitions.
76+
77+
```
78+
"OutcomeSteps":
79+
{
80+
"<<Step1 Id>>": "<<expression>>",
81+
"<<Step2 Id>>": "<<expression>>"
82+
}
83+
```
84+
If the outcome of a step matches a particular expression, that step would be scheduled as the next step to execute.

WorkflowCore.sln

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ReleaseNotes", "ReleaseNote
101101
ReleaseNotes\2.0.0.md = ReleaseNotes\2.0.0.md
102102
ReleaseNotes\2.1.0.md = ReleaseNotes\2.1.0.md
103103
ReleaseNotes\2.1.2.md = ReleaseNotes\2.1.2.md
104+
ReleaseNotes\3.0.0.md = ReleaseNotes\3.0.0.md
105+
ReleaseNotes\3.1.0.md = ReleaseNotes\3.1.0.md
104106
EndProjectSection
105107
EndProject
106108
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample14", "src\samples\WorkflowCore.Sample14\WorkflowCore.Sample14.csproj", "{6BC66637-B42A-4334-ADFB-DBEC9F29D293}"
@@ -135,9 +137,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Tests.Elastics
135137
EndProject
136138
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Tests.Redis", "test\WorkflowCore.Tests.Redis\WorkflowCore.Tests.Redis.csproj", "{78217204-B873-40B9-8875-E3925B2FBCEC}"
137139
EndProject
138-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowCore.DSL", "src\WorkflowCore.DSL\WorkflowCore.DSL.csproj", "{20B98905-08CB-4854-8E2C-A31A078383E9}"
140+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.DSL", "src\WorkflowCore.DSL\WorkflowCore.DSL.csproj", "{20B98905-08CB-4854-8E2C-A31A078383E9}"
139141
EndProject
140-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowCore.Sample18", "src\samples\WorkflowCore.Sample18\WorkflowCore.Sample18.csproj", "{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}"
142+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample18", "src\samples\WorkflowCore.Sample18\WorkflowCore.Sample18.csproj", "{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}"
143+
EndProject
144+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScratchPad", "test\ScratchPad\ScratchPad.csproj", "{FD7B9F06-9970-4C30-A2C0-FD7412FF620B}"
141145
EndProject
142146
Global
143147
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -341,6 +345,10 @@ Global
341345
{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Debug|Any CPU.Build.0 = Debug|Any CPU
342346
{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|Any CPU.ActiveCfg = Release|Any CPU
343347
{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|Any CPU.Build.0 = Release|Any CPU
348+
{FD7B9F06-9970-4C30-A2C0-FD7412FF620B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
349+
{FD7B9F06-9970-4C30-A2C0-FD7412FF620B}.Debug|Any CPU.Build.0 = Debug|Any CPU
350+
{FD7B9F06-9970-4C30-A2C0-FD7412FF620B}.Release|Any CPU.ActiveCfg = Release|Any CPU
351+
{FD7B9F06-9970-4C30-A2C0-FD7412FF620B}.Release|Any CPU.Build.0 = Release|Any CPU
344352
EndGlobalSection
345353
GlobalSection(SolutionProperties) = preSolution
346354
HideSolutionNode = FALSE
@@ -398,6 +406,7 @@ Global
398406
{78217204-B873-40B9-8875-E3925B2FBCEC} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB}
399407
{20B98905-08CB-4854-8E2C-A31A078383E9} = {EF47161E-E399-451C-BDE8-E92AAD3BD761}
400408
{5BE6D628-B9DB-4C76-AAEB-8F3800509A84} = {5080DB09-CBE8-4C45-9957-C3BB7651755E}
409+
{FD7B9F06-9970-4C30-A2C0-FD7412FF620B} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB}
401410
EndGlobalSection
402411
GlobalSection(ExtensibilityGlobals) = postSolution
403412
SolutionGuid = {DC0FA8D3-6449-4FDA-BB46-ECF58FAD23B4}

docs/control-structures.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,34 @@ builder
131131
)
132132
.Then(context => Console.WriteLine("Carry on"));
133133
```
134+
135+
### Decision Branches
136+
137+
You can define multiple independent branches within your workflow and select one based on an expression value.
138+
139+
For the fluent API, we define our branches with the `CreateBranch()` method on the workflow builder. We can then select a branch using the `Decide` step.
140+
141+
Use the `Decide` primitive step and hook up your branches via the `Branch` method. The result of the input expression will be matched to the expressions listed via the `Branch` method, and the matching next step(s) will be scheduled to execute next.
142+
143+
144+
This workflow will select `branch1` if the value of `data.Value1` is `one`, and `branch2` if it is `two`.
145+
```c#
146+
var branch1 = builder.CreateBranch()
147+
.StartWith<PrintMessage>()
148+
.Input(step => step.Message, data => "hi from 1")
149+
.Then<PrintMessage>()
150+
.Input(step => step.Message, data => "bye from 1");
151+
152+
var branch2 = builder.CreateBranch()
153+
.StartWith<PrintMessage>()
154+
.Input(step => step.Message, data => "hi from 2")
155+
.Then<PrintMessage>()
156+
.Input(step => step.Message, data => "bye from 2");
157+
158+
159+
builder
160+
.StartWith<HelloWorld>()
161+
.Decide(data => data.Value1)
162+
.Branch("one", branch1)
163+
.Branch("two", branch2);
164+
```

docs/json-yaml.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,3 +480,69 @@ Do:
480480
- Id: do2
481481
StepType: MyApp.DoSomething2, MyApp
482482
```
483+
484+
### Decision Branches
485+
486+
You can define multiple independent branches within your workflow and select one based on an expression value.
487+
Use the `Decide` primitive step and hook up your branches via the `OutcomeSteps` property. The result of the input expression will be matched to the expressions listed in `OutcomeSteps`, and the matching next step(s) will be scheduled to execute next.
488+
489+
```json
490+
{
491+
"Id": "DecisionWorkflow",
492+
"Version": 1,
493+
"DataType": "MyApp.MyData, MyApp",
494+
"Steps": [
495+
{
496+
"Id": "decide",
497+
"StepType": "WorkflowCore.Primitives.Decide, WorkflowCore",
498+
"Inputs":
499+
{
500+
"Expression": "<<input expression to evaluate>>"
501+
},
502+
"OutcomeSteps":
503+
{
504+
"Branch1": "<<result expression to match for branch 1>>",
505+
"Branch2": "<<result expression to match for branch 2>>"
506+
}
507+
},
508+
{
509+
"Id": "Branch1",
510+
"StepType": "MyApp.PrintMessage, MyApp",
511+
"Inputs":
512+
{
513+
"Message": "\"Hello from 1\""
514+
}
515+
},
516+
{
517+
"Id": "Branch2",
518+
"StepType": "MyApp.PrintMessage, MyApp",
519+
"Inputs":
520+
{
521+
"Message": "\"Hello from 2\""
522+
}
523+
}
524+
]
525+
}
526+
```
527+
528+
```yaml
529+
Id: DecisionWorkflow
530+
Version: 1
531+
DataType: MyApp.MyData, MyApp
532+
Steps:
533+
- Id: decide
534+
StepType: WorkflowCore.Primitives.Decide, WorkflowCore
535+
Inputs:
536+
Expression: <<input expression to evaluate>>
537+
OutcomeSteps:
538+
Branch1: '<<result expression to match for branch 1>>'
539+
Branch2: '<<result expression to match for branch 2>>'
540+
- Id: Branch1
541+
StepType: MyApp.PrintMessage, MyApp
542+
Inputs:
543+
Message: '"Hello from 1"'
544+
- Id: Branch2
545+
StepType: MyApp.PrintMessage, MyApp
546+
Inputs:
547+
Message: '"Hello from 2"'
548+
```

src/WorkflowCore.DSL/Models/v1/StepSourceV1.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ public class StepSourceV1
3030
public ExpandoObject Inputs { get; set; } = new ExpandoObject();
3131

3232
public Dictionary<string, string> Outputs { get; set; } = new Dictionary<string, string>();
33-
3433

34+
public Dictionary<string, string> OutcomeSteps { get; set; } = new Dictionary<string, string>();
35+
3536
}
3637
}

src/WorkflowCore.DSL/Services/DefinitionLoader.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ private WorkflowStepCollection ConvertSteps(ICollection<StepSourceV1> source, Ty
9393

9494
AttachInputs(nextStep, dataType, stepType, targetStep);
9595
AttachOutputs(nextStep, dataType, stepType, targetStep);
96-
96+
9797
if (nextStep.Do != null)
9898
{
9999
foreach (var branch in nextStep.Do)
@@ -115,8 +115,7 @@ private WorkflowStepCollection ConvertSteps(ICollection<StepSourceV1> source, Ty
115115
compensatables.Add(nextStep);
116116
}
117117

118-
if (!string.IsNullOrEmpty(nextStep.NextStepId))
119-
targetStep.Outcomes.Add(new StepOutcome() { ExternalNextStepId = $"{nextStep.NextStepId}" });
118+
AttachOutcomes(nextStep, dataType, targetStep);
120119

121120
result.Add(targetStep);
122121

@@ -234,6 +233,23 @@ private void AttachOutputs(StepSourceV1 source, Type dataType, Type stepType, Wo
234233
}
235234
}
236235

236+
private void AttachOutcomes(StepSourceV1 source, Type dataType, WorkflowStep step)
237+
{
238+
if (!string.IsNullOrEmpty(source.NextStepId))
239+
step.Outcomes.Add(new StepOutcome() { ExternalNextStepId = $"{source.NextStepId}" });
240+
241+
foreach (var nextStep in source.OutcomeSteps)
242+
{
243+
var dataParameter = Expression.Parameter(dataType, "data");
244+
var sourceExpr = DynamicExpressionParser.ParseLambda(new[] { dataParameter }, typeof(object), nextStep.Value);
245+
step.Outcomes.Add(new StepOutcome()
246+
{
247+
Value = sourceExpr,
248+
ExternalNextStepId = $"{nextStep.Key}"
249+
});
250+
}
251+
}
252+
237253
private Type FindType(string name)
238254
{
239255
return Type.GetType(name, true, true);

src/WorkflowCore.DSL/WorkflowCore.DSL.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>
5-
<Version>3.0.0</Version>
5+
<Version>3.1.0</Version>
66
<Description>DSL extenstion for Workflow Core provding support for JSON and YAML workflow definitions.</Description>
77
<Authors>Daniel Gerlag</Authors>
88
<Company />

src/WorkflowCore/Interface/IStepBuilder.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ public interface IStepBuilder<TData, TStepBody>
7373
[Obsolete]
7474
IStepOutcomeBuilder<TData> When(object outcomeValue, string label = null);
7575

76+
/// <summary>
77+
/// Configure an outcome branch for this step, then wire it to another step
78+
/// </summary>
79+
/// <param name="outcomeValue"></param>
80+
/// <returns></returns>
81+
IStepBuilder<TData, TStepBody> Branch<TStep>(object outcomeValue, IStepBuilder<TData, TStep> branch) where TStep : IStepBody;
82+
7683
/// <summary>
7784
/// Map properties on the step to properties on the workflow data object before the step executes
7885
/// </summary>
@@ -158,6 +165,13 @@ public interface IStepBuilder<TData, TStepBody>
158165
/// <returns></returns>
159166
IStepBuilder<TData, Delay> Delay(Expression<Func<TData, TimeSpan>> period);
160167

168+
/// <summary>
169+
/// Evaluate an expression and take a different path depending on the value
170+
/// </summary>
171+
/// <param name="expression">Expression to evaluate for decision</param>
172+
/// <returns></returns>
173+
IStepBuilder<TData, Decide> Decide(Expression<Func<TData, object>> expression);
174+
161175
/// <summary>
162176
/// Execute a block of steps, once for each item in a collection in a parallel foreach
163177
/// </summary>

src/WorkflowCore/Interface/IWorkflowBuilder.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ namespace WorkflowCore.Interface
77
{
88
public interface IWorkflowBuilder
99
{
10+
List<WorkflowStep> Steps { get; }
11+
1012
int LastStep { get; }
1113

1214
IWorkflowBuilder<T> UseData<T>();
1315

1416
WorkflowDefinition Build(string id, int version);
1517

1618
void AddStep(WorkflowStep step);
19+
20+
void AttachBranch(IWorkflowBuilder branch);
1721
}
1822

1923
public interface IWorkflowBuilder<TData> : IWorkflowBuilder
@@ -27,5 +31,8 @@ public interface IWorkflowBuilder<TData> : IWorkflowBuilder
2731
IEnumerable<WorkflowStep> GetUpstreamSteps(int id);
2832

2933
IWorkflowBuilder<TData> UseDefaultErrorBehavior(WorkflowErrorHandling behavior, TimeSpan? retryInterval = null);
34+
35+
IWorkflowBuilder<TData> CreateBranch();
36+
3037
}
3138
}

src/WorkflowCore/Models/StepOutcome.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ namespace WorkflowCore.Models
55
{
66
public class StepOutcome
77
{
8-
private Expression<Func<object, object>> _value;
8+
private LambdaExpression _value;
99

10-
public Expression<Func<object, object>> Value
10+
public LambdaExpression Value
1111
{
1212
set { _value = value; }
1313
}
@@ -23,7 +23,7 @@ public object GetValue(object data)
2323
if (_value == null)
2424
return null;
2525

26-
return _value.Compile().Invoke(data);
26+
return _value.Compile().DynamicInvoke(data);
2727
}
2828
}
2929
}

0 commit comments

Comments
 (0)