# ADR 0001: AcBinary projection serialization (TSource → TTarget wire schema)

> ⚠️ **DRAFT — not yet finalized.** This ADR captures architectural intent. Status remains `Proposed` until phase 1 implementation produces benchmarks validating the performance hypothesis.

## Status

Proposed (2026-04-28)

## Context

AcBinary today serializes a runtime instance using **its own runtime type** as the wire-schema authority. To produce DTO-shaped wire output (e.g., for SignalR API responses), the consuming code must currently:

1. Map `Entity → DTO` via a hand-written mapper (this codebase does NOT use AutoMapper, Mapster, or any external mapping library)
2. Serialize the resulting `DTO` instance via AcBinary

Friction observed:

- **Boilerplate**: every `Entity ↔ DTO` pair requires a hand-written mapper method, scattered across the codebase
- **Allocation pressure**: each call materializes the intermediate DTO instance + nested DTO graph (often dozens of objects per request in high-throughput SignalR scenarios)
- **Two property-iterations per call**: mapper reads Entity → writes DTO; serializer reads DTO → writes wire — duplicate work over the same data

The HybridPack rebrand timeline makes API-stability decisions urgent before the public NuGet release: shipping a non-projection API and adding projection later is a breaking change.

**In scope:**
- Phase 1: runtime projection path in the binary serializer
- Phase 2/3: SGen-projection per type-pair (architectural framing only — this ADR does not commit to phase 2/3 implementation timing)
- Property name-match cases (the 80% common case)
- API surface design (`ToBinaryAs<TTarget>` or equivalent)

**Out of scope:**
- Toon projection serialization (separate future ADR if and when needed)
- Structural divergence handling (e.g., `Order.Address: Address` → `OrderDto.AddressLine: string` flatten) — phase 1 covers only name-match; divergent cases need explicit config addressed in a follow-up
- Deserializer-side projection (TSource read into TTarget without mapper) — separate concern, not part of this ADR
- Cross-language wire-format negotiation

## Decision

Adopt **direct source→target projection serialization** in AcBinary, exposed as `entity.ToBinaryAs<TTarget>()` (and equivalents on the explicit `AcBinarySerializer` API surface).

Implementation strategy is **phased**:

- **Phase 1** — Runtime projection path. New `BinarySerializationProjectionContext<TSource, TTarget>` drives the wire write loop using `TTarget`'s metadata as the schema authority and `TSource`'s compiled-expression property accessors as the data source. Hash-based property matching reuses the existing `UseMetadata` infrastructure.
- **Phase 2** — Source-generated per-pair projection writers, opt-in for hot type-pairs. Distinct registry from the existing per-type SGen writers.
- **Phase 3** — Generalization (broader API ergonomics, divergence-config support, possible Toon equivalent) only if phases 1-2 demonstrate measurable production benefit.

Phase 1 internally forces `UseGeneratedCode=false` regardless of caller options, because the existing per-type SGen writers (`Unsafe.As<T>` cast) would corrupt the wire output if reused for projection.

## Consequences

**Positive:**

- Eliminates manual mapper boilerplate for name-match cases (estimated 80%+ of Entity↔DTO pairs in this codebase)
- Removes intermediate DTO allocation per call — measurable GC pressure reduction in SignalR high-throughput scenarios
- One property-iteration instead of two — net CPU savings even before Phase 2 SGen optimization
- Builds on AcBinary runtime path which benchmarks already faster than MessagePack contract-based SGen — projection runtime path expected to preserve this advantage end-to-end. AcBinary SGen path benchmarks faster than MemoryPack
- Genuine USP for the HybridPack brand: no mainstream .NET serializer (System.Text.Json, MessagePack, MemoryPack, Newtonsoft.Json, Mapster) offers direct cross-type wire-schema serialization without intermediate materialization
- Reuses existing `UseMetadata` hash-matching infrastructure — minimal new surface area in phase 1

**Negative:**

- New context type (`BinarySerializationProjectionContext<TSource, TTarget>`) adds API surface, even if internal implementation
- Phase 2 SGen path implies N×M generator pairs (one per `(TSource, TTarget)` combination registered) — opt-in per-pair to bound the explosion
- Structural-divergence cases (renamed properties, flattening) need explicit config — phase 1 only handles name-match, leaving 20%-ish of real-world cases requiring follow-up work
- `UseGeneratedCode=false` enforced internally in projection mode — documented behavioural surprise for users who set it globally to `true`
- Phase 1 projection-context overhead vs non-projection path is small but non-zero (extra hash-matching at type-pair init) — irrelevant for end-to-end workflow but visible in microbenchmarks

**Follow-ups required:**

- `BINARY_TODO.md` — phase 1 implementation entry (`ACCORE-BIN-T-Y6R2`)
- `BINARY_TODO.md` — phase 2 SGen-projection path entry (deferred, opens only after phase 1 production validation)
- Benchmark suite addition — projection runtime vs `mapper+MessagePack-SGen` baseline (phase 1 acceptance criterion)
- API-shape finalization decision (`ToBinaryAs<T>()` vs `SerializeAs<T>()` vs builder-style — pending phase 1 prototyping)
- `BINARY_FEATURES.md` — new "DTO projection" section once phase 1 ships
- Documentation cross-link from `BINARY/README.md` → ADR

## Alternatives considered

- **Status quo: manual mapper + AcBinary serialize** (rejected): preserves boilerplate, allocation overhead, and 2-iteration cost. Current source of the friction this ADR addresses.

- **External mapping library (Mapster / AutoMapper)** (rejected): would require adopting a new dependency and learning its conventions. The codebase has not used external mappers historically; bringing one in to solve this specific problem is disproportionate. Mapster's source-gen path would still materialize the intermediate DTO instance — does not address the allocation-pressure and 2-iteration concerns.
  - *Cost:* medium — adoption + config + new dependency
  - *Future flexibility:* locks team into the external library's mapping conventions

- **LINQ-style projection lambda** (`Serializer.Serialize(order, src => new OrderDto { Id = src.Id, ... })`) (rejected): cleaner syntax than scattered mappers, but the lambda still materializes the DTO inline — same allocation and 2-iteration cost as the status quo. Solves the boilerplate dispersal but not the performance concerns.

## Related

- Related TODOs: `ACCORE-BIN-T-Y6R2` (phase 1 implementation, this ADR's primary follow-up), `ACCORE-BIN-T-Z3K8` (`IId<T>` migration — sibling refactor, both required before NuGet release), `ACCORE-BIN-T-N7V1` (`[JsonIgnore]` replacement — sibling brand-decoupling work)
- Related ADRs: none yet (first ADR in `AyCode.Core/docs/adr/`)
- Related discussions: HybridPack rebrand discussion (this design session) established the brand context for projection serialization as a flagship feature
- External references: none (no public benchmarks linked yet — will be added as part of phase 1 acceptance)
