Understanding the Dapr Workflow engine & authoring workflows in .NET

In this post, you'll learn about the latest Dapr building block API, Dapr Workflow. You’ll learn how the workflow engine works, and how to author a basic workflow using C#.

Understanding the Dapr Workflow engine & authoring workflows in .NET

In this post, you'll learn about the latest Dapr building block API, Dapr Workflow. You’ll learn how the workflow engine works, and how to author a basic workflow using C#.

ℹ️ This post has been updated to reflect the breaking changes in the Dapr Workflow API from v1.10 to v1.11.

Over the years, there have been many ways to automate business processes or workflows. Some workflow management tools are based on graphical user interfaces, making it easy to author workflows, but hard to version control, or test them. Other solutions, based on json or yaml, are more suitable for version control but less developer friendly because they can be hard to write and understand the logic. In my opinion, the best workflow authoring experience is based on workflows as code. Developers can use a language they know well to describe the workflow and all its business logic. A large benefit of authoring workflows as code is that they can be unit tested, resulting in a well documented and easily maintainable code base. Code-based workflow solutions include Azure Durable Functions, Cadence, Temporal, and since Dapr release 1.10, Dapr Workflow. A great benefit of using Dapr Workflow is that you can combine this with the other Dapr building block APIs when building distributed systems.

The Dapr workflow engine

A workflow is a sequence of tasks or activities that are executed in a specific order to achieve a goal. One of the most important features of a workflow system is that the workflow execution is reliable. A workflow should always run to completion even if the workflow engine is temporarily not available.

Workflow app, Workflow engine, and state store

Dapr Workflows are stateful, the engine saves the workflow state changes to an append only log, a technique called event-sourcing, for each state changing event such as:

  • the workflow starts
  • an activity is scheduled
  • an activity is completed
  • the workflow completes

So even when the workflow engine has temporary failure, the workflow is capable of reading the historical events from the state store and continue where it was previously.

Each time a workflow activity is scheduled, the workflow itself is deactivated. Once the activity has been completed, the engine will schedule the workflow to run again. The workflow will then replay from the beginning. The engine checks against the historical events in the state store what the current state of the workflow is and continues scheduling the next activity. This will continue until all activities have been executed, and the workflow has run to completion.

Dapr workflow animation

Because the workflow engine will replay the workflow several times, the workflow code must be deterministic. This means that each time the workflow replays, the code must behave in the same way as the initial execution, and any arguments used as activity inputs should stay the same.

Any non-deterministic code should go into activities. This includes, calling other (Dapr) services, state stores, pub/sub systems, and external endpoints. The workflow should only be responsible for the business logic that defines the sequence of the activities.

The execution of the workflow is completely asynchronous. When the workflow API start method is called, the Dapr workflow engine uses Dapr actors internally for the scheduling and execution of the workflow and its activities. Here is an example request that starts an instance of a workflow named HelloWorldWorkflow, using a workflow instance ID of 1234a and an input of World:


POST {{dapr_url}}/v1.0-alpha1/workflows/dapr/HelloWorldWorkflow/start?instanceID=1234a
Content-Type: application/text/plain

"World"   

Note that the instanceID parameter is optional. If no instanceID is provided a random ID will be generated.

When a workflow is started, the response is HTTP 202 (Accepted), returning the workflow instance ID as the payload:



HTTP/1.1 202 Accepted
Date: Mon, 22 May 2023 09:44:31 GMT
Content-Type: application/json
Content-Length: 23
Traceparent: 00-96845dbb8d1a3ea94eabc4ffed068df9-8a4391c78413f22e-01
Connection: close

{
  "instanceID": "1234a"
}
    

If a workflow returns a result, this result can be retrieved by executing a GET request that includes the name and instance ID of the workflow:



GET {{dapr_url}}/v1.0-alpha1/workflows/dapr/1234a
    

This results in the following response that contains the input and output, and some other metadata, such as workflow name, runtime status, last updated and custom status:



HTTP/1.1 202 Accepted
Date: Mon, 22 May 2023 09:48:46 GMT
Content-Type: application/json
Content-Length: 328
Traceparent: 00-a552826161307190b5caecc478ff8801-3fcf2dfa124f73ab-01
Connection: close

{
    "instanceID": "",
    "workflowName": "HelloWorldWorkflow",
    "createdAt": "2023-06-19T13:19:18.316956600Z",
    "lastUpdatedAt": "2023-06-19T13:19:18.333226200Z",
    "runtimeStatus": "COMPLETED",
    "properties": {
        "dapr.workflow.custom_status": "",
        "dapr.workflow.input": "\"World\"",
        "dapr.workflow.output": "\"Ciao World\""
				}
}    

Writing a HelloWorld workflow

