In the previous post I was working on very basic concepts of Durable Functions. Since they are connected to a simple in-cloud game engine, I'll go a bit further and show you how sub-orchestrations help in shaping a solution, so all concepts are decoupled and isolated.
Why sub-orchestration?
While it's perfectly fine to build your orchestrations using multiple activities called one-by-one(or parallelized - it's still a valid solution), sometimes you'd like to isolate different concepts emerging from a one project. Let's consider our example - we'd like to create a galaxy with N planets inside it. There're two approaches possible:
- perform all operations inside one orchestration so each action is an activity
- decouple those actions so one orchestration could call another(and we can call them separately)
Let's consider following example:
/
[FunctionName("ProvisionNewDevices")]
public static async Task ProvisionNewDevices(
[OrchestrationTrigger] DurableOrchestrationContext ctx)
{
string[] deviceIds = await ctx.CallActivityAsync<string[]>("GetNewDeviceIds");
// Run multiple device provisioning flows in parallel
var provisioningTasks = new List<Task>();
foreach (string deviceId in deviceIds)
{
Task provisionTask = ctx.CallSubOrchestratorAsync("DeviceProvisioningOrchestration", deviceId);
provisioningTasks.Add(provisionTask);
}
await Task.WhenAll(provisioningTasks);
// ...
}
Here you can see one of the biggest advantages of such approach - you can run multiple flows simultaneously and just await until each is finished. With several activities it's still doable, however I'd rather consider it an antipattern.
Making a working solution
My current orchestration looks like this:
/
[FunctionName("Galaxy_Create_Start")]
public static async Task<HttpResponseMessage> StartOrchestration(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "orchestration/start")] HttpRequestMessage req,
[OrchestrationClient] DurableOrchestrationClient starter,
TraceWriter log)
{
// Function input comes from the request content.
var instanceId = await starter.StartNewAsync("Galaxy_Create", null);
var payload = await req.Content.ReadAsStringAsync();
log.Info($"Started orchestration with ID = '{instanceId}'.");
log.Info($"The payload is: {payload}");
return starter.CreateCheckStatusResponse(req, instanceId);
}
[FunctionName("Galaxy_Create")]
public static async Task<string> RunImpl([OrchestrationTrigger] DurableOrchestrationContext context)
{
var result = await Task.WhenAll(context.CallActivityAsync<string>("Utility_Coords"),
context.CallActivityAsync<string>("Utility_Galaxy_Name"));
var galaxyContext = new CreateGalaxyContext(result[1], result[0]);
await context.CallActivityAsync("Galaxy_Create_Impl", galaxyContext);
await context.CallSubOrchestratorAsync("Planet_Create", galaxyContext);
return "Galaxy created!";
}
[FunctionName("Galaxy_Create_Impl")]
public static async Task CreateGalaxy(
[ActivityTrigger] CreateGalaxyContext context,
[Table("galaxies")] IAsyncCollector<GalaxyDataEntity> galaxies)
{
await galaxies.AddAsync(new GalaxyDataEntity(context.Name, context.Coords));
}
As you can see, there's a special call, which schedules another orchestration within this one:
/
await context.CallSubOrchestratorAsync("Planet_Create", galaxyContext);
Let's look at this new orchestration:
/
[FunctionName("Planet_Create")]
public static async Task<string> RunImpl([OrchestrationTrigger] DurableOrchestrationContext context)
{
var activities = new List<Task>();
var number = await context.CallActivityAsync<int>("Utility_Number");
var galaxyContext = JsonConvert.DeserializeObject<JArray>(context.GetInputAsJson().ToString()).First;
for (var i = 0; i < number; i++)
{
var result = await Task.WhenAll(context.CallActivityAsync<string>("Utility_Coords"),
context.CallActivityAsync<string>("Utility_Planet_Name"),
context.CallActivityAsync<string>("Utility_Planet_Type"));
var planetContext = new CreatePlanetContext(galaxyContext.ToObject<CreateGalaxyContext>(), result[0],
result[1], (PlanetType) Enum.Parse(typeof(PlanetType), result[2]));
activities.Add(context.CallActivityAsync<int>("Planet_Create_Impl", planetContext));
}
await Task.WhenAll(activities);
return "Planet created!";
}
[FunctionName("Planet_Create_Impl")]
public static async Task CreatePlanet(
[ActivityTrigger] CreatePlanetContext context,
[Table("planet")] IAsyncCollector<PlanetDataEntity> planets)
{
await planets.AddAsync(new PlanetDataEntity(context.GalaxyContext.Name, context.Name, context.Coords, context.Type));
}
The great thing is that we can pass a full context to the sub-orchestration, so it can use data, which was obtained by the previous activities.

Summary
Sub-orchestrations are a great addition to the Durable Functions SDK, especially that they're such a simple concept. I strongly encourage you to try it all by yourself, so you can feel how powerful the concept is.