Build dynamic workflows with azure durable functions – Branching logic (part 3)

Photo by Nico Becker on Pexels.com

This is part of a mini-series where we want to build low code product on the shoulders of azure durable functions:

  1. Build dynamic workflows with azure durable functions (NoCode style) – Part 1
  2. Build dynamic workflows with azure durable functions (Low-Code style) – Part 2
  3. Add branching logic to your dynamic workflows with azure durable functions – Part 3 (this)

So far we managed to build a quite a robust solution for having a user configured workflows, but we miss one important feature, giving the users the possibility to configure junction points in the flows. To be a more blunt, give them a construct that would resemble if – else.

So, this is not an easy task, we will require to add a few more constructs to our existing solution (build in the previous two articles) but we will succeed. Now, the solution got a bit more lengthier so instead of the gist approach from the previous article, all the code will be contained in a git repository.

Before we move on, all the ideas and implementation in this article series, is more proof of concept and exploration than production grade code, so be aware.

UI / UX – How to present if else to the user

Before we do a deep dive into the code, let’s take a step back and think about how exactly do we present this new and powerful feature to our users.

If you remember from the past articles, this is how we designed more or less our fictional UI. Now, to present them this branching, I think a good strategy or analogy would be nesting. So, from a UI / design perspective, what actually is happening here is we are creating a flow in a flow, and based on some arbitrary condition that is set dynamically by the user, we will run one or the other.

Now that we have a visual representation for this we could now go in to the main event, how do we manage to pull this off.

As a small remainder this is part of a series of posts, and here I will mostly go through the changes from the previous version.

So, how to do no-code style branching

So, in order to achieve this behavior dynamically we first need to understand and come up with a way in which we will be able to encode the if-else dynamic.

The way I chose to implement this is mostly rudimentary and is inspired by the functional (math), binary style of doing branching. We will have sets of two lists which will contain the steps for each branch called Left and Right, and a condition, if the condition is evaluated to true the left branch will run, otherwise the right branch will run.

From an invocation point of view, we are adding a new orchestrator Branch, to the mix and a new activity that does the actual condition evaluation.

From a call stack, this will be implemented using recursion, since we already have the Dynamic Orchestrator.

Implementing the branching logic

No let’s get to the core of this article, the code.

First as discussed earlier, we need to add the Left and Right lists to the dynamic step class, and also we need to modify the constructor to use these new constructs:

   public class DynamicStep<T, U>
    {
        public string Action { get; private set; }
        public U param { get; private set; }
        public string Fn { get; }

        public List<DynamicStep<T, U>> LeftSteps { get; private set; }
        
        public List<DynamicStep<T, U>> RightSteps { get; private set; }

        public DynamicStep(string action, U param)
        {
            Action = action;
            this.param = param;
            Fn = string.Empty;
        }

        public static DynamicStep<T, U> Branch(string condition, List<DynamicStep<T, U>> leftSteps, List<DynamicStep<T, U>> rightSteps, U param = default)
        {
            return new DynamicStep<T, U>(ActionName.Branch, param, condition, leftSteps, rightSteps);
        }

        [JsonConstructor]
        public DynamicStep(string action, U param, string fn,  List<DynamicStep< T,U>> leftSteps = default, List<DynamicStep< T,U>> rightSteps = default)
        {
            Action = action;
            this.param = param;
            Fn = fn;
            LeftSteps = leftSteps;
            RightSteps = rightSteps;
        }
    }

To be able to test, we added the branch step to the Flowmaker function in order to kick things off. This in theory should be constructed based on a query from a data store.

      [FunctionName("FlowMaker")]
        public static async Task<double> Run([OrchestrationTrigger] IDurableOrchestrationContext context)
        {
            var steps = new List<DynamicStep<double, double>>
            {
                new DynamicStep<double, double>(ActionName.Add, 1),
                new DynamicStep<double, double>(ActionName.Add, 2),
                new DynamicStep<double, double>(ActionName.Add, 3),
                new DynamicStep<double, double>(ActionName.Dynamic, 2, "(2 * r + 1)/p"), // <-- simulate loading for a datasource
                DynamicStep<double, double>.Branch("r % 2 == 0",
                    new List<DynamicStep<double, double>>
                    {
                        new DynamicStep<double, double>(ActionName.Divide, 2)
                    }, new List<DynamicStep<double, double>>() )
            };

            var ctx = new DynamicFlowContext
            {
                Steps = steps
            };

            var result = await context.CallSubOrchestratorAsync<DynamicResult<double>>("DynamicOrchestrator", ctx);
            return result.Result;
        }

