Understanding Azure Durable Functions - Part 12: Sub Orchestrations

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

Sub-orchestrations are a feature of Durable Functions that allow you to further compose and reuse functions.

Essentially sub-orchestrations allow you to call an orchestration from within another orchestration. In this way they are similar to calling activity functions from within an orchestration and just like activity functions can return a value to the calling (parent) orchestration.

As an example, the following client function starts the orchestration called ProcessMultipleCitiesOrhestrator:

[FunctionName("SubOrchExample_HttpStart")]
public static async Task<HttpResponseMessage> HttpStart(
    [HttpTrigger(AuthorizationLevel.Function, "post")]HttpRequestMessage req,
    [OrchestrationClient]DurableOrchestrationClient starter,
    ILogger log)
{

    var data = await req.Content.ReadAsAsync<GreetingsRequest>();

    string instanceId = await starter.StartNewAsync("ProcessMultipleCitiesOrhestrator", data);

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

    return starter.CreateCheckStatusResponse(req, instanceId);
}

The preceding code is no different from what we’ve seen already in this series, the change comes in the ProcessMultipleCitiesOrhestrator:

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

    GreetingsRequest data = context.GetInput<GreetingsRequest>();


    // Perform all greetings in parallel executing sub-orchestrations
    var greetingsSubOrchestrations = new List<Task>();            
    
    foreach (string city in data.Cities)
    {
        Task greetingSubOrchestration = context.CallSubOrchestratorAsync<string>("ProcessSingleCityOrhestrator", city);
        greetingsSubOrchestrations.Add(greetingSubOrchestration);
    }

    await Task.WhenAll(greetingsSubOrchestrations);

    // When all of the sub orchestrations have competed, get the results and append into a single string
    var allGreetings = new StringBuilder();
    foreach (Task<string> greetingSubOrchestration in greetingsSubOrchestrations)
    {
        allGreetings.AppendLine(await greetingSubOrchestration);
    }

    log.LogInformation(allGreetings.ToString());

    return allGreetings.ToString();
}

The main thing to note in the preceding code is the line: Task greetingSubOrchestration = context.CallSubOrchestratorAsync<string>("ProcessSingleCityOrhestrator", city); Here we are calling into another orchestrator function (the sub-orchestration). We are doing this by creating a list of tasks, each representing an instance of the sub-orchestration, and then executing those tasks, and finally getting the return values from each sub-orchestration task and creating a single string result.

The ProcessSingleCityOrhestrator function is as follows:

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

    var city = context.GetInput<string>();

    string greeting = await context.CallActivityAsync<string>("SubOrchExample_ActivityFunction", city);
    string toUpper = await context.CallActivityAsync<string>("SubOrchExample_ActivityFunction_ToUpper", greeting);
    string withTimestamp = await context.CallActivityAsync<string>("SubOrchExample_ActivityFunction_AddTimestamp", toUpper);

    log.LogInformation(withTimestamp);

    return withTimestamp;
}

Now we have the flexibility to compose/reuse, for example the ProcessSingleCityOrhestrator could be called from a different client function if only a single city was being supplied.

If we call the client function SubOrchExample_HttpStart  with the JSON:

{
    "Cities": [
            "London",
            "Tokyo",
            "Perth",
            "Nadi"
    ]
}

We get the following return value (from the ProcessMultipleCitiesOrhestrator):

{
    "name": "ProcessMultipleCitiesOrhestrator",
    "instanceId": "c7a0eb03c56a44ab8a767cd8a487c834",
    "runtimeStatus": "Completed",
    "input": {
        "$type": "DurableDemos.SubOrchExample+GreetingsRequest, DurableDemos",
        "Cities": [
            "London",
            "Tokyo",
            "Perth",
            "Nadi"
        ]
    },
    "customStatus": null,
    "output": "HELLO LONDON! [30/10/2019 11:51:53 AM +08:00]\r\nHELLO TOKYO! [30/10/2019 11:51:58 AM +08:00]\r\nHELLO PERTH! [30/10/2019 11:52:01 AM +08:00]\r\nHELLO NADI! [30/10/2019 11:52:00 AM +08:00]\r\n",
    "createdTime": "2019-10-30T03:51:52Z",
    "lastUpdatedTime": "2019-10-30T03:52:01Z"
}

To learn more about sub-orchestrations, check out the docs.

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:

Comments (1) -

  • Allan

    12/1/2019 2:11:53 AM | Reply

    I've read your entire durable functions series Jason, thank you for your time investment into educating others like me. I see examples, like yours, where the trigger (Client Function)  is always HTTP. Do you have anything or do you plan to post anything where the trigger (Client Function) is Azure Service Bus (Topic or Queue); where you are using the [ServiceBusTrigger(...)]?  We require in-order processing so Topics with Sessions enabled are used frequently. We leverage Logic Apps today, mostly but would like to consider Durable Functions as an alternative as it gives us more control.

Add comment

Loading