# Why AcBinary?

Architectural framing: where AcBinary fits in the serializer landscape, what value it adds over wire-only serializers, and how to read its benchmark numbers in that context.

> Companion docs: features detail in [`BINARY_FEATURES.md`](BINARY_FEATURES.md) · wire format in [`BINARY_FORMAT.md`](BINARY_FORMAT.md) · options/presets in [`BINARY_OPTIONS.md`](BINARY_OPTIONS.md) · streaming I/O in [`BINARY_ASYNCPIPE_ISSUES.md`](BINARY_ASYNCPIPE_ISSUES.md) + [`BINARY_ASYNCPIPE_TODO.md`](BINARY_ASYNCPIPE_TODO.md).

## Category

AcBinary serves a **different category** than wire-only serializers (Protobuf, MessagePack, MemoryPack). It is a **graph-aware** serializer with referential integrity preserved on the wire via IdTracking + StringInterning, and on the receive side via a populate-merge deserialization path that reuses existing reference identity. The primary value is **correctness and developer-ergonomics in stateful live-data scenarios** (Blazor / MAUI / WPF binding, SignalR streaming, server-push reconciliation) — **not** raw single-shot throughput on flat DTO RPC.

## Three-pillar value proposition

### 1. Graph integrity → populate-merge correctness in bound UIs

On a live-data UI, an incoming server update must merge into the in-memory graph **without** breaking binding references, child-list identity, or duplicating shared sub-objects (`Product`, `Customer`, `Partner`, etc. — entities referenced from many parent rows). Wire-only serializers always reconstruct a fresh tree per call → the consumer hand-codes the merge / rebinding logic, which is fragile and verbose. AcBinary's `IId`-keyed reference tracking preserves identity end-to-end: shared sub-objects deduplicate on the wire AND on the client, existing references survive the round-trip, bindings stay valid, change-tracking continues uninterrupted. This is the **central value** for Blazor components, `IList<T>`-bound MAUI grids, and SignalR datasource subscribers.

### 2. Bandwidth → reference + string deduplication on the wire

On real production graphs (many `Order → OrderItem → Product → Customer → Partner → GenericAttribute` references sharing the same back-end entities), IdTracking and StringInterning emit each unique object/string **once** as the full body, then 1-2 bytes per subsequent reference. Wire-only serializers re-emit the full object body every time. Bench numbers (at **20% `IId`-ref-rate** in the test fixture):

- **Latin1Long charset**: -6.9% arith / -7.4% geo wire size vs MemoryPack
- **Latin1FixAscii charset**: -18.2% arith / -21.3% geo wire size vs MemoryPack

