Spread the love

Azure durable functions are a way to create stateful Azure functions, because even though we have all been aiming at making all our code stateless sometimes we need our code to know how far it has gotten, and that is what we are going to do in this blog post.

Stateful vs. Stateless

The major difference between stateful and stateless web services is whether they store data regarding their sessions and how they respond to requests. A stateless web service is a service that you can call multiple times with different inputs without worrying about rather or not it has been called before because you always start on a blank slate, most web services that we call are stateless, if we, for example, had a web service that returned exchange rates, then it does not store anything between the runs and therefor gives you a fresh result every time. Stateful services are services that keep track of what has happened before and thereby know what needs to happen next, these can also be seen as workflows, a common stateful service you be an FTP server, an FTP service keeps track of which directory the user is in using activity logs, stateful services are also helpful for long-running processes where one process is done it should execute another process, or where user interaction might be needed.

Azure Functions

Out of the box Azure functions are stateless, they are created with one task in mind, you trigger them either through an HTTP request or one of the other triggers, and the function will then run its code and return a result, and next time you run it, it will do the same thing, however, if you wanted to create a chain of more functions, this would not be easy, while it is indeed possible using Message Ques, where when one function finishes it creates a message in a message queue which will trigger the next azure function which once it finishes it would do the same to trigger the next function and so on.

This can extremely fast become too complicated to maintain and furthermore, then error handling can also become a pain to implement.

Azure Durable Functions

Durable functions use orchestrator functions to control the state of your workflow, the orchestrator functions let you string together a bunch of Azure functions to make sure that all the functions in your workflow are called in the correct order, there are three main concepts in durable functions, we have the one from before where you can set your functions to wait until the previous function is done before executing the next function, then we have “the fan out fan in” pattern, where you can execute a number of functions at the same time letting them run asynchronous but they will wait until all of them are done before continuing, an example of this could be that you want to push data to many different systems, where the systems do not have any dependency on each other so there is no need to call them in a specific order, but once they are all done you can want to perform some extra task like clean up.

The last concept in durable functions is the wait for the user event, this is where you as part of the workflow can wait for some user interaction of another system event, using this concept you could, in reality, have a workflow that never finishes if whatever event that your durable function is waiting on is never triggered, however as part of your durable action you can set up a timeout, so if the person has not responded for example 2 days you could send a reminder, and if the person still does not react then the workflow could either send yet another reminder or it could cancel the workflow. An example of a user event workflow could be, that you have sent a document to a person, and that person needs to either approve or decline the document that you send, and depending on their choice the workflow will do something.

One thing that you have to keep in mind when working with durable functions is that even though it may look like they are state aware they are still stateless behind the scenes, the way that they work is that, the orchestrator, will write what function it has just started to an azure storage, and when the function is done it will recall the orchestrator function which will then re-run all the code in the orchestrator function, however, it will check the azure storage whether or not each step has been run, so it will not execute the functions inside your orchestrator function more than once, however, if you place code inside the orchestrator function itself that code would be run multiple times, so it is best practice not to place any stateful code inside the orchestrator function, and also not to place an ID like GUID or a DateTime, because next time the orchestrator function runs it will regenerate these variables.

Getting Started

To get started with Azure durable functions you will need an Azure subscription and an editor of choice, I will be using Visual Studio 2022 and C# as my language. When installing Visual Studio remember to select the Azure Development:

Create a new project of type Azure Functions:

Give your project and solution a name and press next, and on the next page choose Durable Functions Orchestration from the dropdown:

The first time that you open your solution you might meet some errors about missing namespaces

to fix these you will have to install some nugget packages, to do so go to Tools->NuGet Package Manger-> Manage NuGet Packages for Solution

And Install the following:

When we look at the example code that Visual Studio has generated for us, we will see that it has created 3 functions the first one is our RunOrchestrator

[FunctionName("Function1")]
        public static async Task<List<string>> RunOrchestrator(
            [OrchestrationTrigger] IDurableOrchestrationContext context)
        {
            var outputs = new List<string>();

            // Replace "hello" with the name of your Durable Activity Function.
            outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "Tokyo"));
            outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "Seattle"));
            outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "London"));

            // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
            return outputs;
        }

