Skip to content

Workflow tutorials C# #1182

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3ce6f18
Add C# TaskChaining
marcduiker Mar 18, 2025
4f59915
Add C# FanOutFanIn
marcduiker Mar 18, 2025
eaeaaae
Add C# Monitor
marcduiker Mar 19, 2025
2da416e
Add C# ExternalEvent
marcduiker Mar 19, 2025
b2d70f1
Add C# ChildWorkflows
marcduiker Mar 20, 2025
72668f5
Add C# CombinedPatterns
marcduiker Mar 20, 2025
8ee7b05
Add C# WorkflowManagement
marcduiker Mar 21, 2025
9cc59c6
Fix port numbers
marcduiker Mar 21, 2025
53996a5
Add C# Basics
marcduiker Mar 21, 2025
ca2f2ad
Add READMEs
marcduiker Mar 21, 2025
7ed9923
Update READMEs
marcduiker Mar 21, 2025
4bc32fd
Update folder structure
marcduiker Mar 21, 2025
b2f3dbc
Update Basic Workflow
marcduiker Mar 21, 2025
790c73e
Add BasicWorkflow comments
marcduiker Mar 24, 2025
551f81e
Add mermaid diagrams
marcduiker Mar 24, 2025
a44a0bd
Add code comments
marcduiker Mar 25, 2025
2f154ea
Merge branch 'master' into workflow-tutorials
marcduiker Mar 25, 2025
d2eeaac
Combine pattern examples is working
marcduiker Mar 26, 2025
2f67550
Add diagram
marcduiker Mar 26, 2025
92ffc2e
Add README content for challenges& tips
marcduiker Mar 26, 2025
9c04f72
Update to .NET 9
marcduiker Mar 31, 2025
9562e15
Use file based namespaces, make classes internal sealed
marcduiker Apr 1, 2025
39fcfcf
Make classes internal sealed
marcduiker Apr 1, 2025
45d3a43
Fix paths for multi-app run
marcduiker Apr 1, 2025
c369d3d
Merge branch 'master' into workflow-tutorials
WhitWaldo Apr 1, 2025
7214b5b
Add workflow input and outputs to READMEs
marcduiker Apr 14, 2025
c3906d1
Add resiliency-and-compensation
marcduiker Apr 14, 2025
29443b0
Add mechanical markdown
marcduiker Apr 15, 2025
5c3e991
Add make files
marcduiker Apr 15, 2025
c47bd4d
Update prerequisites
marcduiker Apr 15, 2025
fffb2c2
Add workflow validation to GH WF
marcduiker Apr 15, 2025
762e1d2
PR review updates
marcduiker Apr 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/validate_tutorials.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,60 @@ jobs:
# pushd tutorials/observability
# make validate
# popd
- name: Validate workflow/csharp/child-workflows
if: matrix.os == 'ubuntu-latest'
run: |
pushd tutorials/workflow/csharp/child-workflows
make validate
popd
- name: Validate workflow/csharp/combined-patterns
if: matrix.os == 'ubuntu-latest'
run: |
pushd tutorials/workflow/csharp/combined-patterns
make validate
popd
- name: Validate workflow/csharp/external-system-interaction
if: matrix.os == 'ubuntu-latest'
run: |
pushd tutorials/workflow/csharp/external-system-interaction
make validate
popd
- name: Validate workflow/csharp/fan-out-fan-in
if: matrix.os == 'ubuntu-latest'
run: |
pushd tutorials/workflow/csharp/fan-out-fan-in
make validate
popd
- name: Validate workflow/csharp/fundamentals
if: matrix.os == 'ubuntu-latest'
run: |
pushd tutorials/workflow/csharp/fundamentals
make validate
popd
- name: Validate workflow/csharp/monitor-pattern
if: matrix.os == 'ubuntu-latest'
run: |
pushd tutorials/workflow/csharp/monitor-pattern
make validate
popd
- name: Validate workflow/csharp/resiliency-and-compensation
if: matrix.os == 'ubuntu-latest'
run: |
pushd tutorials/workflow/csharp/resiliency-and-compensation
make validate
popd
- name: Validate workflow/csharp/task-chaining
if: matrix.os == 'ubuntu-latest'
run: |
pushd tutorials/workflow/csharp/task-chaining
make validate
popd
- name: Validate workflow/csharp/workflow-management
if: matrix.os == 'ubuntu-latest'
run: |
pushd tutorials/workflow/csharp/workflow-management
make validate
popd
- name: Linkcheck README.md
run: |
make validate
24 changes: 24 additions & 0 deletions tutorials/workflow/csharp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Using the Dapr Workflow API with C#

