@cratis/pi 7.102.0 → 7.103.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package/corpus/harnesses/copilot/instructions/profiles.instructions.md +10 -1
- package/package/corpus/harnesses/copilot/instructions/pull-requests.instructions.md +2 -0
- package/package/corpus/harnesses/copilot/instructions/secrets.instructions.md +4 -0
- package/package/corpus/hooks/README.md +11 -1
- package/package/corpus/hooks/scripts/cratis-check-pr.mjs +12 -1
- package/package/corpus/hooks/scripts/cratis-guard-secrets.sh +79 -23
- package/package/corpus/hooks/scripts/cratis-guard-writes.sh +145 -40
- package/package/corpus/rules/profiles.md +10 -1
- package/package/corpus/rules/pull-requests.md +2 -0
- package/package/corpus/rules/secrets.md +4 -0
- package/package/corpus/skills/analyzing-dotnet-performance/LICENSE +23 -0
- package/package/corpus/skills/analyzing-dotnet-performance/NOTICE +36 -0
- package/package/corpus/skills/analyzing-dotnet-performance/SKILL.md +214 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/async-patterns.md +122 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/collections-and-linq.md +220 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/critical-patterns.md +291 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/io-and-serialization.md +135 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/memory-and-strings.md +219 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/regex-patterns.md +95 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/structural-patterns.md +38 -0
- package/package/corpus/skills/binlog-generation/LICENSE +23 -0
- package/package/corpus/skills/binlog-generation/NOTICE +22 -0
- package/package/corpus/skills/binlog-generation/SKILL.md +129 -0
- package/package/corpus/skills/check-bin-obj-clash/LICENSE +23 -0
- package/package/corpus/skills/check-bin-obj-clash/NOTICE +27 -0
- package/package/corpus/skills/check-bin-obj-clash/SKILL.md +127 -0
- package/package/corpus/skills/check-bin-obj-clash/references/common-causes-and-fixes.md +206 -0
- package/package/corpus/skills/coverage-analysis/LICENSE +23 -0
- package/package/corpus/skills/coverage-analysis/NOTICE +32 -0
- package/package/corpus/skills/coverage-analysis/SKILL.md +198 -0
- package/package/corpus/skills/coverage-analysis/references/guidelines.md +68 -0
- package/package/corpus/skills/coverage-analysis/references/output-format.md +91 -0
- package/package/corpus/skills/coverage-analysis/references/report-generation.md +58 -0
- package/package/corpus/skills/coverage-analysis/references/setup-discovery.md +307 -0
- package/package/corpus/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 +80 -0
- package/package/corpus/skills/coverage-analysis/scripts/CoverageData.ps1 +272 -0
- package/package/corpus/skills/coverage-analysis/scripts/Extract-MethodCoverage.ps1 +65 -0
- package/package/corpus/skills/cratis-screenplay-command-surface/SKILL.md +26 -1
- package/package/corpus/skills/cratis-screenplay-projections/SKILL.md +17 -0
- package/package/corpus/skills/cratis-screenplay-render-and-gap-fill/SKILL.md +17 -0
- package/package/corpus/skills/cratis-screenplay-specifications/SKILL.md +8 -0
- package/package/corpus/skills/cratis-screenplay-toolchain/SKILL.md +1 -1
- package/package/corpus/skills/cratis-screenplay-toolchain/references/cheat-sheet-example.md +1 -0
- package/package/corpus/skills/cratis-screenplay-toolchain/references/fast-path.md +7 -0
- package/package/corpus/skills/cratis-screenplay-toolchain/references/traps.md +1 -0
- package/package/corpus/skills/cratis-screenplay-ui-composition/SKILL.md +40 -0
- package/package/corpus/skills/cratis-screenplay-ui-composition/references/response-continuations.md +1 -0
- package/package/corpus/skills/cratis-screenplay-ui-composition/references/worked-flow.md +1 -0
- package/package/corpus/skills/cratis-stage-rendering-and-sandbox/references/admission.md +3 -2
- package/package/corpus/skills/csharp-refactoring/LICENSE +23 -0
- package/package/corpus/skills/csharp-refactoring/NOTICE +22 -0
- package/package/corpus/skills/csharp-refactoring/SKILL.md +175 -0
- package/package/corpus/skills/csharp-refactoring/references/operation-catalog.md +37 -0
- package/package/corpus/skills/directory-build-organization/LICENSE +23 -0
- package/package/corpus/skills/directory-build-organization/NOTICE +21 -0
- package/package/corpus/skills/directory-build-organization/SKILL.md +205 -0
- package/package/corpus/skills/directory-build-organization/references/common-patterns.md +56 -0
- package/package/corpus/skills/directory-build-organization/references/multi-level-examples.md +167 -0
- package/package/corpus/skills/directory-build-organization/references/targetframework-props-pitfall.md +54 -0
- package/package/corpus/skills/dotnet-aot-compat/LICENSE +23 -0
- package/package/corpus/skills/dotnet-aot-compat/NOTICE +18 -0
- package/package/corpus/skills/dotnet-aot-compat/SKILL.md +265 -0
- package/package/corpus/skills/dotnet-aot-compat/references/polyfills.md +43 -0
- package/package/corpus/skills/dotnet-trace-collect/LICENSE +23 -0
- package/package/corpus/skills/dotnet-trace-collect/NOTICE +65 -0
- package/package/corpus/skills/dotnet-trace-collect/SKILL.md +116 -0
- package/package/corpus/skills/dotnet-trace-collect/references/dotnet-monitor.md +97 -0
- package/package/corpus/skills/dotnet-trace-collect/references/dotnet-trace-collect-linux.md +84 -0
- package/package/corpus/skills/dotnet-trace-collect/references/dotnet-trace-collect.md +91 -0
- package/package/corpus/skills/dotnet-trace-collect/references/perfcollect.md +200 -0
- package/package/corpus/skills/dotnet-trace-collect/references/perfview.md +141 -0
- package/package/corpus/skills/dotnet-trace-collect/references/tool-selection.md +160 -0
- package/package/corpus/skills/dump-collect/LICENSE +23 -0
- package/package/corpus/skills/dump-collect/NOTICE +40 -0
- package/package/corpus/skills/dump-collect/SKILL.md +97 -0
- package/package/corpus/skills/dump-collect/references/container-dumps.md +412 -0
- package/package/corpus/skills/dump-collect/references/coreclr-dumps.md +108 -0
- package/package/corpus/skills/dump-collect/references/nativeaot-dumps.md +280 -0
- package/package/corpus/skills/eliminate-redundant-restores/LICENSE +23 -0
- package/package/corpus/skills/eliminate-redundant-restores/NOTICE +19 -0
- package/package/corpus/skills/eliminate-redundant-restores/SKILL.md +112 -0
- package/package/corpus/skills/extension-points/LICENSE +23 -0
- package/package/corpus/skills/extension-points/NOTICE +18 -0
- package/package/corpus/skills/extension-points/SKILL.md +248 -0
- package/package/corpus/skills/filter-syntax/LICENSE +23 -0
- package/package/corpus/skills/filter-syntax/NOTICE +17 -0
- package/package/corpus/skills/filter-syntax/SKILL.md +179 -0
- package/package/corpus/skills/including-generated-files/LICENSE +23 -0
- package/package/corpus/skills/including-generated-files/NOTICE +17 -0
- package/package/corpus/skills/including-generated-files/SKILL.md +139 -0
- package/package/corpus/skills/microbenchmarking/LICENSE +23 -0
- package/package/corpus/skills/microbenchmarking/NOTICE +36 -0
- package/package/corpus/skills/microbenchmarking/SKILL.md +131 -0
- package/package/corpus/skills/microbenchmarking/references/bdn-internals-and-tuning.md +143 -0
- package/package/corpus/skills/microbenchmarking/references/comparison-strategies.md +200 -0
- package/package/corpus/skills/microbenchmarking/references/diagnosers-and-exporters.md +150 -0
- package/package/corpus/skills/microbenchmarking/references/project-setup-and-running.md +189 -0
- package/package/corpus/skills/microbenchmarking/references/writing-benchmarks.md +310 -0
- package/package/corpus/skills/msbuild/LICENSE +23 -0
- package/package/corpus/skills/msbuild/NOTICE +55 -0
- package/package/corpus/skills/msbuild/SKILL.md +117 -0
- package/package/corpus/skills/msbuild/references/antipatterns/additional-antipatterns.md +291 -0
- package/package/corpus/skills/msbuild/references/antipatterns/incremental-build-inputs-outputs.md +57 -0
- package/package/corpus/skills/msbuild/references/antipatterns/private-assets.md +29 -0
- package/package/corpus/skills/msbuild/references/binlog-failure-analysis.md +85 -0
- package/package/corpus/skills/msbuild/references/binlog-generation.md +69 -0
- package/package/corpus/skills/msbuild/references/build-perf-baseline.md +493 -0
- package/package/corpus/skills/msbuild/references/build-perf-diagnostics.md +268 -0
- package/package/corpus/skills/msbuild/references/copy-to-output-directory.md +183 -0
- package/package/corpus/skills/msbuild/references/eval-performance.md +190 -0
- package/package/corpus/skills/msbuild/references/extension-points.md +281 -0
- package/package/corpus/skills/msbuild/references/incremental-build.md +365 -0
- package/package/corpus/skills/msbuild/references/msbuild-antipatterns.md +401 -0
- package/package/corpus/skills/orleans/LICENSE +21 -0
- package/package/corpus/skills/orleans/NOTICE +64 -0
- package/package/corpus/skills/orleans/SKILL.md +140 -0
- package/package/corpus/skills/orleans/references/anti-patterns.md +660 -0
- package/package/corpus/skills/orleans/references/configuration-api.md +933 -0
- package/package/corpus/skills/orleans/references/examples.md +66 -0
- package/package/corpus/skills/orleans/references/grain-api.md +784 -0
- package/package/corpus/skills/orleans/references/grains.md +75 -0
- package/package/corpus/skills/orleans/references/hosting.md +71 -0
- package/package/corpus/skills/orleans/references/implementation.md +591 -0
- package/package/corpus/skills/orleans/references/official-docs-index.md +250 -0
- package/package/corpus/skills/orleans/references/patterns.md +811 -0
- package/package/corpus/skills/orleans/references/persistence-api.md +495 -0
- package/package/corpus/skills/orleans/references/primitive-selection.md +35 -0
- package/package/corpus/skills/orleans/references/scheduling-and-services.md +295 -0
- package/package/corpus/skills/orleans/references/serialization-api.md +299 -0
- package/package/corpus/skills/orleans/references/streaming-api.md +396 -0
- package/package/corpus/skills/orleans/references/testing-patterns.md +210 -0
- package/package/corpus/skills/platform-detection/LICENSE +23 -0
- package/package/corpus/skills/platform-detection/NOTICE +17 -0
- package/package/corpus/skills/platform-detection/SKILL.md +230 -0
- package/package/corpus/skills/platform-detection/references/command-mode.md +27 -0
- package/package/corpus/skills/profiling/LICENSE +21 -0
- package/package/corpus/skills/profiling/NOTICE +50 -0
- package/package/corpus/skills/profiling/SKILL.md +144 -0
- package/package/corpus/skills/profiling/references/commands.md +439 -0
- package/package/corpus/skills/profiling/references/patterns.md +586 -0
- package/package/corpus/skills/profiling/references/profiling.md +190 -0
- package/package/corpus/skills/run-tests/LICENSE +23 -0
- package/package/corpus/skills/run-tests/NOTICE +19 -0
- package/package/corpus/skills/run-tests/SKILL.md +305 -0
- package/package/corpus/skills/target-authoring/LICENSE +23 -0
- package/package/corpus/skills/target-authoring/NOTICE +17 -0
- package/package/corpus/skills/target-authoring/SKILL.md +165 -0
- package/package/corpus/skills/vectorization/LICENSE +23 -0
- package/package/corpus/skills/vectorization/NOTICE +17 -0
- package/package/corpus/skills/vectorization/SKILL.md +189 -0
- package/package/profile-catalog.json +85 -1
- package/package.json +1 -1
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
# Orleans Patterns
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- Grain Patterns
|
|
6
|
+
- Persistence Patterns
|
|
7
|
+
- Streaming Patterns
|
|
8
|
+
- Coordination Patterns
|
|
9
|
+
- Performance Patterns
|
|
10
|
+
- Testing Patterns
|
|
11
|
+
|
|
12
|
+
Detailed patterns for building robust Orleans applications.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Grain Patterns
|
|
17
|
+
|
|
18
|
+
### Stateless Worker Grain
|
|
19
|
+
|
|
20
|
+
Use for stateless operations that can be parallelized across silos.
|
|
21
|
+
|
|
22
|
+
```csharp
|
|
23
|
+
[StatelessWorker(maxLocalWorkers: 4)]
|
|
24
|
+
public class ImageProcessorGrain : Grain, IImageProcessorGrain
|
|
25
|
+
{
|
|
26
|
+
public Task<byte[]> ResizeImage(byte[] imageData, int width, int height)
|
|
27
|
+
{
|
|
28
|
+
// CPU-bound work distributed across workers
|
|
29
|
+
return Task.FromResult(ImageLib.Resize(imageData, width, height));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**When to use:**
|
|
35
|
+
- CPU-bound stateless operations
|
|
36
|
+
- Request distribution across cluster
|
|
37
|
+
- No per-identity state needed
|
|
38
|
+
|
|
39
|
+
### Singleton Grain
|
|
40
|
+
|
|
41
|
+
Ensure only one instance exists in the cluster.
|
|
42
|
+
|
|
43
|
+
```csharp
|
|
44
|
+
public interface ILeaderboardGrain : IGrainWithIntegerKey
|
|
45
|
+
{
|
|
46
|
+
Task<List<LeaderboardEntry>> GetTop(int count);
|
|
47
|
+
Task Submit(string playerId, int score);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Usage: Always use key 0 by convention
|
|
51
|
+
var leaderboard = grainFactory.GetGrain<ILeaderboardGrain>(0);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**When to use:**
|
|
55
|
+
- Global coordination
|
|
56
|
+
- Cluster-wide configuration
|
|
57
|
+
- Rate limiting across cluster
|
|
58
|
+
|
|
59
|
+
### Observer Pattern
|
|
60
|
+
|
|
61
|
+
Push notifications from grains to clients.
|
|
62
|
+
|
|
63
|
+
```csharp
|
|
64
|
+
// Observer interface
|
|
65
|
+
public interface IGameObserver : IGrainObserver
|
|
66
|
+
{
|
|
67
|
+
void OnGameStateChanged(GameState state);
|
|
68
|
+
void OnPlayerJoined(string playerId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Observable grain
|
|
72
|
+
public class GameGrain : Grain, IGameGrain
|
|
73
|
+
{
|
|
74
|
+
private readonly ObserverManager<IGameObserver> _observers;
|
|
75
|
+
|
|
76
|
+
public GameGrain()
|
|
77
|
+
{
|
|
78
|
+
_observers = new ObserverManager<IGameObserver>(
|
|
79
|
+
TimeSpan.FromMinutes(5), // Expiration
|
|
80
|
+
this.GetLogger<GameGrain>());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public Task Subscribe(IGameObserver observer)
|
|
84
|
+
{
|
|
85
|
+
_observers.Subscribe(observer, observer);
|
|
86
|
+
return Task.CompletedTask;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public Task Unsubscribe(IGameObserver observer)
|
|
90
|
+
{
|
|
91
|
+
_observers.Unsubscribe(observer);
|
|
92
|
+
return Task.CompletedTask;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private void NotifyStateChange(GameState state)
|
|
96
|
+
{
|
|
97
|
+
_observers.Notify(o => o.OnGameStateChanged(state));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Grain Call Filter
|
|
103
|
+
|
|
104
|
+
Intercept grain calls for cross-cutting concerns.
|
|
105
|
+
|
|
106
|
+
```csharp
|
|
107
|
+
public class LoggingGrainCallFilter : IIncomingGrainCallFilter
|
|
108
|
+
{
|
|
109
|
+
private readonly ILogger<LoggingGrainCallFilter> _logger;
|
|
110
|
+
|
|
111
|
+
public LoggingGrainCallFilter(ILogger<LoggingGrainCallFilter> logger)
|
|
112
|
+
{
|
|
113
|
+
_logger = logger;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public async Task Invoke(IIncomingGrainCallContext context)
|
|
117
|
+
{
|
|
118
|
+
var grainType = context.Grain.GetType().Name;
|
|
119
|
+
var methodName = context.ImplementationMethod.Name;
|
|
120
|
+
|
|
121
|
+
_logger.LogInformation("Entering {Grain}.{Method}", grainType, methodName);
|
|
122
|
+
|
|
123
|
+
var sw = Stopwatch.StartNew();
|
|
124
|
+
try
|
|
125
|
+
{
|
|
126
|
+
await context.Invoke();
|
|
127
|
+
_logger.LogInformation("{Grain}.{Method} completed in {Elapsed}ms",
|
|
128
|
+
grainType, methodName, sw.ElapsedMilliseconds);
|
|
129
|
+
}
|
|
130
|
+
catch (Exception ex)
|
|
131
|
+
{
|
|
132
|
+
_logger.LogError(ex, "{Grain}.{Method} failed after {Elapsed}ms",
|
|
133
|
+
grainType, methodName, sw.ElapsedMilliseconds);
|
|
134
|
+
throw;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Registration
|
|
140
|
+
silo.AddIncomingGrainCallFilter<LoggingGrainCallFilter>();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Persistence Patterns
|
|
146
|
+
|
|
147
|
+
### Multiple Named States
|
|
148
|
+
|
|
149
|
+
Store different aspects of grain state separately.
|
|
150
|
+
|
|
151
|
+
```csharp
|
|
152
|
+
public class PlayerGrain : Grain, IPlayerGrain
|
|
153
|
+
{
|
|
154
|
+
private readonly IPersistentState<PlayerProfile> _profile;
|
|
155
|
+
private readonly IPersistentState<PlayerInventory> _inventory;
|
|
156
|
+
private readonly IPersistentState<PlayerProgress> _progress;
|
|
157
|
+
|
|
158
|
+
public PlayerGrain(
|
|
159
|
+
[PersistentState("profile", "profiles")]
|
|
160
|
+
IPersistentState<PlayerProfile> profile,
|
|
161
|
+
[PersistentState("inventory", "items")]
|
|
162
|
+
IPersistentState<PlayerInventory> inventory,
|
|
163
|
+
[PersistentState("progress", "progress")]
|
|
164
|
+
IPersistentState<PlayerProgress> progress)
|
|
165
|
+
{
|
|
166
|
+
_profile = profile;
|
|
167
|
+
_inventory = inventory;
|
|
168
|
+
_progress = progress;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public async Task UpdateProfile(string displayName)
|
|
172
|
+
{
|
|
173
|
+
_profile.State.DisplayName = displayName;
|
|
174
|
+
await _profile.WriteStateAsync(); // Only profile is persisted
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Conditional Persistence
|
|
180
|
+
|
|
181
|
+
Write state only when needed.
|
|
182
|
+
|
|
183
|
+
```csharp
|
|
184
|
+
public class CounterGrain : Grain, ICounterGrain
|
|
185
|
+
{
|
|
186
|
+
private readonly IPersistentState<CounterState> _state;
|
|
187
|
+
private int _transientCount;
|
|
188
|
+
private const int PersistThreshold = 100;
|
|
189
|
+
|
|
190
|
+
public async Task Increment()
|
|
191
|
+
{
|
|
192
|
+
_state.State.Count++;
|
|
193
|
+
_transientCount++;
|
|
194
|
+
|
|
195
|
+
// Batch persistence for performance
|
|
196
|
+
if (_transientCount >= PersistThreshold)
|
|
197
|
+
{
|
|
198
|
+
await _state.WriteStateAsync();
|
|
199
|
+
_transientCount = 0;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public override async Task OnDeactivateAsync(
|
|
204
|
+
DeactivationReason reason, CancellationToken ct)
|
|
205
|
+
{
|
|
206
|
+
// Always persist on deactivation
|
|
207
|
+
if (_transientCount > 0)
|
|
208
|
+
{
|
|
209
|
+
await _state.WriteStateAsync();
|
|
210
|
+
}
|
|
211
|
+
await base.OnDeactivateAsync(reason, ct);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Event Sourcing Pattern
|
|
217
|
+
|
|
218
|
+
Store events instead of current state.
|
|
219
|
+
|
|
220
|
+
```csharp
|
|
221
|
+
[GenerateSerializer]
|
|
222
|
+
public abstract record AccountEvent(DateTime Timestamp);
|
|
223
|
+
|
|
224
|
+
[GenerateSerializer]
|
|
225
|
+
public record DepositEvent(DateTime Timestamp, decimal Amount)
|
|
226
|
+
: AccountEvent(Timestamp);
|
|
227
|
+
|
|
228
|
+
[GenerateSerializer]
|
|
229
|
+
public record WithdrawEvent(DateTime Timestamp, decimal Amount)
|
|
230
|
+
: AccountEvent(Timestamp);
|
|
231
|
+
|
|
232
|
+
[GenerateSerializer]
|
|
233
|
+
public class AccountEventLog
|
|
234
|
+
{
|
|
235
|
+
[Id(0)] public List<AccountEvent> Events { get; set; } = [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public class AccountGrain : Grain, IAccountGrain
|
|
239
|
+
{
|
|
240
|
+
private readonly IPersistentState<AccountEventLog> _log;
|
|
241
|
+
private decimal _balance; // Computed from events
|
|
242
|
+
|
|
243
|
+
public AccountGrain(
|
|
244
|
+
[PersistentState("events", "eventStore")]
|
|
245
|
+
IPersistentState<AccountEventLog> log)
|
|
246
|
+
{
|
|
247
|
+
_log = log;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
public override Task OnActivateAsync(CancellationToken ct)
|
|
251
|
+
{
|
|
252
|
+
// Rebuild state from events
|
|
253
|
+
_balance = _log.State.Events.Aggregate(0m, (bal, evt) => evt switch
|
|
254
|
+
{
|
|
255
|
+
DepositEvent d => bal + d.Amount,
|
|
256
|
+
WithdrawEvent w => bal - w.Amount,
|
|
257
|
+
_ => bal
|
|
258
|
+
});
|
|
259
|
+
return base.OnActivateAsync(ct);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
public async Task Deposit(decimal amount)
|
|
263
|
+
{
|
|
264
|
+
var evt = new DepositEvent(DateTime.UtcNow, amount);
|
|
265
|
+
_log.State.Events.Add(evt);
|
|
266
|
+
_balance += amount;
|
|
267
|
+
await _log.WriteStateAsync();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Snapshotting
|
|
273
|
+
|
|
274
|
+
Combine event sourcing with periodic snapshots.
|
|
275
|
+
|
|
276
|
+
```csharp
|
|
277
|
+
[GenerateSerializer]
|
|
278
|
+
public class AccountSnapshot
|
|
279
|
+
{
|
|
280
|
+
[Id(0)] public decimal Balance { get; set; }
|
|
281
|
+
[Id(1)] public int LastEventIndex { get; set; }
|
|
282
|
+
[Id(2)] public DateTime SnapshotTime { get; set; }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
public class AccountGrain : Grain, IAccountGrain
|
|
286
|
+
{
|
|
287
|
+
private readonly IPersistentState<AccountEventLog> _log;
|
|
288
|
+
private readonly IPersistentState<AccountSnapshot> _snapshot;
|
|
289
|
+
private decimal _balance;
|
|
290
|
+
private const int SnapshotInterval = 100;
|
|
291
|
+
|
|
292
|
+
public override async Task OnActivateAsync(CancellationToken ct)
|
|
293
|
+
{
|
|
294
|
+
// Start from snapshot
|
|
295
|
+
_balance = _snapshot.State.Balance;
|
|
296
|
+
|
|
297
|
+
// Apply events since snapshot
|
|
298
|
+
var newEvents = _log.State.Events
|
|
299
|
+
.Skip(_snapshot.State.LastEventIndex);
|
|
300
|
+
|
|
301
|
+
foreach (var evt in newEvents)
|
|
302
|
+
{
|
|
303
|
+
ApplyEvent(evt);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await base.OnActivateAsync(ct);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private async Task AppendEvent(AccountEvent evt)
|
|
310
|
+
{
|
|
311
|
+
_log.State.Events.Add(evt);
|
|
312
|
+
ApplyEvent(evt);
|
|
313
|
+
await _log.WriteStateAsync();
|
|
314
|
+
|
|
315
|
+
// Create snapshot periodically
|
|
316
|
+
if (_log.State.Events.Count % SnapshotInterval == 0)
|
|
317
|
+
{
|
|
318
|
+
_snapshot.State = new AccountSnapshot
|
|
319
|
+
{
|
|
320
|
+
Balance = _balance,
|
|
321
|
+
LastEventIndex = _log.State.Events.Count,
|
|
322
|
+
SnapshotTime = DateTime.UtcNow
|
|
323
|
+
};
|
|
324
|
+
await _snapshot.WriteStateAsync();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Streaming Patterns
|
|
333
|
+
|
|
334
|
+
### Basic Stream Producer
|
|
335
|
+
|
|
336
|
+
```csharp
|
|
337
|
+
public class SensorGrain : Grain, ISensorGrain
|
|
338
|
+
{
|
|
339
|
+
private IAsyncStream<SensorReading>? _stream;
|
|
340
|
+
|
|
341
|
+
public override Task OnActivateAsync(CancellationToken ct)
|
|
342
|
+
{
|
|
343
|
+
var streamProvider = this.GetStreamProvider("StreamProvider");
|
|
344
|
+
_stream = streamProvider.GetStream<SensorReading>(
|
|
345
|
+
StreamId.Create("Sensors", this.GetPrimaryKeyString()));
|
|
346
|
+
return base.OnActivateAsync(ct);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
public async Task ReportReading(double value)
|
|
350
|
+
{
|
|
351
|
+
var reading = new SensorReading
|
|
352
|
+
{
|
|
353
|
+
SensorId = this.GetPrimaryKeyString(),
|
|
354
|
+
Value = value,
|
|
355
|
+
Timestamp = DateTime.UtcNow
|
|
356
|
+
};
|
|
357
|
+
await _stream!.OnNextAsync(reading);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Implicit Stream Subscription
|
|
363
|
+
|
|
364
|
+
Automatic subscription based on grain identity.
|
|
365
|
+
|
|
366
|
+
```csharp
|
|
367
|
+
[ImplicitStreamSubscription("Sensors")]
|
|
368
|
+
public class SensorAggregatorGrain : Grain, ISensorAggregatorGrain
|
|
369
|
+
{
|
|
370
|
+
private double _lastValue;
|
|
371
|
+
|
|
372
|
+
public override async Task OnActivateAsync(CancellationToken ct)
|
|
373
|
+
{
|
|
374
|
+
var streamProvider = this.GetStreamProvider("StreamProvider");
|
|
375
|
+
var stream = streamProvider.GetStream<SensorReading>(
|
|
376
|
+
StreamId.Create("Sensors", this.GetPrimaryKeyString()));
|
|
377
|
+
|
|
378
|
+
await stream.SubscribeAsync(OnReading);
|
|
379
|
+
await base.OnActivateAsync(ct);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private Task OnReading(SensorReading reading, StreamSequenceToken? token)
|
|
383
|
+
{
|
|
384
|
+
_lastValue = reading.Value;
|
|
385
|
+
// Process reading
|
|
386
|
+
return Task.CompletedTask;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Stream Fan-Out
|
|
392
|
+
|
|
393
|
+
Broadcast to multiple consumers.
|
|
394
|
+
|
|
395
|
+
```csharp
|
|
396
|
+
public class NotificationGrain : Grain, INotificationGrain
|
|
397
|
+
{
|
|
398
|
+
public async Task BroadcastNotification(Notification notification)
|
|
399
|
+
{
|
|
400
|
+
var streamProvider = this.GetStreamProvider("Notifications");
|
|
401
|
+
|
|
402
|
+
// Broadcast to topic stream
|
|
403
|
+
var globalStream = streamProvider.GetStream<Notification>(
|
|
404
|
+
StreamId.Create("Notifications", "global"));
|
|
405
|
+
await globalStream.OnNextAsync(notification);
|
|
406
|
+
|
|
407
|
+
// Also send to user-specific streams
|
|
408
|
+
foreach (var userId in notification.TargetUsers)
|
|
409
|
+
{
|
|
410
|
+
var userStream = streamProvider.GetStream<Notification>(
|
|
411
|
+
StreamId.Create("Notifications", userId));
|
|
412
|
+
await userStream.OnNextAsync(notification);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Reliable Stream Consumption
|
|
419
|
+
|
|
420
|
+
Handle failures and resume from last position.
|
|
421
|
+
|
|
422
|
+
```csharp
|
|
423
|
+
public class OrderProcessorGrain : Grain, IOrderProcessorGrain
|
|
424
|
+
{
|
|
425
|
+
private StreamSubscriptionHandle<Order>? _subscription;
|
|
426
|
+
private readonly IPersistentState<StreamPosition> _position;
|
|
427
|
+
|
|
428
|
+
public OrderProcessorGrain(
|
|
429
|
+
[PersistentState("streamPos", "positions")]
|
|
430
|
+
IPersistentState<StreamPosition> position)
|
|
431
|
+
{
|
|
432
|
+
_position = position;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
public override async Task OnActivateAsync(CancellationToken ct)
|
|
436
|
+
{
|
|
437
|
+
var streamProvider = this.GetStreamProvider("Orders");
|
|
438
|
+
var stream = streamProvider.GetStream<Order>(
|
|
439
|
+
StreamId.Create("Orders", "incoming"));
|
|
440
|
+
|
|
441
|
+
// Resume from last known position
|
|
442
|
+
_subscription = await stream.SubscribeAsync(
|
|
443
|
+
OnOrderReceived,
|
|
444
|
+
OnError,
|
|
445
|
+
token: _position.State.LastToken);
|
|
446
|
+
|
|
447
|
+
await base.OnActivateAsync(ct);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private async Task OnOrderReceived(Order order, StreamSequenceToken? token)
|
|
451
|
+
{
|
|
452
|
+
await ProcessOrder(order);
|
|
453
|
+
|
|
454
|
+
// Persist position after successful processing
|
|
455
|
+
_position.State.LastToken = token;
|
|
456
|
+
await _position.WriteStateAsync();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## Coordination Patterns
|
|
464
|
+
|
|
465
|
+
### Distributed Lock
|
|
466
|
+
|
|
467
|
+
Coordinate exclusive access across grains.
|
|
468
|
+
|
|
469
|
+
```csharp
|
|
470
|
+
public interface ILockGrain : IGrainWithStringKey
|
|
471
|
+
{
|
|
472
|
+
Task<bool> TryAcquire(string owner, TimeSpan timeout);
|
|
473
|
+
Task Release(string owner);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
public class LockGrain : Grain, ILockGrain
|
|
477
|
+
{
|
|
478
|
+
private string? _currentOwner;
|
|
479
|
+
private DateTime _expiresAt;
|
|
480
|
+
|
|
481
|
+
public Task<bool> TryAcquire(string owner, TimeSpan timeout)
|
|
482
|
+
{
|
|
483
|
+
var now = DateTime.UtcNow;
|
|
484
|
+
|
|
485
|
+
// Check if lock is available or expired
|
|
486
|
+
if (_currentOwner == null || now >= _expiresAt)
|
|
487
|
+
{
|
|
488
|
+
_currentOwner = owner;
|
|
489
|
+
_expiresAt = now + timeout;
|
|
490
|
+
return Task.FromResult(true);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Already owned
|
|
494
|
+
if (_currentOwner == owner)
|
|
495
|
+
{
|
|
496
|
+
_expiresAt = now + timeout; // Extend
|
|
497
|
+
return Task.FromResult(true);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return Task.FromResult(false);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
public Task Release(string owner)
|
|
504
|
+
{
|
|
505
|
+
if (_currentOwner == owner)
|
|
506
|
+
{
|
|
507
|
+
_currentOwner = null;
|
|
508
|
+
}
|
|
509
|
+
return Task.CompletedTask;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Saga Pattern
|
|
515
|
+
|
|
516
|
+
Coordinate multi-grain transactions with compensation.
|
|
517
|
+
|
|
518
|
+
```csharp
|
|
519
|
+
public interface IOrderSagaGrain : IGrainWithGuidKey
|
|
520
|
+
{
|
|
521
|
+
Task<SagaResult> Execute(OrderRequest request);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
public class OrderSagaGrain : Grain, IOrderSagaGrain
|
|
525
|
+
{
|
|
526
|
+
private readonly IPersistentState<SagaState> _state;
|
|
527
|
+
|
|
528
|
+
public async Task<SagaResult> Execute(OrderRequest request)
|
|
529
|
+
{
|
|
530
|
+
_state.State.Status = SagaStatus.Started;
|
|
531
|
+
_state.State.Request = request;
|
|
532
|
+
await _state.WriteStateAsync();
|
|
533
|
+
|
|
534
|
+
try
|
|
535
|
+
{
|
|
536
|
+
// Step 1: Reserve inventory
|
|
537
|
+
var inventory = GrainFactory.GetGrain<IInventoryGrain>(request.ProductId);
|
|
538
|
+
await inventory.Reserve(request.Quantity);
|
|
539
|
+
_state.State.InventoryReserved = true;
|
|
540
|
+
await _state.WriteStateAsync();
|
|
541
|
+
|
|
542
|
+
// Step 2: Charge payment
|
|
543
|
+
var payment = GrainFactory.GetGrain<IPaymentGrain>(request.CustomerId);
|
|
544
|
+
await payment.Charge(request.Amount);
|
|
545
|
+
_state.State.PaymentCharged = true;
|
|
546
|
+
await _state.WriteStateAsync();
|
|
547
|
+
|
|
548
|
+
// Step 3: Create order
|
|
549
|
+
var order = GrainFactory.GetGrain<IOrderGrain>(this.GetPrimaryKey());
|
|
550
|
+
await order.Create(request);
|
|
551
|
+
|
|
552
|
+
_state.State.Status = SagaStatus.Completed;
|
|
553
|
+
await _state.WriteStateAsync();
|
|
554
|
+
|
|
555
|
+
return SagaResult.Success();
|
|
556
|
+
}
|
|
557
|
+
catch (Exception ex)
|
|
558
|
+
{
|
|
559
|
+
await Compensate();
|
|
560
|
+
return SagaResult.Failed(ex.Message);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private async Task Compensate()
|
|
565
|
+
{
|
|
566
|
+
_state.State.Status = SagaStatus.Compensating;
|
|
567
|
+
await _state.WriteStateAsync();
|
|
568
|
+
|
|
569
|
+
if (_state.State.PaymentCharged)
|
|
570
|
+
{
|
|
571
|
+
var payment = GrainFactory.GetGrain<IPaymentGrain>(
|
|
572
|
+
_state.State.Request!.CustomerId);
|
|
573
|
+
await payment.Refund(_state.State.Request.Amount);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (_state.State.InventoryReserved)
|
|
577
|
+
{
|
|
578
|
+
var inventory = GrainFactory.GetGrain<IInventoryGrain>(
|
|
579
|
+
_state.State.Request!.ProductId);
|
|
580
|
+
await inventory.CancelReservation(_state.State.Request.Quantity);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
_state.State.Status = SagaStatus.Compensated;
|
|
584
|
+
await _state.WriteStateAsync();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Scatter-Gather
|
|
590
|
+
|
|
591
|
+
Parallel queries with result aggregation.
|
|
592
|
+
|
|
593
|
+
```csharp
|
|
594
|
+
public class SearchGrain : Grain, ISearchGrain
|
|
595
|
+
{
|
|
596
|
+
public async Task<SearchResults> Search(SearchQuery query)
|
|
597
|
+
{
|
|
598
|
+
// Scatter: Query all index partitions in parallel
|
|
599
|
+
var partitionCount = 10;
|
|
600
|
+
var tasks = Enumerable.Range(0, partitionCount)
|
|
601
|
+
.Select(i => GrainFactory
|
|
602
|
+
.GetGrain<IIndexPartitionGrain>(i)
|
|
603
|
+
.Search(query))
|
|
604
|
+
.ToList();
|
|
605
|
+
|
|
606
|
+
// Gather: Collect and merge results
|
|
607
|
+
var partialResults = await Task.WhenAll(tasks);
|
|
608
|
+
|
|
609
|
+
return new SearchResults
|
|
610
|
+
{
|
|
611
|
+
Items = partialResults
|
|
612
|
+
.SelectMany(r => r.Items)
|
|
613
|
+
.OrderByDescending(i => i.Score)
|
|
614
|
+
.Take(query.Limit)
|
|
615
|
+
.ToList(),
|
|
616
|
+
TotalCount = partialResults.Sum(r => r.TotalCount)
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
## Performance Patterns
|
|
625
|
+
|
|
626
|
+
### Grain Pooling with Reentrant Calls
|
|
627
|
+
|
|
628
|
+
Allow interleaved calls for high throughput.
|
|
629
|
+
|
|
630
|
+
```csharp
|
|
631
|
+
[Reentrant]
|
|
632
|
+
public class CacheGrain : Grain, ICacheGrain
|
|
633
|
+
{
|
|
634
|
+
private readonly Dictionary<string, CacheEntry> _cache = new();
|
|
635
|
+
|
|
636
|
+
public Task<string?> Get(string key)
|
|
637
|
+
{
|
|
638
|
+
if (_cache.TryGetValue(key, out var entry) && !entry.IsExpired)
|
|
639
|
+
{
|
|
640
|
+
return Task.FromResult<string?>(entry.Value);
|
|
641
|
+
}
|
|
642
|
+
return Task.FromResult<string?>(null);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
public Task Set(string key, string value, TimeSpan ttl)
|
|
646
|
+
{
|
|
647
|
+
_cache[key] = new CacheEntry(value, DateTime.UtcNow + ttl);
|
|
648
|
+
return Task.CompletedTask;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### Read-Through Cache Pattern
|
|
654
|
+
|
|
655
|
+
```csharp
|
|
656
|
+
public class CachedDataGrain : Grain, ICachedDataGrain
|
|
657
|
+
{
|
|
658
|
+
private readonly IPersistentState<CachedDataState> _state;
|
|
659
|
+
private readonly IExternalDataService _dataService;
|
|
660
|
+
|
|
661
|
+
public async Task<Data> GetData(string key)
|
|
662
|
+
{
|
|
663
|
+
// Check cache
|
|
664
|
+
if (_state.State.Cache.TryGetValue(key, out var cached))
|
|
665
|
+
{
|
|
666
|
+
if (!cached.IsExpired)
|
|
667
|
+
{
|
|
668
|
+
return cached.Data;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Cache miss: fetch from external source
|
|
673
|
+
var data = await _dataService.FetchAsync(key);
|
|
674
|
+
|
|
675
|
+
// Update cache
|
|
676
|
+
_state.State.Cache[key] = new CacheEntry<Data>(data, TimeSpan.FromMinutes(5));
|
|
677
|
+
await _state.WriteStateAsync();
|
|
678
|
+
|
|
679
|
+
return data;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Batch Processing
|
|
685
|
+
|
|
686
|
+
Collect requests and process in batches.
|
|
687
|
+
|
|
688
|
+
```csharp
|
|
689
|
+
public class BatchProcessorGrain : Grain, IBatchProcessorGrain
|
|
690
|
+
{
|
|
691
|
+
private readonly List<WorkItem> _pending = [];
|
|
692
|
+
private IDisposable? _timer;
|
|
693
|
+
private const int BatchSize = 100;
|
|
694
|
+
private const int FlushIntervalMs = 1000;
|
|
695
|
+
|
|
696
|
+
public override Task OnActivateAsync(CancellationToken ct)
|
|
697
|
+
{
|
|
698
|
+
_timer = this.RegisterGrainTimer(
|
|
699
|
+
FlushBatch,
|
|
700
|
+
new GrainTimerCreationOptions
|
|
701
|
+
{
|
|
702
|
+
DueTime = TimeSpan.FromMilliseconds(FlushIntervalMs),
|
|
703
|
+
Period = TimeSpan.FromMilliseconds(FlushIntervalMs)
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
return base.OnActivateAsync(ct);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
public async Task Submit(WorkItem item)
|
|
710
|
+
{
|
|
711
|
+
_pending.Add(item);
|
|
712
|
+
|
|
713
|
+
if (_pending.Count >= BatchSize)
|
|
714
|
+
{
|
|
715
|
+
await FlushBatch(default);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
private async Task FlushBatch(CancellationToken _)
|
|
720
|
+
{
|
|
721
|
+
if (_pending.Count == 0) return;
|
|
722
|
+
|
|
723
|
+
var batch = _pending.ToList();
|
|
724
|
+
_pending.Clear();
|
|
725
|
+
|
|
726
|
+
// Process batch efficiently
|
|
727
|
+
await ProcessBatchAsync(batch);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## Testing Patterns
|
|
735
|
+
|
|
736
|
+
### Unit Testing with TestCluster
|
|
737
|
+
|
|
738
|
+
```csharp
|
|
739
|
+
public class PlayerGrainTests : IClassFixture<TestClusterFixture>
|
|
740
|
+
{
|
|
741
|
+
private readonly TestCluster _cluster;
|
|
742
|
+
|
|
743
|
+
public PlayerGrainTests(TestClusterFixture fixture)
|
|
744
|
+
{
|
|
745
|
+
_cluster = fixture.Cluster;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
[Fact]
|
|
749
|
+
public async Task UpdateScore_IncrementsScore()
|
|
750
|
+
{
|
|
751
|
+
// Arrange
|
|
752
|
+
var player = _cluster.GrainFactory.GetGrain<IPlayerGrain>("test-player");
|
|
753
|
+
|
|
754
|
+
// Act
|
|
755
|
+
await player.UpdateScore(100);
|
|
756
|
+
var state = await player.GetState();
|
|
757
|
+
|
|
758
|
+
// Assert
|
|
759
|
+
Assert.Equal(100, state.Score);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
public class TestClusterFixture : IDisposable
|
|
764
|
+
{
|
|
765
|
+
public TestCluster Cluster { get; }
|
|
766
|
+
|
|
767
|
+
public TestClusterFixture()
|
|
768
|
+
{
|
|
769
|
+
var builder = new TestClusterBuilder();
|
|
770
|
+
builder.AddSiloBuilderConfigurator<TestSiloConfigurator>();
|
|
771
|
+
Cluster = builder.Build();
|
|
772
|
+
Cluster.Deploy();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
public void Dispose() => Cluster.StopAllSilos();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
public class TestSiloConfigurator : ISiloConfigurator
|
|
779
|
+
{
|
|
780
|
+
public void Configure(ISiloBuilder siloBuilder)
|
|
781
|
+
{
|
|
782
|
+
siloBuilder.AddMemoryGrainStorage("Default");
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### Mocking Grain Dependencies
|
|
788
|
+
|
|
789
|
+
```csharp
|
|
790
|
+
public class OrderGrainTests
|
|
791
|
+
{
|
|
792
|
+
[Fact]
|
|
793
|
+
public async Task CreateOrder_CallsInventoryGrain()
|
|
794
|
+
{
|
|
795
|
+
// Arrange
|
|
796
|
+
var mockInventory = new Mock<IInventoryGrain>();
|
|
797
|
+
var mockFactory = new Mock<IGrainFactory>();
|
|
798
|
+
mockFactory
|
|
799
|
+
.Setup(f => f.GetGrain<IInventoryGrain>(It.IsAny<string>(), null))
|
|
800
|
+
.Returns(mockInventory.Object);
|
|
801
|
+
|
|
802
|
+
var grain = new OrderGrain(mockFactory.Object);
|
|
803
|
+
|
|
804
|
+
// Act
|
|
805
|
+
await grain.CreateOrder(new OrderRequest { ProductId = "prod-1" });
|
|
806
|
+
|
|
807
|
+
// Assert
|
|
808
|
+
mockInventory.Verify(i => i.Reserve(It.IsAny<int>()), Times.Once);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
```
|