Build dynamic workflows with azure durable functions (Low-Code style) – Part 2

Photo by Harrison Candlin 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 (this)
  3. Add branching logic to your dynamic workflows with azure durable functions – Part 3

Before moving on, if you haven’t read part 1, I highly advice you do, this article won’t go anywhere, I promise 😀.

In the previous article, we looked at building an approach to pulling off a “No Code”- like workflow. We looked at how we can build the required data structures and actions, allowing the user to control the workflow order. 

In this post, we will be looking into giving the user the option to add his small bits of code in our rather “complex” workflow. 

Now, keeping our same calculator theme, we can imagine that our previous version was a hit. Still, the people up in finance need some more steps, like calculating the power of the result or applying a 2n + 1 formula to the mix ( don’t ask me why nobody knows what they do 😀 ).

So, how can we achieve this? 

One way of doing this might be to start adding several actions to our flow in the same way that we did so far for the basic operations. But this might end up like a slippery slope since each new addition will require a deployment, and we might end up like a bottleneck. In this case, all the work we did this far might be for nothing, because if we remember, one of the whole reasons for going on this path was to pass some of the responsibilities to the actual stakeholders.

What if we provide the users a simple interface where they could add their own “math”? Wouldn’t that be awesome? I definitely think it would be super awesome, so let’s see how we can achieve it.

First, we will need to update our UI from last time to give the users the possibility actually to do this:

As you can see, there is a new option in the combo box with custom, and we added a textbox where they can add their math 😎.

Now, since we managed to do all of this, let’s see how we will integrate this into our existing code. 

The Code

First, we need to tackle the elephant in the room. How could we do the math part in our code? Well, as the answer to most problems nowadays, Javascript comes to the rescue.

Wait, what? Javascript? Wasn’t this based on C# / .Net? 

Yes, there are actually multiple ways to solve this problem, two of them would be C# script and JS, and both would work wonderfully. Still, in reality, people that will be using this application would be “citizen developers,” so I think it would be easier for them to use the js. Also, there are tons of content on JS.

So, while playing with this idea for the article, after trying various c# script approaches, Rosylin compilers, etc., the JS idea hit me. After a few google searches, I came across this amazing NuGet that will allow us to parse and execute JS in .Net. The package name is Jint, and by the looks of it, it is awesome. So, yeah, to answer this bluntly, we will run JS in our C# code, and yes, we will be breaking the “NEVER EVER USE EVAL()” rule, but we will be careful. I promise.

So, the first change we need to make is to change the structure of the DynamicStep class to be able to include the custom js code: 

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

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

        [JsonConstructor]
        public DynamicStep(string action, U param, string fn)
        {
            Action = action;
            this.param = param;
            Fn = fn;
        }
    }

The fn variable will hold the JS code, next we will need to add the Dynamic option to the list of accepted actions.

    public static class ActionName
    {
        public const string Add = "Add";
        public const string Subtract = "Subtract";
        public const string Multiply = "Multiply";
        public const string Divide = "Divide";
        public const string Dynamic = "Dynamic";
    }

Next we will need the new “data” from the data source in the orchestrator:

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

            var ctx = new DynamicFlowContext
            {
                Steps = steps
            };

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

The next adjustment will be to the sub orchestrator that does all the work:

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

            foreach (var step in input.Steps)
            {
                // state = await ctx.CallActivityAsync<int>(step.Action, (state, (step.param, step.Fn)));
                state = await ctx.CallActivityAsync<double>(step.Action, new DynamicParam
                {
                    Accumulator = state,
                    Parameter = step.param,
                    Fn = step.Fn,
                });
            }

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

Now, the keen eyed among you might have noticed that we added a new parameter type. This was required to reduce the arity of the functions so that we have a common structure for both coded and dynamic actions. Here is the type, noticed fancy:

        public class DynamicParam
        {
            public double Accumulator { get; set; }
            public int Parameter { get; set; }
            public string Fn { get; set; }
        }

And now for the “magic” part, the dynamic function, let’s see it first, and discuss it after:

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

            var invoked = func.Invoke(param.Accumulator, param.Parameter);

            double.TryParse(invoked.ToString(), out var result);
            return result;
        }

So, let’s break this down. The func is how we register the js function definition. Think of it on how you test something in the browser console, the engine instance being the console process. Once we define the function, we call it with the Invoke ( parameters ) part. And after that … well, we cast the result. I think there may be a better way of doing this. As I said, I just discovered this, but this works fine for our purpose here.

Now, I sense a question… why did I define the function and let the user give a simple expression? This decision is based on this “calculator” example, and it kind of made sense to do it like this, since the fictional users are not developers, but more like finance people, and giving them the option to use an excel-like formula seemed to be more appropriate, of course, you could let the user define the whole function if you want.

So yeah, now you have it, your very own “Low-code” workflow. Amazing right ? Did I add to the no code low code craziness, don’t know. So, is this production ready ? No… Is this an idea worth looking into ? Maybe. There are endless business requirements and ways to fulfill them, and this might be the seed, inception of one of those ways.

Now, in reality, we are building a very crude version of Logic Apps / Power Apps, if you think about it, and you might wonder, why bother ? And it is a good question, sometimes you don’t need to build something like this, and you could get away with other existing products, but sometimes you need some custom behavior, that would be way more complicated to build with existing products, so, as always, it depends.

Now, I plan on doing a one more post in this series, but I can’t make any promise on when it will land, but it will be about integrating branching logic in our dynamic “low-code” platform. Right now I haven’t thought about how to do it, but I imagine it would be quite the feature 🙂

If you want to look at the code here is the link to a the gist.

If you want to be notified when the next post will go live … you know what to do.

Processing…
Success! You're on the list.