AyCode.Core/AyCode.Services.Server/docs/SIGNALR/SIGNALR_DATASOURCE.md

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 see README.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 via SignalDataTypeGetResponseData<T>() direct cast.
  • Raw byte[] response (IsRawBytesData): protocol returns raw byte[] → 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 01
EndSync():   Interlocked.Decrement  fires OnSyncingStateChanged(false) on 10
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