Document SignalR architecture, grid, and ext deps
Added comprehensive docs for SignalR tag-based dispatch (docs/SIGNALR_ARCHITECTURE.md), including message flow, tag system, dynamic method registry, and tech debt (JSON-in-Binary). Updated all related READMEs, glossaries, and conventions to reference this architecture and clarify grid infrastructure (MgGridBase, FruitBankGridBase) and external dependency locations (AyCode.Core, AyCode.Blazor, Mango.Nop Libraries, FruitBank Plugin). Synchronized solution items and copilot-instructions. Improves discoverability, enforces conventions, and clarifies tech debt for all developers.
This commit is contained in:
parent
17daf0fef2
commit
0b27532f17
|
|
@ -18,10 +18,14 @@
|
||||||
8. **MeasuringStatus.Finnished** — intentional legacy typo in consuming projects. Do NOT fix the spelling.
|
8. **MeasuringStatus.Finnished** — intentional legacy typo in consuming projects. Do NOT fix the spelling.
|
||||||
9. **LogLevel enum values** — synchronized with database. Do NOT renumber.
|
9. **LogLevel enum values** — synchronized with database. Do NOT renumber.
|
||||||
|
|
||||||
|
## SignalR
|
||||||
|
10. **Single-method tag-based dispatch** — both directions use `OnReceiveMessage(int messageTag, byte[] messageBytes, int? requestId)`. Tags are integers mapped via `[SignalR(tag)]` attributes. See [`docs/SIGNALR_ARCHITECTURE.md`](../docs/SIGNALR_ARCHITECTURE.md).
|
||||||
|
11. **JSON-in-Binary parameter serialization (⚠️ TEMPORARY)** — Client→server request parameters currently go through JSON→Binary→JSON round-trip (`SignalPostJsonDataMessage<T>.PostData` → `.ToJson()` → Binary envelope → server-side JSON parse). This is planned for replacement with pure Binary. **Do NOT attempt to fix this as a side effect of other work** — it requires coordinated client+server+consuming-project changes. Server→client responses already use pure Binary.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
10. Do not suggest removal/rollback as a solution — find a fix for the problem.
|
12. Do not suggest removal/rollback as a solution — find a fix for the problem.
|
||||||
11. Extension methods over instance methods for CRUD operations (see DbSets pattern).
|
13. Extension methods over instance methods for CRUD operations (see DbSets pattern).
|
||||||
12. Session pattern for reads, Transaction pattern for writes (see DataLayers).
|
14. Session pattern for reads, Transaction pattern for writes (see DataLayers).
|
||||||
13. **Target framework is net9.0** (set in AyCode.Core.targets). The SourceGenerator targets netstandard2.0. Consuming projects (AyCode.Blazor, FruitBankHybridApp UI) may target net10.0 but reference AyCode.Core DLLs built as net9.0.
|
15. **Target framework is net9.0** (set in AyCode.Core.targets). The SourceGenerator targets netstandard2.0. Consuming projects (AyCode.Blazor, FruitBankHybridApp UI) may target net10.0 but reference AyCode.Core DLLs built as net9.0.
|
||||||
14. **No redundant code** — before writing new logic, search for existing methods. Reuse or extract shared logic into smaller methods rather than duplicating. If an existing method does most of what you need, split it into composable parts.
|
16. **No redundant code** — before writing new logic, search for existing methods. Reuse or extract shared logic into smaller methods rather than duplicating. If an existing method does most of what you need, split it into composable parts.
|
||||||
15. **Keep all .md files in sync** — when you modify code, update any affected .md file (README.md, docs/, GLOSSARY, ARCHITECTURE, CONVENTIONS, BINARY_FORMAT, SCHEMA, etc.). If you notice any .md content does not match the current code, fix it automatically.
|
17. **Keep all .md files in sync** — when you modify code, update any affected .md file (README.md, docs/, GLOSSARY, ARCHITECTURE, CONVENTIONS, BINARY_FORMAT, SCHEMA, etc.). If you notice any .md content does not match the current code, fix it automatically.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 18
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 18.0.11222.15
|
VisualStudioVersion = 18.0.11222.15
|
||||||
|
|
@ -42,11 +41,11 @@ EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
AyCode.Core.targets = AyCode.Core.targets
|
AyCode.Core.targets = AyCode.Core.targets
|
||||||
|
CLAUDE.md = CLAUDE.md
|
||||||
|
.github\copilot-instructions.md = .github\copilot-instructions.md
|
||||||
|
README.md = README.md
|
||||||
RunQuickBenchmark.bat = RunQuickBenchmark.bat
|
RunQuickBenchmark.bat = RunQuickBenchmark.bat
|
||||||
RunQuickBenchmark.ps1 = RunQuickBenchmark.ps1
|
RunQuickBenchmark.ps1 = RunQuickBenchmark.ps1
|
||||||
.github\copilot-instructions.md = .github\copilot-instructions.md
|
|
||||||
CLAUDE.md = CLAUDE.md
|
|
||||||
README.md = README.md
|
|
||||||
ToonExtendedInfo.txt = ToonExtendedInfo.txt
|
ToonExtendedInfo.txt = ToonExtendedInfo.txt
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
|
@ -64,6 +63,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{D4B2E9F1-A
|
||||||
docs\BINARY_FORMAT.md = docs\BINARY_FORMAT.md
|
docs\BINARY_FORMAT.md = docs\BINARY_FORMAT.md
|
||||||
docs\CONVENTIONS.md = docs\CONVENTIONS.md
|
docs\CONVENTIONS.md = docs\CONVENTIONS.md
|
||||||
docs\GLOSSARY.md = docs\GLOSSARY.md
|
docs\GLOSSARY.md = docs\GLOSSARY.md
|
||||||
|
docs\SIGNALR_ARCHITECTURE.md = docs\SIGNALR_ARCHITECTURE.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,21 @@
|
||||||
|
|
||||||
Reflection-based infrastructure for dynamically dispatching method calls by message tag, primarily used for SignalR message routing.
|
Reflection-based infrastructure for dynamically dispatching method calls by message tag, primarily used for SignalR message routing.
|
||||||
|
|
||||||
|
> **Context:** This is the server-side dispatch engine for the SignalR tag-based architecture.
|
||||||
|
> See [`docs/SIGNALR_ARCHITECTURE.md`](../../docs/SIGNALR_ARCHITECTURE.md) for the full message flow.
|
||||||
|
|
||||||
|
## How It Fits
|
||||||
|
|
||||||
|
When `AcWebSignalRHubBase.OnReceiveMessage(tag, bytes, requestId)` is called, the hub uses `DynamicMethodRegistry` to resolve which method to invoke for the given tag. The registry scans all registered service instances for `[SignalR(tag)]` attributed methods, caches the lookup, and invokes the matching method with deserialized parameters.
|
||||||
|
|
||||||
|
## Caching Strategy
|
||||||
|
|
||||||
|
- **Per-type cache:** `AcDynamicMethodCallModel` builds a `FrozenDictionary<int, AcMethodInfoModel>` once per service type. Immutable after creation.
|
||||||
|
- **Global cache:** `AcDynamicMethodRegistry` maintains a `ConcurrentDictionary<int, AcMethodInfoModel>` populated lazily per tag on first request.
|
||||||
|
- **Instance tracking:** The registry maps tags to owning instances for correct invocation context.
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
- **`AcMethodInfoModel.cs`** — Wraps a `MethodInfo` and its `TagAttribute` with cached parameter metadata.
|
- **`AcMethodInfoModel.cs`** — Wraps a `MethodInfo` and its `TagAttribute` with cached parameter metadata (`ParamInfos[]` for deserialization).
|
||||||
- **`AcDynamicMethodCallModel.cs`** — Binds an object instance to its attributed methods, using a static `ConcurrentDictionary` and `FrozenDictionary` cache keyed by message tag. Reflection runs once per type.
|
- **`AcDynamicMethodCallModel.cs`** — Binds an object instance to its attributed methods, using a static `ConcurrentDictionary` and `FrozenDictionary` cache keyed by message tag. Reflection runs once per type.
|
||||||
- **`AcDynamicMethodRegistry.cs`** — Registry with lazy method lookup across multiple registered instances. Caches discovered methods statically by message tag and resolves instances per request.
|
- **`AcDynamicMethodRegistry.cs`** — Registry with lazy method lookup across multiple registered instances. Caches discovered methods statically by message tag and resolves instances per request.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
Server-side SignalR hub infrastructure: hub base class, session management, data source with change tracking, and client broadcast service.
|
Server-side SignalR hub infrastructure: hub base class, session management, data source with change tracking, and client broadcast service.
|
||||||
|
|
||||||
|
> **Architecture:** For full dispatch flow, tag system, and tech debt documentation see [`docs/SIGNALR_ARCHITECTURE.md`](../../docs/SIGNALR_ARCHITECTURE.md).
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
### Hub
|
### Hub
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
Custom binary SignalR protocol, client infrastructure, message tagging, and serialization helpers.
|
Custom binary SignalR protocol, client infrastructure, message tagging, and serialization helpers.
|
||||||
|
|
||||||
|
> **Architecture:** For full dispatch flow, tag system, and tech debt documentation see [`docs/SIGNALR_ARCHITECTURE.md`](../../docs/SIGNALR_ARCHITECTURE.md).
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
### Protocol
|
### Protocol
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ AyCode.Services ← AyCode.Services.Server
|
||||||
### Service Layer
|
### Service Layer
|
||||||
- **AyCode.Services** — Client-side: SignalR client, login service, loggers.
|
- **AyCode.Services** — Client-side: SignalR client, login service, loggers.
|
||||||
- **AyCode.Services.Server** — Server-side: SignalR hub with custom binary protocol, email (SendGrid), JWT auth.
|
- **AyCode.Services.Server** — Server-side: SignalR hub with custom binary protocol, email (SendGrid), JWT auth.
|
||||||
|
- **AyCode.Models.Server/DynamicMethods** — Reflection-based tag→method dispatch used by the SignalR hub.
|
||||||
|
|
||||||
|
> **SignalR Dispatch:** Both directions use a single method `OnReceiveMessage(int messageTag, byte[] messageBytes, int? requestId)` with integer tag-based routing instead of standard Hub methods. See [`SIGNALR_ARCHITECTURE.md`](SIGNALR_ARCHITECTURE.md) for full details.
|
||||||
|
|
||||||
### Server Extensions
|
### Server Extensions
|
||||||
- **AyCode.Core.Server**, **AyCode.Interfaces.Server**, **AyCode.Entities.Server**, **AyCode.Models.Server** — Server-only additions that don't belong in shared code.
|
- **AyCode.Core.Server**, **AyCode.Interfaces.Server**, **AyCode.Entities.Server**, **AyCode.Models.Server** — Server-only additions that don't belong in shared code.
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,24 @@
|
||||||
- Chain API for multi-type deserialization with cross-reference resolution.
|
- Chain API for multi-type deserialization with cross-reference resolution.
|
||||||
- Feature flags on `[AcBinarySerializable]` eliminate dead code from generated output.
|
- Feature flags on `[AcBinarySerializable]` eliminate dead code from generated output.
|
||||||
|
|
||||||
|
## SignalR Conventions
|
||||||
|
|
||||||
|
See [`SIGNALR_ARCHITECTURE.md`](SIGNALR_ARCHITECTURE.md) for full architecture documentation.
|
||||||
|
|
||||||
|
- **Single dispatch method** — all communication goes through `OnReceiveMessage(int messageTag, byte[] messageBytes, int? requestId)`. Do not add new hub methods.
|
||||||
|
- **Tag-based routing** — associate methods with integer tags via `[SignalR(tag)]` (server) or `[SignalRSendToClient(tag)]` (client). Tags must be unique across the entire system.
|
||||||
|
- **CRUD bundles** — entities use `SignalRCrudTags(baseTag)` which allocates 5 sequential tags (GetAll, GetItem, Add, Update, Remove). Reserve non-overlapping base tags.
|
||||||
|
- **Binary protocol** — `AcBinaryHubProtocol` is the transport protocol. Responses use pure Binary serialization.
|
||||||
|
|
||||||
|
### ⚠️ Temporary: JSON-in-Binary Request Parameters
|
||||||
|
|
||||||
|
Client→server request parameters currently use a JSON→Binary→JSON round-trip:
|
||||||
|
1. `SignalPostJsonDataMessage<T>` serializes `PostData` to JSON string (`PostDataJson`)
|
||||||
|
2. The JSON-containing message is wrapped in a Binary envelope
|
||||||
|
3. Server deserializes Binary → extracts JSON string → parses JSON per parameter
|
||||||
|
|
||||||
|
This is **planned for replacement** with direct Binary parameter serialization (matching how responses already work). Do not attempt to fix this as a side-effect of other work — it requires coordinated client+server+consuming-project changes.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- **MSTest** framework across all test projects.
|
- **MSTest** framework across all test projects.
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,24 @@ For full specification see [`BINARY_FORMAT.md`](BINARY_FORMAT.md).
|
||||||
| **Session** | Read-only database operation pattern. No transaction, no mutex lock on DbContext. |
|
| **Session** | Read-only database operation pattern. No transaction, no mutex lock on DbContext. |
|
||||||
| **Transaction** | Write operation pattern with auto-rollback on failure. |
|
| **Transaction** | Write operation pattern with auto-rollback on failure. |
|
||||||
|
|
||||||
|
## SignalR Dispatch
|
||||||
|
|
||||||
|
For full architecture see [`SIGNALR_ARCHITECTURE.md`](SIGNALR_ARCHITECTURE.md).
|
||||||
|
|
||||||
|
| Term | Definition |
|
||||||
|
|---|---|
|
||||||
|
| **OnReceiveMessage** | The single SignalR method used for all communication in both directions. Signature: `(int messageTag, byte[] messageBytes, int? requestId)`. |
|
||||||
|
| **Message Tag** | Integer identifier that maps to a specific method via `[SignalR(tag)]` or `[SignalRSendToClient(tag)]` attributes. |
|
||||||
|
| **TagAttribute** | Base attribute associating an integer tag with a method. `SignalRAttribute` and `SignalRSendToClientAttribute` extend it. |
|
||||||
|
| **DynamicMethodRegistry** | Reflection-based registry that resolves message tags to `MethodInfo` at runtime. Uses `ConcurrentDictionary` + `FrozenDictionary` caching. |
|
||||||
|
| **AcDynamicMethodCallModel** | Per-instance-type method cache. Builds a `FrozenDictionary<int, AcMethodInfoModel>` once per type via reflection. |
|
||||||
|
| **SignalRCrudTags** | Sealed record mapping 5 sequential tags (GetAll, GetItem, Add, Update, Remove) for entity CRUD. `GetMessageTagByTrackingState()` maps `TrackingState` → tag. |
|
||||||
|
| **AcBinaryHubProtocol** | Custom `IHubProtocol` replacing SignalR's JSON+Base64 with `AcBinarySerializer`. Protocol name: `"acbinary"`. |
|
||||||
|
| **SignalPostJsonDataMessage\<T\>** | ⚠️ TEMPORARY — Message that serializes request parameters to JSON inside a Binary envelope. Planned for replacement with pure Binary. |
|
||||||
|
| **SignalResponseDataMessage** | Response message supporting Binary or JSON+GZip serialization. Responses already use pure Binary (no JSON overhead). |
|
||||||
|
| **AcSignalRDataSource** | Server-side `IList<T>` with `ChangeTracking` and SignalR CRUD via `SignalRCrudTags`. Supports binary merge for incremental updates. |
|
||||||
|
| **SendToClientType** | Enum controlling broadcast scope: None, Others, Caller, All. |
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
| Term | Definition |
|
| Term | Definition |
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
# 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<TResponse>(tag, postData)
|
||||||
|
└─ AcSignalRClientBase.cs
|
||||||
|
|
||||||
|
2. CreatePostMessage() wraps postData:
|
||||||
|
├─ IdMessage — for primitive IDs (Guid, int, string)
|
||||||
|
└─ SignalPostJsonDataMessage<T> — 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<SignalPostJsonMessage>() — 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<SignalResponseDataMessage>()
|
||||||
|
└─ Complete TaskCompletionSource → caller gets TResponse
|
||||||
|
|
||||||
|
11. SignalResponseDataMessage.GetResponseData<T>():
|
||||||
|
├─ Binary: ResponseData.BinaryTo<T>() — 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<T>` 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<T>` with direct Binary parameter serialization, matching how responses already work.
|
||||||
|
|
||||||
|
**Key files involved:**
|
||||||
|
- `AyCode.Services/SignalRs/IAcSignalRHubClient.cs` — `SignalPostJsonDataMessage<T>.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<int, AcMethodInfoModel> cache
|
||||||
|
├─ Miss? Scan registered instances:
|
||||||
|
│ └─ AcDynamicMethodCallModel.GetMethodByTag(tag)
|
||||||
|
│ └─ FrozenDictionary<int, AcMethodInfoModel> (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<int, AcMethodInfoModel>` — populated lazily per tag
|
||||||
|
- **Instance resolution:** Registry tracks which instance owns which tag for invocation
|
||||||
|
|
||||||
|
## DataSource Pattern
|
||||||
|
|
||||||
|
`AcSignalRDataSource<T, TId, TIList>` provides a server-side `IList<T>` with:
|
||||||
|
|
||||||
|
- **Change tracking:** `ChangeTracking<T, TId>` wraps items in `TrackingItem<T, TId>` (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.
|
||||||
Loading…
Reference in New Issue