# SignalR Communication The transport layer for AyCode real-time communication. Source: `AyCode.Services/SignalRs/` (client), `AyCode.Services.Server/SignalRs/` (server). > For the change-tracked collection built on top of this transport, see [`SIGNALR_DATASOURCE.md`](SIGNALR_DATASOURCE.md). ## Design Overview All communication flows through a **single hub method** with **tag-based dispatch**: ``` Client ──OnReceiveMessage(tag, bytes, requestId)──► Server Client ◄──OnReceiveMessage(tag, bytes, requestId)── Server ``` `OnReceiveMessage` is the only transport channel — all calls go through it. The `tag` (int) determines **which server method to invoke**. The `DynamicMethodRegistry` maps the tag to a concrete `[SignalR(tag)]`-annotated method at runtime. Projects call any endpoint by specifying the tag; the server dispatches accordingly. ``` Client side: Server side: ───────────────────── ───────────────────── AcSignalRClientBase AcWebSignalRHubBase ├─ HubConnection (WebSocket) ├─ Hub ├─ AcBinaryHubProtocol ├─ DynamicMethodRegistry ├─ Pending request tracking ├─ Parameter deserialization └─ Response callbacks └─ Broadcast to other clients ``` ## Tag System ### Tag Definition AyCode.Core defines only the base class and built-in infrastructure tags: ```csharp public class AcSignalRTags { public const int None = 0; public const int PingTag = 90001; public const int EchoTag = 90002; } ``` **Consuming projects** define their own tags by inheriting `AcSignalRTags`: ```csharp public abstract class MyProjectTags : AcSignalRTags { public const int GetOrders = 100; public const int GetOrderById = 101; public const int SaveOrder = 102; // ... project-specific tags } ``` Tags are plain `int` constants. The project decides the numbering. Tags must be unique across all registered services within a hub. ### Tag Attributes Three attribute levels: ```csharp // Base: maps an integer tag to a method [Tag(42)] // Server method: tag + optional broadcast behavior [SignalR(messageTag: 100, sendToOtherClientTag: 100, sendToOtherClientType: SendToClientType.Others)] // messageTag: incoming tag this method handles // sendToOtherClientTag: tag used when broadcasting result to other clients (can differ from messageTag) // sendToOtherClientType: who receives the broadcast // Client receive: marks method for server→client dispatch [SignalRSendToClient(100)] ``` **SendToClientType:** `None` (no broadcast), `Others` (all except caller), `Caller` (response only), `All` (everyone). ### How Projects Use Tags Projects call the SignalR transport directly — not only through DataSource: ```csharp // Direct call: project code calls any endpoint by tag var orders = await signalRClient.PostAsync>(MyTags.GetOrders, companyId); var order = await signalRClient.GetByIdAsync(MyTags.GetOrderById, orderId); await signalRClient.PostDataAsync(MyTags.SaveOrder, order); // Async callback (non-blocking) await signalRClient.PostDataAsync(MyTags.SaveOrder, order, async response => { ... }); // Fire-and-forget await signalRClient.PostDataAsync(MyTags.SaveOrder, order, response => { ... }); ``` The `AcSignalRClientBase` CRUD helpers (`PostAsync`, `GetByIdAsync`, `GetAllAsync`, `PostDataAsync`) are all generic transport methods that work with any tag — they are not tied to DataSource. ## Dynamic Method Dispatch See also: [`AyCode.Models.Server/DynamicMethods/README.md`](../AyCode.Models.Server/DynamicMethods/README.md) ### Server-Side Lookup ``` 1. OnReceiveMessage(tag=100, bytes, requestId) 2. DynamicMethodRegistry.GetMethodByMessageTag(100) ├─ Check static ConcurrentDictionary cache ├─ Hit? → find instance of cached Type from registered instances ├─ Miss? → scan all registered instances' methods for [SignalR(100)] │ cache the result (including negative = null) └─ Return (instance, methodInfoModel) or null 3. AcMethodInfoModel contains: ├─ MethodInfo (the method to invoke) ├─ SignalRAttribute (tag, sendToOtherClientTag, sendToOtherClientType) └─ ParamInfos[] (ParameterInfo for deserialization) ``` The `DynamicMethodRegistry` uses a static `ConcurrentDictionary` for the global tag→method cache. Note: `AcDynamicMethodCallModel` uses a `FrozenDictionary` per-type cache internally, but the registry's own lookup path does direct method scanning with `ConcurrentDictionary` caching. ### Registration The hub registers service instances during initialization: ```csharp DynamicMethodRegistry.Register(myService); // scans [SignalR(tag)] methods lazily DynamicMethodRegistry.Register(anotherService); ``` Reflection runs lazily per tag on first request, then results are cached statically. ## Wire Protocol ### AcBinaryHubProtocol Custom `IHubProtocol` (name: `"acbinary"`), replaces default JSON. Frame format: ``` [4 bytes: payload length, little-endian] [1 byte: message type] [payload...] ``` Message types: Invocation(1), StreamItem(2), Completion(3), Ping(6), Close(7), Ack(8), Sequence(9). Arguments serialized individually with VarUInt length prefix. Direct write to `IBufferWriter` via `BufferWriterBinaryOutput`. ### Response Message `SignalResponseDataMessage` carries: | Field | Type | Purpose | |-------|------|---------| | `MessageTag` | int | Tag identifying the operation | | `Status` | SignalResponseStatus | Success/Error | | `ResponseData` | byte[] | Binary-serialized payload (or GZip+JSON fallback) | | `DataSerializerType` | AcSerializerType | Binary or Json | **Binary mode** (default): `AcBinarySerializer.ToBinary(data)` → raw bytes. **JSON fallback**: `ToJson(data)` → `GzipHelper.Compress(json)` → bytes. ## Request/Response Flow ### Client → Server ``` 1. PostAsync(tag, postData) or PostDataAsync(tag, data, callback) 2. CreatePostMessage(postData): ├─ Primitives/strings/enums/value types → IdMessage (JSON array of serialized values) └─ Complex objects → SignalPostJsonDataMessage ⚠️ tech debt: JSON-in-Binary 3. SerializeToBinary(message) wraps in Binary envelope 4. HubConnection.SendAsync("OnReceiveMessage", tag, bytes, requestId) 5. AcBinaryHubProtocol frames it on the wire ``` ### Server Processing ``` 6. OnReceiveMessage(tag, bytes, requestId) 7. DynamicMethodRegistry.GetMethodByMessageTag(tag) ← ConcurrentDictionary lookup 8. DeserializeParameters(bytes): ├─ DeserializeFromBinary() ← unwrap Binary envelope ├─ IdMessage format? → parse each Ids[i] as JSON per parameter type └─ Complex object? → json.JsonTo(paramType) ⚠️ tech debt: JSON parse 9. MethodInfo.InvokeMethod(instance, params) ← unwraps Task/ValueTask 10. CreateResponseMessage(tag, Success, result) ← pure Binary serialization 11. ResponseToCaller(tag, message, requestId) 12. If SendToOtherClientType != None: └─ SendMessageToOthers(sendToOtherClientTag, result) ← uses sendToOtherClientTag, not messageTag ``` ### Server → Client ``` 13. Client.OnReceiveMessage(tag, bytes, requestId) 14. Has matching requestId in pending ConcurrentDictionary? ├─ YES (response to own request): │ 15. DeserializeFromBinary(bytes) │ 16. Route based on pending request type: │ ├─ null (sync wait) → set ResponseByRequestId, WaitToAsync completes │ ├─ Action → invoke directly │ └─ Func → invoke and await │ 17. GetResponseData(): │ ├─ Binary: ResponseData.BinaryTo() │ └─ JSON: GzipDecompress → AcJsonDeserializer.Deserialize() │ └─ NO (broadcast from another client's action): 18. abstract MessageReceived(tag, bytes).Forget() └─ Consuming project overrides to handle server push ``` **Broadcast receive:** When the server broadcasts to `Others`/`All`, receiving clients have no matching `requestId`. The message falls through to `MessageReceived(int messageTag, byte[] messageBytes)` — an abstract method the consuming project overrides to handle unsolicited server pushes (e.g., refresh UI, update local state). **Request model pooling:** `SignalRRequestModel` instances are managed via `SignalRRequestModelPool` (ObjectPool + IResettable) to avoid allocations per request. ### Response Patterns | Pattern | Method | Blocking | |---------|--------|----------| | Sync wait | `PostAsync(tag, data)` | Yes, polls up to `TransportSendTimeout` (60s) | | Async callback | `PostDataAsync(tag, data, async msg => {...})` | No, `Func` | | Fire-and-forget | `PostDataAsync(tag, data, msg => {...})` | No, `Action` | ## Session Management `AcSessionService` tracks connected clients: ```csharp ConcurrentDictionary Sessions ``` `IAcSessionItem` requires `SessionId` property. Used for targeting messages to specific users/connections. ## Broadcast Service `AcSignalRSendToClientService` provides server-push methods: | Method | Target | |--------|--------| | `SendMessageToAllClients` | All connected | | `SendMessageToConnection(connectionId)` | Single connection | | `SendMessageToUser(userId)` | User (all connections) | | `SendMessageToUsers(userIds)` | Multiple users | All messages wrapped in `SignalResponseDataMessage` → binary serialized → `OnReceiveMessage`. ## Connection Lifecycle **Client configuration:** - Transport: WebSockets only (`SkipNegotiation = true`) - Buffer: 30MB transport + 30MB application - Reconnection: `WithAutomaticReconnect()` + `WithStatefulReconnect()` - Keepalive: 60s interval, 180s server timeout **Hub events:** - `OnConnectedAsync()` — log connection - `OnDisconnectedAsync(exception)` — log disconnection, cleanup session ## Diagnostics Enable with `AcSignalRClientBase.EnableBinaryDiagnostics = true` or `AcWebSignalRHubBase.EnableBinaryDiagnostics = true`. Logs: hex dump (500 byte sample), header parsing (version, marker), property count + names via VarUInt reading. `SignalResponseDataMessage.DiagnosticLogger` — per-response logging: target type info, property list, inheritance chain, hex dump. ## Known Technical Debt **JSON-in-Binary request parameters:** Client→server requests currently wrap parameters in JSON inside the binary envelope (`SignalPostJsonDataMessage`). This adds an unnecessary serialization round-trip. Responses are already pure binary. Do NOT attempt to fix as a side effect — requires coordinated changes across all consuming projects. ## Key Source Files | Component | Path | |-----------|------| | Hub base | `AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs` | | Client base | `AyCode.Services/SignalRs/AcSignalRClientBase.cs` | | Binary protocol | `AyCode.Services/SignalRs/AcBinaryHubProtocol.cs` | | Message types | `AyCode.Services/SignalRs/IAcSignalRHubClient.cs` | | Serialization | `AyCode.Services/SignalRs/SignalRSerializationHelper.cs` | | Tag attributes | `AyCode.Services/SignalRs/SignalMessageTagAttribute.cs` | | Base tags | `AyCode.Services/SignalRs/AcSignalRTags.cs` | | Session service | `AyCode.Services.Server/SignalRs/AcSessionService.cs` | | Broadcast service | `AyCode.Services.Server/SignalRs/AcSignalRSendToClientService.cs` | | Dynamic dispatch | `AyCode.Models.Server/DynamicMethods/AcDynamicMethodRegistry.cs` |