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:

1. Create a .NET Core lib project#

Create a .NET Core lib project.

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

2. 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

3. 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

4. Create host.json file in the wwwroot folder#

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

1{ }

5. 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.

6. 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.

7. 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.

8. 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.

9. 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 .

10. 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!