We will also need to modify the Dynamic Orchestrator to know how to handle they new step type, Branch, since it is a bit different, for branching it will instantiate a sub orchestrator instead of calling a normal activity. Now I had left it like this to not make things even more complicated to understand, but normally I would use the Dynamic Orchestrator technique.

        [FunctionName("DynamicOrchestrator")]
        public static async Task<DynamicResult<double>> RunInnerOrchestrator(
            [OrchestrationTrigger] IDurableOrchestrationContext ctx)
        {
            var input = ctx.GetInput<DynamicFlowContext>();
            double state = input.State != default? input.State : 0;

            foreach (var step in input.Steps)
            {
                if (step.Action == ActionName.Branch)
                {

                    var result = await ctx.CallSubOrchestratorAsync<double>("Branch", new BranchContext<double, double>()
                    {
                        State = state ,
                        DynamicStep = step
                    });

                    return new DynamicResult<double>
                    {
                        Result = Convert.ToDouble(result)
                    };
                }
                else
                {
                    state = await ctx.CallActivityAsync<double>(step.Action, new DynamicParam
                    {
                        Accumulator = state,
                        Parameter = step.param,
                        Fn = step.Fn,
                    });
                }
            }

            return new DynamicResult<double>
            {
                Result = state
            };
        }

Now that we have all this in place, we can call our Branch Orchestrator which looks like this:

        [FunctionName("branch")]
        public static async Task<double> Branch([OrchestrationTrigger] IDurableOrchestrationContext context, ILogger logger)
        {
            var input = context.GetInput<BranchContext<double, double>>();
            var param = input.DynamicStep;
            var newState = input.State;

            var branchToRun = await context.CallActivityAsync<bool>("BranchAction", new DynamicParam()
            {
                Accumulator = input.State,
                Fn = param.Fn,
                Parameter =param.param,
            });

            if (branchToRun)
            {
                if (param.LeftSteps != null && param.LeftSteps.Count > 0)
                {
                    return await context.CallSubOrchestratorAsync<double>("DynamicOrchestrator", new DynamicFlowContext()
                    {
                        State = newState,
                        Steps = param.LeftSteps
                    });
                }

                return newState;
            }
            else
            {
                if (param.RightSteps != null && param.RightSteps.Count > 0)
                {
                    return await context.CallSubOrchestratorAsync<double>("DynamicOrchestrator", new DynamicFlowContext()
                    {
                        State = newState,
                        Steps = param.RightSteps,
                    });
                }

                return newState;
            }
        }

You can see in the orchestrator, that based on the invocation of the condition, we decide which branch will run. The branch itself will be run using the Dynamic Orchestrator, which opens the possibility to have nested branching without any kind of changes on our code, recursion at its best.

The last piece of the puzzle left is Branch action which evaluates the condition in a similar way with Dynamic Action, the main difference being that this one returns a boolean instead of the result of T.

        [FunctionName("BranchAction")]
        public static async Task<bool> DynamicBranchAction([ActivityTrigger] DynamicParam param, ILogger log)
        {
            var func = new Engine()
                .Execute($"function branch(r, p){{ return {param.Fn} }}").GetValue("branch");

            var invoked = func.Invoke(param.Accumulator , param.Parameter);
            bool.TryParse(invoked.ToString(), out var result);

            return result;
        }

As you can see, the actual implementation is quite trivial, and to be honest, this is what I like about the orchestrator functions framework, is that it frees us to make all kind of interesting constructs.

Although there are quite a lot of code bits here, I recommend you check out the git repository and look at the whole solution.

Hope you have enjoyed our little adventure in the realm of azure functions, if you would like to get informed when a new post is added, join the list 🙂

Processing…
Success! You're on the list.