Let’s author a very minimal workflow using C# .NET to see what steps are involved. We’ll create a HelloWorldWorkflow that will call just one activity: CreateGreetingActivity. This activity takes a string as an input, prepends a random greeting to it, and returns the combined string.

HelloWorld Workflow

Normally, a workflow involves calling multiple activities, but in this case, we focus on the authoring experience instead of building a realistic example. Future blog posts are going to cover more examples and realistic use cases.

If you don’t feel like building this from scratch, but want to run the workflow immediately, take a look at the HelloWorldWorkflow in this GitHub repo.

Prerequisites

  1. .NET 7 SDK
  2. Docker Desktop
  3. Dapr CLI (v1.11 or higher)
  4. A REST client, such as cURL, or the VSCode REST client extension.

1. Create a new ASP.Net project

Using the terminal, create a new folder named BasicWorkflowSamples, open the folder and create a new ASP.NET Core web project:



mkdir BasicWorkflowSamples 
cd BasicWorkflowSamples 
dotnet new web

You now have an empty ASP.Net Core web application named BasicWorkflowSamples.csproj.

2. Add the Dapr.Workflow package

We’ll be using the Dapr .NET SDK to author our workflow and activity. The workflow types we need are in a NuGet package called Dapr.Workflow that needs to be added to the project:



dotnet add package Dapr.Workflow

After installation of the package, the BasicWorkflowSamples.csproj file should look like this:

  

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Dapr.Workflow" Version="1.11.0" />
  </ItemGroup>

</Project> 

3. Write the CreateGreetingActivity

Let start with writing the CreateGreetingActivity. Open the folder or project in your IDE.

1. Create a new class file named CreateGreetingActivity.cs.

2.Create a new class named <inline-h>CreateGreetingActivity<inline-h> and inherit from <inline-h>WorkflowActivity<inline-h>, one of the abstract types from the Dapr.Workflow package. The <inline-h>WorkflowActivity<inline-h> class uses generic input and output types. In this case, both input and output are of type <inline-h>string<inline-h>, so use <inline-h>WorkflowActivity<string, string><inline-h>.

3. The <inline-h>WorkflowActivity<inline-h> class has one abstract method that requires implementing: <inline-h>RunAsync<inline-h>. Add the empty implementation of this method:



using Dapr.Workflow;

namespace BasicWorkflowSamples
{
    public class CreateGreetingActivity : WorkflowActivity
    {
        public override Task RunAsync(WorkflowActivityContext context, string input)
        {
            throw new NotImplementedException();
        }
    }
}

4. Add the following implementation of the <inline-h>RunAsync<inline-h> function that selects a random greeting from an array of greetings and prepends this to the input:



upublic override Task RunAsync(WorkflowActivityContext context, string input)
{
    var greetings = new []{"Hello", "Hi", "Hey", "Hola", "Bonjour", "Ciao", "Guten Tag", "Konnichiwa"};
    var selectedGreeting = greetings[new Random().Next(0, greetings.Length)];
    var message = $"{selectedGreeting} {input}";

    return Task.FromResult(message);
}

4. Write the HelloWorldWorkflow

Now, let's write the workflow that will call the <inline-h>CreateGreetingActivity<inline-h>.

1. Create a new class file named HelloWorldWorkflow.cs.

2. Create a new class named <inline-h>HelloWorldWorkflow<inline-h> and inherit from <inline-h>Workflow<inline-h>, one of the abstract types from the Dapr.Workflow package. The Workflow class uses the same pattern as the <inline-h>WorkflowActivity<inline-h> for its input, output and <inline-h>RunAsync<inline-h> method definition.

3. The empty <inline-h>HelloWorldWorkflow<inline-h> definition looks like this:



using Dapr.Workflow;

namespace BasicWorkflowSamples
{
    public class CreateGreetingActivity : WorkflowActivity
    {
        public override Task RunAsync(WorkflowActivityContext context, string input)
        {
            throw new NotImplementedException();
        }
    }
}

4. Update the <inline-h>RunAsync<inline-h> method to call the <inline-h>CreateGreetingActivity<inline-h>:



public override Task RunAsync(WorkflowActivityContext context, string input)
{
    var greetings = new []{"Hello", "Hi", "Hey", "Hola", "Bonjour", "Ciao", "Guten Tag", "Konnichiwa"};
    var selectedGreeting = greetings[new Random().Next(0, greetings.Length)];
    var message = $"{selectedGreeting} {input}";

    return Task.FromResult(message);
}

5. The WorkflowContext type contains the CallActivityAsync method, which is used to schedule and execute activities. This context contains many other methods and properties that I’ll cover in future blog posts.

Note that the <inline-h>CallActivityAsync<inline-h> call is awaited, so add the <inline-h>async<inline-h> keyword to the method definition. The await also means that as soon as this activity is scheduled, the workflow is deactivated. Once the <inline-h>CreateGreetingActivity<inline-h> is finished, the workflow will replay, and the engine will detect that the activity has been executed before and will then complete the workflow.

