# SignalR DataSource Change-tracked real-time collection on top of the SignalR transport layer. Source: `SignalRs/AcSignalRDataSource.cs` in `AyCode.Services.Server`. > For the underlying transport (tag system, wire protocol, client base) see `AyCode.Services/docs/SIGNALR/README.md`. > For server hub infrastructure see `../SIGNALR/README.md`. ## Overview `AcSignalRDataSource` is an **abstract** generic `IList` that synchronizes with the server via CRUD tags. It handles change tracking, rollback, sync state, and pluggable Binary/JSON+GZip deserialization — so consuming code works with a regular list while the DataSource manages communication. ```csharp public abstract class AcSignalRDataSource : IList, IList, IReadOnlyList where TDataItem : class, IId // entity with ID where TId : struct // Guid, int, etc. where TIList : class, IList // List or AcObservableCollection ``` Consumers must subclass — the class is abstract: ```csharp public class MySampleDataSource : AcSignalRDataSource> { public MySampleDataSource(AcSignalRClientBase client, SignalRCrudTags tags) : base(client, tags) { } } ``` **Constructor:** ```csharp new MySampleDataSource( AcSignalRClientBase signalRClient, // transport client SignalRCrudTags crudTags, // 5 tags for CRUD operations object[]? contextIds = null // optional server-side filter context ) ``` ## SignalRCrudTags A `sealed class` that bundles **5 independent tag integers** — one per CRUD operation. Tags are NOT sequential offsets; each tag is independently assigned: ```csharp public sealed class SignalRCrudTags( int getAllTag, int getItemTag, int addTag, int updateTag, int removeTag) ``` **Usage (consuming project — domain-agnostic example):** ```csharp public abstract class MyAppSignalRTags : AcSignalRTags { public const int SampleGetAll = 300; public const int SampleGetItem = 301; public const int SampleAdd = 302; public const int SampleUpdate = 303; public const int SampleRemove = 304; } var crudTags = new SignalRCrudTags( MyAppSignalRTags.SampleGetAll, MyAppSignalRTags.SampleGetItem, MyAppSignalRTags.SampleAdd, MyAppSignalRTags.SampleUpdate, MyAppSignalRTags.SampleRemove ); ``` **Tag lookup:** `GetMessageTagByTrackingState(TrackingState)` maps tracking state to the corresponding tag via switch expression. ## Serializer Selection ```csharp public AcSerializerType SerializerType { get; set; } = AcSerializerType.Binary; ``` Controls the deserialization format on the **raw byte[]** load path (see Data Loading). Must match the server's `SerializerOptions.SerializerType` for that endpoint. Default: `Binary`. Override to `JsonGZip` for JSON datasources. ## Data Loading ```csharp await dataSource.LoadDataSource(clearChangeTracking: true); // sync-wait transport await dataSource.LoadDataSourceAsync(clearChangeTracking: true); // async callback path await dataSource.LoadDataSource(fromSource, refreshDataFromDbAsync: false, setSourceToWorkingReferenceList: false, clearChangeTracking: true); // in-memory load (no transport) await dataSource.LoadDataSourceFromResponseData(responseData, serializerType, refreshDataFromDbAsync: false, setSourceToWorkingReferenceList: false, clearChangeTracking: true); // pre-fetched payload await dataSource.LoadItem(id); // single item by ID // fires OnDataSourceItemChanged // with TrackingState.Get ``` **Three deserialization paths** in `LoadDataSourceFromResponseData`: 1. **Raw byte[]** (`responseData is byte[]`) — consumer-side deserialize: - `AcSerializerType.Binary` + `IAcObservableCollection`: `BeginUpdate()` → `AcBinaryDeserializer.PopulateMerge` → `EndUpdate()` (single batched UI notification). - `AcSerializerType.Binary` + plain `IList`: `AcBinaryDeserializer.Populate`. - `AcSerializerType.JsonGZip` + `IAcObservableCollection`: `GzipHelper.DecompressToString` → `PopulateFromJson`. - `AcSerializerType.JsonGZip` + plain `IList`: `GzipHelper.DecompressToString` → `JsonTo`. 2. **Typed object** (`responseData is TIList`) — protocol already deserialized: - `IAcObservableCollection`: `BeginUpdate` → `Clear` + `foreach Add` → `EndUpdate`. - Otherwise: direct `Clear` + `foreach Add`. 3. **Fallback re-serialize** (responseData neither `byte[]` nor `TIList` — e.g., bare `List` in test scenarios without protocol): `AcBinarySerializer.Serialize(responseData)` → bytes → process as Path 1 (Binary). **Context/Filtering:** `ContextIds` (`object[]`) and `FilterText` (`string`) are sent with every GetAll request for server-side filtering. ## Change Tracking Each modified item is wrapped in `TrackingItem`: | Field | Purpose | |---|---| | `TrackingState` | `Add`, `Update`, `Remove`, `Get` | | `CurrentValue` | Current item reference | | `OriginalValue` | Clone for rollback (`TrackingItemHelpers.JsonClone` or `ReflectionClone`) | The `ChangeTracking` class manages the list of tracked items. ### CRUD Operations ``` Add(item): → TrackingState.Add + InnerList.Add(item) Add(item, autoSave): → optionally calls SaveItem after AddOrUpdate(item, autoSave): → exists? Update : Add (TrackingState auto-determined) Insert(index, item): → TrackingState.Add + InnerList.Insert(index, item) Insert(index, item, autoSave): → optionally calls SaveItem after Update(i, item, autoSave): → TrackingState.Update + clone original + InnerList[i] = item Update(item, autoSave): → as Update(i, ...) by item.Id lookup Remove(id, autoSave): → TrackingState.Remove + clone original + InnerList.RemoveAt Remove(item, autoSave): → as Remove(id) by item.Id TryRemove(id, out item): → no-throw remove (returns bool) RemoveAt(index, autoSave): → by index ``` `autoSave` parameter — if true, immediately calls `SaveItem()` for that single change after the local mutation. **Manual tracking:** `SetTrackingStateToUpdate(item)` marks an existing item as modified without replacing it — useful when properties are mutated in-place. ### Additional collection helpers | Member | Returns | Purpose | |---|---|---| | `AddRange(IEnumerable)` | `void` | batch insert (observable-aware: `IAcObservableCollection.AddRange` or `List.AddRange`, fallback per-item `Add`) | | `TryGetValue(id, out item)` | `bool` | ID-keyed lookup | | `TryGetIndex(id, out index)` | `bool` | index-keyed lookup | | `IndexOf(id)` / `IndexOf(item)` | `int` | -1 if not found | | `Contains(item)` | `bool` | by ID | | `AsReadOnly()` | `ReadOnlyCollection` | wrap | | `BinarySearch(...)` | `int` | currently throws `NotImplementedException` (see SIGNALR_DATASOURCE_ISSUES.md) | ### Events | Event | Signature | Fires when | |---|---|---| | `OnDataSourceItemChanged` | `Func, Task>?` | After each item is saved or `LoadItem` completes (TrackingState in args) | | `OnDataSourceLoaded` | `Func?` | After `LoadDataSource*` completes | | `OnSyncingStateChanged` | `Action?` | On 0→1 (true) and 1→0 (false) sync transitions | `ItemChangedEventArgs` carries `Item: T` and `TrackingState: TrackingState`. ## SaveChanges | Method | Returns | Transport pattern | Use case | |---|---|---|---| | `SaveChanges()` | `Task>` (remaining failures) | Sync-wait via `ContinueWith` | Caller needs to know what failed | | `SaveChangesAsync()` | `Task` (void) | Fire-and-forget callback (`Action`) | Background save, no result inspection | | `SaveItem(id)` | `Task` | Sync-wait | Save a single tracked item | | `SaveItem(id, trackingState)` | `Task` | Sync-wait | Save with explicit state | | `SaveItem(item, trackingState)` | `Task` | Sync-wait | Save without lookup | ``` Both batch flows: BeginSync() for each tracked item: tag = CrudTags.GetMessageTagByTrackingState(state) response = SignalRClient.PostDataAsync(tag, item) on success: ProcessSavedResponseItem → CopyTo InnerList item, remove tracking, fire OnDataSourceItemChanged on failure: TryRollbackItem() → restore OriginalValue EndSync() ``` ## Rollback ```csharp bool TryRollbackItem(TId id, out TDataItem? originalValue); void Rollback(); // reverts all tracked changes ``` `TryRollbackItem` restores `OriginalValue` to `InnerList`: - `TrackingState.Add`: removes item entirely (no original existed). - `TrackingState.Update` / `Remove`: re-applies `OriginalValue.CopyTo(InnerList[index])`, or re-adds if missing. - The tracking entry is removed in either case. `Rollback()` iterates and calls `TryRollbackItem` for every tracked item. ## Sync State ```csharp private int _activeSyncOperations; // Interlocked counter BeginSync(): Interlocked.Increment → fires OnSyncingStateChanged(true) on 0→1 EndSync(): Interlocked.Decrement → fires OnSyncingStateChanged(false) on 1→0 IsSyncing: _activeSyncOperations > 0 ``` UI binds to `IsSyncing` to show loading indicators. The counter supports nested sync operations. ## Working Reference List ```csharp dataSource.SetWorkingReferenceList(externalList); // Now dataSource operates directly on externalList — same reference, no copy TIList innerList = dataSource.GetReferenceInnerList(); bool hasRef = dataSource.HasWorkingReferenceList; ``` Useful when the UI already has a bound collection and you want the DataSource to manage it in-place. ## Locking Strategy | Lock | Scope | Used by | |---|---|---| | `object _syncRoot` | Synchronous | Count, Contains, IndexOf, GetEnumerator, Add (no-save), Insert (no-save), CopyTo | | `SemaphoreSlim _asyncLock` | Asynchronous | LoadDataSource*, AddOrUpdate, Update, Remove (with save), SaveItem, SaveChanges* | `GetEnumerator()` returns `InnerList.ToList().GetEnumerator()` — safe copy to avoid mutation during iteration. ## Relationship to Transport The DataSource is a **consumer** of the SignalR transport, not part of it: ``` DataSource.SaveChanges() → CrudTags.GetMessageTagByTrackingState(state) → tag → AcSignalRClientBase.PostDataAsync(tag, item) ← transport layer → OnReceiveMessage(tag, bytes, requestId) ← wire protocol → Server method with [SignalR(tag)] ← tag dispatch ``` Projects can also call the transport directly without DataSource — see `AyCode.Services/docs/SIGNALR/README.md`. > **Cross-cutting:** `ACCORE-SIG-I-T7S2` (GetAll raw byte[] for populate/merge) lives in `../SIGNALR/SIGNALR_ISSUES.md` because it spans transport + DataSource concerns equally. ## Key Source Files | Component | Path | |---|---| | DataSource (abstract base) | `SignalRs/AcSignalRDataSource.cs` (in `AyCode.Services.Server`) | | Tracking helpers (`JsonClone` / `ReflectionClone`) | `SignalRs/TrackingItemHelpers.cs` (in `AyCode.Services.Server`) | | CRUD tags | `SignalRs/SignalRCrudTags.cs` (in `AyCode.Services`) | | Transport client | `SignalRs/AcSignalRClientBase.cs` (in `AyCode.Services`) |