Skip to content

Commit 33d93f8

Browse files
committed
- Add initial production readme.
- Update if statement indentation in Task.WhenEach of generated code.
1 parent 4e9b3f4 commit 33d93f8

10 files changed

Lines changed: 233 additions & 48 deletions

File tree

AsyncTaskOrchestratorGenerator.UnitTests/Orchestrator.cs

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,40 +8,49 @@ namespace TestLibrary;
88

99
internal class Orchestrator
1010
{
11-
private readonly TestLibrary.One one;
12-
private readonly TestLibrary.Two two;
13-
private readonly TestLibrary.Three three;
14-
private readonly TestLibrary.Four four;
15-
private readonly TestLibrary.Final final;
16-
17-
public Orchestrator(TestLibrary.One one, TestLibrary.Two two, TestLibrary.Three three, TestLibrary.Four four, TestLibrary.Final final)
11+
private readonly TestLibrary.A a;
12+
private readonly TestLibrary.B b;
13+
private readonly TestLibrary.C c;
14+
private readonly TestLibrary.D d;
15+
private readonly TestLibrary.E e;
16+
private readonly TestLibrary.F f;
17+
18+
public Orchestrator(TestLibrary.A a, TestLibrary.B b, TestLibrary.C c, TestLibrary.D d, TestLibrary.E e, TestLibrary.F f)
1819
{
19-
this.one = one;
20-
this.two = two;
21-
this.three = three;
22-
this.four = four;
23-
this.final = final;
20+
this.a = a;
21+
this.b = b;
22+
this.c = c;
23+
this.d = d;
24+
this.e = e;
25+
this.f = f;
2426
}
2527

2628
public async Task<int> Execute()
2729
{
28-
var resultOneTask = one.FuncOne();
29-
var resultTwoTask = two.FuncTwo();
30-
var resultThreeTask = new System.Threading.Tasks.Task<int>(() => default);
31-
var resultFourTask = four.FuncFour();
30+
var resultATask = a.CallA();
31+
var resultBTask = b.CallB();
32+
var resultCTask = new System.Threading.Tasks.Task<int>(() => default);
33+
var resultDTask = d.CallD();
34+
var resultETask = new System.Threading.Tasks.Task<int>(() => default);
3235

33-
var tasksToProcess = new List<Task> { resultOneTask, resultTwoTask, resultFourTask };
36+
var tasksToProcess = new List<Task> { resultATask, resultBTask, resultDTask };
3437

3538
await foreach (var completed in Task.WhenEach(tasksToProcess))
3639
{
37-
if (!resultThreeTask.IsCompleted && resultOneTask.IsCompleted && resultTwoTask.IsCompleted)
40+
if (!resultCTask.IsCompleted && resultATask.IsCompleted && resultBTask.IsCompleted)
41+
{
42+
resultCTask = c.CallC(resultATask.Result, resultBTask.Result);
43+
tasksToProcess.Add(resultCTask);
44+
}
45+
46+
if (!resultETask.IsCompleted && resultDTask.IsCompleted)
3847
{
39-
resultThreeTask = three.FuncThree(resultOneTask.Result, resultTwoTask.Result);
40-
tasksToProcess.Add(resultThreeTask);
48+
resultETask = e.CallE(resultDTask.Result);
49+
tasksToProcess.Add(resultETask);
4150
}
4251
}
4352

44-
var finalResult = await final.FuncFinal(resultThreeTask.Result, resultFourTask.Result);
53+
var finalResult = await f.CallF(resultCTask.Result, resultETask.Result);
4554

4655
return finalResult;
4756
}

AsyncTaskOrchestratorGenerator/OutputGenerator.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ private static string FormatExecuteMethod(ExecuteMethodSignatureData signatureDa
139139

140140
var formattedWhenEach = $@"await foreach (var completed in Task.WhenEach(tasksToProcess))
141141
{{
142-
{ string.Join(@"", formattedHandleTaskCompletions)}
142+
{ string.Join(@"
143+
144+
", formattedHandleTaskCompletions)}
143145
}}";
144146

145147
var dependencyTaskName = finalTaskData.DependenciesOutputNames.Select(depName => data[depName].TaskName);

README.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,158 @@
11
# async-task-orchestrator-generator
2+
23
C# source generator for executing dependent async tasks optimally and easily.
4+
5+
Orchestrating multiple asynchronous operations in C# can be challenging,
6+
especially when optimizing for parallel execution of I/O tasks such as REST API
7+
service calls to microservices.
8+
The optimization challenge is even greater when the operations have dependencies on
9+
each other.
10+
11+
This source generator simplifies the implementation by enabling developers
12+
to easily define the relationship between async tasks via their inputs and outputs.
13+
This generator then uses the specification provided by the developer to generates
14+
optimal code that maximizes parallel execution of the tasks from an I/O standpoint.
15+
This includes immediately handling tasks as they complete.
16+
17+
## Requirements
18+
19+
The generated code uses features from .NET 9.
20+
21+
## Motivation Example
22+
23+
Here is a sequence diagram representing an example orchestration of API calls
24+
to multiple services, where some calls depend on the results of others:
25+
26+
```mermaid
27+
sequenceDiagram
28+
par My to A
29+
My Service ->> Service A:
30+
and My to B
31+
My Service ->> Service B:
32+
and My to D
33+
My Service ->> Service D:
34+
end
35+
36+
Service A -->> My Service: A Result
37+
Service B -->> My Service: B Result
38+
39+
My Service ->> Service C: A Result, B Result
40+
Service C -->> My Service: C Result
41+
42+
My Service ->> Service D:
43+
Service D -->> My Service: D Result
44+
45+
My Service ->> Service E: D Result
46+
Service E -->> My Service: E Result
47+
48+
My Service ->> Service F: C Result, E Result
49+
Service E -->> My Service: F Result
50+
```
51+
52+
As shown in the diagram, calls to A, B, and D can be called in parallel. Once both
53+
A and B complete, C can be called with the responses from A and B. When D returns,
54+
E can be called. Once both C and E return their responses, F can finally be
55+
called with the responses from C and E.
56+
57+
Beginning to try to implement this in C#:
58+
59+
```c#
60+
var taskA = a.CallA();
61+
var taskB = b.CallB();
62+
var taskD = d.CallD();
63+
64+
await Task.WhenAll(taskA, taskB);
65+
66+
var resultA = taskA.Result;
67+
var resultB = taskB.Result;
68+
69+
var taskC = c.CallC(resultA, resultB);
70+
71+
var resultD = await taskD;
72+
73+
var taskE = e.CallE(resultD);
74+
75+
await Task.WhenAll(taskC, taskE);
76+
77+
return await f.CallF(taskC.Result, taskD.Result);
78+
```
79+
80+
While the above code works, it is not optimal from an I/O perspective. D could complete
81+
before A and B while they are being awaited. In this case, E cannot be called until
82+
A and B complete, even though D has already completed.
83+
84+
A more complex implementation is needed to handle tasks as they complete. It is
85+
feasible, but as you'll see in the next section, this source generator can greatly
86+
simplify the implementation.
87+
88+
## Using the Source Generator
89+
90+
Create a specification class, decorated with the `AsyncTaskOrchestrator` attribute, that defines the async tasks to be orchestrated.
91+
This spec is a [domain specific language](https://en.wikipedia.org/wiki/Domain-specific_language)
92+
(DSL) within C# that specifies task dependencies via which task outputs should be used as inputs
93+
to other tasks.
94+
95+
```c#
96+
using AsyncTaskOrchestratorGenerator;
97+
98+
namespace TestLibrary;
99+
100+
[AsyncTaskOrchestrator]
101+
internal class OrchestratorSpec
102+
{
103+
private readonly A a;
104+
private readonly B b;
105+
private readonly C c;
106+
private readonly D d;
107+
private readonly E e;
108+
private readonly F f;
109+
110+
public OrchestratorSpec(A a, B b, C c, D d, E e, F f)
111+
{
112+
this.a = a;
113+
this.b = b;
114+
this.c = c;
115+
this.d = d;
116+
this.e = e;
117+
this.f = f;
118+
}
119+
120+
public Task<int> Spec()
121+
{
122+
var resultA = a.CallA();
123+
var resultB = b.CallB();
124+
var resultC = c.CallC(resultA.Result, resultB.Result);
125+
var resultD = d.CallD();
126+
var resultE = e.CallE(resultD.Result);
127+
return f.CallF(resultC.Result, resultE.Result);
128+
}
129+
}
130+
131+
```
132+
133+
Notice that the call to D is specified after the call to C. This will not affect
134+
the execution order optimization. In other words, as long as the code compiles,
135+
the call order does not matter (which means one less thing to worry about!).
136+
137+
Also take note that there is no need to manage `await` calls or figuring out which
138+
`Task.*` method to call.
139+
140+
The source generator will generate an orchestrator class based on the spec provided
141+
in the same namespace. The class name and the method name can be specified in the
142+
attribute's parameters. The default class name is `Orchestrator` and the default method name
143+
is `Execute`.
144+
145+
Example usage of the generated orchestrator:
146+
147+
```c#
148+
var a = new A();
149+
var b = new B();
150+
var c = new C();
151+
var d = new D();
152+
var e = new E();
153+
var f = new F();
154+
155+
var orchestrator = new Orchestrator(a, b, c, d, e, f);
156+
157+
var result = await orchestrator.Execute();
158+
```
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
namespace TestLibrary;
22

3-
public class One
3+
public class A
44
{
5-
public async Task<int> FuncOne()
5+
public async Task<int> CallA()
66
{
77
System.Diagnostics.Debug.WriteLine("FuncOne started");
88

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
namespace TestLibrary;
22

3-
public class Two
3+
public class B
44
{
5-
public async Task<int> FuncTwo()
5+
public async Task<int> CallB()
66
{
77
System.Diagnostics.Debug.WriteLine("FuncTwo started");
88

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
namespace TestLibrary;
22

3-
public class Three
3+
public class C
44
{
5-
public async Task<int> FuncThree(int inputOne, int inputTwo)
5+
public async Task<int> CallC(int inputOne, int inputTwo)
66
{
77
System.Diagnostics.Debug.WriteLine("FuncThree started");
88

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
namespace TestLibrary;
22

3-
public class Four
3+
public class D
44
{
5-
public async Task<int> FuncFour()
5+
public async Task<int> CallD()
66
{
77
System.Diagnostics.Debug.WriteLine("FuncFour started");
88

TestLibrary/E.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace TestLibrary;
2+
3+
public class E
4+
{
5+
public async Task<int> CallE(int input)
6+
{
7+
System.Diagnostics.Debug.WriteLine("FuncFour started");
8+
9+
await Task.Delay(4000);
10+
11+
System.Diagnostics.Debug.WriteLine("FuncFour ended");
12+
13+
return await Task.FromResult(5 + input);
14+
}
15+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
namespace TestLibrary;
22

3-
public class Final
3+
public class F
44
{
5-
public async Task<int> FuncFinal(int inputOne, int inputTwo)
5+
public async Task<int> CallF(int inputOne, int inputTwo)
66
{
77
System.Diagnostics.Debug.WriteLine("FuncFinal started");
88
return await Task.FromResult(inputOne + inputTwo);

TestLibrary/OrchestratorSpec.cs

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,30 @@ namespace TestLibrary;
55
[AsyncTaskOrchestrator]
66
internal class OrchestratorSpec
77
{
8-
private readonly One one;
9-
private readonly Two two;
10-
private readonly Three three;
11-
private readonly Four four;
12-
private readonly Final final;
8+
private readonly A a;
9+
private readonly B b;
10+
private readonly C c;
11+
private readonly D d;
12+
private readonly E e;
13+
private readonly F f;
1314

14-
public OrchestratorSpec(One one, Two two, Three three, Four four, Final final)
15+
public OrchestratorSpec(A a, B b, C c, D d, E e, F f)
1516
{
16-
this.one = one;
17-
this.two = two;
18-
this.three = three;
19-
this.four = four;
20-
this.final = final;
17+
this.a = a;
18+
this.b = b;
19+
this.c = c;
20+
this.d = d;
21+
this.e = e;
22+
this.f = f;
2123
}
2224

2325
public Task<int> Spec()
2426
{
25-
var resultOne = one.FuncOne();
26-
var resultTwo = two.FuncTwo();
27-
var resultThree = three.FuncThree(resultOne.Result, resultTwo.Result);
28-
var resultFour = four.FuncFour();
29-
return final.FuncFinal(resultThree.Result, resultFour.Result);
27+
var resultA = a.CallA();
28+
var resultB = b.CallB();
29+
var resultC = c.CallC(resultA.Result, resultB.Result);
30+
var resultD = d.CallD();
31+
var resultE = e.CallE(resultD.Result);
32+
return f.CallF(resultC.Result, resultE.Result);
3033
}
3134
}

0 commit comments

Comments
 (0)