7.3 KiB
SignalR DataSource
Change-tracked real-time collection built on top of the SignalR transport layer. Source: SignalRs/AcSignalRDataSource.cs in this project.
For the underlying transport (tag system, wire protocol, client base) see
AyCode.Services/docs/SIGNALR/README.md. For server hub infrastructure seeREADME.md.
Overview
AcSignalRDataSource is a generic IList<T> that synchronizes with the server via CRUD tags. It handles change tracking, rollback, sync state, and binary deserialization — so consuming code works with a regular list while the DataSource manages communication.
AcSignalRDataSource<TDataItem, TId, TIList>
where TDataItem : class, IId<TId> // entity with ID
where TId : struct // Guid, int, etc.
where TIList : class, IList<TDataItem> // List<T> or AcObservableCollection<T>
Implements IList<TDataItem>, IList, IReadOnlyList<TDataItem>.
Constructor:
new AcSignalRDataSource<T, TId, TIList>(
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:
public sealed class SignalRCrudTags(
int getAllTag,
int getItemTag,
int addTag,
int updateTag,
int removeTag)
Usage (consuming project):
public abstract class MyProjectTags : AcSignalRTags
{
public const int OrderGetAll = 300;
public const int OrderGetItem = 301;
public const int OrderAdd = 302;
public const int OrderUpdate = 303;
public const int OrderRemove = 304;
}
var crudTags = new SignalRCrudTags(
MyProjectTags.OrderGetAll,
MyProjectTags.OrderGetItem,
MyProjectTags.OrderAdd,
MyProjectTags.OrderUpdate,
MyProjectTags.OrderRemove
);
Tag lookup: GetMessageTagByTrackingState(TrackingState) maps tracking state to the corresponding tag via switch expression.
Data Loading
await dataSource.LoadDataSource(); // sync-wait transport (polls up to 60s)
await dataSource.LoadDataSourceAsync(); // async callback path
await dataSource.LoadDataSourceFromResponseData(responseData, serializerType);
await dataSource.LoadItem(id); // single item by ID
Deserialization paths:
- Typed response (
T != byte[]): protocol eagerly deserializes viaSignalDataType→GetResponseData<T>()direct cast. - Raw byte[] response (
IsRawBytesData): protocol returns rawbyte[]→ consumer deserializes:AcObservableCollection<T>:BeginUpdate()→PopulateMerge(bytes)→EndUpdate()— single batched UI notification.List<T>:BinaryTo(InnerList)— direct populate.
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<TDataItem, TId>:
| Field | Purpose |
|---|---|
TrackingState |
Add, Update, Remove |
CurrentValue |
Current item reference |
OriginalValue |
Clone for rollback (JsonClone or ReflectionClone) |
The ChangeTracking<TDataItem, TId> class manages the list of tracked items.
CRUD Operations
Add(item): → TrackingState.Add + InnerList.Add(item)
AddOrUpdate(item): → exists? Update : Add (determines TrackingState automatically)
Insert(index, item): → TrackingState.Add + InnerList.Insert(index, item)
Update(i, item): → TrackingState.Update + clone original + InnerList[i] = item
Remove(id): → TrackingState.Remove + clone original + InnerList.RemoveAt(index)
Each operation has an optional autoSave parameter — if true, immediately calls SaveItem() for that single change.
Manual tracking: SetTrackingStateToUpdate(item) marks an existing item as modified without replacing it — useful when properties are mutated in-place.
Events
| Event | Signature | Fires when |
|---|---|---|
OnDataSourceItemChanged |
Func<ItemChangedEventArgs<T>, Task>? |
After each item is saved or loaded |
OnDataSourceLoaded |
Func<Task>? |
After LoadDataSource / LoadDataSourceAsync completes |
OnSyncingStateChanged |
Action<bool>? |
On 0→1 (true) and 1→0 (false) sync transitions |
SaveChanges
| Method | Returns | Transport pattern | Use case |
|---|---|---|---|
SaveChanges() |
List<TrackingItem> (remaining failures) |
Sync-wait (ContinueWith) |
When caller needs to know what failed |
SaveChangesAsync() |
Task (void) |
Fire-and-forget callback (Action) |
Background save, no result inspection |
Both follow the same flow:
BeginSync()
for each tracked item:
tag = CrudTags.GetMessageTagByTrackingState(state)
response = SignalRClient.PostDataAsync(tag, item)
on success: remove from tracking, CopyTo InnerList item with server response
on failure: TryRollbackItem() → restore OriginalValue
EndSync()
Rollback: TryRollbackItem(id) restores OriginalValue to InnerList. For TrackingState.Add: removes item entirely. For Remove: re-adds OriginalValue. Manual Rollback() reverts all tracked changes at once.
Sync State
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
dataSource.SetWorkingReferenceList(externalList);
// Now dataSource operates directly on externalList — same reference, no copy
TIList innerList = dataSource.GetReferenceInnerList();
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 |
SemaphoreSlim _asyncLock |
Asynchronous | Add/Update/Remove with save, LoadDataSource |
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.
Key Source Files
| Component | Path |
|---|---|
| DataSource | SignalRs/AcSignalRDataSource.cs |
| Tracking helpers | SignalRs/TrackingItemHelpers.cs |
| CRUD tags | AyCode.Services/SignalRs/SignalRCrudTags.cs |
| Transport client | AyCode.Services/SignalRs/AcSignalRClientBase.cs |