Azure Durable Functions in short

arun paul
C# Programming
Published in
7 min readJul 20, 2023

Azure Durable functions are meant for handling workflows that takes longer to finish. It also helps to make the run stateful, so that it can track the status of a run and restart from where it stops, if at all.

And you guessed it right, that’s why we assign always a storage account in association with a durable function when you deploy the function in Azure. The storage account holds the data to determine the state of the system. Thankfully you don’t really need to configure literally anything at all when you start to develop your solution.

Visual Studio makes it even easier with the Azure function boilerplate templates and in-built tools to locally develop and run a function.

No wait, just jump in and create a project using Visual Studio.

Prerequisites
Visual Studio 2022
Dotnet 6 Runtime Azure SDK

Use function boiler template in Visual Studio

Select the option of Durable Function.

Check the Azurite tool when you create your project. This will help to simulate the Storage Account in local, when you develop the function.

Other options:

  • StorageSimulator
  • Use connection string of a Storage Account itself.

The solution looks like similar to this 👇

The local.settings.json

{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
}
}

"AzureWebJobsStorage": "UseDevelopmentStorage=true" is the line that compensates for the local Storage Account (lack of Storage Account 😊).

The Function1.cs has got 3 functions.

using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Logging;

namespace FunctionApp1
{
public static class Function1
{
[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;
}

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

[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);
}
}
}

Start with the Starter function with attribute [FunctionName("Function1_HttpStart")]. This is a normal Http triggered azure function, and is used to invoke the Orchestrator which is actually the function or we can say the brain of the durable function logic.

The starter function has a special parameter starter of type IDurableOrchestrationClient and has got an attriibute [DurableClient] and this Starter is used as a durable client to call the Orchestrator function.


// Function input comes from the request content.
string instanceId = await starter.StartNewAsync("Function1", null);

The first parameter is the name of the function to be called. Here the boiler plate Orchestrator name is Function1.

Then we pass a null value, if the instance Id can be randomized. This case an instance value is auto-generated and passed along to the Durable function.

I can also pass the request parameter, if I want to.

