Skip to content

Commit 4a24bce

Browse files
authored
DSL example (#122)
1 parent 0dc8fc9 commit 4a24bce

12 files changed

+612
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Prerequisites:
2121
* [ContextPropagation](src/ContextPropagation) - Context propagation via interceptors.
2222
* [CounterInterceptor](src/CounterInterceptor/) - Simple Workflow and Client Interceptors example.
2323
* [DependencyInjection](src/DependencyInjection) - How to inject dependencies in activities and use generic hosts for workers
24+
* [Dsl](src/Dsl) - Workflow that interprets and executes workflow steps from a YAML-based DSL.
2425
* [EagerWorkflowStart](src/EagerWorkflowStart) - Demonstrates usage of Eager Workflow Start to reduce latency for workflows that start with a local activity.
2526
* [Encryption](src/Encryption) - End-to-end encryption with Temporal payload codecs.
2627
* [EnvConfig](src/EnvConfig) - Load client configuration from TOML files with programmatic overrides

TemporalioSamples.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Timer", "
101101
EndProject
102102
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.UpdatableTimer", "src\UpdatableTimer\TemporalioSamples.UpdatableTimer.csproj", "{5D02DFEA-DC08-4B7B-8E26-EDAC1942D347}"
103103
EndProject
104+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.Dsl", "src\Dsl\TemporalioSamples.Dsl.csproj", "{AF077751-E4B9-4696-93CB-74653F0BB6C4}"
105+
EndProject
104106
Global
105107
GlobalSection(SolutionConfigurationPlatforms) = preSolution
106108
Debug|Any CPU = Debug|Any CPU
@@ -603,6 +605,18 @@ Global
603605
{5D02DFEA-DC08-4B7B-8E26-EDAC1942D347}.Release|x64.Build.0 = Release|Any CPU
604606
{5D02DFEA-DC08-4B7B-8E26-EDAC1942D347}.Release|x86.ActiveCfg = Release|Any CPU
605607
{5D02DFEA-DC08-4B7B-8E26-EDAC1942D347}.Release|x86.Build.0 = Release|Any CPU
608+
{AF077751-E4B9-4696-93CB-74653F0BB6C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
609+
{AF077751-E4B9-4696-93CB-74653F0BB6C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
610+
{AF077751-E4B9-4696-93CB-74653F0BB6C4}.Debug|x64.ActiveCfg = Debug|Any CPU
611+
{AF077751-E4B9-4696-93CB-74653F0BB6C4}.Debug|x64.Build.0 = Debug|Any CPU
612+
{AF077751-E4B9-4696-93CB-74653F0BB6C4}.Debug|x86.ActiveCfg = Debug|Any CPU
613+
{AF077751-E4B9-4696-93CB-74653F0BB6C4}.Debug|x86.Build.0 = Debug|Any CPU
614+
{AF077751-E4B9-4696-93CB-74653F0BB6C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
615+
{AF077751-E4B9-4696-93CB-74653F0BB6C4}.Release|Any CPU.Build.0 = Release|Any CPU
616+
{AF077751-E4B9-4696-93CB-74653F0BB6C4}.Release|x64.ActiveCfg = Release|Any CPU
617+
{AF077751-E4B9-4696-93CB-74653F0BB6C4}.Release|x64.Build.0 = Release|Any CPU
618+
{AF077751-E4B9-4696-93CB-74653F0BB6C4}.Release|x86.ActiveCfg = Release|Any CPU
619+
{AF077751-E4B9-4696-93CB-74653F0BB6C4}.Release|x86.Build.0 = Release|Any CPU
606620
EndGlobalSection
607621
GlobalSection(SolutionProperties) = preSolution
608622
HideSolutionNode = FALSE
@@ -653,5 +667,6 @@ Global
653667
{52CE80AF-09C3-4209-8A21-6CFFAA3B2B01} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
654668
{B37B3E98-4B04-48B8-9017-F0EDEDC7BD98} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
655669
{5D02DFEA-DC08-4B7B-8E26-EDAC1942D347} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
670+
{AF077751-E4B9-4696-93CB-74653F0BB6C4} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
656671
EndGlobalSection
657672
EndGlobal

src/Dsl/DslActivities.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
namespace TemporalioSamples.Dsl;
2+
3+
using Microsoft.Extensions.Logging;
4+
using Temporalio.Activities;
5+
6+
public static class DslActivities
7+
{
8+
[Activity("activity1")]
9+
public static string Activity1(string arg)
10+
{
11+
ActivityExecutionContext.Current.Logger.LogInformation("Executing activity1 with arg: {Arg}", arg);
12+
return $"[result from activity1: {arg}]";
13+
}
14+
15+
[Activity("activity2")]
16+
public static string Activity2(string arg)
17+
{
18+
ActivityExecutionContext.Current.Logger.LogInformation("Executing activity2 with arg: {Arg}", arg);
19+
return $"[result from activity2: {arg}]";
20+
}
21+
22+
[Activity("activity3")]
23+
public static string Activity3(string arg1, string arg2)
24+
{
25+
ActivityExecutionContext.Current.Logger.LogInformation("Executing activity3 with args: {Arg1} and {Arg2}", arg1, arg2);
26+
return $"[result from activity3: {arg1} {arg2}]";
27+
}
28+
29+
[Activity("activity4")]
30+
public static string Activity4(string arg)
31+
{
32+
ActivityExecutionContext.Current.Logger.LogInformation("Executing activity4 with arg: {Arg}", arg);
33+
return $"[result from activity4: {arg}]";
34+
}
35+
36+
[Activity("activity5")]
37+
public static string Activity5(string arg1, string arg2)
38+
{
39+
ActivityExecutionContext.Current.Logger.LogInformation("Executing activity5 with args: {Arg1} and {Arg2}", arg1, arg2);
40+
return $"[result from activity5: {arg1} {arg2}]";
41+
}
42+
}

src/Dsl/DslInput.cs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using System.Text.Json.Serialization;
2+
using YamlDotNet.Serialization;
3+
4+
namespace TemporalioSamples.Dsl;
5+
6+
public record DslInput
7+
{
8+
public record ActivityInvocation
9+
{
10+
required public string Name { get; init; }
11+
12+
public IReadOnlyList<string> Arguments { get; init; } = Array.Empty<string>();
13+
14+
public string? Result { get; init; }
15+
}
16+
17+
[JsonPolymorphic(TypeDiscriminatorPropertyName = "statementType")]
18+
[JsonDerivedType(typeof(ActivityStatement), "activity")]
19+
[JsonDerivedType(typeof(SequenceStatement), "sequence")]
20+
[JsonDerivedType(typeof(ParallelStatement), "parallel")]
21+
public abstract record Statement;
22+
23+
public record ActivityStatement : Statement
24+
{
25+
required public ActivityInvocation Activity { get; init; }
26+
}
27+
28+
public record Sequence
29+
{
30+
required public IReadOnlyList<Statement> Elements { get; init; }
31+
}
32+
33+
public record SequenceStatement : Statement
34+
{
35+
required public Sequence Sequence { get; init; }
36+
}
37+
38+
public record ParallelBranches
39+
{
40+
required public IReadOnlyList<Statement> Branches { get; init; }
41+
}
42+
43+
public record ParallelStatement : Statement
44+
{
45+
required public ParallelBranches Parallel { get; init; }
46+
}
47+
48+
required public Statement Root { get; init; }
49+
50+
public Dictionary<string, object> Variables { get; init; } = new();
51+
52+
public static DslInput Parse(string yamlContent)
53+
{
54+
var deserializer = new DeserializerBuilder().Build();
55+
56+
var yamlObject = deserializer.Deserialize<Dictionary<string, object>>(yamlContent)
57+
?? throw new InvalidOperationException("Failed to parse YAML");
58+
59+
return ConvertToDslInput(yamlObject);
60+
}
61+
62+
private static DslInput ConvertToDslInput(Dictionary<string, object> yaml)
63+
{
64+
var variables = new Dictionary<string, object>();
65+
if (yaml.TryGetValue("variables", out var varsObj) && varsObj is Dictionary<object, object> varsDict)
66+
{
67+
foreach (var kvp in varsDict)
68+
{
69+
variables[kvp.Key.ToString() ?? string.Empty] = kvp.Value;
70+
}
71+
}
72+
73+
var rootObj = yaml["root"];
74+
var root = ConvertToStatement(rootObj);
75+
76+
return new DslInput { Root = root, Variables = variables };
77+
}
78+
79+
private static Statement ConvertToStatement(object obj)
80+
{
81+
if (obj is not Dictionary<object, object> dict)
82+
{
83+
throw new ArgumentException("Statement must be a dictionary");
84+
}
85+
86+
if (dict.TryGetValue("activity", out var activityObj))
87+
{
88+
return new ActivityStatement { Activity = ConvertToActivityInvocation(activityObj) };
89+
}
90+
91+
if (dict.TryGetValue("sequence", out var sequenceObj))
92+
{
93+
var seqDict = (Dictionary<object, object>)sequenceObj;
94+
var elements = ((List<object>)seqDict["elements"])
95+
.Select(ConvertToStatement)
96+
.ToList();
97+
return new SequenceStatement { Sequence = new Sequence { Elements = elements } };
98+
}
99+
100+
if (dict.TryGetValue("parallel", out var parallelObj))
101+
{
102+
var parDict = (Dictionary<object, object>)parallelObj;
103+
var branches = ((List<object>)parDict["branches"])
104+
.Select(ConvertToStatement)
105+
.ToList();
106+
return new ParallelStatement { Parallel = new ParallelBranches { Branches = branches } };
107+
}
108+
109+
throw new ArgumentException("Unknown statement type");
110+
}
111+
112+
private static ActivityInvocation ConvertToActivityInvocation(object obj)
113+
{
114+
var dict = (Dictionary<object, object>)obj;
115+
var name = dict["name"].ToString() ?? throw new ArgumentException("Activity name is required");
116+
117+
var arguments = new List<string>();
118+
if (dict.TryGetValue("arguments", out var argsObj) && argsObj is List<object> argsList)
119+
{
120+
arguments = argsList.Select(a => a.ToString() ?? string.Empty).ToList();
121+
}
122+
123+
string? result = null;
124+
if (dict.TryGetValue("result", out var resultObj))
125+
{
126+
result = resultObj.ToString();
127+
}
128+
129+
return new ActivityInvocation
130+
{
131+
Name = name,
132+
Arguments = arguments,
133+
Result = result,
134+
};
135+
}
136+
}

src/Dsl/DslWorkflow.workflow.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
namespace TemporalioSamples.Dsl;
2+
3+
using Microsoft.Extensions.Logging;
4+
using Temporalio.Workflows;
5+
6+
[Workflow]
7+
public class DslWorkflow
8+
{
9+
private readonly Dictionary<string, object> variables;
10+
11+
[WorkflowInit]
12+
public DslWorkflow(DslInput input) => variables = input.Variables;
13+
14+
[WorkflowRun]
15+
public async Task<Dictionary<string, object>> RunAsync(DslInput input)
16+
{
17+
Workflow.Logger.LogInformation("Running DSL workflow");
18+
await ExecuteStatementAsync(input.Root);
19+
Workflow.Logger.LogInformation("DSL workflow completed");
20+
return variables;
21+
}
22+
23+
private async Task ExecuteStatementAsync(DslInput.Statement statement)
24+
{
25+
switch (statement)
26+
{
27+
case DslInput.ActivityStatement stmt:
28+
// Invoke activity loading arguments from variables and optionally storing result as a variable
29+
var result = await Workflow.ExecuteActivityAsync<object>(
30+
stmt.Activity.Name,
31+
stmt.Activity.Arguments.Select(arg => variables[arg]).ToArray(),
32+
new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(1) });
33+
34+
if (!string.IsNullOrEmpty(stmt.Activity.Result))
35+
{
36+
variables[stmt.Activity.Result] = result;
37+
}
38+
break;
39+
case DslInput.SequenceStatement stmt:
40+
foreach (var element in stmt.Sequence.Elements)
41+
{
42+
await ExecuteStatementAsync(element);
43+
}
44+
break;
45+
case DslInput.ParallelStatement stmt:
46+
await Workflow.WhenAllAsync(stmt.Parallel.Branches.Select(ExecuteStatementAsync));
47+
break;
48+
default:
49+
throw new InvalidOperationException($"Unknown statement type: {statement.GetType().Name}");
50+
}
51+
}
52+
}

src/Dsl/Program.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using Microsoft.Extensions.Logging;
2+
using Temporalio.Client;
3+
using Temporalio.Client.EnvConfig;
4+
using Temporalio.Worker;
5+
using TemporalioSamples.Dsl;
6+
7+
// Create a client to localhost on default namespace
8+
var connectOptions = ClientEnvConfig.LoadClientConnectOptions();
9+
if (string.IsNullOrEmpty(connectOptions.TargetHost))
10+
{
11+
connectOptions.TargetHost = "localhost:7233";
12+
}
13+
14+
connectOptions.LoggerFactory = LoggerFactory.Create(builder =>
15+
builder.
16+
AddSimpleConsole(options => options.TimestampFormat = "[HH:mm:ss] ").
17+
SetMinimumLevel(LogLevel.Information));
18+
var client = await TemporalClient.ConnectAsync(connectOptions);
19+
20+
async Task RunWorkerAsync()
21+
{
22+
// Cancellation token cancelled on ctrl+c
23+
using var tokenSource = new CancellationTokenSource();
24+
Console.CancelKeyPress += (_, eventArgs) =>
25+
{
26+
tokenSource.Cancel();
27+
eventArgs.Cancel = true;
28+
};
29+
30+
// Run worker until cancelled
31+
Console.WriteLine("Running worker");
32+
using var worker = new TemporalWorker(
33+
client,
34+
new TemporalWorkerOptions(taskQueue: "dsl-sample")
35+
.AddAllActivities(typeof(DslActivities), null)
36+
.AddWorkflow<DslWorkflow>());
37+
try
38+
{
39+
await worker.ExecuteAsync(tokenSource.Token);
40+
}
41+
catch (OperationCanceledException)
42+
{
43+
Console.WriteLine("Worker cancelled");
44+
}
45+
}
46+
47+
async Task ExecuteWorkflowAsync(string yamlFile)
48+
{
49+
var yamlContent = await File.ReadAllTextAsync(yamlFile);
50+
var dslInput = DslInput.Parse(yamlContent);
51+
52+
Console.WriteLine($"Executing workflow from {yamlFile}");
53+
var result = await client.ExecuteWorkflowAsync(
54+
(DslWorkflow wf) => wf.RunAsync(dslInput),
55+
new(id: $"dsl-workflow-{Guid.NewGuid()}", taskQueue: "dsl-sample"));
56+
57+
Console.WriteLine("Workflow completed. Final variables:");
58+
foreach (var kvp in result)
59+
{
60+
Console.WriteLine($" {kvp.Key}: {kvp.Value}");
61+
}
62+
}
63+
64+
switch (args.ElementAtOrDefault(0))
65+
{
66+
case "worker":
67+
await RunWorkerAsync();
68+
break;
69+
case "workflow":
70+
var yamlFile = args.ElementAtOrDefault(1)
71+
?? throw new ArgumentException("Must provide YAML file path as second argument");
72+
await ExecuteWorkflowAsync(yamlFile);
73+
break;
74+
default:
75+
throw new ArgumentException("Must pass 'worker' or 'workflow' as the first argument");
76+
}

src/Dsl/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# DSL
2+
3+
This sample demonstrates a Temporal workflow that interprets and executes arbitrary workflow steps defined in a
4+
YAML-based Domain Specific Language (DSL).
5+
6+
To run, first see [README.md](../../README.md) for prerequisites. Then, run the following from this directory
7+
in a separate terminal to start the worker:
8+
9+
dotnet run worker
10+
11+
Then in another terminal, run a workflow from this directory:
12+
13+
dotnet run workflow workflow1.yaml
14+
15+
Or run the more complex parallel workflow:
16+
17+
dotnet run workflow workflow2.yaml
18+
19+
The worker terminal will show logs of activities being executed, and the workflow terminal will display the final
20+
variables after the workflow completes.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="YamlDotNet" Version="16.2.0" />
9+
</ItemGroup>
10+
11+
<ItemGroup>
12+
<None Update="workflow1.yaml">
13+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
14+
</None>
15+
<None Update="workflow2.yaml">
16+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
17+
</None>
18+
</ItemGroup>
19+
20+
</Project>

0 commit comments

Comments
 (0)