Fresh perspectives about distributed application development await you in the State of Dapr 2023 survey - Download the report now
Fresh perspectives about distributed application development await you in the State of Dapr 2023 survey - Download the report now
Marc Duiker

Marc Duiker

May 25 2023

Understanding the Dapr Workflow engine & authoring workflows in .NET

post image

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": "<WORKFLOW_ID>",
"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 CreateGreetingActivity and inherit from WorkflowActivity, one of the abstract types from the Dapr.Workflow package. The WorkflowActivity class uses generic input and output types. In this case, both input and output are of type string, so use WorkflowActivity<string, string>.

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

    using Dapr.Workflow;
    namespace BasicWorkflowSamples
    {
    public class CreateGreetingActivity : WorkflowActivity<string, string>
    {
    public override Task<string> RunAsync(WorkflowActivityContext context, string input)
    {
    throw new NotImplementedException();
    }
    }
    }
  4. Add the following implementation of the RunAsync function that selects a random greeting from an array of greetings and prepends this to the input:

    public override Task<string> 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 CreateGreetingActivity.

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

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

  3. The empty HelloWorldWorkflow definition looks like this:

    using Dapr.Workflow;
    namespace BasicWorkflowSamples
    {
    public class HelloWorldWorkflow : Workflow<string, string>
    {
    public override Task<string> RunAsync(WorkflowContext context, string input)
    {
    throw new NotImplementedException();
    }
    }
    }
  4. Update the RunAsync method to call the CreateGreetingActivity:

    public async override Task<string> RunAsync(WorkflowContext context, string input)
    {
    var message = await context.CallActivityAsync<string>(
    nameof(CreateGreetingActivity),
    input);
    return message;
    }

    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 CallActivityAsync call is awaited, so add the async keyword to the method definition. The await also means that as soon as this activity is scheduled, the workflow is deactivated. Once the CreateGreetingActivity 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<HelloWorldWorkflow>();
    options.RegisterActivity<CreateGreetingActivity>();
    });
    // 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 --app-port in the following command to run the ASP.NET Core app using the Dapr CLI:

    dapr run --app-id basic-workflows --app-port <APP_PORT> --dapr-http-port 3500 dotnet run

    For example: if the applicationUrl is http://localhost:5065 use 5065 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": "<WORKFLOW_ID>",
    "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 COMPLETED. If the workflow did not run successfully, the field will say FAILED.

  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 #workflow channel. Have you made something with Dapr? Post a message in the #show-and-tell channel, we love to see your creations!

Resources

misce leftmisce left (sm)misce left (lg)

Newsletter

Subscribe and get the latest news and updates from us.