5. Register the workflow and activity types

Although the workflow and activity classes are written, the Dapr runtime needs to be aware that these types are available to the Workflow engine, and the workflow can be invoked via the API. This is done by registering the workflow and activity types in the startup code of the application.

1. Update the Program.cs file as follows:



using BasicWorkflowSamples;
using Dapr.Workflow;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprWorkflow(options =>
{
    options.RegisterWorkflow();
    options.RegisterActivity();
});

// Dapr uses a random port for gRPC by default. If we don't know what that port
// is (because this app was started separate from dapr), then assume 50001.
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DAPR_GRPC_PORT")))
{
    Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", "50001");
}

var app = builder.Build();

app.Run(); 

2. Build the ASP.NET Core web app and verify that there are no issues:



dotnet build

6. Run the workflow

1. Ensure that Docker Desktop is running and the dapr_redis container has started. Redis is used as the underlying state store that this HelloWorldWorkflow example is using. Other Dapr state stores can be used as long as they support actors.

2. Check the Profiles/lauchsettings.json file and check the port number of the applicationUrl (http). Use this port for the <inline-h>--app-port<inline-h> in the following command to run the ASP.NET Core app using the Dapr CLI:



dapr run --app-id basic-workflows --app-port  --dapr-http-port 3500 dotnet run

For example: if the applicationUrl is <inline-h>http://localhost:5065<inline-h> use <inline-h>5065<inline-h> when running the Dapr CLI:



dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 dotnet run

3. Dapr will output plenty of information to the terminal. Once you see this:



== APP ==       Sidecar work-item streaming connection established.

a connection between the application and the Dapr workflow engine has now been established, and the workflow can be started.

4. Using your favorite HTTP client, make the following POST request to the Dapr HTTP endpoint to start a new instance of the HelloWorldWorkflow using 12345 as the unique identifier of this instance:



POST http://localhost:3500/v1.0-alpha1/workflows/dapr/HelloWorldWorkflow/start?instanceID=1234a
Content-Type: application/text/plain

"World"

The unique identifier for the workflow, also called instance ID, is something that you need to generate when starting workflows. It’s a good practice to use GUIDs for these identifiers to guarantee their uniqueness.

The expected response should be a 202 Accepted and the body should contain the workflow instance ID:



HTTP/1.1 202 Accepted
Date: Mon, 22 May 2023 14:43:28 GMT
Content-Type: application/json
Content-Length: 23
Traceparent: 00-83ba72818555dd7f6b1bb3bf4e16291a-950b7166e75cb9e1-01
Connection: close

{
"instanceID": "1234a"
}

5. To obtain the result of the workflow, make a GET request to the Dapr HTTP endpoint:



GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234a

The expected response should be a 202 Accepted and the body should contain the workflow metadata:



HTTP/1.1 202 Accepted
Date: Mon, 22 May 2023 14:44:05 GMT
Content-Type: application/json
Content-Length: 330
Traceparent: 00-e62531d26f800c3d3ca28dbe05290b2b-829bdfa93a77aac8-01
Connection: close

	{
			"instanceID": "",
			"workflowName": "HelloWorldWorkflow",
			"createdAt": "2023-06-19T13:19:18.316956600Z",
			"lastUpdatedAt": "2023-06-19T13:19:18.333226200Z",
			"runtimeStatus": "COMPLETED",
			"properties": {
					"dapr.workflow.custom_status": "",
					"dapr.workflow.input": "\"World\"",
					"dapr.workflow.output": "\"Ciao World\""
					}
	}
  

Note that if the workflow has run successfully, the runtime_status field should say <inline-h>COMPLETED<inline-h>. If the workflow did not run successfully, the field will say <inline-h>FAILED<inline-h>.

6. If you want to see more details of the workflow execution, either look at the Dapr console output or take a look at the Zipkin dashboard, which should be running at http://localhost:9411/zipkin/. Check for an entry named basic-workflows: create_orchestration||helloworldworkflow and expand it:

Use Zipkin to visualize the workflow and the performance

This shows the orchestration and all the activities, in this case only one, and the time it took to execute them. Zipkin can be a great tool to understand the performance of your workflow.

Next steps

In this post, you’ve seen how the Dapr workflow engine works and what the required steps are for authoring a minimal workflow in .NET. In the next post, I’ll cover different workflow patterns, such as chaining, fan-out/fan-in, monitor, and external system interaction.

Do you have any questions or comments about this blog post or the code? Join the Dapr discord and post a message in the<inline-h> #workflow<inline-h> channel. Have you made something with Dapr? Post a message in the <inline-h>#show-and-tell<inline-h> channel, we love to see your creations!

Resources

Diagrid Newsletter

Subscribe today for exclusive Dapr insights