Skip to content

Commit bf45445

Browse files
authored
Add AI Instruction Files for GitHub Copilot (#1078)
* Add AI instruction files for DynamicData Three instruction files for GitHub Copilot and AI assistants: 1. .github/copilot-instructions.md — General overview - What DynamicData is and why it matters - Why performance and Rx compliance are critical - Repository structure (public API surface, not internals) - Operator architecture pattern (extension method -> internal class -> Run()) - Thread safety principles - Breaking change policy - Testing patterns 2. .github/instructions/rx-contracts.instructions.md — Comprehensive Rx guide - Core concepts: composability, hot vs cold, Publish/RefCount - The Observable contract (serialized notifications, terminal semantics) - Scheduler guide: all common schedulers, injection for testability - Complete Disposable helper guide: Disposable.Create, CompositeDisposable, SerialDisposable, SingleAssignmentDisposable, RefCountDisposable, BooleanDisposable, CancellationDisposable - Writing custom operators (single-source and multi-source patterns) - Operator review checklist - Common pitfalls (cold re-subscribe, leak, sync-over-async, Subject exposure) 3. .github/instructions/dynamicdata-operators.instructions.md — Operator guide - Changeset model and change reasons - Complete operator catalog with detailed code examples: Filtering, Transformation, Sorting, Paging, Grouping, Joining, Combining, Aggregation, Fan-out/Fan-in, Refresh, Lifecycle, Buffering, Binding, Utilities - How to write a new operator (10-step guide) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revise AI instructions: remove internal fix references, add Rx operator catalog Changes: - Remove all references to deadlock fixes, queue-drain pattern, SharedDeliveryQueue, SynchronizeSafe (these are PR-specific concepts that don't exist in the main branch yet) - Thread safety guidance uses neutral language about Synchronize behavior - Add comprehensive standard Rx operator reference table (~80 operators) organized by category: Creation, Transformation, Filtering, Combining, Aggregation, Error Handling, Scheduling, Utility - Each operator has a concise description * Rename rx-contracts → rx, link both instruction files from main - rx-contracts.instructions.md → rx.instructions.md (covers far more than contracts) - copilot-instructions.md now links to both rx.instructions.md and dynamicdata-operators.instructions.md * Expand testing section, update breaking changes for SemVer Testing section expanded from 7 lines to ~340 lines covering: - Both observation patterns (AsAggregator vs RecordCacheItems) - Required test coverage per operator (10 categories) - Rx contract validation with ValidateSynchronization - Completion/error propagation testing with TestSourceCache - Multi-threaded stress test patterns with Barrier - Regression test requirements (mandatory for all bug fixes) - Stub/fixture pattern, changeset assertion techniques - Complete test utilities reference table - Domain types catalog with Bogus faker usage - Anti-patterns with bad/good examples Breaking changes updated: SemVer allows them in major versions, but they must be explicitly called out to the maintainer. * Expand operator docs: rename, add change reason tables, add list guide - Rename dynamicdata-operators → dynamicdata-cache (it's cache-specific) - Expand cache guide from 278 → 598 lines: - SourceCache, ISourceUpdater, Edit() API documented - Change<T,K> struct, ChangeReason enum, ChangeAwareCache explained - Every operator has a table showing exact handling per ChangeReason - Covers: Filter (4 variants), Transform (7 variants), Sort, Page, Virtualise, Top, Group (3 variants), all 4 Joins, set operations, MergeChangeSets, MergeMany, SubscribeMany, DisposeMany, AutoRefresh, lifecycle callbacks, buffering, binding, utilities, property observers - Writing a new operator: step-by-step with full code example - New dynamicdata-list.instructions.md (424 lines): - SourceList, IExtendedList, Edit() API - ListChangeReason (8 reasons vs cache's 5), ChangeAwareList - Every list operator with change reason handling tables - List vs Cache comparison table - Converting between list and cache - Writing a new list operator with checklist - Update main copilot-instructions.md links * Fix malformed single-row tables in operator docs Replace 10 broken markdown tables (single data row with no proper header) with plain prose notes. These were cases where an operator's behavior is the same as another and a full table wasn't needed. * Move Cache vs List comparison to main instructions The list-vs-cache guidance belongs in the main copilot-instructions.md where it serves as a top-level navigation aid, not buried in the list operator file. Also consolidates the instruction file links into the new section. * Split testing into main + cache + list instruction files Testing section was too cache-specific. Now split into three: - copilot-instructions.md (Testing section, 73 lines): Philosophy, requirements, frameworks, Rx contract validation, regression test rules, stress test principles, utilities reference, domain types, anti-patterns — all universal, no cache/list specifics - testing-cache.instructions.md (175 lines, NEW): Both observation patterns (AsAggregator + RecordCacheItems), cache changeset assertions, TestSourceCache, stub/fixture pattern, cache stress tests, per-operator test checklist - testing-list.instructions.md (143 lines, NEW): List aggregator, RecordListItems, list changeset assertions (item vs range changes), cache-vs-list testing differences table, list fixture pattern, list stress tests, per-operator test checklist * Add ObservableChangeSet.Create docs, instruction maintenance rule Cache instructions: Added ObservableChangeSet.Create section with 8 overloads documented, 3 practical examples (sync event bridge, async API loading, SignalR live updates), key behaviors explained. List instructions: Added ObservableChangeSet.Create<T> section with sync and async examples (FileSystemWatcher, async stream). Main instructions: Added 'Maintaining These Instructions' section requiring new operators to be added to instruction files, operator behavior changes to update tables, new utilities/domain types to be documented. * Add deterministic completion pattern and stress test guidelines Testing instructions updated with three new subsections: - Waiting for Pipeline Completion: Publish + LastOrDefaultAsync + ToTask pattern. Never use Task.Delay to wait for pipelines to settle. - Chaining Intermediate Caches: AsObservableCache() to materialize stages, Connect() to chain, completion cascades through the chain. - Stress Test Pattern: Full 7-step pattern for stress testing operators: deterministic seed data, chained pipelines, multi-threaded writers, pre-computed LINQ expectations, exact content verification. Anti-patterns updated: Task.Delay for settling → completion pattern. * Potential fix for pull request finding * Make UTF-8 * docs: add composition-first Rx guidance with Defer pattern examples Captures the principle that Observable.Create with manual observer forwarding is a code smell — prefer declarative composition using Defer, Do, Merge, Concat, and other existing operators. Includes concrete before/after examples and criteria for when Observable.Create IS appropriate. * docs: strengthen Rx contract trust — axioms, not guidelines The contracts are unconditional guarantees. Doubting them and adding safety wrappers abandons the very thing that makes Rx code correct by construction.
1 parent 9bc6b44 commit bf45445

File tree

6 files changed

+2922
-0
lines changed

6 files changed

+2922
-0
lines changed

.github/copilot-instructions.md

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
# DynamicData — AI Instructions
2+
3+
## What is DynamicData?
4+
5+
DynamicData is a reactive collections library for .NET, built on top of [Reactive Extensions (Rx)](https://github.com/dotnet/reactive). It provides `SourceCache<TObject, TKey>` and `SourceList<TObject>` — observable data collections that emit **changesets** when modified. These changesets flow through operator pipelines (Sort, Filter, Transform, Group, Join, etc.) that maintain live, incrementally-updated views of the data.
6+
7+
DynamicData is used in production by thousands of applications. It is the reactive data layer for [ReactiveUI](https://reactiveui.net/), making it foundational infrastructure for the .NET reactive ecosystem.
8+
9+
## Cache vs List — Two Collection Types
10+
11+
DynamicData provides two parallel collection types. **Choose the right one — they are not interchangeable.**
12+
13+
| | **Cache** (`SourceCache<T, TKey>`) | **List** (`SourceList<T>`) |
14+
|---|---|---|
15+
| **Identity** | Items identified by unique key | Items identified by index position |
16+
| **Duplicates** | Not allowed (key must be unique) | Allowed (same item at multiple positions) |
17+
| **Ordering** | Unordered by default (Sort adds ordering) | Inherently ordered (like `List<T>`) |
18+
| **Best for** | Entities with IDs, lookup by key | Ordered sequences, duplicates OK |
19+
| **Change types** | Add, Update, Remove, Refresh, Moved | Add, AddRange, Replace, Remove, RemoveRange, Moved, Refresh, Clear |
20+
| **Changeset** | `IChangeSet<TObject, TKey>` | `IChangeSet<T>` |
21+
22+
**Rule of thumb:** If your items have a natural unique key (ID, name, etc.), use **Cache**. If order matters and/or duplicates are possible, use **List**. Cache is used far more often in practice.
23+
24+
See `.github/instructions/dynamicdata-cache.instructions.md` for the complete cache operator reference.
25+
26+
See `.github/instructions/dynamicdata-list.instructions.md` for the complete list operator reference.
27+
28+
## Why Performance Matters
29+
30+
Every item flowing through a DynamicData pipeline passes through multiple operators. Each operator processes changesets — not individual items — so a single cache edit with 1000 items creates a changeset that flows through every operator in the chain. At library scale:
31+
32+
- **Per-item overhead compounds**: 1 allocation × 10 operators × 1000 items × 100 pipelines = 1M allocations per batch
33+
- **Lock contention is the bottleneck**: operators serialize access to shared state. Minimizing lock hold time is a core design goal.
34+
- **Prefer value types and stack allocation**: use structs, `ref struct`, `Span<T>`, and avoid closures in hot paths where possible
35+
36+
When optimizing, measure allocation rates and lock contention, not just wall-clock time.
37+
38+
## Why Rx Contract Compliance is Critical
39+
40+
DynamicData operators compose — the output of one is the input of the next. If any operator violates the Rx contract (e.g., concurrent `OnNext` calls, calls after `OnCompleted`), every downstream operator can corrupt its internal state. This is not a crash — it's silent data corruption that manifests as wrong results, missing items, or phantom entries. In a reactive UI, this means the user sees stale or incorrect data with no error message.
41+
42+
See `.github/instructions/rx.instructions.md` for comprehensive Rx contract rules, scheduler usage, disposable patterns, and a complete standard Rx operator reference.
43+
44+
## Breaking Changes
45+
46+
DynamicData follows [Semantic Versioning (SemVer)](https://semver.org/). Breaking changes **are possible** in major version bumps, but they are never done lightly. This library has thousands of downstream consumers — every breaking change has a blast radius.
47+
48+
**Rules:**
49+
- Breaking changes require a major version bump. **You MUST explicitly call out any potentially breaking change to the user** before making it — even if you think it's minor. Let the maintainers decide.
50+
- Prefer non-breaking alternatives first: new overloads, new methods, optional parameters with safe defaults.
51+
- When a breaking change is justified, mark the old API with `[Obsolete("Use XYZ instead. This will be removed in vN+1.")]` in the current version and remove it in the next major.
52+
- Behavioral changes (different ordering, different filtering semantics, different error propagation) are breaking even if the signature is unchanged. Call these out.
53+
- Internal types (`internal` visibility) can change freely — they are not part of the public contract.
54+
55+
**What counts as breaking:**
56+
- Changing the signature of a public extension method (parameters, return type, generic constraints)
57+
- Changing observable behavior (emission order, filtering semantics, error/completion propagation)
58+
- Removing or renaming public types, methods, or properties
59+
- Adding required parameters to existing methods
60+
- Changing the default behavior of an existing overload
61+
62+
## Maintaining These Instructions
63+
64+
These instruction files are living documentation. **They must be kept in sync with the code.**
65+
66+
- When a **new operator** is added, it **MUST** be added to the appropriate instruction file (`dynamicdata-cache.instructions.md` or `dynamicdata-list.instructions.md`) with its change reason handling table.
67+
- When an **operator's behavior changes**, update its table in the instruction file.
68+
- When a **new test utility** is added, update the test utilities reference in the main instructions and the appropriate `testing-*.instructions.md`.
69+
- When a **new domain type** is added to `Tests/Domain/`, add it to the Domain Types section.
70+
71+
## Repository Structure
72+
73+
```
74+
src/
75+
├── DynamicData/ # The library
76+
│ ├── Cache/ # Cache (keyed collection) operators
77+
│ │ ├── Internal/ # Operator implementations (private)
78+
│ │ ├── ObservableCache.cs # Core observable cache implementation
79+
│ │ └── ObservableCacheEx.cs # Public API: extension methods for cache operators
80+
│ ├── List/ # List (ordered collection) operators
81+
│ │ ├── Internal/ # Operator implementations (private)
82+
│ │ └── ObservableListEx.cs # Public API: extension methods for list operators
83+
│ ├── Binding/ # UI binding operators (SortAndBind, etc.)
84+
│ ├── Internal/ # Shared internal infrastructure
85+
│ └── Kernel/ # Low-level types (Optional<T>, Error<T>, etc.)
86+
├── DynamicData.Tests/ # Tests (xUnit + FluentAssertions)
87+
│ ├── Cache/ # Cache operator tests
88+
│ ├── List/ # List operator tests
89+
│ └── Domain/ # Test domain types using Bogus fakers
90+
```
91+
92+
## Operator Architecture Pattern
93+
94+
Most operators follow the same two-part pattern:
95+
96+
```csharp
97+
// 1. Public API: extension method in ObservableCacheEx.cs (thin wrapper)
98+
public static IObservable<IChangeSet<TDest, TKey>> Transform<TSource, TKey, TDest>(
99+
this IObservable<IChangeSet<TSource, TKey>> source,
100+
Func<TSource, TDest> transformFactory)
101+
{
102+
return new Transform<TDest, TSource, TKey>(source, transformFactory).Run();
103+
}
104+
105+
// 2. Internal: sealed class in Cache/Internal/ with a Run() method
106+
internal sealed class Transform<TDest, TSource, TKey>
107+
{
108+
public IObservable<IChangeSet<TDest, TKey>> Run() =>
109+
Observable.Create<IChangeSet<TDest, TKey>>(observer =>
110+
{
111+
// Subscribe to source, process changesets, emit results
112+
// Use ChangeAwareCache<T,K> for incremental state
113+
// Call CaptureChanges() to produce the output changeset
114+
});
115+
}
116+
```
117+
118+
**Key points:**
119+
- The extension method is the **public API surface** — keep it thin
120+
- The internal class holds constructor parameters and implements `Run()`
121+
- `Run()` returns `Observable.Create<T>` which **defers subscription** (cold observable)
122+
- Inside `Create`, operators subscribe to sources and wire up changeset processing
123+
- `ChangeAwareCache<T,K>` tracks incremental changes and produces immutable snapshots via `CaptureChanges()`
124+
- Operators must handle all change reasons: `Add`, `Update`, `Remove`, `Refresh`
125+
126+
## Thread Safety in Operators
127+
128+
When an operator has multiple input sources that share mutable state:
129+
- All sources must be serialized through a shared lock
130+
- Use `Synchronize(gate)` with a shared lock object to serialize multiple sources
131+
- Keep lock hold times as short as practical
132+
133+
When operators use `Synchronize(lock)` from Rx:
134+
- The lock is held during the **entire** downstream delivery chain
135+
- This ensures serialized delivery across multiple sources sharing a lock
136+
- Always use a private lock object — never expose it to external consumers
137+
138+
## Testing
139+
140+
**All new code MUST come with unit tests that prove 100% correctness. All bug fixes MUST include a regression test that reproduces the bug before verifying the fix.** No exceptions. Untested code is broken code — you just don't know it yet.
141+
142+
### Frameworks and Tools
143+
144+
- **xUnit** — test framework (`[Fact]`, `[Theory]`, `[InlineData]`)
145+
- **FluentAssertions** — via the `AwesomeAssertions` NuGet package (`.Should().Be()`, `.Should().BeEquivalentTo()`, etc.)
146+
- **Bogus** — fake data generation via `Faker<T>` in `DynamicData.Tests/Domain/Fakers.cs`
147+
- **TestSourceCache<T,K>** and **TestSourceList<T>** — enhanced source collections in `Tests/Utilities/` that support `.Complete()` and `.SetError()` for testing terminal Rx events
148+
149+
### Test File Naming and Organization
150+
151+
Tests live in `src/DynamicData.Tests/` mirroring the library structure:
152+
- `Cache/` — cache operator tests (one fixture class per operator, e.g., `TransformFixture.cs`)
153+
- `List/` — list operator tests
154+
- `Domain/` — shared domain types (`Person`, `Animal`, `AnimalOwner`, `Market`, etc.) and Bogus fakers
155+
- `Utilities/` — test infrastructure (aggregators, validators, recording observers, stress helpers)
156+
157+
Naming convention: `{OperatorName}Fixture.cs`. For operators with multiple overloads, use partial classes: `FilterFixture.Static.cs`, `FilterFixture.DynamicPredicate.IntegrationTests.cs`, etc.
158+
159+
For cache-specific testing patterns (observation patterns, changeset assertions, stub pattern), see `.github/instructions/testing-cache.instructions.md`.
160+
161+
For list-specific testing patterns, see `.github/instructions/testing-list.instructions.md`.
162+
163+
### Testing the Rx Contract
164+
165+
`.ValidateSynchronization()` detects Rx contract violations. It tracks in-flight notifications with `Interlocked.Exchange` — if two threads enter `OnNext` simultaneously, it throws `UnsynchronizedNotificationException`. It uses raw observer/observable types to bypass Rx's built-in safety guards so violations are surfaced, not masked.
166+
167+
```csharp
168+
source.Connect()
169+
.Transform(x => new ViewModel(x))
170+
.ValidateSynchronization() // THROWS if concurrent delivery detected
171+
.RecordCacheItems(out var results);
172+
```
173+
174+
### Writing Regression Tests for Bug Fixes
175+
176+
Every bug fix **must** include a test that:
177+
1. **Reproduces the bug** — the test fails without the fix
178+
2. **Verifies the fix** — the test passes with the fix
179+
3. **Is named descriptively** — describes the scenario, not the bug ID
180+
181+
```csharp
182+
// GOOD: describes the scenario that was broken
183+
[Fact]
184+
public void RemoveThenReAddWithSameKey_ShouldNotDuplicate()
185+
186+
// BAD: meaningless to future readers
187+
[Fact]
188+
public void FixBug1234()
189+
```
190+
191+
### Waiting for Pipeline Completion
192+
193+
**Never use `Task.Delay` to wait for pipelines to settle.** Use the `Publish` + `LastOrDefaultAsync` + `ToTask` pattern for deterministic completion:
194+
195+
```csharp
196+
var published = source.Connect()
197+
.Filter(x => x.IsActive)
198+
.Sort(comparer)
199+
.Publish();
200+
201+
var hasCompleted = published.LastOrDefaultAsync().ToTask();
202+
using var results = published.AsAggregator();
203+
using var connect = published.Connect();
204+
205+
// Do stuff — write to source
206+
source.AddOrUpdate(items);
207+
208+
// Signal completion — cascades OnCompleted through the chain
209+
source.Dispose();
210+
211+
await hasCompleted; // deterministic wait — no timeout, no polling
212+
213+
// Assert exact results
214+
results.Data.Items.Should().BeEquivalentTo(expectedItems);
215+
```
216+
217+
**Why this works:** `LastOrDefaultAsync()` subscribes to the published stream and completes only when the source completes. `ToTask()` converts it to an awaitable. Disposing the source cascades `OnCompleted` through the entire chain.
218+
219+
### Chaining Intermediate Caches
220+
221+
Use `AsObservableCache()` to materialize pipeline stages into queryable caches. Each cache's `Connect()` feeds the next stage:
222+
223+
```csharp
224+
// Stage 1: complex pipeline → materialized cache
225+
using var cache1 = source.Connect()
226+
.AutoRefresh(x => x.Rating)
227+
.Filter(x => x.Rating > 3.0)
228+
.Transform(x => new ViewModel(x))
229+
.AsObservableCache();
230+
231+
// Stage 2: chain from cache1 → another cache
232+
using var cache2 = cache1.Connect()
233+
.GroupOn(x => x.Category)
234+
.MergeManyChangeSets(g => g.Cache.Connect())
235+
.AsObservableCache();
236+
237+
// Stage 3: final pipeline with completion tracking
238+
var published = cache2.Connect()
239+
.Sort(comparer)
240+
.Publish();
241+
242+
var hasCompleted = published.LastOrDefaultAsync().ToTask();
243+
using var finalResults = published.AsAggregator();
244+
using var connect = published.Connect();
245+
246+
source.Dispose(); // cascades through cache1 → cache2 → finalResults
247+
await hasCompleted;
248+
249+
// Verify exact contents at every stage
250+
cache1.Items.Should().BeEquivalentTo(expectedStage1);
251+
cache2.Items.Should().BeEquivalentTo(expectedStage2);
252+
finalResults.Data.Items.Should().BeEquivalentTo(expectedFinal);
253+
```
254+
255+
### Stress Test Pattern
256+
257+
The complete pattern for stress testing DynamicData operators:
258+
259+
1. **Generate deterministic test data** from a `Bogus.Randomizer` with a fixed seed. All quantities (item counts, property values, thread counts) come from the seed — nothing hardcoded except the seed itself.
260+
261+
2. **Wire up chained pipelines** with intermediate `AsObservableCache()` stages. Use long fluent operator chains (10+ operators per chain). Each operator should appear in at least 2 different chains.
262+
263+
3. **Track completion** with `Publish()` + `LastOrDefaultAsync().ToTask()` on terminal chains.
264+
265+
4. **Feed data from multiple threads** using `Barrier` for simultaneous start — maximum contention. Interleave adds, updates, removes, and property mutations.
266+
267+
5. **Pre-compute expected results** by simulating each chain's logic in plain LINQ on the same generated data — before the Rx pipelines run.
268+
269+
6. **Dispose sources**`await hasCompleted` on all terminal chains.
270+
271+
7. **Verify exact contents** of ALL intermediate and final caches — not just counts, but the actual items via `BeEquivalentTo`.
272+
273+
```csharp
274+
// Pre-compute expected results via LINQ
275+
var expectedFiltered = generatedItems.Where(x => x.IsActive).ToList();
276+
var expectedTransformed = expectedFiltered.Select(x => Transform(x)).ToList();
277+
278+
// After pipelines complete
279+
cache1.Items.Should().BeEquivalentTo(expectedFiltered);
280+
cache2.Items.Should().BeEquivalentTo(expectedTransformed);
281+
```
282+
283+
### Stress Test Principles
284+
285+
- Use `Barrier` for simultaneous start — maximizes contention. Include the main thread in participant count.
286+
- Use deterministic data so failures are reproducible. Use `Bogus.Randomizer` with a fixed seed — **never `System.Random`**.
287+
- Assert the **exact final state**, not just "count > 0".
288+
- Use `Task.WhenAny(completed, Task.Delay(timeout))` to detect deadlocks with a meaningful timeout.
289+
- Include mixed operations: adds, updates, removes, property mutations, dynamic parameter changes.
290+
- Use the `StressAddRemove` extension methods in `Tests/Utilities/` for standard add/remove patterns with timed removal.
291+
292+
### Test Utilities Reference
293+
294+
The `Tests/Utilities/` directory provides powerful helpers — **use them** instead of reinventing:
295+
296+
| Utility | Purpose |
297+
|---------|---------|
298+
| `ValidateSynchronization()` | Detects concurrent `OnNext` — Rx contract violation |
299+
| `ValidateChangeSets(keySelector)` | Validates structural integrity of every changeset |
300+
| `RecordCacheItems(out results)` | Cache recording observer with keyed + sorted tracking |
301+
| `RecordListItems(out results)` | List recording observer |
302+
| `TestSourceCache<T,K>` | SourceCache with `.Complete()` and `.SetError()` support |
303+
| `TestSourceList<T>` | SourceList with `.Complete()` and `.SetError()` support |
304+
| `StressAddRemove` extensions | Add/remove stress patterns with timed automatic removal |
305+
| `ForceFail(count, exception)` | Forces an observable to error after N emissions |
306+
| `Parallelize(count, parallel)` | Creates parallel subscriptions for stress testing |
307+
| `ObservableSpy` | Diagnostic logging for pipeline debugging |
308+
| `FakeScheduler` | Controlled scheduler for time-dependent tests |
309+
| `Fakers.*` | Bogus fakers for `Person`, `Animal`, `AnimalOwner`, `Market` |
310+
311+
### Domain Types
312+
313+
Shared domain types in `Tests/Domain/`:
314+
315+
- **`Person`**`Name` (key), `Age`, `Gender`, `FavoriteColor`, `PetType`. Implements `INotifyPropertyChanged`.
316+
- **`Animal`**`Name` (key), `Type`, `Family` (enum: Mammal, Reptile, Fish, Amphibian, Bird)
317+
- **`AnimalOwner`**`Name` (key), `Animals` (ObservableCollection). Ideal for `TransformMany`/`MergeManyChangeSets` tests.
318+
- **`Market`** / **`MarketPrice`** — financial-style streaming data tests
319+
- **`PersonWithGender`**, **`PersonWithChildren`**, etc. — transform output types
320+
321+
Generate test data with Bogus:
322+
```csharp
323+
var people = Fakers.Person.Generate(100);
324+
var animals = Fakers.Animal.Generate(50);
325+
var owners = Fakers.AnimalOwnerWithAnimals.Generate(10); // pre-populated with animals
326+
```
327+
328+
### Test Anti-Patterns
329+
330+
**❌ Testing implementation details instead of behavior:**
331+
```csharp
332+
// BAD: message count is an implementation detail — fragile
333+
results.Messages.Count.Should().Be(3);
334+
// GOOD: test the observable behavior and final state
335+
results.Data.Count.Should().Be(expectedCount);
336+
results.Data.Items.Should().BeEquivalentTo(expectedItems);
337+
```
338+
339+
**❌ Using `Task.Delay` to wait for pipelines to settle:**
340+
```csharp
341+
// BAD: flaky, slow, non-deterministic
342+
await Task.Delay(2000); // "wait for async deliveries to settle"
343+
results.Data.Count.Should().Be(expected);
344+
// GOOD: deterministic completion via Publish + LastOrDefaultAsync
345+
var published = pipeline.Publish();
346+
var hasCompleted = published.LastOrDefaultAsync().ToTask();
347+
using var results = published.AsAggregator();
348+
using var connect = published.Connect();
349+
source.Dispose();
350+
await hasCompleted;
351+
results.Data.Items.Should().BeEquivalentTo(expectedItems);
352+
```
353+
354+
**❌ Using `Thread.Sleep` for timing:**
355+
```csharp
356+
// BAD: flaky and slow
357+
Thread.Sleep(1000);
358+
// GOOD: use test schedulers or deterministic waiting
359+
var scheduler = new TestScheduler();
360+
scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks);
361+
```
362+
363+
**❌ Ignoring disposal:**
364+
```csharp
365+
// BAD: leaks subscriptions, masks errors
366+
var results = source.Connect().Filter(p => true).AsAggregator();
367+
// GOOD: using ensures cleanup even if assertion throws
368+
using var results = source.Connect().Filter(p => true).AsAggregator();
369+
```
370+
371+
**❌ Non-deterministic data without seeds:**
372+
```csharp
373+
// BAD: failures aren't reproducible across runs
374+
var random = new Random();
375+
// GOOD: use Bogus Randomizer with a fixed seed
376+
var randomizer = new Randomizer(42);
377+
var people = Fakers.Person.UseSeed(randomizer.Int()).Generate(100);
378+
```

0 commit comments

Comments
 (0)