@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.
- 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/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,396 @@
|
|
|
1
|
+
# Streaming, Broadcast Channels, and Observers API
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- Orleans Streams
|
|
6
|
+
- Broadcast Channels
|
|
7
|
+
- Observers
|
|
8
|
+
- Stream Providers
|
|
9
|
+
- Streams Programming API Details
|
|
10
|
+
- Direct Calls vs Streams vs Broadcast vs Observers vs IAsyncEnumerable
|
|
11
|
+
|
|
12
|
+
Detailed streaming and communication patterns from official Orleans documentation.
|
|
13
|
+
|
|
14
|
+
## Orleans Streams
|
|
15
|
+
|
|
16
|
+
Streams are virtual, always exist, never fail, identified by `StreamId` (namespace + key). Decouple data generation from processing in time and space.
|
|
17
|
+
|
|
18
|
+
### Configuration
|
|
19
|
+
|
|
20
|
+
```csharp
|
|
21
|
+
// Silo — memory streams (dev only)
|
|
22
|
+
siloBuilder.AddMemoryStreams("StreamProvider")
|
|
23
|
+
.AddMemoryGrainStorage("PubSubStore");
|
|
24
|
+
|
|
25
|
+
// Silo — Azure Event Hubs
|
|
26
|
+
siloBuilder.AddEventHubStreams("EventHubProvider", options =>
|
|
27
|
+
{
|
|
28
|
+
options.ConfigureEventHub(eh => eh.Configure(o =>
|
|
29
|
+
{
|
|
30
|
+
o.ConnectionString = connectionString;
|
|
31
|
+
o.ConsumerGroup = "$Default";
|
|
32
|
+
o.Path = "my-hub";
|
|
33
|
+
}));
|
|
34
|
+
options.UseAzureTableCheckpointer(c => c.ConfigureTableServiceClient(storageConnection));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Client
|
|
38
|
+
clientBuilder.AddMemoryStreams("StreamProvider");
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Producing Events
|
|
42
|
+
|
|
43
|
+
```csharp
|
|
44
|
+
var streamProvider = this.GetStreamProvider("StreamProvider");
|
|
45
|
+
var streamId = StreamId.Create("SensorData", this.GetPrimaryKeyString());
|
|
46
|
+
var stream = streamProvider.GetStream<SensorReading>(streamId);
|
|
47
|
+
await stream.OnNextAsync(new SensorReading { Value = 42.0 });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Consuming Events — Implicit Subscription
|
|
51
|
+
|
|
52
|
+
```csharp
|
|
53
|
+
[ImplicitStreamSubscription("SensorData")]
|
|
54
|
+
public class AggregatorGrain : Grain, IAggregatorGrain
|
|
55
|
+
{
|
|
56
|
+
public override async Task OnActivateAsync(CancellationToken ct)
|
|
57
|
+
{
|
|
58
|
+
var streamProvider = this.GetStreamProvider("StreamProvider");
|
|
59
|
+
var stream = streamProvider.GetStream<SensorReading>(
|
|
60
|
+
StreamId.Create("SensorData", this.GetPrimaryKeyString()));
|
|
61
|
+
|
|
62
|
+
await stream.SubscribeAsync(
|
|
63
|
+
(reading, token) =>
|
|
64
|
+
{
|
|
65
|
+
// Process reading
|
|
66
|
+
return Task.CompletedTask;
|
|
67
|
+
});
|
|
68
|
+
await base.OnActivateAsync(ct);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Consuming Events — Explicit Subscription
|
|
74
|
+
|
|
75
|
+
```csharp
|
|
76
|
+
var stream = streamProvider.GetStream<Order>(StreamId.Create("Orders", "incoming"));
|
|
77
|
+
|
|
78
|
+
StreamSubscriptionHandle<Order> handle = await stream.SubscribeAsync(
|
|
79
|
+
onNextAsync: (item, token) => ProcessAsync(item),
|
|
80
|
+
onErrorAsync: ex => HandleError(ex),
|
|
81
|
+
onCompletedAsync: () => Task.CompletedTask,
|
|
82
|
+
token: lastKnownToken); // resume from position
|
|
83
|
+
|
|
84
|
+
// Unsubscribe
|
|
85
|
+
await handle.UnsubscribeAsync();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Delivery Semantics
|
|
89
|
+
|
|
90
|
+
| Provider | Delivery | Ordering |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| Memory | Best-effort | FIFO per stream |
|
|
93
|
+
| Azure Queue | At-least-once | FIFO per queue |
|
|
94
|
+
| Event Hubs | At-least-once | Per partition |
|
|
95
|
+
| Broadcast Channel | At-most-once | No guarantee |
|
|
96
|
+
|
|
97
|
+
Rewindable streams support subscribing from arbitrary point using `StreamSequenceToken`.
|
|
98
|
+
|
|
99
|
+
### IAsyncEnumerable (Orleans 7+)
|
|
100
|
+
|
|
101
|
+
Request-response scoped streaming, not pub-sub:
|
|
102
|
+
|
|
103
|
+
```csharp
|
|
104
|
+
// Interface
|
|
105
|
+
public interface IDataGrain : IGrainWithStringKey
|
|
106
|
+
{
|
|
107
|
+
IAsyncEnumerable<DataItem> GetItems(
|
|
108
|
+
int count,
|
|
109
|
+
[EnumeratorCancellation] CancellationToken ct = default);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Implementation
|
|
113
|
+
public class DataGrain : Grain, IDataGrain
|
|
114
|
+
{
|
|
115
|
+
public async IAsyncEnumerable<DataItem> GetItems(
|
|
116
|
+
int count,
|
|
117
|
+
[EnumeratorCancellation] CancellationToken ct = default)
|
|
118
|
+
{
|
|
119
|
+
for (int i = 0; i < count && !ct.IsCancellationRequested; i++)
|
|
120
|
+
{
|
|
121
|
+
yield return await FetchItem(i);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Consumer
|
|
127
|
+
await foreach (var item in grain.GetItems(100).WithCancellation(cts.Token))
|
|
128
|
+
{
|
|
129
|
+
Process(item);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Default batch size: 100. Configure with `.WithBatchSize(50)`.
|
|
134
|
+
|
|
135
|
+
## Broadcast Channels
|
|
136
|
+
|
|
137
|
+
Fire-and-forget broadcast to all implicitly subscribed grains. No persistence, no delivery guarantees.
|
|
138
|
+
|
|
139
|
+
### Configuration
|
|
140
|
+
|
|
141
|
+
```csharp
|
|
142
|
+
siloBuilder.AddBroadcastChannel("announcements");
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Consumer
|
|
146
|
+
|
|
147
|
+
```csharp
|
|
148
|
+
[ImplicitChannelSubscription]
|
|
149
|
+
public class NotificationGrain : Grain, INotificationGrain, IOnBroadcastChannelSubscribed
|
|
150
|
+
{
|
|
151
|
+
public Task OnSubscribed(IBroadcastChannelSubscription subscription)
|
|
152
|
+
{
|
|
153
|
+
subscription.Attach<Announcement>(
|
|
154
|
+
item => OnAnnouncement(item),
|
|
155
|
+
ex => OnError(ex));
|
|
156
|
+
return Task.CompletedTask;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private Task OnAnnouncement(Announcement item)
|
|
160
|
+
{
|
|
161
|
+
// Process broadcast message
|
|
162
|
+
return Task.CompletedTask;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Producer
|
|
168
|
+
|
|
169
|
+
```csharp
|
|
170
|
+
var provider = client.GetBroadcastChannelProvider("announcements");
|
|
171
|
+
var channelId = ChannelId.Create("system", Guid.Empty);
|
|
172
|
+
var writer = provider.GetChannelWriter<Announcement>(channelId);
|
|
173
|
+
await writer.Publish(new Announcement { Message = "System update" });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Observers
|
|
177
|
+
|
|
178
|
+
One-way async push notifications from grains to clients or other grains.
|
|
179
|
+
|
|
180
|
+
### Observer Interface
|
|
181
|
+
|
|
182
|
+
```csharp
|
|
183
|
+
public interface IChatObserver : IGrainObserver
|
|
184
|
+
{
|
|
185
|
+
[OneWay] // fire-and-forget
|
|
186
|
+
Task OnMessage(string user, string message);
|
|
187
|
+
|
|
188
|
+
Task OnUserJoined(string user);
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Server-Side (ObserverManager)
|
|
193
|
+
|
|
194
|
+
```csharp
|
|
195
|
+
public class ChatRoomGrain : Grain, IChatRoomGrain
|
|
196
|
+
{
|
|
197
|
+
private readonly ObserverManager<IChatObserver> _observers;
|
|
198
|
+
|
|
199
|
+
public ChatRoomGrain(ILogger<ChatRoomGrain> logger)
|
|
200
|
+
{
|
|
201
|
+
_observers = new ObserverManager<IChatObserver>(
|
|
202
|
+
TimeSpan.FromMinutes(5), logger);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
public Task Subscribe(IChatObserver observer)
|
|
206
|
+
{
|
|
207
|
+
_observers.Subscribe(observer, observer);
|
|
208
|
+
return Task.CompletedTask;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public Task Unsubscribe(IChatObserver observer)
|
|
212
|
+
{
|
|
213
|
+
_observers.Unsubscribe(observer);
|
|
214
|
+
return Task.CompletedTask;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public Task SendMessage(string user, string message)
|
|
218
|
+
{
|
|
219
|
+
_observers.Notify(o => o.OnMessage(user, message));
|
|
220
|
+
return Task.CompletedTask;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Client-Side
|
|
226
|
+
|
|
227
|
+
```csharp
|
|
228
|
+
// Create observer
|
|
229
|
+
var observerInstance = new ChatObserver();
|
|
230
|
+
var observerRef = grainFactory.CreateObjectReference<IChatObserver>(observerInstance);
|
|
231
|
+
|
|
232
|
+
// Subscribe
|
|
233
|
+
var chatRoom = client.GetGrain<IChatRoomGrain>("lobby");
|
|
234
|
+
await chatRoom.Subscribe(observerRef);
|
|
235
|
+
|
|
236
|
+
// Cleanup — must call to avoid memory leaks (held as WeakReference)
|
|
237
|
+
grainFactory.DeleteObjectReference<IChatObserver>(observerRef);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Grain-as-Observer
|
|
241
|
+
|
|
242
|
+
```csharp
|
|
243
|
+
public class MonitorGrain : Grain, IMonitorGrain, IChatObserver
|
|
244
|
+
{
|
|
245
|
+
public async Task StartMonitoring(string room)
|
|
246
|
+
{
|
|
247
|
+
var chatRoom = GrainFactory.GetGrain<IChatRoomGrain>(room);
|
|
248
|
+
await chatRoom.Subscribe(this.AsReference<IChatObserver>());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
public Task OnMessage(string user, string message)
|
|
252
|
+
{
|
|
253
|
+
// Handle notification
|
|
254
|
+
return Task.CompletedTask;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Observer Execution Model
|
|
260
|
+
|
|
261
|
+
- Non-reentrant, single-threaded per observer reference
|
|
262
|
+
- `[Reentrant]` / `[AlwaysInterleave]` attributes don't affect observers
|
|
263
|
+
- `CancellationToken` support added in Orleans 9.0
|
|
264
|
+
|
|
265
|
+
## Stream Providers
|
|
266
|
+
|
|
267
|
+
### Azure Event Hubs
|
|
268
|
+
|
|
269
|
+
NuGet: `Microsoft.Orleans.Streaming.EventHubs`. Real-time ingestion, rewindable, supports replay from arbitrary point.
|
|
270
|
+
|
|
271
|
+
```csharp
|
|
272
|
+
siloBuilder.AddEventHubStreams("EventHubProvider", options =>
|
|
273
|
+
{
|
|
274
|
+
options.ConfigureEventHub(eh => eh.Configure(o =>
|
|
275
|
+
{
|
|
276
|
+
o.ConnectionString = connectionString;
|
|
277
|
+
o.ConsumerGroup = "$Default";
|
|
278
|
+
o.Path = "my-hub";
|
|
279
|
+
}));
|
|
280
|
+
options.UseAzureTableCheckpointer(c =>
|
|
281
|
+
c.ConfigureTableServiceClient(storageConnection));
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Azure Queue (AQ)
|
|
286
|
+
|
|
287
|
+
NuGet: `Microsoft.Orleans.Streaming.AzureStorage`. Uses pulling agents inside silos. Not rewindable. Does not guarantee FIFO on failures.
|
|
288
|
+
|
|
289
|
+
```csharp
|
|
290
|
+
siloBuilder.AddAzureQueueStreams("AQProvider", optionsBuilder =>
|
|
291
|
+
optionsBuilder.ConfigureAzureQueue(options =>
|
|
292
|
+
options.Configure(opt =>
|
|
293
|
+
opt.QueueServiceClient = new QueueServiceClient(endpoint, credential))));
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Aspire Streaming Integration
|
|
297
|
+
|
|
298
|
+
```csharp
|
|
299
|
+
// Azure Queue with Aspire
|
|
300
|
+
var storage = builder.AddAzureStorage("storage");
|
|
301
|
+
var queues = storage.AddQueues("streaming");
|
|
302
|
+
var orleans = builder.AddOrleans("cluster")
|
|
303
|
+
.WithStreaming("AQProvider", queues);
|
|
304
|
+
|
|
305
|
+
// In-memory streaming
|
|
306
|
+
orleans.WithMemoryStreaming("MemoryProvider");
|
|
307
|
+
|
|
308
|
+
// Broadcast channels
|
|
309
|
+
orleans.WithBroadcastChannel("BroadcastChannel");
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Pub-Sub Management
|
|
313
|
+
|
|
314
|
+
Managed by `PubSubRendezvousGrain`, persisted via `"PubSubStore"`:
|
|
315
|
+
|
|
316
|
+
```csharp
|
|
317
|
+
siloBuilder.AddAzureTableGrainStorage("PubSubStore",
|
|
318
|
+
options => options.TableServiceClient = new TableServiceClient(endpoint, credential));
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Streams Programming API Details
|
|
322
|
+
|
|
323
|
+
### Core Interfaces
|
|
324
|
+
|
|
325
|
+
```csharp
|
|
326
|
+
public interface IAsyncObserver<in T>
|
|
327
|
+
{
|
|
328
|
+
Task OnNextAsync(T item, StreamSequenceToken token = null);
|
|
329
|
+
Task OnCompletedAsync();
|
|
330
|
+
Task OnErrorAsync(Exception ex);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
public interface IAsyncObservable<T>
|
|
334
|
+
{
|
|
335
|
+
Task<StreamSubscriptionHandle<T>> SubscribeAsync(IAsyncObserver<T> observer);
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Failure Recovery
|
|
340
|
+
|
|
341
|
+
Consumer must re-attach processing logic in `OnActivateAsync`:
|
|
342
|
+
|
|
343
|
+
```csharp
|
|
344
|
+
public override async Task OnActivateAsync(CancellationToken ct)
|
|
345
|
+
{
|
|
346
|
+
var stream = this.GetStreamProvider("SP").GetStream<string>(streamId);
|
|
347
|
+
var handles = await stream.GetAllSubscriptionHandles();
|
|
348
|
+
foreach (var handle in handles)
|
|
349
|
+
await handle.ResumeAsync(this);
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Implicit Subscriptions with IStreamSubscriptionObserver
|
|
354
|
+
|
|
355
|
+
```csharp
|
|
356
|
+
[ImplicitStreamSubscription("MyNamespace")]
|
|
357
|
+
public class MyGrain : Grain, IMyGrain, IStreamSubscriptionObserver
|
|
358
|
+
{
|
|
359
|
+
public async Task OnSubscribed(IStreamSubscriptionHandleFactory handleFactory)
|
|
360
|
+
{
|
|
361
|
+
var handle = handleFactory.Create<string>();
|
|
362
|
+
await handle.ResumeAsync(this);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Key Characteristics
|
|
368
|
+
|
|
369
|
+
- Subscriptions are per-grain (not per-activation) — durable across activations
|
|
370
|
+
- Multiple producers and consumers per stream
|
|
371
|
+
- Subscribing X times delivers event X times
|
|
372
|
+
- Rewindable streams (Event Hubs) support replay from `StreamSequenceToken`
|
|
373
|
+
|
|
374
|
+
### Why Orleans Streams?
|
|
375
|
+
|
|
376
|
+
Existing stream systems (Kafka, Storm, Spark) are for uniform dataflow graphs. Orleans Streams target:
|
|
377
|
+
1. **Flexible processing logic** — imperative, functional, Rx, stateful, with side effects
|
|
378
|
+
2. **Dynamic topologies** — add/remove nodes at runtime
|
|
379
|
+
3. **Fine-grained granularity** — each stream link is an independent entity
|
|
380
|
+
4. **Distribution** — scalable, elastic, reliable
|
|
381
|
+
|
|
382
|
+
Typical use: per-user streams with different processing logic per user, subscriptions changing dynamically.
|
|
383
|
+
|
|
384
|
+
## Direct Calls vs Streams vs Broadcast vs Observers vs IAsyncEnumerable
|
|
385
|
+
|
|
386
|
+
| Feature | Direct call | Streams | Broadcast | Observers | IAsyncEnumerable |
|
|
387
|
+
|---|---|---|---|---|---|
|
|
388
|
+
| Purpose | Command/query a known owner | Decoupled event flow | Loss-tolerant fan-out | Live callback to a connected subscriber | Progressive response to one caller |
|
|
389
|
+
| Persistence | No message persistence | Provider-dependent | No | No | No |
|
|
390
|
+
| Delivery | At-most-once by default; retries can duplicate | Provider-dependent | Best-effort | Best-effort for clients | Bound to the request |
|
|
391
|
+
| Subscription | None | Implicit or explicit | Implicit | Manual and renewable | None |
|
|
392
|
+
| Replay/history | No | Provider-dependent | No | No | No |
|
|
393
|
+
| Lifetime | One request/response | Long-lived logical subscription | Active implicit subscriber mapping | Client/object reference lifetime | One enumeration/call |
|
|
394
|
+
| Primary use | Typed result, completion, error | Dynamic pub/sub and queue-backed processing | Cache invalidation, latest-value hints, announcements | UI/client notifications | Large/progressive result sets with backpressure |
|
|
395
|
+
|
|
396
|
+
Use a one-way request only as an advanced direct-call variant when the caller explicitly accepts no completion, error, result, or receipt guarantee. Do not treat one-way calls, broadcast channels, or observers as durable messaging.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Orleans Testing Patterns
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- Choose The Harness First
|
|
6
|
+
- Runtime-Only Orleans Tests
|
|
7
|
+
- Shared AppHost Fixture For Real Topology Tests
|
|
8
|
+
- Mix AppHost Infra With `WebApplicationFactory`
|
|
9
|
+
- TUnit Per-Session Sharing
|
|
10
|
+
- SignalR And Browser Flows
|
|
11
|
+
- Failure Diagnostics
|
|
12
|
+
- Anti-Patterns
|
|
13
|
+
|
|
14
|
+
Use this reference when the task is about testing Orleans grains together with real hosting, HTTP/SignalR surfaces, Aspire resources, or browser flows.
|
|
15
|
+
|
|
16
|
+
These patterns are grounded in working test harnesses used in `AIBase` and `WA.Storied.Agents`: one shared AppHost fixture for the distributed topology, an optional `WebApplicationFactory` layer for direct Host DI/grain access, and separate browser contexts per UI test.
|
|
17
|
+
|
|
18
|
+
Sharing the AppHost reduces startup cost and does not make the suite sequential. Keep tests parallel, make fixture helpers concurrency-safe, and use unique grain IDs, tenant IDs, storage prefixes, database keys, connections, and browser contexts per test. Constrain only the smallest keyed group that performs destructive changes to the exact same shared state.
|
|
19
|
+
|
|
20
|
+
## Choose The Harness First
|
|
21
|
+
|
|
22
|
+
| Need | Best Harness | Why |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| Grain runtime behavior only | `InProcessTestCluster` | Fastest Orleans-focused harness without HTTP/UI layers |
|
|
25
|
+
| Real API, SignalR, SSE, or AppHost wiring | Shared Aspire/AppHost fixture | Exercises the actual distributed topology and resource graph |
|
|
26
|
+
| Direct Host DI services or `IGrainFactory` from the hosted app | Shared Aspire/AppHost fixture + `WebApplicationFactory<TEntryPoint>` | Keeps real infra while letting tests resolve services from the Host container |
|
|
27
|
+
| Browser automation against a co-hosted Orleans app | Shared Aspire/AppHost fixture + Playwright | Reuses one browser process and one AppHost boot, but isolates browser state per test |
|
|
28
|
+
|
|
29
|
+
## Runtime-Only Orleans Tests
|
|
30
|
+
|
|
31
|
+
When the test is purely about grain scheduling, persistence, reentrancy, or state transitions, stay close to Orleans and use `InProcessTestCluster`:
|
|
32
|
+
|
|
33
|
+
```csharp
|
|
34
|
+
var builder = new InProcessTestClusterBuilder();
|
|
35
|
+
builder.ConfigureSilo(siloBuilder =>
|
|
36
|
+
{
|
|
37
|
+
siloBuilder.AddMemoryGrainStorageAsDefault();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await using var cluster = await builder.BuildAndStartAsync();
|
|
41
|
+
var grain = cluster.GrainFactory.GetGrain<IOrderGrain>(Guid.NewGuid());
|
|
42
|
+
|
|
43
|
+
await grain.SubmitAsync(new SubmitOrder("PO-42"));
|
|
44
|
+
var state = await grain.GetStateAsync();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Use this harness when HTTP, SignalR, and AppHost resources are irrelevant to the assertion.
|
|
48
|
+
|
|
49
|
+
## Shared AppHost Fixture For Real Topology Tests
|
|
50
|
+
|
|
51
|
+
Use Aspire when the assertion depends on the actual Orleans host plus the real surrounding resources:
|
|
52
|
+
|
|
53
|
+
```csharp
|
|
54
|
+
public sealed class AspireTestFixture : IAsyncDisposable
|
|
55
|
+
{
|
|
56
|
+
private DistributedApplication? _app;
|
|
57
|
+
|
|
58
|
+
public DistributedApplication App =>
|
|
59
|
+
_app ?? throw new InvalidOperationException("App not initialized.");
|
|
60
|
+
|
|
61
|
+
public string HostUrl { get; private set; } = string.Empty;
|
|
62
|
+
|
|
63
|
+
public async Task InitializeAsync()
|
|
64
|
+
{
|
|
65
|
+
var builder = await DistributedApplicationTestingBuilder.CreateAsync<Projects.My_AppHost>();
|
|
66
|
+
|
|
67
|
+
builder.Services.AddLogging(logging =>
|
|
68
|
+
{
|
|
69
|
+
logging.ClearProviders();
|
|
70
|
+
logging.AddConsole();
|
|
71
|
+
logging.SetMinimumLevel(LogLevel.Warning);
|
|
72
|
+
logging.AddFilter("Aspire.Hosting.Dcp", LogLevel.Warning);
|
|
73
|
+
logging.AddFilter("Aspire.Hosting.Backchannel", LogLevel.Critical);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
_app = await builder.BuildAsync();
|
|
77
|
+
await _app.StartAsync();
|
|
78
|
+
|
|
79
|
+
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
|
|
80
|
+
await _app.ResourceNotifications.WaitForResourceHealthyAsync("host", cts.Token);
|
|
81
|
+
|
|
82
|
+
HostUrl = _app.CreateHttpClient("host", "http").BaseAddress?.ToString()
|
|
83
|
+
?? throw new InvalidOperationException("Host URL was not resolved.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public HttpClient CreateApiClient() => App.CreateHttpClient("host", "http");
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This is the right shape for:
|
|
91
|
+
|
|
92
|
+
- HTTP endpoint tests
|
|
93
|
+
- SignalR/SSE/OpenResponses tests
|
|
94
|
+
- admin or operator UI tests
|
|
95
|
+
- Orleans flows that require the real host pipeline, auth, or middleware
|
|
96
|
+
|
|
97
|
+
## Mix AppHost Infra With `WebApplicationFactory`
|
|
98
|
+
|
|
99
|
+
This is the production-style pattern for co-hosted Orleans apps: boot infrastructure in Aspire once, then create a `WebApplicationFactory` over the Host/API assembly for direct DI and grain access.
|
|
100
|
+
|
|
101
|
+
```csharp
|
|
102
|
+
public sealed class TestApplication
|
|
103
|
+
: WebApplicationFactory<HostEntryPointMarker>, IAsyncDisposable
|
|
104
|
+
{
|
|
105
|
+
private static readonly AspireTestFixture SharedFixture = new();
|
|
106
|
+
private readonly Dictionary<string, string?> _overrides = new(StringComparer.OrdinalIgnoreCase);
|
|
107
|
+
|
|
108
|
+
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
|
109
|
+
{
|
|
110
|
+
builder.UseEnvironment(Environments.Development);
|
|
111
|
+
builder.ConfigureAppConfiguration((_, config) => config.AddInMemoryCollection(_overrides));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public async Task InitializeAsync()
|
|
115
|
+
{
|
|
116
|
+
await SharedFixture.InitializeAsync();
|
|
117
|
+
|
|
118
|
+
var tables = await SharedFixture.App.GetConnectionStringAsync("tables");
|
|
119
|
+
var blobs = await SharedFixture.App.GetConnectionStringAsync("blobs");
|
|
120
|
+
|
|
121
|
+
_overrides["ConnectionStrings:Tables"] = tables;
|
|
122
|
+
_overrides["ConnectionStrings:Blobs"] = blobs;
|
|
123
|
+
|
|
124
|
+
CreateClient();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public AsyncServiceScope CreateScope() => Services.CreateAsyncScope();
|
|
128
|
+
|
|
129
|
+
public HttpClient CreateApiClient()
|
|
130
|
+
{
|
|
131
|
+
var client = CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
|
|
132
|
+
client.Timeout = TimeSpan.FromMinutes(5);
|
|
133
|
+
return client;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Use this when the test needs:
|
|
139
|
+
|
|
140
|
+
- `IGrainFactory`
|
|
141
|
+
- repositories or managers from DI
|
|
142
|
+
- direct access to runtime services
|
|
143
|
+
- real HTTP clients and hub connections against the same host
|
|
144
|
+
- whichever async fixture contract the test framework expects: xUnit, TUnit, or a repo-local wrapper
|
|
145
|
+
|
|
146
|
+
## TUnit Per-Session Sharing
|
|
147
|
+
|
|
148
|
+
For TUnit-based Orleans suites, keep the AppHost boot outside individual tests:
|
|
149
|
+
|
|
150
|
+
```csharp
|
|
151
|
+
[ClassDataSource<TestApplication>(Shared = SharedType.PerTestSession)]
|
|
152
|
+
public sealed class OrderGrainIntegrationTests(TestApplication app)
|
|
153
|
+
{
|
|
154
|
+
[Test]
|
|
155
|
+
public async Task Order_grain_persists_state_through_real_host()
|
|
156
|
+
{
|
|
157
|
+
await using var scope = app.CreateScope();
|
|
158
|
+
var grainFactory = scope.ServiceProvider.GetRequiredService<IGrainFactory>();
|
|
159
|
+
|
|
160
|
+
var grain = grainFactory.GetGrain<IOrderGrain>(Guid.NewGuid());
|
|
161
|
+
await grain.SubmitAsync(new SubmitOrder("PO-42"));
|
|
162
|
+
|
|
163
|
+
var state = await grain.GetStateAsync();
|
|
164
|
+
await Assert.That(state.Number).IsEqualTo("PO-42");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The same pattern works for API-only tests with `ClassDataSource<AspireTestFixture>(Shared = SharedType.PerTestSession)`.
|
|
170
|
+
|
|
171
|
+
Do not add `[NotInParallel]` to the class or force one test module/worker merely because the fixture is shared. If a maintenance test drops a shared schema or resets the whole cluster, give only those destructive tests the same keyed non-parallel constraint.
|
|
172
|
+
|
|
173
|
+
## SignalR And Browser Flows
|
|
174
|
+
|
|
175
|
+
For SignalR or UI flows, keep connection and browser helpers on the shared fixture:
|
|
176
|
+
|
|
177
|
+
```csharp
|
|
178
|
+
public HubConnection CreateAgentHubConnection(string token)
|
|
179
|
+
{
|
|
180
|
+
var hubUrl = new Uri(new Uri(HostUrl), "/agenthub");
|
|
181
|
+
|
|
182
|
+
return new HubConnectionBuilder()
|
|
183
|
+
.WithUrl(hubUrl, options =>
|
|
184
|
+
{
|
|
185
|
+
options.AccessTokenProvider = () => Task.FromResult(token)!;
|
|
186
|
+
options.Headers["Authorization"] = $"Bearer {token}";
|
|
187
|
+
options.Transports = HttpTransportType.LongPolling;
|
|
188
|
+
})
|
|
189
|
+
.WithAutomaticReconnect()
|
|
190
|
+
.Build();
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
For Playwright:
|
|
195
|
+
|
|
196
|
+
- initialize Playwright once in the shared fixture
|
|
197
|
+
- create a new browser context per test
|
|
198
|
+
- set `BaseURL`, viewport, and `IgnoreHTTPSErrors` in the helper, not inline in every test
|
|
199
|
+
|
|
200
|
+
## Failure Diagnostics
|
|
201
|
+
|
|
202
|
+
Follow the `SKILL.md` Diagnostic Output Budget: keep progress/ANSI visible, collect Warning/Error/Critical logs in a size-bounded collector, and show only the exact test/resource failure, root exception, and relevant stack frames (at most 80 lines / 8 KiB per response). Never print the full collector on HTTP 500, startup failures, or crashes. Link Playwright screenshots/HTML and inspect only bounded resource-error excerpts when health waits fail. Preserve the original exception and runner exit code.
|
|
203
|
+
|
|
204
|
+
## Anti-Patterns
|
|
205
|
+
|
|
206
|
+
- Creating `DistributedApplicationTestingBuilder.CreateAsync<...>()` inside each test method
|
|
207
|
+
- Booting a second Orleans client inside a co-hosted Host test when the app already uses `UseOrleans`
|
|
208
|
+
- Copy-pasting local connection strings into tests instead of resolving them from `SharedFixture.App`
|
|
209
|
+
- Using in-memory substitutes for persistence-sensitive or streaming-sensitive integration tests
|
|
210
|
+
- Sharing a Playwright page or browser context across tests instead of sharing only the browser process
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) .NET Foundation and Contributors
|
|
4
|
+
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Test Platform and Framework Detection
|
|
2
|
+
Copyright (c) .NET Foundation and Contributors
|
|
3
|
+
Licensed under the MIT License. See LICENSE.
|
|
4
|
+
|
|
5
|
+
Upstream source: https://github.com/dotnet/skills/blob/a6bf43ab61967c30919334986515163ab24d7635/plugins/dotnet-test/skills/platform-detection/SKILL.md
|
|
6
|
+
|
|
7
|
+
Changes made by Cratis:
|
|
8
|
+
|
|
9
|
+
LICENSE:
|
|
10
|
+
- Copy upstream MIT license and copyright verbatim.
|
|
11
|
+
|
|
12
|
+
SKILL.md:
|
|
13
|
+
- Normalize frontmatter serialization, add MIT license where absent; preserve name and description.
|
|
14
|
+
- Add marked modification/source comment and applicability/Cratis precedence line with the skill-specific safety, ownership or compatibility caveat.
|
|
15
|
+
- Rewrite the description to the Cratis shape (what it does, Use when, Not for) and drop references to skills that are not part of this corpus.
|
|
16
|
+
|
|
17
|
+
NOTICE: Add this attribution, change list, and any download/execution disclosure.
|