This folder contains tutorials of using the Dapr Workflow API with C#. All examples can be run locally on your machine.

Before you start, it's recommended to read though the Dapr docs to get familiar with the many [Workflow features, concepts, and patterns](https://docs.dapr.io/developing-applications/building-blocks/workflow/).

## Prerequisites

- [Docker Desktop](https://www.docker.com/products/docker-desktop/)
- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) & [Initialization](https://docs.dapr.io/getting-started/install-dapr-selfhost/)
- [.NET 9](https://dotnet.microsoft.com/download/dotnet/9.0)

## Tutorials

- [Workflow Basics](./fundamentals/README.md)
- [Task Chaining](./task-chaining/README.md)
- [Fan-out/Fan-in](./fan-out-fan-in/README.md)
- [Monitor](./monitor-pattern/README.md)
- [External Events](./external-system-interaction/README.md)
- [Child Workflows](./child-workflows/README.md)
- [Resiliency & Compensation](./resiliency-and-compensation/README.md)
- [Combined Patterns](./combined-patterns/README.md)
- [WorkflowManagement](./workflow-management/README.md)
- [Challenges & Tips](./challenges-tips/README.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using Dapr.Workflow;

namespace WorkflowApp;

internal sealed class NonDeterministicWorkflow : Workflow<string, string>
{
public override async Task<string> RunAsync(WorkflowContext context, string orderItem)
{
// Do not use non-deterministic operations in a workflow!
// These operations will create a new value every time the
// workflow is replayed.
var orderId = Guid.NewGuid().ToString();
var orderDate = DateTime.UtcNow;

var idResult = await context.CallActivityAsync<string>(
"SubmitId",
orderId);

await context.CallActivityAsync<string>(
"SubmitDate",
orderDate);

return idResult;
}
}

internal sealed class DeterministicWorkflow : Workflow<string, string>
{
public override async Task<string> RunAsync(WorkflowContext context, string orderItem)
{
// Do use these deterministic methods and properties on the WorkflowContext instead.
// These operations create the same value when the workflow is replayed.
var orderId = context.NewGuid().ToString();
var orderDate = context.CurrentUtcDateTime;

var idResult = await context.CallActivityAsync<string>(
"SubmitId",
orderId);

await context.CallActivityAsync<string>(
"SubmitDate",
orderDate);

return idResult;
}
}
19 changes: 19 additions & 0 deletions tutorials/workflow/csharp/challenges-tips/IdempotentActivity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Dapr.Client;
using Dapr.Workflow;

namespace WorkflowApp;

internal sealed class IdempotentActivity : WorkflowActivity<string, string>
{
public override async Task<InventoryResult> RunAsync(WorkflowActivityContext context, OrderItem orderItem)
{
// Beware of non-idempotent operations in an activity.
// Dapr Workflow guarantees at-least-once execution of activities, so activities might be executed more than once
// in case an activity is not ran to completion successfully.
// For instance, can you insert a record to a database twice without side effects?
// var insertSql = $"INSERT INTO Orders (Id, Description, UnitPrice, Quantity) VALUES ('{orderItem.Id}', '{orderItem.Description}', {orderItem.UnitPrice}, {orderItem.Quantity})";
// It's best to perform a check if an record already exists before inserting it.
}
}

internal sealed record OrderItem(string Id, string Description, double UnitPrice, int Quantity);
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Dapr.Workflow;

namespace WorkflowApp;

internal sealed class LargePayloadSizeWorkflow : Workflow<string, LargeDocument>
{
public override async Task<LargeDocument> RunAsync(WorkflowContext context, string id)
{
// Do not pass large payloads between activities.
// They are stored in the Dapr state store twice, one as output argument
// for GetDocument, and once as input argument for UpdateDocument.
var document = await context.CallActivityAsync<LargeDocument>(
"GetDocument",
id);

var updatedDocument = await context.CallActivityAsync<LargeDocument>(
"UpdateDocument",
document);

// More activities to process the updated document...

return updatedDocument;
}
}


public class SmallPayloadSizeWorkflow : Workflow<string, string>
{
public override async Task<string> RunAsync(WorkflowContext context, string id)
{
// Do pass small payloads between activities, preferably IDs only, or objects that are quick to (de)serialize in large volumes.
// Combine multiple actions, such as document retrieval and update, into a single activity.
var documentId = await context.CallActivityAsync<string>(
"GetAndUpdateDocument",
id);

// More activities to process the updated document...

return documentId;
}
}

internal sealed record LargeDocument(string Id, object Data);
10 changes: 10 additions & 0 deletions tutorials/workflow/csharp/challenges-tips/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Workflow Challenges & Tips

Workflow systems are very powerful tools but also have their challenges & limitations as described in the [Dapr docs](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-features-concepts/#limitations).

This section provides some tips with code snippets to understand the limitations and get the most out of the Dapr Workflow API.

- [Deterministic workflows](DeterministicWorkflow.cs)
- [Idempotent activities](IdempotentActivity.cs)
- [Versioning workflows](VersioningWorkflow.cs)
- [Workflow & activity payload size](PayloadSizeWorkflow.cs)
50 changes: 50 additions & 0 deletions tutorials/workflow/csharp/challenges-tips/VersioningWorkflow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Dapr.Workflow;

namespace WorkflowApp;

/// <summary>
/// This is the initial version of the workflow.
/// Note that the input argument for both activities is the orderItem (string).
/// </summary>
internal sealed class VersioningWorkflow1 : Workflow<string, int>
{
public override async Task<int> RunAsync(WorkflowContext context, string orderItem)
{
var resultA = await context.CallActivityAsync<int>(
"ActivityA",
orderItem);

var resultB = await context.CallActivityAsync<int>(
"ActivityB",
orderItem);

return resultA + resultB;
}
}

/// <summary>
/// This is the updated version of the workflow.
/// The input for ActivityB has changed from orderItem (string) to resultA (int).
/// If there are in-flight workflow instances that were started with the previous version
/// of this workflow, these will fail when the new version of the workflow is deployed
/// and the workflow name remains the same, since the runtime parameters do not match with the persisted state.
/// It is recommended to version workflows by creating a new workflow class with a new name:
/// {workflowname}1 -> {workflowname}2
/// Try to avoid making breaking changes in perpetual workflows (that use the `ContinueAsNew` method)
/// since these are difficult to replace with a new version.
/// </summary>
internal sealed class VersioningWorkflow2 : Workflow<string, int>
{
public override async Task<int> RunAsync(WorkflowContext context, string orderItem)
{
var resultA = await context.CallActivityAsync<int>(
"ActivityA",
orderItem);

var resultB = await context.CallActivityAsync<int>(
"ActivityB",
resultA);

return resultA + resultB;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Dapr.Workflow;

namespace ChildWorkflows.Activities;

internal sealed class Activity1 : WorkflowActivity<string, string>
{
public override Task<string> RunAsync(WorkflowActivityContext context, string input)
{
Console.WriteLine($"{nameof(Activity1)}: Received input: {input}.");
return Task.FromResult($"{input} is processed" );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Dapr.Workflow;

namespace ChildWorkflows.Activities;

internal sealed class Activity2 : WorkflowActivity<string, string>
{
public override Task<string> RunAsync(WorkflowActivityContext context, string input)
{
Console.WriteLine($"{nameof(Activity2)}: Received input: {input}.");
return Task.FromResult($"{input} as a child workflow." );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ChildWorkflows.Activities;
using Dapr.Workflow;

namespace ChildWorkflows;
internal sealed class ChildWorkflow : Workflow<string, string>
{
public override async Task<string> RunAsync(WorkflowContext context, string input)
{
var result1 = await context.CallActivityAsync<string>(
nameof(Activity1),
input);
var childWorkflowResult = await context.CallActivityAsync<string>(
nameof(Activity2),
result1);

return childWorkflowResult;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

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

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

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Dapr.Workflow;
using ChildWorkflows.Activities;

namespace ChildWorkflows;

internal sealed class ParentWorkflow : Workflow<string[], string[]>
{
public override async Task<string[]> RunAsync(WorkflowContext context, string[] input)
{
List<Task<string>> childWorkflowTasks = [];

foreach (string item in input)
{
childWorkflowTasks.Add(context.CallChildWorkflowAsync<string>(
nameof(ChildWorkflow),
item));
}

var allChildWorkflowResults = await Task.WhenAll(childWorkflowTasks);

return allChildWorkflowResults;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using ChildWorkflows;
using ChildWorkflows.Activities;
using Dapr.Workflow;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprWorkflow(options =>
{
options.RegisterWorkflow<ParentWorkflow>();
options.RegisterWorkflow<ChildWorkflow>();
options.RegisterActivity<Activity1>();
options.RegisterActivity<Activity2>();
});
var app = builder.Build();

app.MapPost("/start", async (
string[] input,
DaprWorkflowClient workflowClient) =>
{
var instanceId = await workflowClient.ScheduleNewWorkflowAsync(
name: nameof(ParentWorkflow),
input: input);

return Results.Accepted(instanceId);
});

app.Run();
Loading
Loading