13 KiB
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, throwsInvalidOperationExceptionon circular referencefalse+ ref handling enabled: only IId types tracked for deduplication, ⚠️ non-IId circular refs silently truncated atMaxDepth— seeBINARY_ISSUES.md#accore-bin-i-j6t9.
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 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.
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 toReferenceHandling=AllFastMode/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 |