Production graphs with higher ref-rate (the typical case — many rows pointing at the same `Product`/`Customer`) see significantly larger savings; see [How to read AcBinary benchmarks](#how-to-read-acbinary-benchmarks) below.

### 3. Streaming → memory pressure mitigation on memory-constrained hosts

`AsyncPipeReaderInput` + `AsyncPipeWriterOutput` deliver chunked I/O with **peak memory ≈ chunk-size**, not full payload size. On WASM (Mono / NativeAOT-LLVM), a 10 MB monolithic `byte[]` allocation triggers GC pressure or OOM under concurrent task load; chunked delivery keeps the working set bounded. Combined with the wire-size win, decode CPU drops proportionally — **fewer bytes on the wire → fewer varint + UTF-8 decode operations** during deserialization. SignalR's `AcBinaryHubProtocol` consumes via `AsyncPipeReaderInput` directly, no monolithic buffer materialization.

## Real-world reference: WASM SignalR receive

Measured production payload: ~10 MB / ~7900 orders (full graph `Order → OrderItem → Product → Customer → Partner → GenericAttribute`), 4 SignalR messages, Blazor WASM client.

- **Before AcBinary (MemoryPack baseline)**: ~8 seconds Deser for ~4K records / ~20 MB payload — the MemoryPack output was nearly 2× larger because the same `Product`/`Customer` entities re-serialized per Order. JSON baseline was ~30 seconds.
- **After AcBinary**: ~470 ms Deser for ~8K records / ~10 MB payload — wire compaction from graph deduplication + chunked-pipe streaming + decode-CPU reduction stack multiplicatively.

The speed-up is **not** primarily "AcBinary is faster than MemoryPack on the same bytes." It is **"AcBinary emits ~50% fewer bytes for the same graph, then decodes them with bounded memory pressure"**. The feature stack is the win — single-cell bench Ser/Deser ratios alone do not capture it.

## Alternative usage: shallow serialization for delta updates

The three pillars above describe AcBinary's **deep, graph-aware** mode (the default — entire object graph round-trips with identity preserved). The same serializer also covers the **opposite** scenario via explicit opt-in: deliberately shallow serialization where only the root's flat scalar columns ship and nested sub-graphs become `Null` markers on the wire.

### Use case

Client-side grid / form edits → server endpoint accepts a flat-shape entity to UPDATE a single DB row. The full nested graph (`Items`, `Owner`, `Tags`, ...) is wasted bandwidth — the server already has those, and the contract is "update only the columns I touched". Traditional wire-only serializers force consumers to either maintain a separate Edit-DTO type per editable entity (boilerplate explosion) or null nested fields manually before serialize (fragile, error-prone, easy to miss a new property added later).

### How it works

Use the `AcBinarySerializerOptions.FlatCopy` preset:

```csharp
var options = AcBinarySerializerOptions.FlatCopy;
// MaxDepth = 0                              → only the root, no nested levels
// MaxDepthBehavior = Truncate               → nested complex props become Null markers
// UseStringInterning = None                 → no scan-phase overhead
// ReferenceHandling = None                  → no ref tracking (single-shot flat write)
```

Or hand-tune for the "root + one level" case:

```csharp
var options = new AcBinarySerializerOptions
{
    MaxDepth = 1,                                      // root + first level
    MaxDepthBehavior = MaxDepthBehavior.Truncate,
};
```

The deserializer reconstructs the root's scalar columns; nested complex properties remain at their CLR default. The `Null` at the truncation boundary is the **developer's contract** — the receiving endpoint defines what "nested null" means in its protocol (typically "no change at this level, keep server state untouched"). Works with any persistence layer (Dapper, ADO.NET, Cosmos DB, MongoDB, Redis, EF Core, etc.) — no requirement for the full graph to perform a single-row UPDATE.

### Why it matters

For data-grid / master-detail / inline-edit UI flows this is a frequent and performance-critical pattern: dozens of edits per minute × many concurrent clients × small per-update wire-size compounds at scale. **One entity model serves both the deep-read AND shallow-write paths** — no separate DTO hierarchy, no manual null-out boilerplate, no risk of drift between read-DTO and write-DTO schemas. The default `MaxDepthBehavior = Throw` prevents unintentional truncation from sneaking through; `Truncate` is explicit opt-in per call (or per endpoint via a configured options instance).

## When AcBinary fits

- Live-data UIs requiring graph-merge into a bound client-side model (Blazor / MAUI / WPF / WinForms with two-way binding, SignalR datasource subscribers)
- SignalR / WebSocket / IPC transports with repeated entity references across messages
- Server-push reconciliation flows where ID-keyed identity must survive the wire round-trip
- Memory-bounded clients (WASM, mobile, embedded) receiving large object graphs
- Delta-update / partial-write flows (client grid → server single-row UPDATE) — see [Shallow serialization](#alternative-usage-shallow-serialization-for-delta-updates) above
- Workloads where **wire-size + decode-CPU + memory-peak** matter together, not just single-message raw Ser throughput

## When AcBinary is NOT the right fit

- Single-shot DTO RPC with no entity reuse (e.g., `(int, int) → int` calculator endpoints) — wire-only serializers have less per-message overhead on flat payloads
- Schema-evolving public contracts where wire-format stability across language ecosystems is the primary requirement → Protobuf is the established choice
- Cross-platform clients on big-endian hosts → currently unsupported on the wire (see [`BINARY_ISSUES.md#accore-bin-i-e4n9`](BINARY_ISSUES.md#accore-bin-i-e4n9-wire-format-is-host-native-endian-not-canonical-little-endian))
- Append-only log formats where each record is independent — IdTracking's scan-phase overhead is wasted when there are no shared references to deduplicate

## How to read AcBinary benchmarks

`Console.FullBenchmark` test cells use **20% `IId`-ref-rate** test fixtures — flat enough to compare against wire-only serializers like MemoryPack on the same data shape. The cells **deliberately undermeasure** the feature wins:

- Production graphs typically have 50-90% `IId`-ref-rate (many rows → same `Product` / `Customer`); IdTracking's wire-size win on those graphs is **multiples larger** than the bench numbers show.
- `Repeated Strings` is the only bench cell stress-testing StringInterning. Other cells have low string-repetition, so the interning win there is bounded by the fixture.
- The bench does NOT cover the `AsyncPipe` path (only `Byte[]` and `BufferWriter` I/O modes). The streaming-memory advantage doesn't surface in the bench numbers.

Bench wins on the current fixture (-6 to -10% RT, -7 to -18% size) should be read as a **lower bound** for AcBinary's production advantage on graph-shaped, ref-heavy workloads. The matching position against MemoryPack on flat-payload Ser/Deser is **intentional** — AcBinary should not pay a tax for its features on workloads that don't benefit from them. The cases where the features pay back are workloads the bench does not measure directly; the WASM reference numbers above are the production-scale signal.
