# SignalR Architecture > This is the core communication layer of the AyCode framework. > The dispatch mechanism is **not self-evident** — read this document before modifying SignalR-related code. ## Overview AyCode uses a **single-method, tag-based RPC dispatch** pattern over SignalR instead of the standard Hub method-per-endpoint approach. Both client→server and server→client communication go through **one method**: ``` OnReceiveMessage(int messageTag, byte[] messageBytes, int? requestId) ``` The `messageTag` integer determines which method to invoke. The `messageBytes` contain the serialized parameters (Binary envelope). The optional `requestId` enables request/response correlation. ## Why Not Standard SignalR Hub Methods? Standard SignalR hubs expose one C# method per endpoint. This approach: - Requires maintaining proxy interfaces on both sides - Makes it hard to dynamically register/unregister endpoints - Adds friction for CRUD-heavy data sources (5 methods × N entities) The tag-based approach: - **Single transport method** — no interface synchronization needed - **Dynamic dispatch** — methods discovered via reflection + attributes at startup - **CRUD bundles** — `SignalRCrudTags` maps 5 tags per entity automatically - **Binary protocol** — `AcBinaryHubProtocol` replaces JSON+Base64 with `AcBinarySerializer` ## Message Flow ### Client → Server (Request) ``` 1. Client calls PostAsync(tag, postData) └─ AcSignalRClientBase.cs 2. CreatePostMessage() wraps postData: ├─ IdMessage — for primitive IDs (Guid, int, string) └─ SignalPostJsonDataMessage — for complex objects └─ PostData setter → PostDataJson = _postData.ToJson() ⚠️ TECH DEBT └─ Result serialized to Binary via SignalRSerializationHelper.SerializeToBinary() 3. SendMessageToServerAsync(tag, binaryBytes, requestId) └─ HubConnection.InvokeAsync("OnReceiveMessage", tag, bytes, reqId) 4. AcBinaryHubProtocol encodes as raw binary frame (not JSON+Base64) ``` ### Server Processing ``` 5. AcWebSignalRHubBase.OnReceiveMessage(tag, bytes, requestId) 6. ProcessOnReceiveMessage(): ├─ TryFindAndInvokeMethod(tag) via DynamicMethodRegistry │ ├─ Static ConcurrentDictionary cache (per tag) │ └─ FrozenDictionary per-instance-type (built once, immutable) │ ├─ DeserializeParameters(methodInfo, bytes): │ ├─ DeserializeFromBinary() — unwrap Binary envelope │ ├─ Extract PostDataJson string ⚠️ TECH DEBT │ └─ AcJsonDeserializer.Deserialize() per parameter ⚠️ TECH DEBT │ └─ Invoke method with deserialized parameters ``` ### Server → Client (Response) ``` 7. Method returns result 8. CreateResponseMessage(tag, result, serializerOptions): └─ SignalResponseDataMessage serializes result: ├─ Binary mode: AcBinarySerializer directly (no JSON step) └─ JSON mode: JSON + GZip compression 9. ResponseToCaller(responseMessage) └─ Clients.Caller.OnReceiveMessage(tag, bytes, requestId) ``` ### Client Response Processing ``` 10. AcSignalRClientBase.OnReceiveMessage(tag, bytes, requestId) ├─ Lookup pending request by requestId ├─ DeserializeFromBinary() └─ Complete TaskCompletionSource → caller gets TResponse 11. SignalResponseDataMessage.GetResponseData(): ├─ Binary: ResponseData.BinaryTo() — direct, no JSON └─ JSON: GZip decompress → AcJsonDeserializer (pooled buffer) ``` ## ⚠️ Technical Debt: JSON-in-Binary Parameter Serialization **Status:** Temporary — planned for replacement with pure Binary serialization. **Current behavior:** Request parameters (client→server) go through a JSON→Binary→JSON round-trip: | Step | Location | What happens | |---|---|---| | 1 | `SignalPostJsonDataMessage` setter | `PostDataJson = _postData.ToJson()` — object → JSON string | | 2 | `SignalRSerializationHelper.SerializeToBinary()` | JSON string wrapped in Binary envelope | | 3 | `AcWebSignalRHubBase.DeserializeParameters()` | Binary → `SignalPostJsonMessage` → `PostDataJson` → JSON parse per param | **Why it exists:** The Binary serializer was added after the SignalR layer was built. Responses were migrated to pure Binary, but request parameters still use the original JSON path. **Impact:** Extra serialization overhead on every client→server request. Responses are already pure Binary (no JSON overhead). **Planned fix:** Replace `SignalPostJsonDataMessage` with direct Binary parameter serialization, matching how responses already work. **Key files involved:** - `AyCode.Services/SignalRs/IAcSignalRHubClient.cs` — `SignalPostJsonDataMessage.PostData` setter (line ~89) - `AyCode.Services/SignalRs/AcSignalRClientBase.cs` — `CreatePostMessage()` (line ~220) - `AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs` — `DeserializeParameters()` (line ~340) > **Rule:** Do NOT attempt to fix the JSON-in-Binary pattern as a side effect of other work. > It requires coordinated changes across client, server, and all consuming projects. ## Tag System ### Tag Attributes ```csharp // Base: associates an integer tag with a method [Tag(42)] // Server method: tag + optional client notification behavior [SignalR(42, SendToClientType.Others)] // Client receive: marks method for server→client dispatch [SignalRSendToClient(42)] ``` ### Built-in Tags | Tag | Constant | Purpose | |---|---|---| | 0 | `AcSignalRTags.None` | No tag / unset | | 90001 | `AcSignalRTags.PingTag` | Keep-alive ping | | 90002 | `AcSignalRTags.EchoTag` | Echo test | ### CRUD Tag Bundles `SignalRCrudTags` maps 5 sequential tags per entity: ```csharp var tags = new SignalRCrudTags(300); // base tag // tags.GetAll = 300 // tags.GetItem = 301 // tags.AddTag = 302 // tags.UpdateTag = 303 // tags.RemoveTag = 304 ``` `GetMessageTagByTrackingState()` maps `TrackingState` → appropriate tag for automatic CRUD dispatch. ## Dynamic Method Registry See also: [`AyCode.Models.Server/DynamicMethods/README.md`](../AyCode.Models.Server/DynamicMethods/README.md) ### Lookup Flow ``` 1. OnReceiveMessage(tag=42, ...) 2. DynamicMethodRegistry.TryFindMethod(tag) ├─ Check static ConcurrentDictionary cache ├─ Miss? Scan registered instances: │ └─ AcDynamicMethodCallModel.GetMethodByTag(tag) │ └─ FrozenDictionary (built once per type) └─ Cache result for future lookups 3. AcMethodInfoModel contains: ├─ MethodInfo (reflection) ├─ TagAttribute (tag value, metadata) └─ ParamInfos[] (parameter types for deserialization) ``` ### Caching Strategy - **Per-type:** `FrozenDictionary` built once via `ConcurrentDictionary.GetOrAdd()` — immutable after creation - **Global:** `ConcurrentDictionary` — populated lazily per tag - **Instance resolution:** Registry tracks which instance owns which tag for invocation ## DataSource Pattern `AcSignalRDataSource` provides a server-side `IList` with: - **Change tracking:** `ChangeTracking` wraps items in `TrackingItem` (Added/Modified/Deleted/Unchanged) - **SignalR CRUD:** Uses `SignalRCrudTags` for automatic tag mapping - **Binary merge:** `BinaryToMerge()` deserializes and applies changes to the local list - **Load from response:** `LoadDataSourceFromResponseData()` populates from `SignalResponseDataMessage` ## Binary Hub Protocol `AcBinaryHubProtocol` (`IHubProtocol`) replaces SignalR's default JSON protocol: - All 9 SignalR message types handled (Invocation, StreamItem, Completion, Ping, Close, CancelInvocation, StreamInvocation, Ack, Sequence) - Inner `SpanReader` ref struct for zero-allocation parsing - `TransferFormat.Binary` — no Base64 encoding needed - Protocol name: `"acbinary"` ## Testing Infrastructure Test classes bypass real SignalR connections: | Class | Purpose | |---|---| | `TestableSignalRClient2` | Client that calls hub directly (no network) | | `TestableSignalRHub2` | Hub that processes messages without real `HubCallerContext` | | `TestSignalRTags` | 100+ tag constants for test scenarios | | `TestSignalRService2` | `[SignalR(tag)]` attributed test methods | Test data flows through the full serialization pipeline (Binary envelope, tag dispatch, parameter deserialization) — only the network transport is bypassed.