This is our “main” function and the one that defines our workflow, in our example, it will call our Activity function SayHello three times async with the await property and return the results, the next function is our activity function SayHello

[FunctionName(nameof(SayHello))]
        public static string SayHello([ActivityTrigger] string name, ILogger log)
        {
            log.LogInformation("Saying hello to {name}.", name);
            return $"Hello {name}!";
        }

This is a simple function that takes a string input, creates a log message, and returns Hello and the input value. Our last function is our HttpStart

[FunctionName("Function1_HttpStart")]
        public static async Task<HttpResponseMessage> HttpStart(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestMessage req,
            [DurableClient] IDurableOrchestrationClient starter,
            ILogger log)
        {
            // Function input comes from the request content.
            string instanceId = await starter.StartNewAsync("Function1", null);

            log.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId);

            return starter.CreateCheckStatusResponse(req, instanceId);
        }

This is our starter function, this is the function that a user or a system can call to start our workflow, it will call our Function1 which is our RunOrchestrator function, and get an instanceID this InstanceId is then logged and returned to the user, using the CreateCheckStatusResponse method, and this is actually quite powerful because the CreateCheckStatusResponse will return a json request with different URL’s that can be used to interact with our workflow, to see what I mean let us try to run our function by pressing F5. This will launch a local running Azure function with a Function1_HttpStart and an URL:

You can also see the other two functions but since they do not have a HttpTrigger they will not display an URL, If we take our URL and past it into whatever tool you prefer a browser, postman, or like me Thunder Client the result should be the same, you should get a result JSON object like the following:

The one that we will use at the moment is the statusQueryGetUri if we call this, you should get something like this:

This tells us that Function1 has runtime status Completed and you can see our output, so using Status URI we can get the status of our workflow, now let us change our code a little, first of I do not like the hardcoded FunctionName so I will change them to use the nameof instead, and then I have added three activity functions called SendDataToWebShop,SendDataToCRM, and SendDataToPIM which do nothing but writes to the log and sleeps for 10 sec, so my code looks as follows.

public static class Function1
    {
        [FunctionName(nameof(RunOrchestrator))]
        public static async Task<List<string>> RunOrchestrator(
            [OrchestrationTrigger] IDurableOrchestrationContext context)
        {
            var outputs = new List<string>();
            
            outputs.Add(await context.CallActivityAsync<string>(nameof(SendDataToWebShop),"Webshop"));
            outputs.Add(await context.CallActivityAsync<string>(nameof(SendDataToCRM), "CRM"));
            outputs.Add(await context.CallActivityAsync<string>(nameof(SendDataToPIM), "PIM"));
            
            return outputs;
        }

        [FunctionName(nameof(SendDataToWebShop))]
        public static string SendDataToWebShop([ActivityTrigger]string name, ILogger log)
        {
            log.LogInformation("Sand Data to {name}",name);
            Thread.Sleep(10000);
            return $"Data send to webshop";
        }

        [FunctionName(nameof(SendDataToCRM))]
        public static string SendDataToCRM([ActivityTrigger] string name, ILogger log)
        {
            log.LogInformation("Sand Data to {name}", name);
            Thread.Sleep(10000);
            return $"Data send to webshop";
        }

        [FunctionName(nameof(SendDataToPIM))]
        public static string SendDataToPIM([ActivityTrigger] string name, ILogger log)
        {
            log.LogInformation("Sand Data to {name}", name);
            Thread.Sleep(10000);
            return $"Data send to PIM";
        }

        [FunctionName(nameof(HttpStart))]
        public static async Task<HttpResponseMessage> HttpStart(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestMessage req,
            [DurableClient] IDurableOrchestrationClient starter,
            ILogger log)
        {
            // Function input comes from the request content.
            string instanceId = await starter.StartNewAsync(nameof(RunOrchestrator), null);

            log.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId);

            return starter.CreateCheckStatusResponse(req, instanceId);
        }
    }

When I run my code now, the start URL looks like this:

If I then run this URL, and afterward the Status URL I will get a result like the following.

Which tells me that the function is still running. If I wanted to stop my workflow, I would call the terminatePostUri if I do a Post request to my URL giving it a reason which in my case is abandoned

