Skip to content

Commit c4dc0d4

Browse files
dwcullopCopilot
andcommitted
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. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e0052ba commit c4dc0d4

3 files changed

Lines changed: 121 additions & 0 deletions

File tree

.github/copilot-instructions.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ DynamicData follows [Semantic Versioning (SemVer)](https://semver.org/). Breakin
5959
- Adding required parameters to existing methods
6060
- Changing the default behavior of an existing overload
6161

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+
6271
## Repository Structure
6372

6473
```

.github/instructions/dynamicdata-cache.instructions.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,78 @@ cache.Edit(updater =>
5656
});
5757
```
5858

59+
## ObservableChangeSet.Create — Implicit Cache Factory
60+
61+
`ObservableChangeSet.Create` is the cache equivalent of `Observable.Create`. It gives you a `SourceCache` inside a lambda and returns `IObservable<IChangeSet<T,K>>` — the cache is created and disposed automatically per subscriber.
62+
63+
This is the **preferred way to bridge imperative code into DynamicData** without managing a `SourceCache` lifetime yourself.
64+
65+
```csharp
66+
// Synchronous — populate the cache, return a cleanup action
67+
IObservable<IChangeSet<Person, string>> people = ObservableChangeSet.Create<Person, string>(
68+
cache =>
69+
{
70+
// Populate the cache — changes flow to subscribers automatically
71+
cache.AddOrUpdate(new Person("Alice", 30));
72+
cache.AddOrUpdate(new Person("Bob", 25));
73+
74+
// Return cleanup action (called on unsubscribe)
75+
return () => { /* cleanup resources */ };
76+
},
77+
keySelector: p => p.Name);
78+
79+
// Synchronous — return IDisposable for cleanup
80+
IObservable<IChangeSet<Device, Guid>> devices = ObservableChangeSet.Create<Device, Guid>(
81+
cache =>
82+
{
83+
// Subscribe to an external event source and pump into the cache
84+
var watcher = new DeviceWatcher();
85+
watcher.DeviceAdded += (s, d) => cache.AddOrUpdate(d);
86+
watcher.DeviceRemoved += (s, d) => cache.Remove(d.Id);
87+
watcher.Start();
88+
89+
return Disposable.Create(() => watcher.Dispose());
90+
},
91+
keySelector: d => d.Id);
92+
93+
// Async — useful for loading from APIs, databases, etc.
94+
IObservable<IChangeSet<Product, int>> products = ObservableChangeSet.Create<Product, int>(
95+
async (cache, cancellationToken) =>
96+
{
97+
var items = await _api.GetProductsAsync(cancellationToken);
98+
cache.AddOrUpdate(items);
99+
100+
// Set up SignalR for live updates
101+
var connection = new HubConnectionBuilder().WithUrl("/products").Build();
102+
connection.On<Product>("Updated", p => cache.AddOrUpdate(p));
103+
connection.On<int>("Removed", id => cache.Remove(id));
104+
await connection.StartAsync(cancellationToken);
105+
106+
return Disposable.Create(() => connection.DisposeAsync().AsTask().Wait());
107+
},
108+
keySelector: p => p.Id);
109+
```
110+
111+
**Key behaviors:**
112+
- A new `SourceCache` is created **per subscriber** (cold observable)
113+
- The cache's `Connect()` is wired to the subscriber automatically
114+
- The lambda can populate the cache synchronously or asynchronously
115+
- On unsubscribe, cleanup runs and the cache is disposed
116+
- Exceptions in the lambda propagate as `OnError`
117+
118+
**Overloads:**
119+
120+
| Signature | Use when |
121+
|-----------|----------|
122+
| `Create(Func<ISourceCache, Action>, keySelector)` | Sync, cleanup is an Action |
123+
| `Create(Func<ISourceCache, IDisposable>, keySelector)` | Sync, cleanup is IDisposable |
124+
| `Create(Func<ISourceCache, Task<IDisposable>>, keySelector)` | Async setup |
125+
| `Create(Func<ISourceCache, CancellationToken, Task<IDisposable>>, keySelector)` | Async with cancellation |
126+
| `Create(Func<ISourceCache, Task<Action>>, keySelector)` | Async, cleanup is Action |
127+
| `Create(Func<ISourceCache, CancellationToken, Task<Action>>, keySelector)` | Async with cancellation, Action cleanup |
128+
| `Create(Func<ISourceCache, Task>, keySelector)` | Async, no explicit cleanup |
129+
| `Create(Func<ISourceCache, CancellationToken, Task>, keySelector)` | Async with cancellation, no cleanup |
130+
59131
## Changesets — The Core Data Model
60132

61133
A changeset (`IChangeSet<TObject, TKey>`) is an `IEnumerable<Change<TObject, TKey>>` — a batch of individual changes.

.github/instructions/dynamicdata-list.instructions.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,46 @@ list.Edit(inner =>
6262
});
6363
```
6464

65+
## ObservableChangeSet.Create — Implicit List Factory
66+
67+
`ObservableChangeSet.Create<T>` (single type parameter, no key) is the list equivalent. It gives you a `SourceList` inside a lambda and returns `IObservable<IChangeSet<T>>`.
68+
69+
```csharp
70+
// Synchronous — populate the list, return cleanup
71+
IObservable<IChangeSet<string>> logLines = ObservableChangeSet.Create<string>(
72+
list =>
73+
{
74+
var watcher = new FileSystemWatcher("logs");
75+
watcher.Changed += (s, e) =>
76+
{
77+
var newLines = File.ReadAllLines(e.FullPath);
78+
list.Edit(inner => inner.AddRange(newLines));
79+
};
80+
watcher.EnableRaisingEvents = true;
81+
82+
return Disposable.Create(() => watcher.Dispose());
83+
});
84+
85+
// Async with cancellation
86+
IObservable<IChangeSet<LogEntry>> entries = ObservableChangeSet.Create<LogEntry>(
87+
async (list, cancellationToken) =>
88+
{
89+
var stream = _client.GetLogStreamAsync(cancellationToken);
90+
await foreach (var entry in stream.WithCancellation(cancellationToken))
91+
{
92+
list.Add(entry);
93+
}
94+
95+
return Disposable.Empty;
96+
});
97+
```
98+
99+
**Key behaviors:**
100+
- A new `SourceList` is created **per subscriber** (cold observable)
101+
- No key selector needed (lists are unkeyed)
102+
- Same overload set as the cache version (sync, async, cancellable)
103+
- On unsubscribe, cleanup runs and the list is disposed
104+
65105
## List Changesets — The Core Data Model
66106

67107
A list changeset (`IChangeSet<T>`) is an `IEnumerable<Change<T>>`. Each change has a different structure than cache changes.

0 commit comments

Comments
 (0)