@cratis/pi 7.102.0 → 7.103.0

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.
Files changed (140) hide show
  1. package/package/corpus/harnesses/copilot/instructions/profiles.instructions.md +10 -1
  2. package/package/corpus/harnesses/copilot/instructions/pull-requests.instructions.md +2 -0
  3. package/package/corpus/harnesses/copilot/instructions/secrets.instructions.md +4 -0
  4. package/package/corpus/hooks/README.md +11 -1
  5. package/package/corpus/hooks/scripts/cratis-check-pr.mjs +12 -1
  6. package/package/corpus/hooks/scripts/cratis-guard-secrets.sh +79 -23
  7. package/package/corpus/hooks/scripts/cratis-guard-writes.sh +145 -40
  8. package/package/corpus/rules/profiles.md +10 -1
  9. package/package/corpus/rules/pull-requests.md +2 -0
  10. package/package/corpus/rules/secrets.md +4 -0
  11. package/package/corpus/skills/analyzing-dotnet-performance/LICENSE +23 -0
  12. package/package/corpus/skills/analyzing-dotnet-performance/NOTICE +36 -0
  13. package/package/corpus/skills/analyzing-dotnet-performance/SKILL.md +214 -0
  14. package/package/corpus/skills/analyzing-dotnet-performance/references/async-patterns.md +122 -0
  15. package/package/corpus/skills/analyzing-dotnet-performance/references/collections-and-linq.md +220 -0
  16. package/package/corpus/skills/analyzing-dotnet-performance/references/critical-patterns.md +291 -0
  17. package/package/corpus/skills/analyzing-dotnet-performance/references/io-and-serialization.md +135 -0
  18. package/package/corpus/skills/analyzing-dotnet-performance/references/memory-and-strings.md +219 -0
  19. package/package/corpus/skills/analyzing-dotnet-performance/references/regex-patterns.md +95 -0
  20. package/package/corpus/skills/analyzing-dotnet-performance/references/structural-patterns.md +38 -0
  21. package/package/corpus/skills/binlog-generation/LICENSE +23 -0
  22. package/package/corpus/skills/binlog-generation/NOTICE +22 -0
  23. package/package/corpus/skills/binlog-generation/SKILL.md +129 -0
  24. package/package/corpus/skills/check-bin-obj-clash/LICENSE +23 -0
  25. package/package/corpus/skills/check-bin-obj-clash/NOTICE +27 -0
  26. package/package/corpus/skills/check-bin-obj-clash/SKILL.md +127 -0
  27. package/package/corpus/skills/check-bin-obj-clash/references/common-causes-and-fixes.md +206 -0
  28. package/package/corpus/skills/coverage-analysis/LICENSE +23 -0
  29. package/package/corpus/skills/coverage-analysis/NOTICE +32 -0
  30. package/package/corpus/skills/coverage-analysis/SKILL.md +198 -0
  31. package/package/corpus/skills/coverage-analysis/references/guidelines.md +68 -0
  32. package/package/corpus/skills/coverage-analysis/references/output-format.md +91 -0
  33. package/package/corpus/skills/coverage-analysis/references/report-generation.md +58 -0
  34. package/package/corpus/skills/coverage-analysis/references/setup-discovery.md +307 -0
  35. package/package/corpus/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 +80 -0
  36. package/package/corpus/skills/coverage-analysis/scripts/CoverageData.ps1 +272 -0
  37. package/package/corpus/skills/coverage-analysis/scripts/Extract-MethodCoverage.ps1 +65 -0
  38. package/package/corpus/skills/csharp-refactoring/LICENSE +23 -0
  39. package/package/corpus/skills/csharp-refactoring/NOTICE +22 -0
  40. package/package/corpus/skills/csharp-refactoring/SKILL.md +175 -0
  41. package/package/corpus/skills/csharp-refactoring/references/operation-catalog.md +37 -0
  42. package/package/corpus/skills/directory-build-organization/LICENSE +23 -0
  43. package/package/corpus/skills/directory-build-organization/NOTICE +21 -0
  44. package/package/corpus/skills/directory-build-organization/SKILL.md +205 -0
  45. package/package/corpus/skills/directory-build-organization/references/common-patterns.md +56 -0
  46. package/package/corpus/skills/directory-build-organization/references/multi-level-examples.md +167 -0
  47. package/package/corpus/skills/directory-build-organization/references/targetframework-props-pitfall.md +54 -0
  48. package/package/corpus/skills/dotnet-aot-compat/LICENSE +23 -0
  49. package/package/corpus/skills/dotnet-aot-compat/NOTICE +18 -0
  50. package/package/corpus/skills/dotnet-aot-compat/SKILL.md +265 -0
  51. package/package/corpus/skills/dotnet-aot-compat/references/polyfills.md +43 -0
  52. package/package/corpus/skills/dotnet-trace-collect/LICENSE +23 -0
  53. package/package/corpus/skills/dotnet-trace-collect/NOTICE +65 -0
  54. package/package/corpus/skills/dotnet-trace-collect/SKILL.md +116 -0
  55. package/package/corpus/skills/dotnet-trace-collect/references/dotnet-monitor.md +97 -0
  56. package/package/corpus/skills/dotnet-trace-collect/references/dotnet-trace-collect-linux.md +84 -0
  57. package/package/corpus/skills/dotnet-trace-collect/references/dotnet-trace-collect.md +91 -0
  58. package/package/corpus/skills/dotnet-trace-collect/references/perfcollect.md +200 -0
  59. package/package/corpus/skills/dotnet-trace-collect/references/perfview.md +141 -0
  60. package/package/corpus/skills/dotnet-trace-collect/references/tool-selection.md +160 -0
  61. package/package/corpus/skills/dump-collect/LICENSE +23 -0
  62. package/package/corpus/skills/dump-collect/NOTICE +40 -0
  63. package/package/corpus/skills/dump-collect/SKILL.md +97 -0
  64. package/package/corpus/skills/dump-collect/references/container-dumps.md +412 -0
  65. package/package/corpus/skills/dump-collect/references/coreclr-dumps.md +108 -0
  66. package/package/corpus/skills/dump-collect/references/nativeaot-dumps.md +280 -0
  67. package/package/corpus/skills/eliminate-redundant-restores/LICENSE +23 -0
  68. package/package/corpus/skills/eliminate-redundant-restores/NOTICE +19 -0
  69. package/package/corpus/skills/eliminate-redundant-restores/SKILL.md +112 -0
  70. package/package/corpus/skills/extension-points/LICENSE +23 -0
  71. package/package/corpus/skills/extension-points/NOTICE +18 -0
  72. package/package/corpus/skills/extension-points/SKILL.md +248 -0
  73. package/package/corpus/skills/filter-syntax/LICENSE +23 -0
  74. package/package/corpus/skills/filter-syntax/NOTICE +17 -0
  75. package/package/corpus/skills/filter-syntax/SKILL.md +179 -0
  76. package/package/corpus/skills/including-generated-files/LICENSE +23 -0
  77. package/package/corpus/skills/including-generated-files/NOTICE +17 -0
  78. package/package/corpus/skills/including-generated-files/SKILL.md +139 -0
  79. package/package/corpus/skills/microbenchmarking/LICENSE +23 -0
  80. package/package/corpus/skills/microbenchmarking/NOTICE +36 -0
  81. package/package/corpus/skills/microbenchmarking/SKILL.md +131 -0
  82. package/package/corpus/skills/microbenchmarking/references/bdn-internals-and-tuning.md +143 -0
  83. package/package/corpus/skills/microbenchmarking/references/comparison-strategies.md +200 -0
  84. package/package/corpus/skills/microbenchmarking/references/diagnosers-and-exporters.md +150 -0
  85. package/package/corpus/skills/microbenchmarking/references/project-setup-and-running.md +189 -0
  86. package/package/corpus/skills/microbenchmarking/references/writing-benchmarks.md +310 -0
  87. package/package/corpus/skills/msbuild/LICENSE +23 -0
  88. package/package/corpus/skills/msbuild/NOTICE +55 -0
  89. package/package/corpus/skills/msbuild/SKILL.md +117 -0
  90. package/package/corpus/skills/msbuild/references/antipatterns/additional-antipatterns.md +291 -0
  91. package/package/corpus/skills/msbuild/references/antipatterns/incremental-build-inputs-outputs.md +57 -0
  92. package/package/corpus/skills/msbuild/references/antipatterns/private-assets.md +29 -0
  93. package/package/corpus/skills/msbuild/references/binlog-failure-analysis.md +85 -0
  94. package/package/corpus/skills/msbuild/references/binlog-generation.md +69 -0
  95. package/package/corpus/skills/msbuild/references/build-perf-baseline.md +493 -0
  96. package/package/corpus/skills/msbuild/references/build-perf-diagnostics.md +268 -0
  97. package/package/corpus/skills/msbuild/references/copy-to-output-directory.md +183 -0
  98. package/package/corpus/skills/msbuild/references/eval-performance.md +190 -0
  99. package/package/corpus/skills/msbuild/references/extension-points.md +281 -0
  100. package/package/corpus/skills/msbuild/references/incremental-build.md +365 -0
  101. package/package/corpus/skills/msbuild/references/msbuild-antipatterns.md +401 -0
  102. package/package/corpus/skills/orleans/LICENSE +21 -0
  103. package/package/corpus/skills/orleans/NOTICE +64 -0
  104. package/package/corpus/skills/orleans/SKILL.md +140 -0
  105. package/package/corpus/skills/orleans/references/anti-patterns.md +660 -0
  106. package/package/corpus/skills/orleans/references/configuration-api.md +933 -0
  107. package/package/corpus/skills/orleans/references/examples.md +66 -0
  108. package/package/corpus/skills/orleans/references/grain-api.md +784 -0
  109. package/package/corpus/skills/orleans/references/grains.md +75 -0
  110. package/package/corpus/skills/orleans/references/hosting.md +71 -0
  111. package/package/corpus/skills/orleans/references/implementation.md +591 -0
  112. package/package/corpus/skills/orleans/references/official-docs-index.md +250 -0
  113. package/package/corpus/skills/orleans/references/patterns.md +811 -0
  114. package/package/corpus/skills/orleans/references/persistence-api.md +495 -0
  115. package/package/corpus/skills/orleans/references/primitive-selection.md +35 -0
  116. package/package/corpus/skills/orleans/references/scheduling-and-services.md +295 -0
  117. package/package/corpus/skills/orleans/references/serialization-api.md +299 -0
  118. package/package/corpus/skills/orleans/references/streaming-api.md +396 -0
  119. package/package/corpus/skills/orleans/references/testing-patterns.md +210 -0
  120. package/package/corpus/skills/platform-detection/LICENSE +23 -0
  121. package/package/corpus/skills/platform-detection/NOTICE +17 -0
  122. package/package/corpus/skills/platform-detection/SKILL.md +230 -0
  123. package/package/corpus/skills/platform-detection/references/command-mode.md +27 -0
  124. package/package/corpus/skills/profiling/LICENSE +21 -0
  125. package/package/corpus/skills/profiling/NOTICE +50 -0
  126. package/package/corpus/skills/profiling/SKILL.md +144 -0
  127. package/package/corpus/skills/profiling/references/commands.md +439 -0
  128. package/package/corpus/skills/profiling/references/patterns.md +586 -0
  129. package/package/corpus/skills/profiling/references/profiling.md +190 -0
  130. package/package/corpus/skills/run-tests/LICENSE +23 -0
  131. package/package/corpus/skills/run-tests/NOTICE +19 -0
  132. package/package/corpus/skills/run-tests/SKILL.md +305 -0
  133. package/package/corpus/skills/target-authoring/LICENSE +23 -0
  134. package/package/corpus/skills/target-authoring/NOTICE +17 -0
  135. package/package/corpus/skills/target-authoring/SKILL.md +165 -0
  136. package/package/corpus/skills/vectorization/LICENSE +23 -0
  137. package/package/corpus/skills/vectorization/NOTICE +17 -0
  138. package/package/corpus/skills/vectorization/SKILL.md +189 -0
  139. package/package/profile-catalog.json +85 -1
  140. 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
+ ```