Greetings readers! Hope you all a Happy New Year!

Last post I was about running a Precompiled .NET Core Azure Function in a Container. This time let’s go one step further and Run a Durable Azure Function in a Container

Prerequisites:

Create a .NET Core lib project

Create a .NET Core lib project.

1dotnet new lib --name durable
2cd dni
3rm .\Class1.cs

Add the required references to be able to use Azure Functions API and the Durable Task Extensions

Be sure to add the following packages to the project. Sample code will not work with the latest versions of some of the Microsoft.AspNetCore.* packages, so pay special attention to the version parameter.

Note:

  • Microsoft.AspNetCore.Mvc.WebApiCompatShim is needed to use the ReadAsAsync extension (Assembly: System.Net.Http.Formatting) in one of the functions.
  • Microsoft.Azure.WebJobs.Script.ExtensionsMetadataGenerator is need to generate, during build time, the extension file needed to run durable fucntions.
1dotnet add package Microsoft.AspNetCore.Http -v 2.0.0
2dotnet add package Microsoft.AspNetCore.Mvc -v 2.0.0
3dotnet add package Microsoft.AspNetCore.Mvc.WebApiCompatShim -v 2.0.0
4dotnet add package Microsoft.Azure.WebJobs -v 3.0.0-beta4
5dotnet add package Microsoft.Azure.WebJobs.Extensions.DurableTask -v 1.0.0-beta
6dotnet add package Microsoft.Azure.WebJobs.Script.ExtensionsMetadataGenerator -v 1.0.0-beta2

Create the wwwroot and the folders needed to hold the functions

Create some foders. The first one wwwroot will hold all the functions and global configuration. The others will hold the specific files for each of the 3 azure functions you’ll deploy:

1md wwwroot
2md wwwroot/httpstart
3md wwwroot/activity
4md wwwroot/orchestrator

Create host.json file in the wwwroot folder

Create a host.json file in the wwwroot folder with the following contents:

1{ }

Create the activity function

In the wwwroot/activity folder create a functions.json file with the following contents:

 1{
 2    "bindings": [
 3        {
 4            "name": "name",
 5            "type": "activityTrigger",
 6            "direction": "in"
 7        }
 8    ],
 9    "disabled": false,
10    "scriptFile": "..//bin//durable.dll",
11    "entryPoint": "DurableFunctions.Activity.Run"
12}

Now create a Activity.cs file with the following contents:

 1using System.Collections.Generic;
 2using System.Threading.Tasks;
 3using Microsoft.Azure.WebJobs;
 4
 5namespace DurableFunctions
 6{
 7    public class Activity
 8    {
 9        public static string Run(string name)
10        {
11            return $"Hello {name}!";
12        }
13    }
14}

Note: this function uses an Activity Trigger.

Create an orchestrator function

In the wwwroot/orchestrator folder create a functions.json file with the following contents:

 1{
 2    "bindings": [
 3        {
 4            "name": "context",
 5            "type": "orchestrationTrigger",
 6            "direction": "in"
 7        }
 8    ],
 9    "disabled": false,
10    "scriptFile": "..//bin//durable.dll",
11    "entryPoint": "DurableFunctions.Orchestrator.Run"
12}

Now create a Orchestrator.cs file with the following contents:

 1using System.Collections.Generic;
 2using System.Threading.Tasks;
 3using Microsoft.Azure.WebJobs;
 4
 5namespace DurableFunctions
 6{
 7    public class Orchestrator
 8    {
 9
10        public static async Task<List<string>> Run(
11            DurableOrchestrationContext context)
12        {
13            var outputs = new List<string>();
14
15            outputs.Add(await context.CallActivityAsync<string>("activity", "Tokyo"));
16            outputs.Add(await context.CallActivityAsync<string>("activity", "Seattle"));
17            outputs.Add(await context.CallActivityAsync<string>("activity", "London"));
18
19            // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
20            return outputs;
21        }
22    }
23}

Note: this function uses an Orchestration Trigger.

Create the Orchestrator Client

In the wwwroot/httpstart folder create a functions.json file with the following contents:

 1{
 2    "bindings": [
 3        {
 4            "authLevel": "anonymous",
 5            "name": "req",
 6            "type": "httpTrigger",
 7            "direction": "in",
 8            "route": "orchestrators/{functionName}",
 9            "methods": [
10                "post"
11            ]
12        },
13        {
14            "name": "starter",
15            "type": "orchestrationClient",
16            "direction": "in"
17        }
18    ],
19    "disabled": false,
20    "scriptFile": "..//bin//durable.dll",
21    "entryPoint": "DurableFunctions.HttpStart.Run"
22}

Now create a HttpStart.cs file with the following contents:

 1using System;
 2using System.Net.Http;
 3using System.Net.Http.Headers;
 4using System.Threading.Tasks;
 5using Microsoft.Azure.WebJobs;
 6using Microsoft.Azure.WebJobs.Host;
 7
 8namespace DurableFunctions
 9{
10    public class HttpStart
11    {
12        public static async Task<HttpResponseMessage> Run(
13            HttpRequestMessage req,
14            DurableOrchestrationClient starter,
15            string functionName,
16            TraceWriter log)
17        {
18            // Function input comes from the request content.
19            dynamic eventData = await req.Content.ReadAsAsync<object>();
20            string instanceId = await starter.StartNewAsync(functionName, eventData);
21
22            log.Info($"Started orchestration with ID = '{instanceId}'.");
23
24            var res = starter.CreateCheckStatusResponse(req, instanceId);
25            res.Headers.RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10));
26            return res;
27        }
28    }
29}

Note: this function uses an http trigger and an Orchestration client.

Create a dockerfile

Create a dockerfile with the following contents:

 1FROM microsoft/azure-functions-runtime:2.0.0-jessie
 2
 3# If we don't set WEBSITE_HOSTNAME Azure Function triggers other than HttpTrigger won't run. (FunctionInvocationException: Webhooks are not configured)
 4# I've created a pull request to add this variable to the original dockerfile: https://github.com/Azure/azure-webjobs-sdk-script/pull/2285
 5ENV WEBSITE_HOSTNAME=localhost:80
 6
 7# Copy all Function files and binaries to /home/site/wwwroot
 8ADD wwwroot /home/site/wwwroot
 9
10# Functions must live in: /home/site/wwwroot
11# We expect to receive the Storage Account Connection String from the command line.
12ARG STORAGE_ACCOUNT
13
14# Set the AzureWebJobsStorage Environment Variable. Otherwise Durable Functions Extensions won't work.
15ENV AzureWebJobsStorage=$STORAGE_ACCOUNT

Note that once you execute a docker build on the file all the contents of the wwwroot folder will be copied to the /home/site/wwwroot inside the image.

Build the project and the docker image

Build the .NET Core project so you get the precompiled funtions and then build the docker image:

Note: you’ll need a valid Azure Storage Account connection string.

1dotnet build -o .\wwwroot\bin\
2docker build --build-arg STORAGE_ACCOUNT="[Valid Azure Storage Account Connection String]" -t durable .

Create a container and test the function

Run the following to run the Azure Function in a container:

1docker run -p 5000:80 durable

Now POST to the orchestrator client to start the durable functions:

1curl -X POST http://localhost:5000/api/orchestrators/orchestrator -i -H "Content-Length: 0"

You can download all code and files here.

Note: The function code is based on the original Azure Functions Samples.

Happy coding!