Refactor: Split and expand project-level documentation

- Move all major feature docs (logging, binary, SignalR, DataSource) into per-project `docs/` folders with dedicated Markdown files.
- Split monolithic docs into focused files: `BINARY_FORMAT.md`, `BINARY_FEATURES.md`, `BINARY_OPTIONS.md`, `LOGGING.md`, `LOGGING_SERVER.md`, `LOGGING_REMOTE.md`, `SIGNALR.md`, `SIGNALR_SERVER.md`, `SIGNALR_DATASOURCE.md`.
- Update all references in `README.md`, `copilot-instructions.md`, `GLOSSARY.md`, and `ARCHITECTURE.md` to point to the correct per-project doc.
- Add documentation tables to each project’s `README.md` and clarify folder structure.
- Update `.csproj` files to include `docs/**/*.md` for packaging.
- Remove obsolete/moved docs from the solution file.
- Ensure all technical debt warnings and cross-references are preserved and accurate.
- No code changes; documentation only.
This commit is contained in:
Loretta 2026-03-29 22:16:28 +02:00
parent 03d606164c
commit ffd537b5eb
31 changed files with 1014 additions and 589 deletions

View File

@ -15,7 +15,7 @@
## SignalR
7. **Single-method transport** — all SignalR communication uses `OnReceiveMessage(tag, bytes, requestId)`. Tags are `int` constants resolved via `DynamicMethodRegistry`. Never add conventional hub methods.
8. **AcSignalRDataSource** — generic `IList<T>` with change tracking, CRUD via `SignalRCrudTags`, binary merge, rollback. See `docs/SIGNALR_DATASOURCE.md`. Transport docs: `docs/SIGNALR.md`.
8. **AcSignalRDataSource** — generic `IList<T>` with change tracking, CRUD via `SignalRCrudTags`, binary merge, rollback. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md`. Transport docs: `AyCode.Services/docs/SIGNALR.md`.
9. **JSON-in-Binary tech debt** — client→server request parameters are currently JSON inside a Binary envelope (`SignalPostJsonDataMessage`). Do NOT attempt to fix as a side effect — requires coordinated changes across all consuming projects.
## Critical Warnings

View File

@ -15,4 +15,8 @@
<ProjectReference Include="..\AyCode.Core\AyCode.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="docs\**\*.md" />
</ItemGroup>
</Project>

View File

@ -1,7 +1,9 @@
# Loggers
Provides a singleton `GlobalLogger` for application-wide logging with multiple severity levels (Detail, Debug, Info, Warning, Suggest, Error) and support for pluggable log writers.
Server-side singleton logger for static access across the application.
> For full logging architecture see [`docs/LOGGING.md`](../../docs/LOGGING.md). For core logger and writer abstractions see [`AyCode.Core/Loggers/README.md`](../../AyCode.Core/Loggers/README.md).
## Key Files
- **`GlobalLogger.cs`** — Singleton static logger that delegates to `AcLoggerBase`. Supports category names, caller member tracking, and configurable `LogLevel` and `AppType`.
- **`GlobalLogger.cs`** — Singleton static wrapper around an internal `AcGlobalLoggerBase` (sealed `AcLoggerBase` subclass). Provides static methods for all log levels (`Detail`, `Debug`, `Info`, `Warning`, `Suggest`, `Error`, `Write`). Default category: `"GLOBAL_LOGGER"`. Reads config from `appsettings.json` like any `AcLoggerBase`. Exposes `GetWriters` and `Writer<T>()` for accessing specific writer instances.

View File

@ -2,6 +2,12 @@
Server-side extension of AyCode.Core. Provides server-specific implementations that build on the shared core library.
## Documentation
| Document | Topic |
|---|---|
| [LOGGING_SERVER.md](docs/LOGGING_SERVER.md) | GlobalLogger singleton, server-side logging |
## Folder Structure
| Folder | Purpose |

View File

@ -0,0 +1,23 @@
# Server Logging
Server-side logging extensions. For core framework (base classes, configuration, LogLevel, ILogger bridge) see [`AyCode.Core/docs/LOGGING.md`](../../AyCode.Core/docs/LOGGING.md). For remote writers (HTTP, browser, SignalR) see [`AyCode.Services/docs/LOGGING_REMOTE.md`](../../AyCode.Services/docs/LOGGING_REMOTE.md).
## GlobalLogger
Server-side singleton for static access. Wraps an internal `AcGlobalLoggerBase` instance (sealed `AcLoggerBase` subclass):
```csharp
GlobalLogger.Info("Server started");
GlobalLogger.Error("Failed to process", ex, "MyCategory");
GlobalLogger.Writer<IAcConsoleLogWriter>()?.Suggest("hint");
```
Default category: `"GLOBAL_LOGGER"`. Reads config from `appsettings.json` like any other `AcLoggerBase` instance.
All static methods mirror the `IAcLogWriterBase` contract: `Detail`, `Debug`, `Info`, `Warning`, `Suggest`, `Error`, `Write`.
## Key Source Files
| Component | Path |
|-----------|------|
| GlobalLogger | `Loggers/GlobalLogger.cs` |

View File

@ -60,10 +60,8 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{D4B2E9F1-A6C3-4F7E-8D5B-3E2A1C4F6B8D}"
ProjectSection(SolutionItems) = preProject
docs\ARCHITECTURE.md = docs\ARCHITECTURE.md
docs\BINARY_FORMAT.md = docs\BINARY_FORMAT.md
docs\CONVENTIONS.md = docs\CONVENTIONS.md
docs\GLOSSARY.md = docs\GLOSSARY.md
docs\SIGNALR_ARCHITECTURE.md = docs\SIGNALR_ARCHITECTURE.md
EndProjectSection
EndProject
Global

View File

@ -24,6 +24,10 @@
<Folder Include="Expressions\" />
</ItemGroup>
<ItemGroup>
<None Include="docs\**\*.md" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="AyCode.Core.Serializers.Console" />
<InternalsVisibleTo Include="AyCode.Core.Tests" />

View File

@ -1,34 +1,39 @@
# Loggers
Custom logging framework with `Microsoft.Extensions.Logging` integration. Supports multiple output writers, colored console output, and structured log formatting.
Custom logging framework with multi-writer fan-out and `Microsoft.Extensions.Logging` integration. This directory contains the **core** logger and writer abstractions — the defining layer for the logging system.
> For full architecture, configuration, all writers, and remote logging flow see [`docs/LOGGING.md`](../../docs/LOGGING.md).
## Architecture
```
IAcLoggerBase (: IAcLogWriterBase, ILogger)
└─ AcLoggerBase (abstract, delegates to writers)
└─ [concrete loggers per app]
└─ AcLoggerBase (abstract, multi-writer fan-out, ILogger bridge)
└─ [concrete loggers per consuming project]
IAcLogWriterBase
└─ AcLogWriterBase (abstract, config from appsettings)
└─ AcTextLogWriterBase (abstract, text formatting)
└─ AcConsoleLogWriter (colored console output)
└─ AcLogWriterBase (abstract, per-writer config from appsettings)
├─ AcTextLogWriterBase (abstract, text formatting)
│ └─ AcConsoleLogWriter (colored console output)
└─ [AcLogItemWriterBase<T> in AyCode.Entities — structured writers]
```
**Two-level filtering:** Logger has a global `LogLevel` gate; each writer has its own `LogLevel`. Both must pass for a log entry to be written. See [`docs/LOGGING.md` → Design Overview](../../docs/LOGGING.md#design-overview).
## Key Files
### Logger Core
- **`IAcLoggerBase.cs`** — Unified interface combining `IAcLogWriterBase` + `ILogger`. Exposes `GetWriters` and `Writer<T>()`.
- **`AcLoggerBase.cs`** — Abstract logger implementing `ILogger`. Manages a list of `IAcLogWriterBase` writers. Maps MS `LogLevel` to `AcLogLevel`. Reads writer config from `appsettings.json` (`AyCode:Logger:LogWriters`).
- **`AcLoggerAdapter.cs`** — `AcLoggerProvider<TLogger>` implementing `ILoggerProvider` for DI integration. Extension methods: `AddAcLogger<T>()`, `UseOnlyAcLogger<T>()`.
- **`AcLoggerBase.cs`** — Abstract logger implementing `ILogger`. Manages `List<IAcLogWriterBase>`. Reads config from `appsettings.json` (`AyCode:Logger:{AppType, LogLevel, LogWriters[]}`). Maps MS `LogLevel` to AC `LogLevel`. Fan-out dispatch to all writers. `[Conditional("DEBUG")]` variants for all methods.
- **`AcLoggerAdapter.cs`** — `AcLoggerProvider<TLogger>` implementing `ILoggerProvider` with `ConcurrentDictionary<string, TLogger>` per-category cache. Extension methods: `AddAcLogger<T>()`, `UseOnlyAcLogger<T>()`.
### Writers
- **`IAcLogWriterBase.cs`** — Writer contract: `Detail()`, `Debug()`, `Info()`, `Warning()`, `Suggest()`, `Error()`, `Write()`.
- **`IAcLogWriterClientBase.cs`** — Marker interface for client-side writers.
- **`AcLogWriterBase.cs`** — Abstract base with config loading from appsettings.
- **`AcTextLogWriterBase.cs`** — Abstract text formatter. Output format: `[TIME] [APP] [LEVEL] [CATEGORY->METHOD] [THREADID] TEXT [ERROR]`.
- **`AcConsoleLogWriter.cs`** — Colored console writer (Gray=Trace, Cyan=Suggest, Yellow=Warning, Red=Error). Thread-safe via lock.
- **`IAcLogWriterBase.cs`** — Writer contract: `Detail()`, `Debug()`, `Info()`, `Warning()`, `Suggest()`, `Error()`, plus `Write()` overloads and `Write(IAcLogItemClient)`.
- **`IAcLogWriterClientBase.cs`** — Marker interface for client-side writers (no additional members).
- **`AcLogWriterBase.cs`** — Abstract base. Own `LogLevel` loaded from appsettings by matching `AssemblyQualifiedName`. Named methods delegate to terminal `Write(AppType, LogLevel, text, caller, category, errorType, exMessage)`.
- **`AcTextLogWriterBase.cs`** — Abstract text formatter. Format: `[HH:mm:ss.fff] [AppType[0]] [Level] [Category->Method] [ThreadId] Text [Error]`. Subclasses implement `WriteText(string, LogLevel)`.
- **`AcConsoleLogWriter.cs`** — Colored console writer. Thread-safe via `static lock`. Colors: Gray=≤Trace, White=DebugInfo, Cyan=Suggest, Yellow=Warning, Red=≥Error.
### Supporting
- **`IAcLogItemClient.cs`** — Structured log item DTO for remote transmission.
- **`LogLevel.cs`** — Byte enum: Detail(0), Trace(5), Debug(10), Info(15), Suggest(17), Warning(20), Error(25), Disabled(255). **Must match the database LogLevel table.**
- **`IAcLogItemClient.cs`** — Structured log item DTO interface for remote transmission (TimeStampUtc, AppType, LogLevel, ThreadId, CategoryName, CallerName, Text, Exception, ErrorType).
- **`LogLevel.cs`** — Byte enum: Detail(0), Trace(5), Debug(10), Info(15), Suggest(17), Warning(20), Error(25), Disabled(255). ⚠️ **Values synchronized with database `LogLevel` table — do NOT renumber.**

View File

@ -2,6 +2,15 @@
Core library for the AyCode platform. Targets .NET 9 (set in `AyCode.Core.targets`). Provides serialization (Binary, JSON, Toon), compression, logging, validation, and shared utilities.
## Documentation
| Document | Topic |
|---|---|
| [BINARY_FORMAT.md](docs/BINARY_FORMAT.md) | Binary wire format spec (stream layout, type markers) |
| [BINARY_FEATURES.md](docs/BINARY_FEATURES.md) | Binary features (interning, ref tracking, property ordering) |
| [BINARY_OPTIONS.md](docs/BINARY_OPTIONS.md) | Binary configuration options & presets |
| [LOGGING.md](docs/LOGGING.md) | Logging framework (hierarchy, config, ILogger bridge) |
## Folder Structure
| Folder | Purpose | README |

View File

@ -0,0 +1,93 @@
# AcBinary Features
Advanced serialization features built on top of the wire format. For core type markers and encoding see [`BINARY_FORMAT.md`](BINARY_FORMAT.md). For configuration options and presets see [`BINARY_OPTIONS.md`](BINARY_OPTIONS.md).
## Compact Encoding Selection
The serializer applies compact encodings automatically:
| Data | Condition | Encoding | Savings |
|------|-----------|----------|---------|
| Integer | 16 ≤ v ≤ 47 | TinyInt (1 byte) | 25 bytes |
| String | ≤31 bytes, ASCII | FixStr (1+N bytes) | 1 byte (no length prefix) |
| Object | type index < 64 | FixObj (1 byte) | 15 bytes (no VarUInt index) |
| String | empty | StringEmpty (1 byte) | 1+ bytes |
| Bool | — | True/False (1 byte) | no payload |
## String Interning Protocol
Controls deduplication of repeated string values.
**Modes** (`StringInterningMode`):
- `None` — all strings inline, no overhead
- `Attribute` — only `[AcStringIntern]` properties interned (default)
- `All` — all strings within length limits interned
**Length limits:** `MinStringInternLength=4`, `MaxStringInternLength=64` (configurable).
**Wire protocol:**
1. Serializer pre-scans all eligible strings to build a plan (which strings repeat)
2. First occurrence: `[StringInternFirst(94)] [VarUInt cacheIndex] [VarUInt byteLength] [UTF-8 bytes]`
3. Subsequent: `[StringInterned(92)] [VarUInt cacheIndex]`
4. Single-occurrence strings: written as normal `String`/`FixStr` (no interning overhead)
## Reference Tracking
Prevents infinite loops and preserves object identity for repeated references.
**Modes** (`ReferenceHandlingMode`):
- `None` — no tracking (fastest, use when graph is a tree)
- `OnlyId` — track only `IId` objects (matched by ID value)
- `All` — track all reference types (two-phase scan required)
**Two-phase process:**
1. **Scan pass** (`ScanPass.cs`) — walks the object graph, detects multi-referenced objects and repeated strings. Builds a `WriteDuplicateEntry[]` array (the "write plan") containing `VisitIndex`, `CacheMapIndex`, `IsFirst`, and `Value` for each duplicate.
2. **Sort** — write plan entries are sorted by `VisitIndex` to match the write pass traversal order.
3. **Serialize pass** — consumes the sorted write plan via `TryConsumeWritePlanEntry()`. A cursor (`_nextWritePlanVisitIndex`) advances through the plan in O(1) — no dictionary lookups during serialization.
**Wire protocol:**
- First occurrence: `[ObjectRefFirst(70)] [VarUInt refCacheIndex] [object body...]`
- Subsequent: `[ObjectRef(65)] [VarUInt refCacheIndex]`
**Example — same object referenced twice:**
```
Input: { Users: [userA, userA] } (same instance)
Scan pass → WritePlan:
[{VisitIndex:2, CacheMapIndex:0, IsFirst:true},
{VisitIndex:3, CacheMapIndex:0, IsFirst:false}]
Wire output (Compact mode, ReferenceHandling=All):
[version=1] [flags=0x96] [VarUInt cacheCount=1] ← header
[FixObj(0)] ← root object
[Array(66)] [VarUInt(2)] ← Users array, 2 elements
[ObjectRefFirst(70)] [VarUInt(0)] [props...] ← userA, 1st occurrence
[ObjectRef(65)] [VarUInt(0)] ← userA, 2nd (2 bytes only)
```
## Property Ordering
Properties are serialized in a deterministic order defined by `TypeMetadataBase.GetUnfilteredProperties()`:
1. Walk the inheritance chain from **derived → base** (`currentType.BaseType` loop)
2. At each level, collect declared public instance properties
3. Sort **alphabetically** (`StringComparer.Ordinal`) within each level
4. Result: **base properties first, then derived, alphabetical within each level**
This order is stable across serializer/deserializer as long as the type hierarchy doesn't change.
### Cross-Type Deserialization (UseMetadata)
When `UseMetadata=true`, property name hashes (FNV-1a via `FnvHash.ComputeString`) are written per type, enabling schema evolution:
- **Serializer** writes property hashes in the metadata section (`ObjectWithMetadata(69)`)
- **Deserializer** builds an index mapping array (`GetIndexMapping()`) that maps source property indices to destination indices by matching FNV-1a hashes
- This allows deserialization even when source and destination types have different property sets or ordering
When `UseMetadata=false`, properties are matched by **positional index only** — source and destination must have identical property layouts.
**Edge cases:**
- **Hash collision** (`CheckDuplicatePropName=true`, default): throws `InvalidOperationException`. When `false`: collision silently ignored — risk of data corruption.
- **Source has unknown property** (not in destination): silently skipped via `SkipValue()`, no error.
- **Destination has extra property** (not in source): left at default value (new instance) or unchanged (populate mode).

View File

@ -0,0 +1,167 @@
# AcBinary Wire Format
Complete wire format specification for the AcBinary serializer. Source of truth: [`Serializers/Binaries/BinaryTypeCode.cs`](../Serializers/Binaries/BinaryTypeCode.cs).
> For advanced features (compact encoding, string interning, reference tracking, property ordering) see [`BINARY_FEATURES.md`](BINARY_FEATURES.md).
> For configuration options, presets, and option interactions see [`BINARY_OPTIONS.md`](BINARY_OPTIONS.md).
## Stream Layout
```
[version : 1 byte] [flags : 1 byte] [cacheCount : VarUInt?] [payload...]
```
- **version**`FormatVersion = 1` (current).
- **flags** — See [Header Flags](#header-flags).
- **cacheCount** — Present only when `HeaderFlag_HasCacheCount` is set. Number of type wrapper slots used by serializer.
## Header Flags
The flags byte uses `0x90` (144) as base with bit flags in the lower nibble:
| Bit | Mask | Flag | Meaning |
|-----|------|------|---------|
| 0 | `0x01` | Metadata | Property hash metadata included (cross-type deserialization) |
| 1 | `0x02` | RefHandling_OnlyId | Reference tracking for `IId` objects only |
| 2 | `0x04` | RefHandling_All | Reference tracking for all objects (always combined with bit 1) |
| 3 | `0x08` | HasCacheCount | VarUInt cache count follows the flags byte |
**Reference handling modes:** None = `0x00`, OnlyId = `0x02`, All = `0x06` (bits 1+2).
## Variable-Length Encoding
### VarUInt (unsigned)
LEB128: 7 data bits per byte, MSB = continuation flag.
```
value < 128 1 byte [0xxxxxxx]
value < 16384 2 bytes [1xxxxxxx] [0xxxxxxx]
value < 2097152 3 bytes ...
(max 5 bytes for uint32)
```
### VarInt (signed)
ZigZag encoding maps signed to unsigned, then LEB128:
```
encode: (value << 1) ^ (value >> 31)
decode: (raw >> 1) ^ -(raw & 1)
```
Maps: `0 → 0`, `-1 → 1`, `1 → 2`, `-2 → 3`, etc.
### VarULong (unsigned 64-bit)
Same LEB128 encoding, max 10 bytes for uint64.
## Type Markers
All markers defined in `BinaryTypeCode.cs`. `SlotCount = 64`.
### FixObj (063)
Single-byte object type. The marker byte **is** the type slot index — no additional type identifier needed.
```
[FixObj(N)] [properties...]
```
**Slot allocation:** Slots 063 are reserved for runtime polymorphic types, assigned dynamically on first encounter during serialization. Source-generated (SGen) types receive slots starting at 64+ via `AllocateWrapperSlot()` (sequential, `Interlocked.Increment`). SGen slots are compile-time stable; runtime slots depend on serialization order.
### Complex Types (6471)
| Code | Name | Wire format |
|------|------|-------------|
| 64 | Object | `[64] [VarUInt typeIndex] [properties...]` |
| 65 | ObjectRef | `[65] [VarUInt refCacheIndex]` |
| 66 | Array | `[66] [VarUInt count] [elements...]` |
| 67 | Dictionary | `[67] [VarUInt count] [key, value pairs...]` |
| 68 | ByteArray | `[68] [VarUInt length] [raw bytes]` |
| 69 | ObjectWithMetadata | `[69] [VarUInt typeIndex] [VarUInt hashCount] [hashes...] [properties...]` |
| 70 | ObjectRefFirst | `[70] [VarUInt refCacheIndex] [object body...]` |
| 71 | ObjectWithMetadataRefFirst | `[71] [VarUInt refCacheIndex] [metadata + properties...]` |
### Polymorphic Types (7275)
Used when runtime type differs from declared property type and `UseMetadata=false`.
| Code | Name | Wire format |
|------|------|-------------|
| 72 | ObjectWithTypeName | `[72] [UTF8 typeName] [inner marker] [body...]` — prefix, inner Object/Array/Dict follows |
| 73 | ObjectWithTypeNameRefFirst | `[73] [UTF8 typeName] [VarUInt refCacheIndex] [properties...]` — combined, no inner marker |
| 74 | ObjectWithTypeIndex | `[74] [VarUInt typeIndex] [inner marker] [body...]` — prefix |
| 75 | ObjectWithTypeIndexRefFirst | `[75] [VarUInt typeIndex] [VarUInt refCacheIndex] [properties...]` — combined |
Second occurrence of a referenced polymorphic object uses plain `ObjectRef(65)` — no polymorphic prefix needed.
### Primitives (7690)
| Code | Name | Wire format |
|------|------|-------------|
| 76 | Null | `[76]` — no payload |
| 77 | True | `[77]` — no payload |
| 78 | False | `[78]` — no payload |
| 79 | Int8 | `[79] [1 byte]` |
| 80 | UInt8 | `[80] [1 byte]` |
| 81 | Int16 | `[81] [VarInt]` |
| 82 | UInt16 | `[82] [VarUInt]` |
| 83 | Int32 | `[83] [VarInt]` |
| 84 | UInt32 | `[84] [VarUInt]` |
| 85 | Int64 | `[85] [VarLong]` |
| 86 | UInt64 | `[86] [VarULong]` |
| 87 | Float32 | `[87] [4 bytes IEEE 754]` |
| 88 | Float64 | `[88] [8 bytes IEEE 754]` |
| 89 | Decimal | `[89] [16 bytes]` |
| 90 | Char | `[90] [VarUInt]` |
### Strings (9194)
| Code | Name | Wire format |
|------|------|-------------|
| 91 | String | `[91] [VarUInt byteLength] [UTF-8 bytes]` |
| 92 | StringInterned | `[92] [VarUInt cacheIndex]` — 2nd+ occurrence |
| 93 | StringEmpty | `[93]` — no payload |
| 94 | StringInternFirst | `[94] [VarUInt cacheIndex] [VarUInt byteLength] [UTF-8 bytes]` — 1st occurrence |
### Date/Time (9598)
| Code | Name | Wire format |
|------|------|-------------|
| 95 | DateTime | `[95] [8 bytes ticks]` |
| 96 | DateTimeOffset | `[96] [8 bytes ticks] [VarInt offsetMinutes]` |
| 97 | TimeSpan | `[97] [VarLong ticks]` |
| 98 | Guid | `[98] [16 bytes]` |
### Other Markers
| Code | Name | Wire format |
|------|------|-------------|
| 99 | Enum | `[99] [VarInt underlyingValue]` |
| 100 | MetadataHeader | Legacy: implies `RefHandling=true` + metadata present |
| 101 | NoMetadataHeader | Legacy: implies `RefHandling=true`, no metadata |
| 102 | PropertySkip | `[102]` — marks skipped property (default/null value) |
### FixStr (103134)
Short ASCII strings encoded in a single marker byte + raw bytes (no length prefix):
```
[FixStrBase + byteLength] [ASCII bytes]
```
- Length range: 031 bytes (`FixStrBase=103`, `FixStrMax=134`)
- Saves 1 byte vs `String` marker + VarUInt length
- Falls back to `String(91)` if content is non-ASCII
### TinyInt (192255)
Single-byte integer encoding for small values:
```
value = marker - 192 - 16 (range: -16 to 47)
marker = value + 16 + 192 (64 values total)
```
Saves 2+ bytes vs `Int32(83)` + VarInt for frequently occurring small integers.

View File

@ -0,0 +1,154 @@
# AcBinary Configuration
Configuration options, presets, and option interactions for `AcBinarySerializerOptions`. For wire format see [`BINARY_FORMAT.md`](BINARY_FORMAT.md). For features (interning, ref tracking, property ordering) see [`BINARY_FEATURES.md`](BINARY_FEATURES.md).
## WireMode
| Value | Integers | Strings | Output size | Speed |
|-------|----------|---------|-------------|-------|
| `Compact` (default) | VarInt/VarUInt (15 bytes) | UTF-8 with speculative ASCII fast path | Smaller | Slightly slower |
| `Fast` | Fixed-width raw bytes (4/8 bytes) | UTF-16 memcpy (`charCount * 2` bytes) | Larger | Fastest encode/decode |
**Format difference for strings:**
- Compact: `[VarUInt byteLength] [UTF-8 bytes]` — speculative ASCII (1 pass if all ASCII, rewind+UTF-8 fallback otherwise)
- Fast: `[VarUInt charCount] [raw UTF-16 bytes]` — zero-encoding memcpy
**Code branch:** `context.FastWire` flag set at `context.Reset()`. Checked in `WriteStringUtf8()` and integer write methods. FixStr optimization is skipped in Fast mode (UTF-8 specific).
## ReferenceHandling
| Value | Tracked objects | Scan pass | Header flags | Wire markers |
|-------|----------------|-----------|--------------|-------------|
| `None` | Nothing | Skipped | `0x00` | Standard object markers only |
| `OnlyId` | `IId` objects only (by ID value) | Partial | `0x02` | `ObjectRefFirst(70)` + `ObjectRef(65)` |
| `All` (default) | All reference types | Full graph walk | `0x06` | `ObjectRefFirst(70)` + `ObjectRef(65)` |
**Format impact:** When enabled, multi-referenced objects are written once with `ObjectRefFirst(70) + VarUInt(refCacheIndex)` on first encounter, then replaced by `ObjectRef(65) + VarUInt(refCacheIndex)` on subsequent encounters. Header `HasCacheCount` flag is set and cache count written.
**Interaction with `ThrowOnCircularReference` (default: `true`):**
- `true` + ref handling enabled: all objects tracked for cycle detection, throws `InvalidOperationException` on circular reference
- `false` + ref handling enabled: only IId types tracked for deduplication, non-IId circular refs silently truncated at `MaxDepth`
## UseMetadata
| Value | Wire markers | Property matching | Overhead |
|-------|-------------|-------------------|----------|
| `false` (default) | `FixObj`/`Object` | Positional index only — types must match | None |
| `true` | `ObjectWithMetadata(69)` / `ObjectWithMetadataRefFirst(71)` | FNV-1a property name hashes | 4 bytes per property per type |
**Format impact:** When enabled, each type's first occurrence writes `[VarUInt hashCount] [FNV-1a hash × N]` before properties. Deserializer uses hashes to build source→destination index mapping, enabling cross-type deserialization (different property sets/ordering).
**Code branch:** `context.UseMetadata` controls whether `ObjectWithMetadata(69)` or plain `Object(64)` markers are used. When `false`, `IsDirectObjectWrite=true` allows source-generated writers to bypass `WriteObject` entirely and inline property writes.
**Related:** `CheckDuplicatePropName` (default: `true`) — throws if FNV-1a hash collision detected between property names of the same type. Disable in production for performance.
## UseStringInterning
| Value | Eligible strings | Scan overhead | Wire markers |
|-------|-----------------|---------------|-------------|
| `None` | Nothing | None | `String(91)` / `FixStr` only |
| `Attribute` (default) | Properties with `[AcStringIntern(true)]` | Scans marked properties | `StringInternFirst(94)` + `StringInterned(92)` |
| `All` | All strings within length limits | Scans all strings | `StringInternFirst(94)` + `StringInterned(92)` |
**Length limits:** `MinStringInternLength` (default: 4) and `MaxStringInternLength` (default: 64, 0=unlimited). Strings outside this range are always written inline.
**Format impact:** Interned strings on first occurrence: `[StringInternFirst(94)] [VarUInt cacheIndex] [string data]`. Subsequent: `[StringInterned(92)] [VarUInt cacheIndex]` (12 bytes vs full string). Single-occurrence strings are never interned — no overhead for unique strings.
**Code branch:** `context.StringInternEligible` flag set per-property before `WriteString`. Scan pass builds a `WriteDuplicateEntry[]` plan; write pass consumes it via cursor.
## MaxDepth
| Value | Behavior |
|-------|----------|
| `255` (default) | Effectively unlimited nesting |
| `0` | Root level only — nested objects/collections written as `Null(76)` |
| `N` | Objects deeper than N levels written as `Null(76)` |
**Format impact:** Depth-exceeded values appear as `Null(76)` in the stream — indistinguishable from actual null values. No special marker.
**Code branch:** Checked at entry of every object/collection write: `if (depth > MaxDepth) { WriteByte(Null); return; }`.
## UseCompression
| Value | Method | Granularity | Memory |
|-------|--------|-------------|--------|
| `None` (default) | No compression | — | — |
| `Block` | LZ4 single block | Entire payload | Full buffer in memory |
| `BlockArray` | LZ4 chunked | 64KB chunks | Streaming-friendly, lower peak memory |
**Format impact:** Compression is applied **post-serialization** as a transparent wrapper — the inner wire format is unchanged. Both modes are pure managed C# (WASM-compatible, no native dependencies).
**Code branch:** Applied in `AcBinarySerializer.Serialize()` after the serialization context produces the raw buffer: `if (UseCompression != None) Lz4.Compress(buffer, mode)`. Decompression is automatic on deserialize.
## PropertyFilter
Optional delegate `BinaryPropertyFilter?` (default: `null`). When set, invoked for each property to decide inclusion.
```
delegate bool BinaryPropertyFilter(in BinaryPropertyFilterContext context);
```
**BinaryPropertyFilterContext fields:** `DeclaringType`, `PropertyName`, `PropertyType`, `Instance` (null during metadata phase), `IsMetadataPhase`, `GetValue()` (lazy).
**Format impact:** Excluded properties are completely absent from the stream — no marker, no placeholder. The deserializer must use `UseMetadata=true` or identical filter to correctly match property indices.
**Code branch:** `context.HasPropertyFilter` checked in `ShouldSerializeProperty()`. Called twice: once during metadata registration (`Instance=null`), once during write phase.
## PropertyMapper
Optional delegate `PropertyMapperDelegate?` (default: `null`) for cross-type deserialization property remapping.
```
delegate PropertyInfo? PropertyMapperDelegate(PropertyInfo sourceProperty, Type destinationType);
```
**Purpose:** Maps properties between different class hierarchies (renamed properties, external DTOs). Result is cached — zero overhead on same-type operations (`Deserialize<T>`).
## WASM Options
| Option | Default | Purpose |
|--------|---------|---------|
| `IsWasm` | `OperatingSystem.IsBrowser()` | Auto-detect WASM environment |
| `UseStringCaching` | follows `IsWasm` | Cache short strings during deserialization to reduce GC pressure |
| `MaxCachedStringLength` | 64 | Max string length to cache |
**Format impact:** None — these are deserialization-only optimizations. When `UseStringCaching=true`, the deserializer maintains an intern cache for strings ≤ `MaxCachedStringLength` chars. Disabled automatically when `StringInternFirst` marker is encountered (interning takes precedence).
## Other Options
| Option | Type | Default | Purpose |
|--------|------|---------|---------|
| `UseGeneratedCode` | bool | `true` | Use source-generated writers/readers when available |
| `InitialBufferCapacity` | int | 4096 | Starting buffer size (bytes) for serialization output |
| `RemoveOrphanedItems` | bool | `false` | During `PopulateMerge`: remove destination collection items with no matching source ID |
| `UseAsync` | bool | `false` | Async context pool return via ThreadPool. Auto-disabled in WASM and when `ReferenceHandling=None` |
| `MaxContextPoolSize` | int | 8 | Max serialization contexts kept in pool |
## Presets
| Preset | WireMode | Metadata | StringInterning | RefHandling | MaxDepth | Compression | Other |
|--------|----------|----------|-----------------|-------------|----------|-------------|-------|
| `Default` | Compact | false | Attribute | All | 255 | None | — |
| `FastMode` | Compact | false | None | None | 255 | None | No scan pass |
| `ShallowCopy` | Compact | false | None | None | **0** | None | Root level only |
| `WasmOptimized` | Compact | false | Attribute | All | 255 | None | +StringCaching |
| `WithoutReferenceHandling` | Compact | false | Attribute | **None** | 255 | None | No scan pass |
| `WithoutMetadata` | Compact | **false** | Attribute | All | 255 | None | — |
**Performance implication of presets:**
- `Default` / `WasmOptimized` — two-phase (scan + serialize) due to `ReferenceHandling=All`
- `FastMode` / `ShallowCopy` — single-phase (no scan pass) since both interning and refs are disabled
- The scan pass adds ~20-30% overhead; disable it when the object graph is a simple tree
## Option Interactions
Key interdependencies that affect which code branches execute:
| Combination | Effect |
|-------------|--------|
| `ReferenceHandling=None` + `UseStringInterning=None` | **No scan pass** — fastest path, single-phase serialization |
| `ReferenceHandling=All` + `UseMetadata=true` | Uses `ObjectWithMetadataRefFirst(71)` marker — combined ref + metadata |
| `UseMetadata=false` + `UseGeneratedCode=true` | `IsDirectObjectWrite=true` — generated code inlines property writes, bypasses `WriteObject` |
| `UseMetadata=true` + `PropertyFilter` set | Filter invoked twice (metadata phase + write phase); filter results must be stable |
| `WireMode=Fast` + `UseStringInterning!=None` | Interned strings still use the fast string path (UTF-16 for first occurrence, VarUInt index for subsequent) |
| `UseCompression!=None` + any other option | Compression is orthogonal — applied post-serialization, inner format unchanged |

243
AyCode.Core/docs/LOGGING.md Normal file
View File

@ -0,0 +1,243 @@
# Logging
Custom logging framework with multi-writer fan-out and `Microsoft.Extensions.Logging` integration. Source: `Loggers/` in this project.
> For server-side GlobalLogger see [`AyCode.Core.Server/docs/LOGGING_SERVER.md`](../../AyCode.Core.Server/docs/LOGGING_SERVER.md).
> For remote writers (HTTP, browser console, SignalR) see [`AyCode.Services/docs/LOGGING_REMOTE.md`](../../AyCode.Services/docs/LOGGING_REMOTE.md).
## Design Overview
A logger holds a list of writers. Every log call fans out to all writers that pass the level filter. Two independent level gates control what gets written:
```
logger.Info("msg")
├─ Gate 1: Logger.LogLevel <= Info? ← global minimum
│ NO → discard
│ YES ↓
├─ Writer[0].Write(...)
│ └─ Gate 2: Writer.LogLevel <= Info? ← per-writer minimum
│ NO → discard
│ YES → write to output
├─ Writer[1].Write(...)
│ └─ Gate 2: ...
└─ Writer[N].Write(...)
└─ Gate 2: ...
```
## Class Hierarchy
```
IAcLogWriterBase (writer contract)
└─ AcLogWriterBase (abstract, config from appsettings)
├─ AcTextLogWriterBase (abstract, text formatting)
│ ├─ AcConsoleLogWriter (colored console output)
│ └─ AcBrowserConsoleLogWriter (AyCode.Services — Blazor JSInterop console)
└─ AcLogItemWriterBase<TLogItem> (AyCode.Entities — abstract, structured → ThreadPool + Mutex)
├─ AcDbLogItemWriter<TCtx,TItem> (AyCode.Database — EF Core database writer)
├─ AcHttpClientLogItemWriter<T> (AyCode.Services — HTTP POST as JSON)
└─ AcSignaRClientLogItemWriter (AyCode.Services — SignalR hub transport)
IAcLoggerBase (: IAcLogWriterBase, ILogger)
└─ AcLoggerBase (abstract, multi-writer fan-out)
└─ AcGlobalLoggerBase (sealed, used by GlobalLogger singleton)
└─ [concrete loggers in consuming projects]
IAcLogWriterClientBase (: IAcLogWriterBase) (marker for client-side writers)
```
**Two writer branches:**
| Branch | Base class | Output | Concurrency |
|--------|-----------|--------|-------------|
| **Text** | `AcTextLogWriterBase` | Formatted string → `WriteText(string, LogLevel)` | Depends on subclass (Console uses `lock`) |
| **Structured** | `AcLogItemWriterBase<TLogItem>` | `TLogItem` object → `WriteLogItemCallback(TLogItem)` | `TaskHelper.RunOnThreadPool` + `Mutex` |
## LogLevel
```csharp
public enum LogLevel : byte
{
Detail = 0,
Trace = 5,
Debug = 10,
Info = 15,
Suggest = 17,
Warning = 20,
Error = 25,
Disabled = 255,
}
```
⚠️ **Values are synchronized with the database `LogLevel` table.** Do NOT renumber.
Comparison is `<=`: a logger/writer with `LogLevel = Info` will process `Info`, `Suggest`, `Warning`, `Error` (anything ≥ 15).
## Configuration
All config lives under `AyCode:Logger` in `appsettings.json`:
```json
{
"AyCode": {
"Logger": {
"AppType": "Server",
"LogLevel": "Debug",
"LogWriters": [
{
"LogWriterType": "MyApp.Loggers.MyConsoleLogWriter, MyApp",
"LogLevel": "Info"
},
{
"LogWriterType": "MyApp.Loggers.MyDbLogWriter, MyApp",
"LogLevel": "Warning"
}
]
}
}
}
```
| Key | Type | Purpose |
|-----|------|---------|
| `AppType` | `AppType` enum | Identifies the application (Server, Web, etc.). Stamped on every log entry. |
| `LogLevel` | `LogLevel` enum | Global minimum — the logger's own gate. |
| `LogWriters[]` | array | One entry per writer. Each has `LogWriterType` (AssemblyQualifiedName) and `LogLevel` (per-writer gate). |
### Writer Instantiation
`AcLoggerBase` constructor iterates `LogWriters[]` and calls:
```csharp
Activator.CreateInstance(logWriterType, AppType, logWriterLogLevel, CategoryName)
```
Each writer's constructor signature must accept `(AppType, LogLevel, string?)`.
### Writer Config Self-Lookup
`AcLogWriterBase` also reads its own `LogLevel` from config by matching its `AssemblyQualifiedName` against the `LogWriterType` entries. This means a writer instantiated manually (not via config) can still pick up config values if a matching entry exists.
## Core Components
### AcLoggerBase
Abstract logger implementing both `IAcLogWriterBase` and `ILogger`. Central responsibilities:
1. **Writer management**`List<IAcLogWriterBase> LogWriters`, `GetWriters`, `Writer<T>()`
2. **Fan-out dispatch** — Named methods (`Detail`, `Debug`, `Info`, `Warning`, `Suggest`, `Error`) check `LogLevel` then `ForEach` writers
3. **Write overloads** — Terminal: `Write(AppType, LogLevel, text, caller, category, errorType, exMessage)`
4. **Write(IAcLogItemClient)** — Accepts structured log items (from remote clients)
5. **ILogger bridge**`Log<TState>`, `IsEnabled`, `BeginScope` (no-op `NullScope`)
6. **[Conditional("DEBUG")]** — Every named method has a `*Conditional` variant stripped from Release builds
**Constructors:**
| Signature | Behavior |
|-----------|----------|
| `(string? categoryName)` | Reads config from `appsettings.json`, creates writers via `Activator.CreateInstance` |
| `(string? categoryName, params IAcLogWriterBase[])` | Reads `AppType` + `LogLevel` from config, uses provided writers |
| `(AppType, LogLevel, string?, params IAcLogWriterBase[])` | Fully manual, no config reading |
### AcLogWriterBase
Abstract writer base. Each writer has its own `LogLevel` and `AppType`. Named methods (`Detail`, `Debug`, etc.) delegate to the terminal `Write(AppType, LogLevel, text, caller, category, errorType, exMessage)` which subclasses override.
The base `Write` throws `NotImplementedException` — subclasses **must** override either the 7-parameter `Write` (for text writers) or `Write(IAcLogItemClient)` (for structured writers), or both.
### AcTextLogWriterBase
Text formatting base for human-readable output. Overrides `Write` to format via `GetDiagnosticText` then calls abstract `WriteText(string, LogLevel)`.
**Output format:**
```
[HH:mm:ss.fff] [S] [Level] [Category->Method] [ThreadId] Text
[ERROR_TYPE]: exception details...
```
- Timestamp: `DateTime.UtcNow.ToLocalTime()` (local display, UTC storage)
- `[S]` — First character of `AppType` (e.g., `S`=Server, `W`=Web)
- Level: Left-padded to 9 chars
- Category→Method: Left-padded to 54 chars
- Error text on new line only if `errorType` is non-empty
### AcConsoleLogWriter
Colored console output. Thread-safe via `static readonly object ForWriterLock`.
| LogLevel | Color |
|----------|-------|
| ≤ Trace | Gray |
| Debug Info | White (default) |
| Suggest | Cyan |
| Warning | Yellow |
| ≥ Error | Red (with extra newlines) |
## Microsoft.Extensions.Logging Bridge
### ILogger Bridge
`AcLoggerBase` implements `ILogger` directly. The `Log<TState>` method maps MS log levels to AC methods:
| Microsoft.Extensions.Logging.LogLevel | AyCode LogLevel | Method called |
|---------------------------------------|-----------------|---------------|
| Trace | Detail | `Detail()` |
| Debug | Debug | `Debug()` |
| Information | Info | `Info()` |
| Warning | Warning | `Warning()` |
| Error | Error | `Error()` |
| Critical | Error | `Error("[CRITICAL] ...")` |
| None | Disabled | — (ignored) |
`BeginScope` returns a no-op `NullScope` (scopes not supported).
**ShortenCategoryNames** (default: `true`): When MS logging provides fully-qualified type names as categories (e.g., `Microsoft.AspNetCore.SignalR.HubConnectionHandler`), shortens to just the class name (`HubConnectionHandler`).
### ILoggerProvider
`AcLoggerProvider<TLogger>` implements `ILoggerProvider` with a `ConcurrentDictionary<string, TLogger>` per-category cache. Factory function provided at registration.
**Extension methods:**
```csharp
// Add alongside default providers
builder.Logging.AddAcLogger<MyLogger>(categoryName => new MyLogger(categoryName));
// Replace all providers with only AcLogger
builder.Logging.UseOnlyAcLogger<MyLogger>(categoryName => new MyLogger(categoryName));
```
## Log Item Entity Hierarchy
Log items flow across projects as a chain of types:
```
IAcLogItemClient (AyCode.Core — DTO interface)
└─ AcLogItemClient (AyCode.Entities — [MessagePackObject])
└─ AcLogItem (AyCode.Entities.Server — [Table("LogItem")])
```
**IAcLogItemClient Fields:** TimeStampUtc, AppType, LogLevel, ThreadId, CategoryName, CallerName, Text, ErrorType, Exception.
## Patterns
### [Conditional("DEBUG")] Pattern
Every named log method has a `*Conditional` counterpart (e.g., `InfoConditional`, `ErrorConditional`) decorated with `[Conditional("DEBUG")]`. These are completely stripped from Release builds by the compiler. Both `AcLoggerBase` and `AcLogWriterBase` provide these. Use them for development-only diagnostics that should have zero cost in production.
### CallerMemberName Auto-Capture
All named log methods use `[CallerMemberName] string? memberName = null`. The compiler auto-fills the calling method name. For `ILogger.Log<TState>` calls (from MS logging), the `EventId.Name` is used as member name if available, otherwise defaults to `"Log"`.
## Key Source Files
| Component | Path |
|-----------|------|
| Logger base | `Loggers/AcLoggerBase.cs` |
| Writer base | `Loggers/AcLogWriterBase.cs` |
| Text writer base | `Loggers/AcTextLogWriterBase.cs` |
| Console writer | `Loggers/AcConsoleLogWriter.cs` |
| ILoggerProvider | `Loggers/AcLoggerAdapter.cs` |
| LogLevel enum | `Loggers/LogLevel.cs` |
| Log item DTO interface | `Loggers/IAcLogItemClient.cs` |
| Client marker | `Loggers/IAcLogWriterClientBase.cs` |

View File

@ -1,8 +1,10 @@
# DbContexts / Loggers
Logger-specific EF Core DbContext with NoTracking query behavior for read performance.
Logger-specific EF Core DbContext with `NoTracking` query behavior for read performance. Used by `AcDbLogItemWriter` to persist log items.
> For full logging architecture see [`docs/LOGGING.md`](../../../../docs/LOGGING.md).
## Key Files
- **`IAcLoggerDbContextBase.cs`** — Interface for log item DbSet.
- **`AcLoggerDbContextBase.cs`** — Implementation with NoTracking default, optimized for log reads.
- **`IAcLoggerDbContextBase.cs`** — Interface extending `IAcLogItemDbSetBase<TLogItem>` for log item DbSet.
- **`AcLoggerDbContextBase.cs`** — Generic implementation (`AcDbContextBase` subclass). Configures `QueryTrackingBehavior.NoTracking` for read performance. Exposes `DbSet<TLogItem> LogItems`.

View File

@ -1,7 +1,9 @@
# DbSets / Loggers
Log item DbSet interface.
Log item DbSet interface for EF Core log storage.
> For full logging architecture see [`docs/LOGGING.md`](../../../../docs/LOGGING.md).
## Key Files
- **`IAcLogItemDbSetBase.cs`** — Interface for LogItem DbSet.
- **`IAcLogItemDbSetBase.cs`** — Interface exposing `DbSet<TLogItem>` for log items. Implemented by `AcLoggerDbContextBase`.

View File

@ -1,8 +1,10 @@
# LogItems
Server-side log item entity and interface, extending the client-side `AcLogItemClient` with a database-mapped identity and header reference.
Server-side log item entity and interface, extending the client-side `AcLogItemClient` with database-mapped identity and header reference.
> For full logging architecture see [`docs/LOGGING.md`](../../docs/LOGGING.md). For client-side entity see [`AyCode.Entities/LogItems/README.md`](../../AyCode.Entities/LogItems/README.md).
## Key Files
- **`IAcLogItem.cs`** — Interface extending `IAcLogItemClient` and `IEntityInt`, adding a `LogHeaderId` property.
- **`AcLogItem.cs`** — Entity class mapped to the `LogItem` database table with MessagePack serialization. Provides `Id` (auto-generated) and `LogHeaderId`.
- **`IAcLogItem.cs`** — Interface extending `IAcLogItemClient` + `IEntityInt`, adding `LogHeaderId` property.
- **`AcLogItem.cs`** — `[Table("LogItem")]` entity extending `AcLogItemClient`. MessagePack keys: `Id` (Key 55, auto-generated PK), `LogHeaderId` (Key 60). Used by `AcDbLogItemWriter` for EF Core persistence.

View File

@ -1,7 +1,9 @@
# LogItems
Client-side log item entity used for structured logging. Serialized with MessagePack for efficient transport.
Client-side log item entity used for structured logging. Serialized with MessagePack for efficient transport over SignalR.
> For full logging architecture see [`docs/LOGGING.md`](../../docs/LOGGING.md).
## Key Files
- **`AcLogItemClient.cs`** — Implements `IAcLogItemClient` with fields for `AppType`, `LogLevel`, `ThreadId`, `CategoryName`, `CallerName`, `Text`, and exception details. Uses `[MessagePackObject]` serialization with explicit key indices.
- **`AcLogItemClient.cs`** — Implements `IAcLogItemClient`. `[MessagePackObject]` with explicit key indices (5, 15, 20, 25, 30, 35, 40, 45, 50). Fields: `TimeStampUtc`, `AppType`, `LogLevel`, `ThreadId`, `CategoryName`, `CallerName`, `Text`, `ErrorType`, `Exception`. Extended by `AcLogItem` on the server side.

View File

@ -33,4 +33,8 @@
<Folder Include="Emails\" />
</ItemGroup>
<ItemGroup>
<None Include="docs\**\*.md" />
</ItemGroup>
</Project>

View File

@ -2,6 +2,13 @@
Server-side service implementations: JWT authentication, SendGrid email delivery, SignalR hub infrastructure with binary protocol, session management, and change-tracked data sources.
## Documentation
| Document | Topic |
|---|---|
| [SIGNALR_SERVER.md](docs/SIGNALR_SERVER.md) | Server-side SignalR hub (dispatch, session, broadcast) |
| [SIGNALR_DATASOURCE.md](docs/SIGNALR_DATASOURCE.md) | Real-time DataSource with CRUD & change tracking |
## Folder Structure
| Folder | Purpose |

View File

@ -1,8 +1,9 @@
# SignalR DataSource
Change-tracked real-time collection built on top of the SignalR transport layer. Source: `AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs`.
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, dispatch) see [`SIGNALR.md`](SIGNALR.md).
> For the underlying transport (tag system, wire protocol, client base) see [`AyCode.Services/docs/SIGNALR.md`](../../AyCode.Services/docs/SIGNALR.md).
> For server hub infrastructure see [`SIGNALR_SERVER.md`](SIGNALR_SERVER.md).
## Overview
@ -43,7 +44,6 @@ public sealed class SignalRCrudTags(
**Usage (consuming project):**
```csharp
// Tags are defined as independent constants in the project's tag class
public abstract class MyProjectTags : AcSignalRTags
{
public const int OrderGetAll = 300;
@ -51,10 +51,8 @@ public abstract class MyProjectTags : AcSignalRTags
public const int OrderAdd = 302;
public const int OrderUpdate = 303;
public const int OrderRemove = 304;
// Tags don't have to be sequential — they just happen to be here
}
// Construct with 5 explicit tags
var crudTags = new SignalRCrudTags(
MyProjectTags.OrderGetAll,
MyProjectTags.OrderGetItem,
@ -69,21 +67,12 @@ var crudTags = new SignalRCrudTags(
## Data Loading
```csharp
// Awaits full response via sync-wait transport pattern (polls up to 60s)
await dataSource.LoadDataSource();
// Async callback — requests SignalResponseDataMessage directly to avoid double deserialization
await dataSource.LoadDataSourceAsync();
// Load from raw response bytes (public — usable when response is already available)
await dataSource.LoadDataSource(); // sync-wait transport (polls up to 60s)
await dataSource.LoadDataSourceAsync(); // async callback path
await dataSource.LoadDataSourceFromResponseData(responseData, serializerType);
// Single item by ID — updates or adds to InnerList
await dataSource.LoadItem(id);
await dataSource.LoadItem(id); // single item by ID
```
All load methods are `async Task`. `LoadDataSource` uses the sync-wait transport pattern (`GetAllAsync<T>` → polls for response), while `LoadDataSourceAsync` uses the async callback path (avoids deserializing `ResponseData` into a typed object — works directly with `byte[]`).
**Binary deserialization paths:**
- `AcObservableCollection<T>`: `BeginUpdate()``BinaryToMerge()``EndUpdate()` — single batched UI notification.
- `List<T>`: `BinaryTo(InnerList)` — direct populate.
@ -120,14 +109,12 @@ Each operation has an optional `autoSave` parameter — if true, immediately cal
| Event | Signature | Fires when |
|-------|-----------|------------|
| `OnDataSourceItemChanged` | `Func<ItemChangedEventArgs<T>, Task>?` | After each item is saved or loaded (carries item + TrackingState) |
| `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
Two variants with different transport patterns:
| Method | Returns | Transport pattern | Use case |
|--------|---------|-------------------|----------|
| `SaveChanges()` | `List<TrackingItem>` (remaining failures) | Sync-wait (`ContinueWith`) | When caller needs to know what failed |
@ -144,8 +131,6 @@ Both follow the same flow:
EndSync()
```
**SaveItem(item, trackingState):** saves a single item (same flow). **SaveItem(id):** looks up tracking item by ID, then saves.
**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
@ -162,17 +147,14 @@ UI binds to `IsSyncing` to show loading indicators. The counter supports nested
## Working Reference List
Allows an external list to become the DataSource's inner storage:
```csharp
dataSource.SetWorkingReferenceList(externalList);
// Now dataSource operates directly on externalList — same reference, no copy
// Read back the current inner list reference
TIList innerList = dataSource.GetReferenceInnerList();
```
This is useful when the UI already has a bound collection and you want the DataSource to manage it in-place. `HasWorkingReferenceList` indicates whether an external list has been set.
Useful when the UI already has a bound collection and you want the DataSource to manage it in-place.
## Locking Strategy
@ -185,7 +167,7 @@ This is useful when the UI already has a bound collection and you want the DataS
## Relationship to Transport
The DataSource is a **consumer** of the SignalR transport, not part of it. The flow:
The DataSource is a **consumer** of the SignalR transport, not part of it:
```
DataSource.SaveChanges()
@ -195,14 +177,13 @@ DataSource.SaveChanges()
→ Server method with [SignalR(tag)] ← tag dispatch
```
Projects can also call the transport directly without DataSource — see [`SIGNALR.md` § How Projects Use Tags](SIGNALR.md#how-projects-use-tags).
Projects can also call the transport directly without DataSource — see [`AyCode.Services/docs/SIGNALR.md`](../../AyCode.Services/docs/SIGNALR.md).
## Key Source Files
| Component | Path |
|-----------|------|
| DataSource | `AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs` |
| DataSource | `SignalRs/AcSignalRDataSource.cs` |
| Tracking helpers | `SignalRs/TrackingItemHelpers.cs` |
| CRUD tags | `AyCode.Services/SignalRs/SignalRCrudTags.cs` |
| Tracking helpers | `AyCode.Services.Server/SignalRs/TrackingItemHelpers.cs` |
| Transport client | `AyCode.Services/SignalRs/AcSignalRClientBase.cs` |
| Transport doc | [`docs/SIGNALR.md`](SIGNALR.md) |

View File

@ -0,0 +1,104 @@
# SignalR Server
Server-side SignalR hub infrastructure: method dispatch, session management, broadcast, and diagnostics. Source: `SignalRs/` in this project.
> For client-side transport (tags, wire protocol, client base) see [`AyCode.Services/docs/SIGNALR.md`](../../AyCode.Services/docs/SIGNALR.md).
> For the DataSource collection see [`SIGNALR_DATASOURCE.md`](SIGNALR_DATASOURCE.md).
## Server Processing
```
6. OnReceiveMessage(tag, bytes, requestId)
7. DynamicMethodRegistry.GetMethodByMessageTag(tag) ← ConcurrentDictionary lookup
8. DeserializeParameters(bytes):
├─ DeserializeFromBinary<SignalPostJsonMessage>() ← 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
```
## 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<int, (Type, AcMethodInfoModel)?> 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.
### 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.
## Session Management
`AcSessionService<TSessionItem, TSessionItemId>` tracks connected clients:
```csharp
ConcurrentDictionary<TSessionItemId, TSessionItem> Sessions
```
`IAcSessionItem<TSessionItemId>` requires `SessionId` property. Used for targeting messages to specific users/connections.
## Broadcast Service
`AcSignalRSendToClientService<THub, TTags, TLogger>` 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`.
## Hub Events
- `OnConnectedAsync()` — log connection
- `OnDisconnectedAsync(exception)` — log disconnection, cleanup session
## Diagnostics
Enable with `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.
## Key Source Files
| Component | Path |
|-----------|------|
| Hub base | `SignalRs/AcWebSignalRHubBase.cs` |
| Session service | `SignalRs/AcSessionService.cs` |
| Broadcast service | `SignalRs/AcSignalRSendToClientService.cs` |
| Logger hub | `SignalRs/AcLoggerSignalRHub.cs` |
| Tracking helpers | `SignalRs/TrackingItemHelpers.cs` |
| Dynamic dispatch | `AyCode.Models.Server/DynamicMethods/AcDynamicMethodRegistry.cs` |

View File

@ -18,4 +18,8 @@
<ProjectReference Include="..\AyCode.Models\AyCode.Models.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="docs\**\*.md" />
</ItemGroup>
</Project>

View File

@ -1,11 +1,11 @@
# Loggers
Remote log writers for sending log items over HTTP, SignalR, or to the browser console.
Remote log writers for sending log items over HTTP, SignalR, or to the browser console. All three implement `IAcLogWriterClientBase`.
> For full logging architecture see [`docs/LOGGING.md`](../../docs/LOGGING.md). For core logger and writer abstractions see [`AyCode.Core/Loggers/README.md`](../../AyCode.Core/Loggers/README.md).
## Key Files
- **`AcHttpClientLogItemWriter.cs`** — Abstract generic writer extending `AcLogItemWriterBase<TLogItem>`. Sends log items via HTTP POST as JSON. Manages its own `HttpClient` lifecycle.
- **`AcBrowserConsoleLogWriter.cs`** — Browser console writer via JS interop (`IJSRuntime`). Maps `LogLevel` to console methods (log, warn, error). Used in Blazor apps.
- **`AcSignaRClientLogItemWriter.cs`** — Sends `AcLogItemClient` items to a SignalR hub. Manages `HubConnection` lifecycle with `StartConnection()`/`StopConnection()`.
All three implement `IAcLogWriterClientBase`.
- **`AcHttpClientLogItemWriter.cs`** — Abstract structured writer extending `AcLogItemWriterBase<TLogItem>`. Sends log items via `HttpClient.PostAsJsonAsync()` (fire-and-forget). Manages its own `HttpClient` + `HttpClientHandler`. HTTP/2 default.
- **`AcBrowserConsoleLogWriter.cs`** — Blazor browser console writer extending `AcTextLogWriterBase` (text branch). Uses `IJSRuntime.InvokeVoidAsync()` to call `console.info` / `console.warn` / `console.error` based on `LogLevel`.
- **`AcSignaRClientLogItemWriter.cs`** — SignalR log transport writer extending `AcLogItemWriterBase<AcLogItemClient>`. Sends structured log items to `AcLoggerSignalRHub` via `HubConnection.SendAsync("AddLogItem", logItem)`. Manages connection lifecycle (`StartConnection`/`StopConnection`). Converts `TimeStampUtc` to UTC before sending (SignalR doesn't transmit `DateTime.Kind`).

View File

@ -2,6 +2,13 @@
Shared service implementations: SignalR communication (custom binary protocol), login services, and remote log writers.
## Documentation
| Document | Topic |
|---|---|
| [SIGNALR.md](docs/SIGNALR.md) | Client-side SignalR transport (tags, wire protocol, req/resp flow) |
| [LOGGING_REMOTE.md](docs/LOGGING_REMOTE.md) | Remote log writers (HTTP, browser console, SignalR) |
## Folder Structure
| Folder | Purpose |

View File

@ -0,0 +1,69 @@
# Remote Log Writers
Client-side log writers that send log data to remote endpoints. Source: `Loggers/` in this project. For core logging framework see [`AyCode.Core/docs/LOGGING.md`](../../AyCode.Core/docs/LOGGING.md). For server-side GlobalLogger see [`AyCode.Core.Server/docs/LOGGING_SERVER.md`](../../AyCode.Core.Server/docs/LOGGING_SERVER.md).
## AcBrowserConsoleLogWriter
Blazor browser console writer. Extends `AcTextLogWriterBase` (text branch, not structured). Uses `IJSRuntime` to invoke browser console methods:
| LogLevel | JS method |
|----------|-----------|
| Detail Suggest | `console.info` |
| Warning | `console.warn` |
| Error | `console.error` |
Implements `IAcLogWriterClientBase` (client-side marker).
## AcHttpClientLogItemWriter\<TLogItem\>
HTTP POST writer. Extends `AcLogItemWriterBase<TLogItem>` (structured branch). Manages its own `HttpClient` + `HttpClientHandler`. Sends log items as JSON:
```csharp
_httpClient.PostAsJsonAsync(_url, logItem).Forget();
```
Note: `Forget()` — fire-and-forget, no await on the HTTP response. HTTP/2 by default.
## AcSignaRClientLogItemWriter
SignalR log transport writer. Extends `AcLogItemWriterBase<AcLogItemClient>`. Sends items to a dedicated logger hub. Manages its own `HubConnection`:
```csharp
_hubConnection.SendAsync("AddLogItem", logItem).Forget();
```
**UTC conversion note:** SignalR doesn't transmit `DateTime.Kind`. The writer calls `logItem.TimeStampUtc.ToUniversalTime()` before sending to ensure the server receives actual UTC.
Connection lifecycle: `StartConnection()` waits up to 10s for `Connected` state. `StopConnection()` stops and disposes the hub.
## Remote Logging Flow
Client applications can send log items to a server via SignalR:
```
Client App Server
────────── ──────
AcSignaRClientLogItemWriter AcLoggerSignalRHub<TLogger>
│ │
│ logItem.ToUniversalTime() │
│ StartConnection() │
├──SendAsync("AddLogItem", item)──► │
│ ├─ logItem.TimeStampUtc = UtcNow
│ ├─ _logger.Write(logItem)
│ │ ├─ Console writer
│ │ ├─ DB writer
│ │ └─ ... (all server writers)
```
Note: `AcLoggerSignalRHub` overrides the client's `TimeStampUtc` with `DateTime.UtcNow` on the server side for authoritative timestamps. The hub is a simple `Hub` (not `AcWebSignalRHubBase`) — it does NOT use tag-based dispatch.
## Key Source Files
| Component | Path |
|-----------|------|
| Browser writer | `Loggers/AcBrowserConsoleLogWriter.cs` |
| HTTP writer | `Loggers/AcHttpClientLogItemWriter.cs` |
| SignalR writer | `Loggers/AcSignaRClientLogItemWriter.cs` |
| Structured writer base | `AyCode.Entities/AcLogItemWriterBase.cs` |
| DB writer | `AyCode.Database/AcDbLogItemWriter.cs` |
| Logger hub (server) | `AyCode.Services.Server/SignalRs/AcLoggerSignalRHub.cs` |

View File

@ -1,8 +1,9 @@
# SignalR Communication
# SignalR Client
The transport layer for AyCode real-time communication. Source: `AyCode.Services/SignalRs/` (client), `AyCode.Services.Server/SignalRs/` (server).
Client-side SignalR transport with custom binary protocol and tag-based dispatch. Source: `SignalRs/` in this project.
> For the change-tracked collection built on top of this transport, see [`SIGNALR_DATASOURCE.md`](SIGNALR_DATASOURCE.md).
> For server-side hub, session, broadcast see [`AyCode.Services.Server/docs/SIGNALR_SERVER.md`](../../AyCode.Services.Server/docs/SIGNALR_SERVER.md).
> For the DataSource collection see [`AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md`](../../AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md).
## Design Overview
@ -13,7 +14,7 @@ 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.
`OnReceiveMessage` is the only transport channel — all calls go through it. The `tag` (int) determines **which server method to invoke**. Projects call any endpoint by specifying the tag; the server dispatches accordingly.
```
Client side: Server side:
@ -48,7 +49,6 @@ public abstract class MyProjectTags : AcSignalRTags
public const int GetOrders = 100;
public const int GetOrderById = 101;
public const int SaveOrder = 102;
// ... project-specific tags
}
```
@ -64,9 +64,6 @@ Three attribute levels:
// 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)]
@ -79,7 +76,6 @@ Three attribute levels:
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<List<Order>>(MyTags.GetOrders, companyId);
var order = await signalRClient.GetByIdAsync<Order>(MyTags.GetOrderById, orderId);
await signalRClient.PostDataAsync(MyTags.SaveOrder, order);
@ -91,42 +87,7 @@ await signalRClient.PostDataAsync(MyTags.SaveOrder, order, async response => { .
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<int, (Type, AcMethodInfoModel)?> 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.
The CRUD helpers (`PostAsync`, `GetByIdAsync`, `GetAllAsync`, `PostDataAsync`) are all generic transport methods that work with any tag — they are not tied to DataSource.
## Wire Protocol
@ -170,22 +131,6 @@ Arguments serialized individually with VarUInt length prefix. Direct write to `I
5. AcBinaryHubProtocol frames it on the wire
```
### Server Processing
```
6. OnReceiveMessage(tag, bytes, requestId)
7. DynamicMethodRegistry.GetMethodByMessageTag(tag) ← ConcurrentDictionary lookup
8. DeserializeParameters(bytes):
├─ DeserializeFromBinary<SignalPostJsonMessage>() ← 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
```
@ -206,11 +151,11 @@ Arguments serialized individually with VarUInt length prefix. Direct write to `I
└─ 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).
**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.
**Request model pooling:** `SignalRRequestModel` instances are managed via `SignalRRequestModelPool` (ObjectPool<T> + IResettable) to avoid allocations per request.
### Response Patterns
## Response Patterns
| Pattern | Method | Blocking |
|---------|--------|----------|
@ -218,29 +163,6 @@ Arguments serialized individually with VarUInt length prefix. Direct write to `I
| Async callback | `PostDataAsync(tag, data, async msg => {...})` | No, `Func<SignalResponseDataMessage, Task>` |
| Fire-and-forget | `PostDataAsync(tag, data, msg => {...})` | No, `Action<SignalResponseDataMessage>` |
## Session Management
`AcSessionService<TSessionItem, TSessionItemId>` tracks connected clients:
```csharp
ConcurrentDictionary<TSessionItemId, TSessionItem> Sessions
```
`IAcSessionItem<TSessionItemId>` requires `SessionId` property. Used for targeting messages to specific users/connections.
## Broadcast Service
`AcSignalRSendToClientService<THub, TTags, TLogger>` 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:**
@ -249,18 +171,12 @@ All messages wrapped in `SignalResponseDataMessage` → binary serialized → `O
- 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`.
Enable with `AcSignalRClientBase.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.
@ -269,13 +185,11 @@ Logs: hex dump (500 byte sample), header parsing (version, marker), property cou
| 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` |
| Client base | `SignalRs/AcSignalRClientBase.cs` |
| Binary protocol | `SignalRs/AcBinaryHubProtocol.cs` |
| Tag attributes | `SignalRs/SignalMessageTagAttribute.cs` |
| Base tags | `SignalRs/AcSignalRTags.cs` |
| CRUD tags | `SignalRs/SignalRCrudTags.cs` |
| SendToClientType | `SignalRs/SendToClientType.cs` |
| Message types | `SignalRs/IAcSignalRHubClient.cs` |
| Serialization | `SignalRs/SignalRSerializationHelper.cs` |

View File

@ -12,7 +12,22 @@ Domain rules and key pitfalls live in a single file: [`.github/copilot-instructi
| Claude Code | ✅ `CLAUDE.md` → references above | None |
| Cursor / Windsurf | ✅ `README.md` | Read `copilot-instructions.md` via @file |
Detailed docs: [`docs/`](docs/) — GLOSSARY.md, ARCHITECTURE.md, CONVENTIONS.md
Solution-level docs in [`docs/`](docs/):
| Document | Topic |
|---|---|
| [GLOSSARY.md](docs/GLOSSARY.md) | Core terminology reference |
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Solution layers, dependency rules |
| [CONVENTIONS.md](docs/CONVENTIONS.md) | Coding conventions |
Project-level docs — each project's `docs/` folder documents the code it defines:
| Project | Documents |
|---|---|
| [`AyCode.Core/docs/`](AyCode.Core/docs/) | [BINARY_FORMAT](AyCode.Core/docs/BINARY_FORMAT.md), [BINARY_FEATURES](AyCode.Core/docs/BINARY_FEATURES.md), [BINARY_OPTIONS](AyCode.Core/docs/BINARY_OPTIONS.md), [LOGGING](AyCode.Core/docs/LOGGING.md) |
| [`AyCode.Core.Server/docs/`](AyCode.Core.Server/docs/) | [LOGGING_SERVER](AyCode.Core.Server/docs/LOGGING_SERVER.md) |
| [`AyCode.Services/docs/`](AyCode.Services/docs/) | [SIGNALR](AyCode.Services/docs/SIGNALR.md), [LOGGING_REMOTE](AyCode.Services/docs/LOGGING_REMOTE.md) |
| [`AyCode.Services.Server/docs/`](AyCode.Services.Server/docs/) | [SIGNALR_SERVER](AyCode.Services.Server/docs/SIGNALR_SERVER.md), [SIGNALR_DATASOURCE](AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md) |
## Solution Structure

View File

@ -36,7 +36,7 @@ AyCode.Services ← AyCode.Services.Server
- **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.md`](SIGNALR.md) for full details.
> **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 [`AyCode.Services/docs/SIGNALR.md`](../AyCode.Services/docs/SIGNALR.md) for full details.
### Server Extensions
- **AyCode.Core.Server**, **AyCode.Interfaces.Server**, **AyCode.Entities.Server**, **AyCode.Models.Server** — Server-only additions that don't belong in shared code.

View File

@ -1,409 +0,0 @@
# AcBinary Wire Format
Complete wire format specification for the AcBinary serializer. Source of truth: [`AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs`](../AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs).
## Stream Layout
```
[version : 1 byte] [flags : 1 byte] [cacheCount : VarUInt?] [payload...]
```
- **version**`FormatVersion = 1` (current).
- **flags** — See [Header Flags](#header-flags).
- **cacheCount** — Present only when `HeaderFlag_HasCacheCount` is set. Number of type wrapper slots used by serializer.
## Header Flags
The flags byte uses `0x90` (144) as base with bit flags in the lower nibble:
| Bit | Mask | Flag | Meaning |
|-----|------|------|---------|
| 0 | `0x01` | Metadata | Property hash metadata included (cross-type deserialization) |
| 1 | `0x02` | RefHandling_OnlyId | Reference tracking for `IId` objects only |
| 2 | `0x04` | RefHandling_All | Reference tracking for all objects (always combined with bit 1) |
| 3 | `0x08` | HasCacheCount | VarUInt cache count follows the flags byte |
**Reference handling modes:** None = `0x00`, OnlyId = `0x02`, All = `0x06` (bits 1+2).
## Variable-Length Encoding
### VarUInt (unsigned)
LEB128: 7 data bits per byte, MSB = continuation flag.
```
value < 128 1 byte [0xxxxxxx]
value < 16384 2 bytes [1xxxxxxx] [0xxxxxxx]
value < 2097152 3 bytes ...
(max 5 bytes for uint32)
```
### VarInt (signed)
ZigZag encoding maps signed to unsigned, then LEB128:
```
encode: (value << 1) ^ (value >> 31)
decode: (raw >> 1) ^ -(raw & 1)
```
Maps: `0 → 0`, `-1 → 1`, `1 → 2`, `-2 → 3`, etc.
### VarULong (unsigned 64-bit)
Same LEB128 encoding, max 10 bytes for uint64.
## Type Markers
All markers defined in `BinaryTypeCode.cs`. `SlotCount = 64`.
### FixObj (063)
Single-byte object type. The marker byte **is** the type slot index — no additional type identifier needed.
```
[FixObj(N)] [properties...]
```
**Slot allocation:** Slots 063 are reserved for runtime polymorphic types, assigned dynamically on first encounter during serialization. Source-generated (SGen) types receive slots starting at 64+ via `AllocateWrapperSlot()` (sequential, `Interlocked.Increment`). SGen slots are compile-time stable; runtime slots depend on serialization order.
### Complex Types (6471)
| Code | Name | Wire format |
|------|------|-------------|
| 64 | Object | `[64] [VarUInt typeIndex] [properties...]` |
| 65 | ObjectRef | `[65] [VarUInt refCacheIndex]` |
| 66 | Array | `[66] [VarUInt count] [elements...]` |
| 67 | Dictionary | `[67] [VarUInt count] [key, value pairs...]` |
| 68 | ByteArray | `[68] [VarUInt length] [raw bytes]` |
| 69 | ObjectWithMetadata | `[69] [VarUInt typeIndex] [VarUInt hashCount] [hashes...] [properties...]` |
| 70 | ObjectRefFirst | `[70] [VarUInt refCacheIndex] [object body...]` |
| 71 | ObjectWithMetadataRefFirst | `[71] [VarUInt refCacheIndex] [metadata + properties...]` |
### Polymorphic Types (7275)
Used when runtime type differs from declared property type and `UseMetadata=false`.
| Code | Name | Wire format |
|------|------|-------------|
| 72 | ObjectWithTypeName | `[72] [UTF8 typeName] [inner marker] [body...]` — prefix, inner Object/Array/Dict follows |
| 73 | ObjectWithTypeNameRefFirst | `[73] [UTF8 typeName] [VarUInt refCacheIndex] [properties...]` — combined, no inner marker |
| 74 | ObjectWithTypeIndex | `[74] [VarUInt typeIndex] [inner marker] [body...]` — prefix |
| 75 | ObjectWithTypeIndexRefFirst | `[75] [VarUInt typeIndex] [VarUInt refCacheIndex] [properties...]` — combined |
Second occurrence of a referenced polymorphic object uses plain `ObjectRef(65)` — no polymorphic prefix needed.
### Primitives (7690)
| Code | Name | Wire format |
|------|------|-------------|
| 76 | Null | `[76]` — no payload |
| 77 | True | `[77]` — no payload |
| 78 | False | `[78]` — no payload |
| 79 | Int8 | `[79] [1 byte]` |
| 80 | UInt8 | `[80] [1 byte]` |
| 81 | Int16 | `[81] [VarInt]` |
| 82 | UInt16 | `[82] [VarUInt]` |
| 83 | Int32 | `[83] [VarInt]` |
| 84 | UInt32 | `[84] [VarUInt]` |
| 85 | Int64 | `[85] [VarLong]` |
| 86 | UInt64 | `[86] [VarULong]` |
| 87 | Float32 | `[87] [4 bytes IEEE 754]` |
| 88 | Float64 | `[88] [8 bytes IEEE 754]` |
| 89 | Decimal | `[89] [16 bytes]` |
| 90 | Char | `[90] [VarUInt]` |
### Strings (9194)
| Code | Name | Wire format |
|------|------|-------------|
| 91 | String | `[91] [VarUInt byteLength] [UTF-8 bytes]` |
| 92 | StringInterned | `[92] [VarUInt cacheIndex]` — 2nd+ occurrence |
| 93 | StringEmpty | `[93]` — no payload |
| 94 | StringInternFirst | `[94] [VarUInt cacheIndex] [VarUInt byteLength] [UTF-8 bytes]` — 1st occurrence |
### Date/Time (9598)
| Code | Name | Wire format |
|------|------|-------------|
| 95 | DateTime | `[95] [8 bytes ticks]` |
| 96 | DateTimeOffset | `[96] [8 bytes ticks] [VarInt offsetMinutes]` |
| 97 | TimeSpan | `[97] [VarLong ticks]` |
| 98 | Guid | `[98] [16 bytes]` |
### Other Markers
| Code | Name | Wire format |
|------|------|-------------|
| 99 | Enum | `[99] [VarInt underlyingValue]` |
| 100 | MetadataHeader | Legacy: implies `RefHandling=true` + metadata present |
| 101 | NoMetadataHeader | Legacy: implies `RefHandling=true`, no metadata |
| 102 | PropertySkip | `[102]` — marks skipped property (default/null value) |
### FixStr (103134)
Short ASCII strings encoded in a single marker byte + raw bytes (no length prefix):
```
[FixStrBase + byteLength] [ASCII bytes]
```
- Length range: 031 bytes (`FixStrBase=103`, `FixStrMax=134`)
- Saves 1 byte vs `String` marker + VarUInt length
- Falls back to `String(91)` if content is non-ASCII
### TinyInt (192255)
Single-byte integer encoding for small values:
```
value = marker - 192 - 16 (range: -16 to 47)
marker = value + 16 + 192 (64 values total)
```
Saves 2+ bytes vs `Int32(83)` + VarInt for frequently occurring small integers.
## Compact Encoding Selection
The serializer applies compact encodings automatically:
| Data | Condition | Encoding | Savings |
|------|-----------|----------|---------|
| Integer | 16 ≤ v ≤ 47 | TinyInt (1 byte) | 25 bytes |
| String | ≤31 bytes, ASCII | FixStr (1+N bytes) | 1 byte (no length prefix) |
| Object | type index < 64 | FixObj (1 byte) | 15 bytes (no VarUInt index) |
| String | empty | StringEmpty (1 byte) | 1+ bytes |
| Bool | — | True/False (1 byte) | no payload |
## String Interning Protocol
Controls deduplication of repeated string values.
**Modes** (`StringInterningMode`):
- `None` — all strings inline, no overhead
- `Attribute` — only `[AcStringIntern]` properties interned (default)
- `All` — all strings within length limits interned
**Length limits:** `MinStringInternLength=4`, `MaxStringInternLength=64` (configurable).
**Wire protocol:**
1. Serializer pre-scans all eligible strings to build a plan (which strings repeat)
2. First occurrence: `[StringInternFirst(94)] [VarUInt cacheIndex] [VarUInt byteLength] [UTF-8 bytes]`
3. Subsequent: `[StringInterned(92)] [VarUInt cacheIndex]`
4. Single-occurrence strings: written as normal `String`/`FixStr` (no interning overhead)
## Reference Tracking
Prevents infinite loops and preserves object identity for repeated references.
**Modes** (`ReferenceHandlingMode`):
- `None` — no tracking (fastest, use when graph is a tree)
- `OnlyId` — track only `IId` objects (matched by ID value)
- `All` — track all reference types (two-phase scan required)
**Two-phase process:**
1. **Scan pass** (`ScanPass.cs`) — walks the object graph, detects multi-referenced objects and repeated strings. Builds a `WriteDuplicateEntry[]` array (the "write plan") containing `VisitIndex`, `CacheMapIndex`, `IsFirst`, and `Value` for each duplicate.
2. **Sort** — write plan entries are sorted by `VisitIndex` to match the write pass traversal order.
3. **Serialize pass** — consumes the sorted write plan via `TryConsumeWritePlanEntry()`. A cursor (`_nextWritePlanVisitIndex`) advances through the plan in O(1) — no dictionary lookups during serialization.
**Wire protocol:**
- First occurrence: `[ObjectRefFirst(70)] [VarUInt refCacheIndex] [object body...]`
- Subsequent: `[ObjectRef(65)] [VarUInt refCacheIndex]`
**Example — same object referenced twice:**
```
Input: { Users: [userA, userA] } (same instance)
Scan pass → WritePlan:
[{VisitIndex:2, CacheMapIndex:0, IsFirst:true},
{VisitIndex:3, CacheMapIndex:0, IsFirst:false}]
Wire output (Compact mode, ReferenceHandling=All):
[version=1] [flags=0x96] [VarUInt cacheCount=1] ← header
[FixObj(0)] ← root object
[Array(66)] [VarUInt(2)] ← Users array, 2 elements
[ObjectRefFirst(70)] [VarUInt(0)] [props...] ← userA, 1st occurrence
[ObjectRef(65)] [VarUInt(0)] ← userA, 2nd (2 bytes only)
```
## Property Ordering
Properties are serialized in a deterministic order defined by `TypeMetadataBase.GetUnfilteredProperties()`:
1. Walk the inheritance chain from **derived → base** (`currentType.BaseType` loop)
2. At each level, collect declared public instance properties
3. Sort **alphabetically** (`StringComparer.Ordinal`) within each level
4. Result: **base properties first, then derived, alphabetical within each level**
This order is stable across serializer/deserializer as long as the type hierarchy doesn't change.
### Cross-Type Deserialization (UseMetadata)
When `UseMetadata=true`, property name hashes (FNV-1a via `FnvHash.ComputeString`) are written per type, enabling schema evolution:
- **Serializer** writes property hashes in the metadata section (`ObjectWithMetadata(69)`)
- **Deserializer** builds an index mapping array (`GetIndexMapping()`) that maps source property indices to destination indices by matching FNV-1a hashes
- This allows deserialization even when source and destination types have different property sets or ordering
When `UseMetadata=false`, properties are matched by **positional index only** — source and destination must have identical property layouts.
**Edge cases:**
- **Hash collision** (`CheckDuplicatePropName=true`, default): throws `InvalidOperationException`. When `false`: collision silently ignored — risk of data corruption.
- **Source has unknown property** (not in destination): silently skipped via `SkipValue()`, no error.
- **Destination has extra property** (not in source): left at default value (new instance) or unchanged (populate mode).
## Configuration Options
Options defined in `AcBinarySerializerOptions` (inherits `AcSerializerOptions`). Each option controls which code paths execute and how the wire format changes.
### WireMode
| Value | Integers | Strings | Output size | Speed |
|-------|----------|---------|-------------|-------|
| `Compact` (default) | VarInt/VarUInt (15 bytes) | UTF-8 with speculative ASCII fast path | Smaller | Slightly slower |
| `Fast` | Fixed-width raw bytes (4/8 bytes) | UTF-16 memcpy (`charCount * 2` bytes) | Larger | Fastest encode/decode |
**Format difference for strings:**
- Compact: `[VarUInt byteLength] [UTF-8 bytes]` — speculative ASCII (1 pass if all ASCII, rewind+UTF-8 fallback otherwise)
- Fast: `[VarUInt charCount] [raw UTF-16 bytes]` — zero-encoding memcpy
**Code branch:** `context.FastWire` flag set at `context.Reset()`. Checked in `WriteStringUtf8()` and integer write methods. FixStr optimization is skipped in Fast mode (UTF-8 specific).
### ReferenceHandling
| Value | Tracked objects | Scan pass | Header flags | Wire markers |
|-------|----------------|-----------|--------------|-------------|
| `None` | Nothing | Skipped | `0x00` | Standard object markers only |
| `OnlyId` | `IId` objects only (by ID value) | Partial | `0x02` | `ObjectRefFirst(70)` + `ObjectRef(65)` |
| `All` (default) | All reference types | Full graph walk | `0x06` | `ObjectRefFirst(70)` + `ObjectRef(65)` |
**Format impact:** When enabled, multi-referenced objects are written once with `ObjectRefFirst(70) + VarUInt(refCacheIndex)` on first encounter, then replaced by `ObjectRef(65) + VarUInt(refCacheIndex)` on subsequent encounters. Header `HasCacheCount` flag is set and cache count written.
**Interaction with `ThrowOnCircularReference` (default: `true`):**
- `true` + ref handling enabled: all objects tracked for cycle detection, throws `InvalidOperationException` on circular reference
- `false` + ref handling enabled: only IId types tracked for deduplication, non-IId circular refs silently truncated at `MaxDepth`
### UseMetadata
| Value | Wire markers | Property matching | Overhead |
|-------|-------------|-------------------|----------|
| `false` (default) | `FixObj`/`Object` | Positional index only — types must match | None |
| `true` | `ObjectWithMetadata(69)` / `ObjectWithMetadataRefFirst(71)` | FNV-1a property name hashes | 4 bytes per property per type |
**Format impact:** When enabled, each type's first occurrence writes `[VarUInt hashCount] [FNV-1a hash × N]` before properties. Deserializer uses hashes to build source→destination index mapping, enabling cross-type deserialization (different property sets/ordering).
**Code branch:** `context.UseMetadata` controls whether `ObjectWithMetadata(69)` or plain `Object(64)` markers are used. When `false`, `IsDirectObjectWrite=true` allows source-generated writers to bypass `WriteObject` entirely and inline property writes.
**Related:** `CheckDuplicatePropName` (default: `true`) — throws if FNV-1a hash collision detected between property names of the same type. Disable in production for performance.
### UseStringInterning
| Value | Eligible strings | Scan overhead | Wire markers |
|-------|-----------------|---------------|-------------|
| `None` | Nothing | None | `String(91)` / `FixStr` only |
| `Attribute` (default) | Properties with `[AcStringIntern(true)]` | Scans marked properties | `StringInternFirst(94)` + `StringInterned(92)` |
| `All` | All strings within length limits | Scans all strings | `StringInternFirst(94)` + `StringInterned(92)` |
**Length limits:** `MinStringInternLength` (default: 4) and `MaxStringInternLength` (default: 64, 0=unlimited). Strings outside this range are always written inline.
**Format impact:** Interned strings on first occurrence: `[StringInternFirst(94)] [VarUInt cacheIndex] [string data]`. Subsequent: `[StringInterned(92)] [VarUInt cacheIndex]` (12 bytes vs full string). Single-occurrence strings are never interned — no overhead for unique strings.
**Code branch:** `context.StringInternEligible` flag set per-property before `WriteString`. Scan pass builds a `WriteDuplicateEntry[]` plan; write pass consumes it via cursor.
### MaxDepth
| Value | Behavior |
|-------|----------|
| `255` (default) | Effectively unlimited nesting |
| `0` | Root level only — nested objects/collections written as `Null(76)` |
| `N` | Objects deeper than N levels written as `Null(76)` |
**Format impact:** Depth-exceeded values appear as `Null(76)` in the stream — indistinguishable from actual null values. No special marker.
**Code branch:** Checked at entry of every object/collection write: `if (depth > MaxDepth) { WriteByte(Null); return; }`.
### UseCompression
| Value | Method | Granularity | Memory |
|-------|--------|-------------|--------|
| `None` (default) | No compression | — | — |
| `Block` | LZ4 single block | Entire payload | Full buffer in memory |
| `BlockArray` | LZ4 chunked | 64KB chunks | Streaming-friendly, lower peak memory |
**Format impact:** Compression is applied **post-serialization** as a transparent wrapper — the inner wire format is unchanged. Both modes are pure managed C# (WASM-compatible, no native dependencies).
**Code branch:** Applied in `AcBinarySerializer.Serialize()` after the serialization context produces the raw buffer: `if (UseCompression != None) Lz4.Compress(buffer, mode)`. Decompression is automatic on deserialize.
### PropertyFilter
Optional delegate `BinaryPropertyFilter?` (default: `null`). When set, invoked for each property to decide inclusion.
```
delegate bool BinaryPropertyFilter(in BinaryPropertyFilterContext context);
```
**BinaryPropertyFilterContext fields:** `DeclaringType`, `PropertyName`, `PropertyType`, `Instance` (null during metadata phase), `IsMetadataPhase`, `GetValue()` (lazy).
**Format impact:** Excluded properties are completely absent from the stream — no marker, no placeholder. The deserializer must use `UseMetadata=true` or identical filter to correctly match property indices.
**Code branch:** `context.HasPropertyFilter` checked in `ShouldSerializeProperty()`. Called twice: once during metadata registration (`Instance=null`), once during write phase.
### PropertyMapper
Optional delegate `PropertyMapperDelegate?` (default: `null`) for cross-type deserialization property remapping.
```
delegate PropertyInfo? PropertyMapperDelegate(PropertyInfo sourceProperty, Type destinationType);
```
**Purpose:** Maps properties between different class hierarchies (renamed properties, external DTOs). Result is cached — zero overhead on same-type operations (`Deserialize<T>`).
### WASM Options
| Option | Default | Purpose |
|--------|---------|---------|
| `IsWasm` | `OperatingSystem.IsBrowser()` | Auto-detect WASM environment |
| `UseStringCaching` | follows `IsWasm` | Cache short strings during deserialization to reduce GC pressure |
| `MaxCachedStringLength` | 64 | Max string length to cache |
**Format impact:** None — these are deserialization-only optimizations. When `UseStringCaching=true`, the deserializer maintains an intern cache for strings ≤ `MaxCachedStringLength` chars. Disabled automatically when `StringInternFirst` marker is encountered (interning takes precedence).
### Other Options
| Option | Type | Default | Purpose |
|--------|------|---------|---------|
| `UseGeneratedCode` | bool | `true` | Use source-generated writers/readers when available |
| `InitialBufferCapacity` | int | 4096 | Starting buffer size (bytes) for serialization output |
| `RemoveOrphanedItems` | bool | `false` | During `PopulateMerge`: remove destination collection items with no matching source ID |
| `UseAsync` | bool | `false` | Async context pool return via ThreadPool. Auto-disabled in WASM and when `ReferenceHandling=None` |
| `MaxContextPoolSize` | int | 8 | Max serialization contexts kept in pool |
## Presets
| Preset | WireMode | Metadata | StringInterning | RefHandling | MaxDepth | Compression | Other |
|--------|----------|----------|-----------------|-------------|----------|-------------|-------|
| `Default` | Compact | false | Attribute | All | 255 | None | — |
| `FastMode` | Compact | false | None | None | 255 | None | No scan pass |
| `ShallowCopy` | Compact | false | None | None | **0** | None | Root level only |
| `WasmOptimized` | Compact | false | Attribute | All | 255 | None | +StringCaching |
| `WithoutReferenceHandling` | Compact | false | Attribute | **None** | 255 | None | No scan pass |
| `WithoutMetadata` | Compact | **false** | Attribute | All | 255 | None | — |
**Performance implication of presets:**
- `Default` / `WasmOptimized` — two-phase (scan + serialize) due to `ReferenceHandling=All`
- `FastMode` / `ShallowCopy` — single-phase (no scan pass) since both interning and refs are disabled
- The scan pass adds ~20-30% overhead; disable it when the object graph is a simple tree
## Option Interactions
Key interdependencies that affect which code branches execute:
| Combination | Effect |
|-------------|--------|
| `ReferenceHandling=None` + `UseStringInterning=None` | **No scan pass** — fastest path, single-phase serialization |
| `ReferenceHandling=All` + `UseMetadata=true` | Uses `ObjectWithMetadataRefFirst(71)` marker — combined ref + metadata |
| `UseMetadata=false` + `UseGeneratedCode=true` | `IsDirectObjectWrite=true` — generated code inlines property writes, bypasses `WriteObject` |
| `UseMetadata=true` + `PropertyFilter` set | Filter invoked twice (metadata phase + write phase); filter results must be stable |
| `WireMode=Fast` + `UseStringInterning!=None` | Interned strings still use the fast string path (UTF-16 for first occurrence, VarUInt index for subsequent) |
| `UseCompression!=None` + any other option | Compression is orthogonal — applied post-serialization, inner format unchanged |

View File

@ -24,11 +24,11 @@
## SignalR Conventions
See [`SIGNALR.md`](SIGNALR.md) for full architecture documentation.
See [`AyCode.Services/docs/SIGNALR.md`](../AyCode.Services/docs/SIGNALR.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(getAllTag, getItemTag, addTag, updateTag, removeTag)` with 5 independent tag integers. Tags must be unique across the system. See [`SIGNALR_DATASOURCE.md`](SIGNALR_DATASOURCE.md).
- **CRUD bundles** — entities use `SignalRCrudTags(getAllTag, getItemTag, addTag, updateTag, removeTag)` with 5 independent tag integers. Tags must be unique across the system. See [`AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md`](../AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md).
- **Binary protocol**`AcBinaryHubProtocol` is the transport protocol. Responses use pure Binary serialization.
### ⚠️ Temporary: JSON-in-Binary Request Parameters

View File

@ -19,11 +19,11 @@ Core terminology for the AyCode framework. Read this before working on unfamilia
| **Toon** | Token-Oriented Object Notation. LLM-optimized format with @meta (schema) and @data (values) sections. Designed for maximum LLM comprehension accuracy. |
| **AcJson** | Newtonsoft.Json wrapper with $id/$ref reference handling, IId-based resolution, and chain deserialization API. |
| **Chain API** | Fluent deserialization: `CreateDeserializeChain<T>().ThenDeserialize<U>()...Execute()`. Resolves cross-references across multiple types. |
| **String Interning** | Binary serializer deduplicates repeated strings. Controlled via `[AcStringIntern]` attribute or `StringInterningMode`. See [BINARY_FORMAT.md](BINARY_FORMAT.md#string-interning-protocol). |
| **String Interning** | Binary serializer deduplicates repeated strings. Controlled via `[AcStringIntern]` attribute or `StringInterningMode`. See [`AyCode.Core/docs/BINARY_FEATURES.md`](../AyCode.Core/docs/BINARY_FEATURES.md#string-interning-protocol). |
## Binary Wire Format
For full specification see [`BINARY_FORMAT.md`](BINARY_FORMAT.md).
For full specification see [`AyCode.Core/docs/BINARY_FORMAT.md`](../AyCode.Core/docs/BINARY_FORMAT.md).
| Term | Definition |
|---|---|
@ -66,14 +66,14 @@ For full specification see [`BINARY_FORMAT.md`](BINARY_FORMAT.md).
## SignalR Infrastructure
For full architecture see [`SIGNALR.md`](SIGNALR.md).
For full architecture see [`AyCode.Services/docs/SIGNALR.md`](../AyCode.Services/docs/SIGNALR.md).
| Term | Definition |
|---|---|
| **OnReceiveMessage** | The single SignalR method for all communication. Signature: `(int messageTag, byte[] messageBytes, int? requestId)`. |
| **Message Tag** | Integer identifier mapping to a method via `[SignalR(tag)]` or `[SignalRSendToClient(tag)]` attributes. |
| **DynamicMethodRegistry** | Resolves message tags to `MethodInfo` at runtime. Static `ConcurrentDictionary` cache with lazy scan on miss. |
| **SignalRCrudTags** | Sealed class bundling 5 independent tag integers (getAllTag, getItemTag, addTag, updateTag, removeTag) for entity CRUD. See [`SIGNALR_DATASOURCE.md`](SIGNALR_DATASOURCE.md). |
| **SignalRCrudTags** | Sealed class bundling 5 independent tag integers (getAllTag, getItemTag, addTag, updateTag, removeTag) for entity CRUD. See [`AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md`](../AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md). |
| **AcBinaryHubProtocol** | Custom `IHubProtocol` replacing SignalR's JSON+Base64 with `AcBinarySerializer`. Protocol name: `"acbinary"`. |
| **SignalResponseDataMessage** | Response message supporting Binary or JSON+GZip. Responses use pure Binary (no JSON overhead). |
| **SignalPostJsonDataMessage** | ⚠️ TECH DEBT — request params serialized to JSON inside Binary envelope. Planned for pure Binary replacement. |
@ -88,7 +88,20 @@ For full architecture see [`SIGNALR.md`](SIGNALR.md).
## Logging
For full architecture see [`AyCode.Core/docs/LOGGING.md`](../AyCode.Core/docs/LOGGING.md).
| Term | Definition |
|---|---|
| **AcLoggerBase** | Custom logger with Detail(0)→Debug(1)→Info(2)→Warning(3)→Suggest(4)→Error(5)→Disabled(255). Values are DB-synced — do not renumber. |
| **GlobalLogger** | Server-side singleton (`AcGlobalLogger`) for cross-service logging. |
| **LogLevel** | Byte enum: Detail(0), Trace(5), Debug(10), Info(15), Suggest(17), Warning(20), Error(25), Disabled(255). Values are DB-synced — do NOT renumber. |
| **AcLoggerBase** | Abstract logger with multi-writer fan-out. Two-level filtering: logger-level + per-writer level. Implements both `IAcLogWriterBase` and MS `ILogger`. |
| **AcLogWriterBase** | Abstract writer base. Own `LogLevel` loaded from appsettings by `AssemblyQualifiedName` match. Two branches: text (`AcTextLogWriterBase`) and structured (`AcLogItemWriterBase<T>`). |
| **AcTextLogWriterBase** | Text formatting base. Format: `[HH:mm:ss.fff] [AppType[0]] [Level] [Category->Method] [ThreadId] Text`. |
| **AcConsoleLogWriter** | Colored console writer. Gray=≤Trace, Cyan=Suggest, Yellow=Warning, Red=≥Error. Thread-safe via static lock. |
| **AcLogItemWriterBase\<T\>** | Abstract structured writer. ThreadPool dispatch + Mutex serialization. `Activator.CreateInstance<T>()` factory for log items. |
| **GlobalLogger** | Server-side singleton wrapping `AcGlobalLoggerBase`. Static methods for all log levels. Default category: `"GLOBAL_LOGGER"`. |
| **AcLoggerProvider\<T\>** | `ILoggerProvider` implementation with `ConcurrentDictionary<string, T>` per-category cache. Registered via `AddAcLogger<T>()` or `UseOnlyAcLogger<T>()`. |
| **IAcLogItemClient** | Structured log item DTO interface. Fields: TimeStampUtc, AppType, LogLevel, ThreadId, CategoryName, CallerName, Text, Exception, ErrorType. |
| **AcLogItemClient** | `[MessagePackObject]` implementation of `IAcLogItemClient`. Client-side transport entity. |
| **AcLogItem** | Server-side entity extending `AcLogItemClient`. `[Table("LogItem")]` with auto-generated `Id` and `LogHeaderId`. |
| **AcLoggerSignalRHub\<T\>** | Server hub receiving log items via `AddLogItem(AcLogItem)`. Simple `Hub` (not tag-based dispatch). |
| **IAcLogWriterClientBase** | Empty marker interface for client-side writers (`: IAcLogWriterBase`). |