workflow-core icon indicating copy to clipboard operation
workflow-core copied to clipboard

How to implement Switch with a default branch

Open tocsi-hun opened this issue 4 years ago • 6 comments

Hello @danielgerlag I have had very hard times to implement switch and if-then-else. I've read that you advised to use the obsoleted 'When' step (#312). But I see there is a fairly new 'Decide' implementation.

With the 'Decide' I still have difficulties to implement the aforementioned control flow constructs. The if-then-else is possible but in a weird way - had to have a branch with the 'true' expression and another branch with the negated 'true' expression. Acceptable but resource wasting, since it has to evaluate the expression twice in the 'false' case.

Because of the same reason I just cannot implement the switch, since I do not know how to have a branch which executes if all the others cannot. To create an expression with all the previous expressions negated is not acceptable. Also, it executes all the branches with 'true' expression. Which is never the case with the traditional switch neither with an if-then-elseif-then-else scenario.

So I came up empty. Maybe I completely misunderstood the concept. Could you point me out how these two control flow construct could be implemented?

tocsi-hun avatar Mar 03 '20 20:03 tocsi-hun

I'd like to build a control flow like this: diagram-thegoal

The equivalent plain C# implementation could be somethig like this

int number = 11;

System.Console.WriteLine($"started with value {number} (thread: {Thread.CurrentThread.ManagedThreadId})");

if(number > 24)
{
    System.Console.WriteLine($"greater than 24 (thread: {Thread.CurrentThread.ManagedThreadId})");
    System.Console.WriteLine($"greater than 24 second step  (thread: {Thread.CurrentThread.ManagedThreadId})");
}
else if (number < 12)
{
    System.Console.WriteLine($"less than 12 (thread: {Thread.CurrentThread.ManagedThreadId})");
    System.Console.WriteLine($"less than 12 second step (thread: {Thread.CurrentThread.ManagedThreadId})");
} 
else if (number < 21)
{
    System.Console.WriteLine($"between 12 and 21 (thread: {Thread.CurrentThread.ManagedThreadId})");
    System.Console.WriteLine($"between 12 and 21 second step (thread: {Thread.CurrentThread.ManagedThreadId})");
}
else
{
    System.Console.WriteLine($"none of the above (thread: {Thread.CurrentThread.ManagedThreadId})");
    System.Console.WriteLine($"none of the above second step (thread: {Thread.CurrentThread.ManagedThreadId})");
}

System.Console.WriteLine($"finished (thread: {Thread.CurrentThread.ManagedThreadId})");
System.Console.WriteLine($"really finished (thread: {Thread.CurrentThread.ManagedThreadId})")

The way I tried to implement it with Workflow-Core

public class DummyData
{
    public bool Flag { get; set; }
    public string Text { get; set; }
    public int Number { get; set; }
}

public class DecideExpressionWorkflow : IWorkflow<DummyData>
{
    public string Id => nameof(DecideExpressionWorkflow);

    public int Version => 1;

    public void Build(IWorkflowBuilder<DummyData> builder)
    {

        var greaterBranch = builder.CreateBranch()
            .StartWith(ctx => System.Console.WriteLine($"greater than 24 (thread: {Thread.CurrentThread.ManagedThreadId})"))
            .Then(ctx => System.Console.WriteLine($"greater than 24 second step  (thread: {Thread.CurrentThread.ManagedThreadId})"));
        var someBranch = builder.CreateBranch()
            .StartWith(ctx => System.Console.WriteLine($"less than 12 (thread: {Thread.CurrentThread.ManagedThreadId})"))
            .Then(ctx => System.Console.WriteLine($"less than 12 second step (thread: {Thread.CurrentThread.ManagedThreadId})"));
        var betweenBranch = builder.CreateBranch()
            .StartWith(ctx => System.Console.WriteLine($"between 12 and 21 (thread: {Thread.CurrentThread.ManagedThreadId})"))
            .Then(ctx => System.Console.WriteLine($"between 12 and 21 second step (thread: {Thread.CurrentThread.ManagedThreadId})"));


        builder
            .StartWith(ctx => System.Console.WriteLine($"started with value {((DummyData)ctx.Workflow.Data).Number} (thread: {Thread.CurrentThread.ManagedThreadId})"))
            .Decide(data => data.Number)
                .Branch((data, outcome) => data.Number > 24, greaterBranch)
                .Branch((data, outcome) => data.Number < 12, someBranch)
                .Branch((data, outcome) => data.Number < 21, betweenBranch)

        .Then(ctx => System.Console.WriteLine($"finished (thread: {Thread.CurrentThread.ManagedThreadId})"))
        .Then(ctx => System.Console.WriteLine($"really finished (thread: {Thread.CurrentThread.ManagedThreadId})"))

        ;
    }
}