POST: http://localhost:7217/runtime/webhooks/durabletask/instances/bf457c506150427ba4524c76564ceabf/terminate?reason=abandoned&taskHub=TestHubName&connection=Storage&code=YZgk1iH-k126u7_pChGaMCL9iUOHgYN_wK_CCwqnrhVdAzFus3UPmQ==

When I check my status URI I will get a request like the following:

You can see that the runtimeStatus is Terminated and the output is the reason that I passed to my terminatePostUri, and I that way we have the ability to end a workflow if we need to. Below is a short example of how you could call the above Azure Durable Function from Business Central and store the URL’s for later use.

I have created a table:

table 50300 "Azure Durable Setup"
{
    Caption = 'Azure Durable Setup';
    DataClassification = CustomerContent;

    fields
    {
        field(1; "Code"; Code[10])
        {
            Caption = 'Code';
            DataClassification = CustomerContent;
        }
        field(2; "Status URI"; Text[300])
        {
            Caption = 'Status URI';
            DataClassification = CustomerContent;
        }
        field(3; "Terminate URI"; Text[300])
        {
            Caption = 'Terminate URI';
            DataClassification = CustomerContent;
        }
        field(4; "Function URL"; Text[300])
        {
            Caption = 'Function URL';
        }
        field(5; "Function Code"; Text[300])
        {
            Caption = 'Function Code';
        }
    }
    keys
    {
        key(PK; "Code")
        {
            Clustered = true;
        }
    }
}

And a codeunit to call our function and store the return URI’s, I have chosen to use the Azure function codeuint in Business Central but you could use HttpClient if you prefer:

codeunit 50300 DurableFunction
{
    procedure CallAzureDurableFunction()
    var
        AzureDurableSetup: Record "Azure Durable Setup";
        AzureFunctions: Codeunit "Azure Functions";
        AzureFunctionsAuthentication: Codeunit "Azure Functions Authentication";
        AzureFunctionsResponse: Codeunit "Azure Functions Response";
        IAzureFunctionsAuthentication: Interface "Azure Functions Authentication";
        Response: HttpResponseMessage;
        QurreyDic: Dictionary of [text, text];
        Result: Text;
        error: JsonToken;
        Jobject: JsonObject;
        Jtoken: JsonToken;
    begin

        AzureDurableSetup.get();
        IAzureFunctionsAuthentication := AzureFunctionsAuthentication.CreateCodeAuth(AzureDurableSetup."Function URL", AzureDurableSetup."Function Code");
        AzureFunctionsResponse := AzureFunctions.SendGetRequest(IAzureFunctionsAuthentication, QurreyDic);
        AzureFunctionsResponse.GetHttpResponse(Response);

        if not response.IsSuccessStatusCode() then begin
            response.Content().ReadAs(Result);
            Jobject.ReadFrom(Result);
            Jobject.Get('error', error);
            Error('Failed: Reason: %1', error.AsValue().AsText());
        end else begin
            Response.Content.ReadAs(Result);
            Jobject.ReadFrom(Result);
            Jobject.Get('statusQueryGetUri', Jtoken);
            AzureDurableSetup."Status URI" := Jtoken.AsValue().AsText();
            Jobject.Get('terminatePostUri', Jtoken);
            AzureDurableSetup."Terminate URI" := Jtoken.AsValue().AsText();
            AzureDurableSetup.Modify();
        end;
    end;
}

And a simple page to call our functions and show the results:

page 50300 "Azure Durable Setup"
{
    ApplicationArea = All;
    Caption = 'Azure Durable Setup';
    PageType = Card;
    SourceTable = "Azure Durable Setup";
    UsageCategory = Administration;

    layout
    {
        area(content)
        {
            group(General)
            {
                Caption = 'General';

                field("Function Code"; Rec."Function Code")
                {
                    ToolTip = 'Specifies the value of the Function Code field.';
                }
                field("Function URL"; Rec."Function URL")
                {
                    ToolTip = 'Specifies the value of the Function URL field.';
                }
                field("Status URI"; Rec."Status URI")
                {
                    ToolTip = 'Specifies the value of the Status URI field.';
                }
                field("Terminate URI"; Rec."Terminate URI")
                {
                    ToolTip = 'Specifies the value of the Terminate URI field.';
                }
            }
        }
    }

    actions
    {
        area(Processing)
        {
            action(CallAzureDurableFunction)
            {
                Image = Web;
                trigger OnAction()
                var
                    DurableFunction: Codeunit DurableFunction;
                begin
                    DurableFunction.CallAzureDurableFunction();
                end;
            }
        }

        area(Promoted)
        {
            actionref(CallAzureDurableFunctionRef; CallAzureDurableFunction) { }
        }
    }

    trigger OnOpenPage()
    begin
        if not rec.FindFirst() then
            rec.Insert();
    end;
}

