Understanding Azure Durable Functions - Part 7: The Function Chaining Pattern

This is the seventh part in a series of articles. If you’re not familiar with Durable Functions you should check out the previous articles before reading this.

There are a number of patterns that Durable Functions make easier to implement, we’ll look at some more later in this series of articles.

One common scenario is the requirement to create a “pipeline” of processing where the output from one Azure Function feeds into the next function in the chain/pipeline. This pattern can be implemented without Durable Functions, for example by manually setting up different queues to pass work down the chain. One downside to this manual approach is that it’s not sometimes immediately obvious what functions are involved in the the pipeline. Function chaining with Durable Functions allows the chain/pipeline to be easy to understand because the entire pipeline is represented in code.

To implement the function chaining pattern, you simply call one activity function and pass in  the input from a previous activity function.

As an example, the following orchestrator function chains 3 activity functions together:

[FunctionName("ChainPatternExample")]
public static async Task<string> RunOrchestrator(
    [OrchestrationTrigger] DurableOrchestrationContext context, ILogger log)
{
    log.LogInformation($"************** RunOrchestrator method executing ********************");            

    string greeting = await context.CallActivityAsync<string>("ChainPatternExample_ActivityFunction", "London");
    string toUpper = await context.CallActivityAsync<string>("ChainPatternExample_ActivityFunction_ToUpper", greeting);
    string withTimestamp = await context.CallActivityAsync<string>("ChainPatternExample_ActivityFunction_AddTimestamp", toUpper);

    log.LogInformation(withTimestamp);
    return withTimestamp;
}

In the preceding code, the result of the function (e.g. in the greeting variable) is passed in as data to the next activity function: await context.CallActivityAsync<string>("ChainPatternExample_ActivityFunction_ToUpper", greeting);

This example is fairly simple, but you could add condition logic to only call an activity based on the result of the previous function. In this way you can build up more complex pipelines which using the manual (non Durable Functions) approach would be even harder to reason about. At least with Durable Functions, the entire flow (even if it has conditional logic) can be easily understood.

The complete listing is as follows (for simplicity we’re not taking in any input in the client function.):

using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;

namespace DurableDemos
{
    public static class ChainPatternExample
    {
        [FunctionName("ChainPatternExample")]
        public static async Task<string> RunOrchestrator(
            [OrchestrationTrigger] DurableOrchestrationContext context, ILogger log)
        {
            log.LogInformation($"************** RunOrchestrator method executing ********************");            

            string greeting = await context.CallActivityAsync<string>("ChainPatternExample_ActivityFunction", "London");
            string toUpper = await context.CallActivityAsync<string>("ChainPatternExample_ActivityFunction_ToUpper", greeting);
            string withTimestamp = await context.CallActivityAsync<string>("ChainPatternExample_ActivityFunction_AddTimestamp", toUpper);

            log.LogInformation(withTimestamp);
            return withTimestamp;
        }

        [FunctionName("ChainPatternExample_ActivityFunction")]
        public static string SayHello([ActivityTrigger] string name, ILogger log)
        {            
            return $"Hello {name}!";
        }

        [FunctionName("ChainPatternExample_ActivityFunction_ToUpper")]
        public static string ToUpper([ActivityTrigger] string s, ILogger log)
        {
            return s.ToUpperInvariant();
        }

        [FunctionName("ChainPatternExample_ActivityFunction_AddTimestamp")]
        public static string AddTimeStamp([ActivityTrigger] string s, ILogger log)
        {            
            return $"{s} [{DateTimeOffset.Now}]";
        }

        [FunctionName("ChainPatternExample_HttpStart")]
        public static async Task<HttpResponseMessage> HttpStart(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post")]HttpRequestMessage req,
            [OrchestrationClient]DurableOrchestrationClient starter,
            ILogger log)
        {
            string instanceId = await starter.StartNewAsync("ChainPatternExample", null);

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

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

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

Pingbacks and trackbacks (1)+

Add comment

Loading