and run it like

...
host.StartWorkflow(nameof(DecideExpressionWorkflow), new DummyData { Number = 11 });
...

Since my initial value is 11 the expected output is something like this:

started with value 11 (thread:...
less than 12 (thread:...
less than 12 second step (thread:...
finished (thread:...
really finished (thread:...

diagram-expected

The actual result is

started with value 11 (thread: 9)
less than 12 (thread: 11)
between 12 and 21 (thread: 11)
finished (thread: 11)
less than 12 second step (thread: 3)
between 12 and 21 second step (thread: 3)
really finished (thread: 3)

diagram-actual

There are three things which i'd like to point out, and get some advise from you how to solve them. I really hope that it is possible just I couldn't figure it out.

1. The lack of 'default' I couldn't find a way how to express the final else branch. It is not the same as the first Then<> after the three Branch call, because the 'default' should not be executed if any other branch is executed. Could you show me how it can be done?

2. Interlaced execution flow The control flow is not exactly as I expected. The 'finished' step (the first step after the Branch calls) should not be executed in the middle of the selected branch. Yes, I see the thread numbers, which explains how it happened, but then those threads shouldn't be created in that fashion. The 'finished' step presumes that everything is finished before.

3. The selection of a single branch This construction actually does not select one branch but all the matching branches. Which is not how an if-elseif works. But I need the same behavior, so to select only one matching branch (or could be none if there is no final else or default), like in if{ } else if {} else {} or in a switch in a programming language.

The biggest issue is the threading. In a flow - as its name suggests - I do not expect parallel executions but only when I explicitly ask for it. Because the steps most probably work on the workflow data (shared data) and the next step might rely on the previous steps results.

tocsi-hun avatar Mar 05 '20 14:03 tocsi-hun

The Branch is not a switch case. It will select all matching branches and schedule them to run in parallel, this is by design... you would need to be more explicit with your matching expression if you only wish a single branch to execute.

danielgerlag avatar Mar 07 '20 03:03 danielgerlag

Sure it is not, and thanks for the confirmation. But it still does not answer any of my questions. If it was by design, then I presume executing the branches in parallell was also by desig - which is perfectly ok - but was it also by design to execute the Then before any of the branches finishes? I picked the Branch example because that was the closest I could find - it differs only in just a couple of lines of code from what I am looking for. But anyway I am not keen to use the Branch if I could achive my goal with something else. But I couldn't. The When is different only in the execution of the final Then step. It also does not select one but all matching conditions. Also neither of them seems to be capable of a 'default'.

Unfortunately your advise wouldn't work for me, since I am not the one who writes those conditions, but our users through a graphical designer. So what I am looking for is something that resembles the traditional control flows. Including the switch and the if-elseif-...-else.

I desperately need the conventional control flows, otherwise this workflow engine would be the best fit for me and the designer we have.

So please tell me if and how in Workflow-Core can I construct the usual control flows? And if it is not possible currently, still would be possible to implement them without violating the basics of the engine? If so, then do you have intention to do so, or letting someone to do?

tocsi-hun avatar Mar 07 '20 11:03 tocsi-hun

By picking a single branch, it would imply that the order in which the branches are defined would affect the outcome... not sure I'd be comfortable with that ambiguity. What you're looking to achieve is not impossible on WorkflowCore... we'd need to implement a primitive that achieves this. see https://github.com/danielgerlag/workflow-core/tree/master/src/WorkflowCore/Primitives I am currently travelling, so I can't look into too much detail on this at the moment,

danielgerlag avatar Mar 07 '20 15:03 danielgerlag

Why,I am also stuck in this problem. Workflow designers sometimes need a lot of "if else if else". Without "if else if -... - else" would make things worse. I wanted to use when, but there was no JSON document.

samchenws avatar Oct 12 '20 03:10 samchenws

Is there any solution? I have the same problem

ChengYen-Tang avatar Jul 11 '23 03:07 ChengYen-Tang