And the result is this page:

I will leave it up to you to decide how you can use this functionality in your Business Central customizations, if we return to our Azure function let us just quickly run through two last things which is the fan in fan out pattern and the user event, to implement the fan in fan out pattern is simply, all we have to do is create an array of our tasks, use the WhenAll method to wait for all the tasks. So the RunOrchestrator function looks like this:

[FunctionName(nameof(RunOrchestrator))]
        public static async Task<List<string>> RunOrchestrator(
            [OrchestrationTrigger] IDurableOrchestrationContext context)
        {
            var outputs = new List<string>();

            var tasks = new Task<string>[3];

            tasks[0] = context.CallActivityAsync<string>(nameof(SendDataToWebShop),"Webshop");
            tasks[1] = context.CallActivityAsync<string>(nameof(SendDataToCRM), "CRM");
            tasks[2] = context.CallActivityAsync<string>(nameof(SendDataToPIM), "PIM");

            await Task.WhenAll(tasks);

            outputs.Add(tasks[0].Result);
            outputs.Add(tasks[1].Result);
            outputs.Add(tasks[2].Result);

            return outputs;
        }

In my example you will not see much difference, however, the idea is that using this pattern you can let all the functions run at the same time, and it will then wait for the slowest function to finish before executing the next step, the last thing that I will cover in this post is the wait for an event functionality and this is done by using the WaitForExternalEvent which will then wait for an event to be sent to the sendEventPostUri, so if I change my RunOrchestrator to the following:

[FunctionName(nameof(RunOrchestrator))]
        public static async Task<List<string>> RunOrchestrator(
            [OrchestrationTrigger] IDurableOrchestrationContext context)
        {
            var outputs = new List<string>();

            var tasks = new Task<string>[3];
            bool approved = await context.WaitForExternalEvent<bool>("Approval");
            if (approved)
            {
                tasks[0] = context.CallActivityAsync<string>(nameof(SendDataToWebShop), "Webshop");
                tasks[1] = context.CallActivityAsync<string>(nameof(SendDataToCRM), "CRM");
                tasks[2] = context.CallActivityAsync<string>(nameof(SendDataToPIM), "PIM");
            }
            else
            {
                outputs.Add("Declined");
                return outputs;
            }
            await Task.WhenAll(tasks);

            outputs.Add(tasks[0].Result);
            outputs.Add(tasks[1].Result);
            outputs.Add(tasks[2].Result);

            return outputs;
        }

When I call my function, and my status uri it will tell me that the function is still running, if I then call my sendEventPostUri with my event name which is Approval, and the payload true, the workflow will continue to run.

POST: http://localhost:7217/runtime/webhooks/durabletask/instances/9ed07646d7b74a96ba6fa71b8816fc3a/raiseEvent/Approval?taskHub=TestHubName&connection=Storage&code=YZgk1iH-k126u7_pChGaMCL9iUOHgYN_wK_CCwqnrhVdAzFus3UPmQ==

payload

"true"

Conclusion

In this post, we took a look at how we can use durable functions to create workflows with Azure functions, we looked at running functions in a chain, how we could use the fan in fan out pattern, and how to wait for an event before continuing our workflow. So why would we use a durable function instead of something like logic apps, I will let you make that decision, I think that it is always a matter of choosing the right tool for the job, so while logic apps’ power lies in its ease of use with all its connectors, azure functions have the advantage of price, performance, and ability to support complex custom code, however, they do not have the ease of use as Logic apps, since everything in azure functions is raw code. Well that was it for this post, I know that I did not cover everything about durable functions but hopefully I covered enough to help you get started, and until next time stay safe.

Leave a Reply