|
1 | 1 | # async-task-orchestrator-generator |
| 2 | + |
2 | 3 | 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 | +``` |
0 commit comments