# AcBinary Configuration

Configuration options, presets, and option interactions for `AcBinarySerializerOptions`. Wire format: `BINARY_FORMAT.md` | Features: `BINARY_FEATURES.md` | Internal architecture: `BINARY_IMPLEMENTATION.md`.

## WireMode

| Value | Integers | Strings | Output size | Speed |
|-------|----------|---------|-------------|-------|
| `Compact` (default) | VarInt/VarUInt (1–5 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:** Multi-referenced objects written once with `ObjectRefFirst(70) + VarUInt(refCacheIndex)` on first encounter, then `ObjectRef(65) + VarUInt(refCacheIndex)` subsequently. Header `HasCacheCount` flag set; 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` — see [`BINARY_ISSUES.md#accore-bin-i-j6t9`](BINARY_ISSUES.md#accore-bin-i-j6t9-non-iid-circular-references-silently-truncated-when-throwoncircularreferencefalse).

## 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 on FNV-1a hash collision between property names of the same type. ⚠️ Disabling trades correctness for speed — see [`BINARY_ISSUES.md#accore-bin-i-c5r7`](BINARY_ISSUES.md#accore-bin-i-c5r7-checkduplicatepropnamefalse-silently-corrupts-on-fnv-1a-hash-collision) before turning off.

## 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), `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]` (1–2 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 + MaxDepthBehavior

`MaxDepth` (`byte`, default `255`) is the recursion depth limit. `MaxDepthBehavior` (enum, default `Throw`) decides what happens at the limit.

| `MaxDepth` value | Behavior |
|---|---|
| `255` (default) | Effectively unlimited nesting — safety net only triggers on pathological graphs |
| `N` (1..254) | Action at depth N decided by `MaxDepthBehavior` |
| `0` | First nested object hits the limit immediately — useful with `MaxDepthBehavior.Truncate` for the "root + null nested" shallow-copy semantic |

| `MaxDepthBehavior` | Default | Action at depth limit | Use case |
|---|---|---|---|
| `Throw` | ✓ default | Throws `InvalidOperationException` with type name + position | Cycle detection, bug surfacing (fail-fast) |
| `Truncate` | | Writes `Null(76)` marker in place of the object | Intentional shallow serialization (e.g. client→server delta updates where deep references are intentionally cut) |
| `Disable` | | Skips the depth check entirely | Maximum hot-path perf; consumer guarantees a cycle-free graph |

**Only `MaxDepthBehavior.Disable` opts out the depth check entirely.** `Throw` stays active even with `ReferenceHandling = All`, because per-type `[AcBinarySerializable(EnableRefHandlingFeature = false)]` attribute opt-outs leave non-tracked types in the graph — those can still form cycles that ref-handling alone cannot catch. The safety net covers per-type-opt-out cycle gaps + pathological-depth non-cyclic graphs.

**Truncate mode — the key shallow-serialization feature.** Depth-exceeded values appear as `Null(76)` on the wire. This is intentional and developer-controlled: the consumer endpoint contract decides what "null nested collection" means in context — typically "no change at this level" on a partial-update endpoint (`POST /orders/{id}/header` flat update) vs a separate full-save endpoint (`POST /orders/{id}/full`).

This pattern is significantly cheaper than the alternatives (DTO-mapping per entity, EF-Core `.Select()` projections, manual flat-DTOs) — zero setup-overhead, no DTO type-system duplication, and the unused sub-graphs are never serialized to wire (zero byte + CPU savings). The same entity type lives on client + server; `MaxDepthBehavior=Truncate` is the toggle. Works with any persistence layer (Dapper, ADO.NET raw SQL, Cosmos DB, MongoDB, Redis, etc.) — not EF-Core-specific.

The default `Throw` surfaces accidental depth-exceeded cases as a debuggable exception (offending type comes from the stack trace) so unintentional truncation can't sneak through — closes `BINARY_ISSUES.md#accore-bin-i-p2h8`.

**Code branch:** Depth check fires BEFORE any marker byte is written (in runtime `WriteObject` + SGen `WriteObjectFullMarker*` / direct-marker emit paths). On limit hit, `TryEnterRecursion(hasTruncatePath)` on the context (combined check + inc) dispatches `Throw` / `Truncate` / `Disable`. `RecursionDepth` is a private `byte` field on the context; `ExitRecursion` is the inlined helper that decrements at body exit. The `_needsDepthCheck` gate is pre-computed at `Reset()` as `MaxDepthBehavior != Disable`. Hot path: 1 ctx field-read + 1 byte-cmp + 1 inc per object-recursion entry (Truncate use case can be combined with any `ReferenceHandling` mode).

**Known issue:** SGen path + `Truncate` currently exhibits a wire-misalignment in specific test scenarios (`DECIMAL_DRIFT` on round-trip) — see `BINARY_ISSUES.md#accore-bin-i-sg-truncate-pending` (TBD). Runtime path + `Truncate` and SGen path + `Throw`/`Disable` are unaffected.

## 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 context produces the raw buffer: `if (UseCompression != None) Lz4.Compress(buffer, mode)`. Decompression auto 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, otherwise positional drift causes silent corruption — see [`BINARY_ISSUES.md#accore-bin-i-w3f4`](BINARY_ISSUES.md#accore-bin-i-w3f4-propertyfilter--usemetadatafalse-silently-corrupts-via-index-drift).

**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. Enables SGen root fast path (see `BINARY_SGEN.md`) |
| `InitialBufferCapacity` | int | 16384 | 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 | MaxDepthBehavior | Compression | Other |
|--------|----------|----------|-----------------|-------------|----------|------------------|-------------|-------|
| `Default` | Compact | false | Attribute | All | 255 | Throw | None | — |
| `FastMode` | Compact | false | None | None | 255 | Throw | None | No scan pass |
| `FlatCopy` | Compact | false | None | None | **0** | **Truncate** ⚠️ | None | Root + Null nested (the Truncate behavior makes this preset's semantic meaningful — under default `Throw` it would throw on first nested object) |
| `WasmOptimized` | Compact | false | Attribute | All | 255 | Throw | None | +StringCaching |
| `WithoutReferenceHandling` | Compact | false | Attribute | **None** | 255 | Throw | None | No scan pass |
| `WithoutMetadata` | Compact | **false** | Attribute | All | 255 | Throw | None | — |

**Performance implication of presets:**
- `Default` / `WasmOptimized` — two-phase (scan + serialize) due to `ReferenceHandling=All`
- `FastMode` / `FlatCopy` — 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 affecting code branches:

| 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 |