I have re-written the code a little bit. And kept the different types of functions a bit organized. I have moved the HTTP functions to a HttpFunctions.cs and folderized.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace DurableVideoProcessor.HttpFunctions
{
public static class HttpFunction
{

[FunctionName("HttpStarterFunction")]
public static async Task<IActionResult> HttpStart(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req,
[DurableClient] IDurableOrchestrationClient starter,
ILogger log)
{
var video = req.GetQueryParameterDictionary()["video"];
// Function input comes from the request content.
string instanceId = await starter.StartNewAsync("DurableVideoProcessorOrchestrator", null, video);

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

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

Same goes for OrchestratorFunction.cs

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;

namespace DurableVideoProcessor.OrchestratorFunctions
{
public static class OrchestratorFunction
{
[FunctionName(nameof(DurableVideoProcessorOrchestrator))]
public static async Task<List<string>> DurableVideoProcessorOrchestrator(
[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(ActivityFunctions.ActivityFunction.SayHello), "Tokyo"));
outputs.Add(await context.CallActivityAsync<string>(nameof(ActivityFunctions.ActivityFunction.SayHello), "Seattle"));
outputs.Add(await context.CallActivityAsync<string>(nameof(ActivityFunctions.ActivityFunction.SayHello), "London"));

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


}

Similarly, for ActivityFunctions functions:

using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Extensions.Logging;

namespace DurableVideoProcessor.ActivityFunctions
{
public static class ActivityFunction
{

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

Solution looks much cleaner now.

The solution looks like similar to this 👆

Let’s simulate now some realistic video processing workflows.

We are going to define some simple workflow activity functions, related to the video processing….

Let’s update the activity functions.

Rename the Activity Functions class as ProcessVideo and add 3 activities as workflow functions.

using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Threading.Tasks;

namespace DurableVideoProcessor.ActivityFunctions
{
public static class ProcessVideo
{

[FunctionName(nameof(Transcode))]
public static async Task<string> Transcode([ActivityTrigger] string inputVideo, ILogger log)
{
log.LogInformation($"Transcoding {inputVideo}.");
// Simulte transcoding
await Task.Delay(5000);
return $"{Path.GetFileNameWithoutExtension(inputVideo)}-transcoded.mp4";
}

[FunctionName(nameof(Thumbnail))]
public static async Task<string> Thumbnail([ActivityTrigger] string inputVideo, ILogger log)
{
log.LogInformation($"Thumbnailing {inputVideo}.");
// Simulte thumbnailing
await Task.Delay(5000);
return $"{Path.GetFileNameWithoutExtension(inputVideo)}-thumbnailed.mp4";
}

[FunctionName(nameof(PrependIntro))]
public static async Task<string> PrependIntro([ActivityTrigger] string inputVideo, ILogger log)
{
var introLocation = Environment.GetEnvironmentVariable("IntroLocation");
log.LogInformation($"Prepending intro into {introLocation} to {inputVideo}.");
// Simulte transcoding
await Task.Delay(5000);
return $"{Path.GetFileNameWithoutExtension(inputVideo)}-withintro.mp4";
}
}
}

Let’s build the code for Orchestrator now....

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Client;

namespace DurableVideoProcessor.OrchestratorFunctions
{
public static class OrchestratorFunction
{
[FunctionName(nameof(DurableVideoProcessorOrchestrator))]
public static async Task<object> DurableVideoProcessorOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context, ILogger log)
{
log = context.CreateReplaySafeLogger(log);
var videoLocation = context.GetInput<string>();

log.LogInformation("Orchestrator starts the transcode and goes to sleep...");
var transcodelLocation = await context.CallActivityAsync<string>(nameof(ActivityFunctions.ProcessVideo.Transcode), videoLocation);

log.LogInformation("Orchestrator starts the thumbnailing and goes to sleep...");
var thumpnailLocation = await context.CallActivityAsync<string>(nameof(ActivityFunctions.ProcessVideo.Thumbnail), transcodelLocation);

log.LogInformation("Orchestrator starts the intro preluding and goes to sleep...");
var introLocation = await context.CallActivityAsync<string>(nameof(ActivityFunctions.ProcessVideo.PrependIntro), transcodelLocation);

return new
{
Transcoded = transcodelLocation,
Thumpnail = thumpnailLocation,
Intro = introLocation
};
}
}


}

The Starter function looks almost the same…

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace DurableVideoProcessor.HttpFunctions
{
public static class HttpFunction
{

[FunctionName("HttpStarterFunction")]
public static async Task<IActionResult> HttpStart(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req,
[DurableClient] IDurableOrchestrationClient starter,
ILogger log)
{
var inputVideo = req.GetQueryParameterDictionary()["video"];
// Function input comes from the request content.
string instanceId = await starter.StartNewAsync("DurableVideoProcessorOrchestrator", null, inputVideo);

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

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

Okay…It’s time to run the function in Visual Studio.

Azure Functions Core Tools
Core Tools Version: 4.0.5198 Commit hash: N/A (64-bit)
Function Runtime Version: 4.21.1.20667

[2023-07-21T19:32:50.122Z] Found C:\My\Code\source\repos\Azure Functions\DurableVideoProcessor\DurableVideoProcessor.csproj. Using for user secrets file configuration.

Functions:

HttpStarterFunction: [GET,POST] http://localhost:7153/api/HttpStarterFunction

DurableVideoProcessorOrchestrator: orchestrationTrigger

PrependIntro: activityTrigger

Thumbnail: activityTrigger

Transcode: activityTrigger

For detailed output, run func with --verbose flag.
[2023-07-21T19:32:59.160Z] Host lock lease acquired by instance ID '000000000000000000000000A8EB91B9'.

Thankfully, it gives the trigger url to start the function

curl  http://localhost:7153/api/HttpStarterFunction?video=http://this-is-a-test.com

You can see the output terminal logs, saying the execution of the workflow in order….

[2023-07-21T19:38:09.654Z] Executing 'HttpStarterFunction' (Reason='This function was programmatically called via the host APIs.', Id=b8694f9a-bb71-408e-a467-80225a2b7171)
[2023-07-21T19:38:10.312Z] Started orchestration with ID = '3f1b0acbe5fa4f4b9214da00e2b5b244'.
[2023-07-21T19:38:10.387Z] Executed 'HttpStarterFunction' (Succeeded, Id=b8694f9a-bb71-408e-a467-80225a2b7171, Duration=783ms)
[2023-07-21T19:38:10.608Z] Executing 'DurableVideoProcessorOrchestrator' (Reason='(null)', Id=effd49ae-1aa9-40cb-9b71-73cdf70f67a9)
[2023-07-21T19:38:10.687Z] Orchestrator starts the transcode and goes to sleep...
[2023-07-21T19:38:10.730Z] Executed 'DurableVideoProcessorOrchestrator' (Succeeded, Id=effd49ae-1aa9-40cb-9b71-73cdf70f67a9, Duration=127ms)
[2023-07-21T19:38:11.057Z] Executing 'Transcode' (Reason='(null)', Id=282af283-d69f-4fc4-a63c-b0ca86ae839e)
[2023-07-21T19:38:11.112Z] Transcoding http://this-is-a-test.com.
[2023-07-21T19:38:16.117Z] Executed 'Transcode' (Succeeded, Id=282af283-d69f-4fc4-a63c-b0ca86ae839e, Duration=5110ms)
[2023-07-21T19:38:16.413Z] Executing 'DurableVideoProcessorOrchestrator' (Reason='(null)', Id=f2af3e03-3f46-41e7-afe3-3ffa81a630aa)
[2023-07-21T19:38:16.429Z] Orchestrator starts the thumbnailing and goes to sleep...
[2023-07-21T19:38:16.435Z] Executed 'DurableVideoProcessorOrchestrator' (Succeeded, Id=f2af3e03-3f46-41e7-afe3-3ffa81a630aa, Duration=21ms)
[2023-07-21T19:38:16.611Z] Executing 'Thumbnail' (Reason='(null)', Id=673ae0ce-97cb-42fe-baed-1a65bba26644)
[2023-07-21T19:38:16.631Z] Thumbnailing this-is-a-test-transcoded.mp4.
[2023-07-21T19:38:21.686Z] Executed 'Thumbnail' (Succeeded, Id=673ae0ce-97cb-42fe-baed-1a65bba26644, Duration=5075ms)
[2023-07-21T19:38:21.987Z] Executing 'DurableVideoProcessorOrchestrator' (Reason='(null)', Id=b91c1c8c-2161-447c-a445-2bb1fd026612)
[2023-07-21T19:38:21.993Z] Orchestrator starts the intro preluding and goes to sleep...
[2023-07-21T19:38:21.995Z] Executed 'DurableVideoProcessorOrchestrator' (Succeeded, Id=b91c1c8c-2161-447c-a445-2bb1fd026612, Duration=9ms)
[2023-07-21T19:38:22.162Z] Executing 'PrependIntro' (Reason='(null)', Id=3deccb47-e058-4b33-899d-14b1a6337eb3)
[2023-07-21T19:38:22.236Z] Prepending intro into to this-is-a-test-transcoded.mp4.
[2023-07-21T19:38:27.271Z] Executed 'PrependIntro' (Succeeded, Id=3deccb47-e058-4b33-899d-14b1a6337eb3, Duration=5110ms)
[2023-07-21T19:38:27.555Z] Executing 'DurableVideoProcessorOrchestrator' (Reason='(null)', Id=eef8a294-9a59-40c0-b903-1b7b13ec003a)
[2023-07-21T19:38:27.575Z] Executed 'DurableVideoProcessorOrchestrator' (Succeeded, Id=eef8a294-9a59-40c0-b903-1b7b13ec003a, Duration=20ms)

We can see that we could you could easily develop and run a simple long running(in short) workflow using a durable function implementation.

Please try out different complex patterns in durable function to make use of the full power.

Happy coding….

Originally published at https://iarunpaul.github.io on July 20, 2023.

--

--

arun paul
C# Programming

Evergreen student who could experience the application design and development, devops, software configuration and build/release management