# AsyncPipe — TODO

Streaming I/O layer for the binary serializer. Open / resolved issues this work targets are in
[`BINARY_ASYNCPIPE_ISSUES.md`](BINARY_ASYNCPIPE_ISSUES.md).

## Priority legend
- **P0** blocker · **P1** important · **P2** nice-to-have · **P3** idea

## ACCORE-BIN-T-D6H4: ~~Create `AsyncPipeReaderInput` class~~ (Step 1 of ADR-0003)
**Status:** Closed (2026-05-02) · **Priority:** ~~P1~~ · **Type:** ~~Refactor~~ · **Related ADR:** [`docs/adr/0003-acbinary-streaming-receive-architecture.md`](../../../docs/adr/0003-acbinary-streaming-receive-architecture.md) Step 1

~~Add new `sealed class AsyncPipeReaderInput : IBinaryInputBase, IDisposable` in `AyCode.Core/Serializers/Binaries/AsyncPipeReaderInput.cs`. Self-contained sliding-window buffer (`byte[]` + `_writePos` + `_readPos` + `_completed` + `ManualResetEventSlim`) with reset-to-0 cycling preserved verbatim from today's `SegmentBufferReader`. Producer API: `Feed(ReadOnlySpan<byte>)`, `Complete()`. Consumer API (IBinaryInputBase): `Initialize` / `TryAdvanceSegment` / `Release`.~~

### Resolution (2026-05-02)

Class shipped at `AyCode.Core/Serializers/Binaries/AsyncPipeReaderInput.cs` — sealed class implementing `IBinaryInputBase, IDisposable`. Producer API: `Feed(ReadOnlySpan<byte>)`, `Complete()`. Consumer API: `Initialize` / `TryAdvanceSegment` / `Release` / `MessageDone` (the latter added during the multi-message-reuse work, atomically resets buffer cursors at message boundaries). Framed-wire-format awareness via `multiMessage: true` ctor flag; raw-bytes pass-through via `multiMessage: false`. Class summary contains the "When chunked-streaming is the right fit" use-case catalogue (push-pattern only). Companion struct `AsyncPipeReaderInputAdapter` bridges to the deserializer's generic `TInput : struct, IBinaryInputBase` constraint with zero heap cost.

Tests in `AyCode.Core.Tests/Serialization/AcBinarySerializerPipeParallelTests.cs` cover `Feed` / `TryAdvanceSegment` / `Complete` / `Dispose` contracts including framing-state-machine, multi-chunk handoff, sliding-window cycle, and producer-consumer concurrency. Existing SignalR tests on the legacy `SegmentBufferReader` path continue to pass (no behavioral regression).

## ACCORE-BIN-T-M2K1: ~~Add `AsyncPipeReaderInput.DrainFromAsync` extension~~ (Step 2 of ADR-0003)
**Status:** Closed (2026-05-02) — moved to test-only assembly · **Priority:** ~~P1~~ · **Type:** ~~Feature~~ · **Related ADR:** [`docs/adr/0003-acbinary-streaming-receive-architecture.md`](../../../docs/adr/0003-acbinary-streaming-receive-architecture.md) Step 2 · **Depends on:** `ACCORE-BIN-T-D6H4`

~~Add `public static async Task DrainFromAsync(this AsyncPipeReaderInput input, PipeReader reader, CancellationToken ct)` in `AyCode.Core/Serializers/Binaries/AsyncPipeReaderInputExtensions.cs` (NEW file).~~

### Resolution (2026-05-02)

The drain-extension was originally added as a public framework helper, paired with the public `AcBinaryDeserializer.DeserializeFromPipeReaderAsync<T>` method. Subsequent design review concluded that **both** are unnecessary on the public NuGet API surface:

- **Real production consumers** always have their own reader-task that owns the pipe-lifecycle and threading model (SignalR → Kestrel; NamedPipe IPC → consumer's connection loop; FileStream → consumer's read loop). They naturally push bytes via `AsyncPipeReaderInput.Feed` from their own task.
- **A framework-provided `DrainFromAsync` / `DeserializeFromPipeReaderAsync`** would either duplicate that responsibility or impose a specific threading model on consumers. Either is bad NuGet API design.

**Resolution actions:**
- `AsyncPipeReaderInputExtensions` (containing `DrainFromAsync`) **moved** from `AyCode.Core/Serializers/Binaries/` → `AyCode.Core.Tests/Serialization/` (namespace `AyCode.Core.Tests.Serialization`). Test-only assembly; the streaming benchmark project references the Tests project to access it.
- `AcBinaryDeserializer.DeserializeFromPipeReaderAsync<T>` overloads **removed** from public API. Tests that previously used it inline the canonical drain+deserialize pair (`Task.Run(() => Deserialize<T>(input))` + `await input.DrainFromAsync(reader)`) so the test assertion still exercises the same code paths.
- `AsyncPipeReaderInput` and `AcBinarySerializer` doc-comments updated to remove references to `DrainFromAsync` and `DeserializeFromPipeReaderAsync` from the public-facing pattern docs; the canonical recommended pattern is "consumer's reader-task pushes via Feed."

**Acceptance:** existing `AcBinarySerializerPipeParallelTests` and `AcBinarySerializerNamedPipeTests` continue to pass on the new test-only extension; benchmark (`Console.AsyncPipe` mode) continues to work.

## ACCORE-BIN-T-V7C9: ~~Replace misleading parallel test with real parallel pipeline test~~ (Step 3 of ADR-0003)
**Status:** Closed (2026-05-02) · **Priority:** ~~P1~~ · **Type:** ~~Test~~ · **Related ADR:** [`docs/adr/0003-acbinary-streaming-receive-architecture.md`](../../../docs/adr/0003-acbinary-streaming-receive-architecture.md) Step 3 · **Depends on:** `ACCORE-BIN-T-M2K1`

~~The current `AcBinarySerializerPipeParallelTests.cs` is misleading — it does not actually exercise serializer↔deserializer parallelism (single-threaded in practice). Rewrite to drive a producer thread (serializer) and a consumer thread (deserializer) through an in-memory `Pipe`, with `AsyncPipeReaderInput.DrainFromAsync` on the receive side. Measure ser+deser overlap and verify the ADR-0003 claimed ~1 µs / MB perf delta vs today's struct-based path.~~

### Resolution (2026-05-02)

`AcBinarySerializerPipeParallelTests.RealParallelPipeline_*` tests added: 3-task pipeline (serializer Task.Run → drain Task → deserializer Task.Run) running concurrently over an in-memory `Pipe`, exercised at both `BufferWriterChunkSize=256` (small payload, multi-chunk crossing) and `BufferWriterChunkSize=4096` + production-scale payload from `TestDataFactory.CreateLargeScaleBenchmarkOrder(rootItemCount: 100)` (~3700 nested objects, hundreds of KB → many chunks → real backpressure-driven streaming). Uses the test-only `AsyncPipeReaderInputExtensions.DrainFromAsync` (per `M2K1` resolution). Asserts payload structural equality on round-trip; no perf-delta assertion encoded — the dedicated benchmark suite (`Console.AsyncPipe` mode) covers performance regression detection. Tests pass on Windows; Linux CI not yet wired but no platform-specific code paths exist.

## ACCORE-BIN-T-R5K2: Multi-message reuse for AsyncPipeReaderInput
**Priority:** P3 · **Type:** Feature · **Related:** [`BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-q4t8`](BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-q4t8-asyncpipereaderinput-multi-message-reuse-not-supported)

Reuse a long-lived `AsyncPipeReaderInput` across multiple `Deserialize<T>(input, opts)` calls without setting up a fresh instance per message. Removes the per-message ArrayPool rent + `ManualResetEventSlim` allocation + two `Task.Run` calls that the canonical fresh-instance pattern requires today, opening a true zero-alloc-per-message path on long-lived raw IPC transports.

**Possible directions** (none fixed; pre-design):

- **`Initialize` emits `_readPos` as starting position** (instead of always 0), and the sliding-window reset becomes "anytime `_readPos > 0` after a `Deserialize` completes, reset both `_writePos` and `_readPos` to 0". Smallest API change, no public surface added. Caveat: requires the deserializer to call `TryAdvanceSegment` at least once during message read so `_readPos` reflects the consumed boundary — small fully-buffered messages currently skip it entirely.

- **New `SetReadCursor(int position)` / `AdvanceReadTo(int position)` method**: caller (deserializer or wrapper) reports the consumed offset after each `Deserialize`. Sliding-window reset triggers explicitly. Cleaner separation of concerns (consumer knows where it stopped), but adds a public API surface.

- **`ResetCompletion()` for framed mode**: orthogonal to the cursor design — needed for framed multi-message reuse where the `[202]` CHUNK_END marker currently makes `_completed = true` irreversible. Combine with whichever cursor design is chosen.

- **Consumer-thread atomic position-reset on message-done**: a `MessageDone()`-style method called from the consumer (typically the deserializer's finally-block) atomically resets BOTH `_writePos` AND `_readPos` to 0 on the calling thread BEFORE the next `Initialize` runs. Trade-off: relies on the calling thread being the consumer; multi-thread feed+deserialize patterns need different coordination.

**Acceptance** (for whichever direction is picked):
- New tests exercise `N` consecutive `Deserialize<T>(sharedInput, opts)` calls on the same instance, both raw and framed modes, with payload sizes both above and below the initial buffer capacity. All `N` results match their respective inputs (no buffer-position aliasing, no message-#1-duplicate-on-#2 regression).
- Existing single-message tests continue to pass.
- Wire format unchanged (consumer-side reader plumbing, not a wire-level change).
- Allocation profile of `N` consecutive reads on the shared input: **0 bytes per call after warmup**.

## ACCORE-BIN-T-V4Q9: Convenience API for the strictly-sequential single-thread pattern
**Priority:** P3 · **Type:** API · **Related:** [`BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-j2x7`](BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-j2x7-multi-pattern-documentation-gap-on-the-public-api-surface), [`BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-q4t8`](BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-q4t8-asyncpipereaderinput-multi-message-reuse-not-supported)

A higher-level wrapper for the typical single-thread sequential `Feed → Deserialize → reset` cycle. Possible directions (none committed; pre-design):

- **Delegate-based** API on the input: caller passes a deserializer lambda; the input encapsulates the lifecycle (`try` / `finally` + atomic reset). Stateful overload with `state`-parameter for zero-alloc on hot paths (avoids closure capture).
- **`IDisposable` scope object**: caller acquires a per-message scope; the scope's `Dispose` triggers reset.
- **Method-pair on the input**: explicit `BeginMessage` / `EndMessage` in the consumer's hands.

Trade-offs: delegate has runtime cost but is most ergonomic; scope-object adds one allocation per message but composes naturally with `using`; method-pair is most explicit but most error-prone (forget `EndMessage` → leak / corruption).

Implementation only after the related issues are documented and the multi-pattern catalogue exists (so the convenience API's scope is clear: it covers ONE pattern, not all).

## ACCORE-BIN-T-M7B3: Implicit session-end signal in `Dispose()`
**Priority:** P3 · **Type:** API · **Related:** [`BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-w6k4`](BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-w6k4-disposal-trap-idisposabledispose-does-not-guarantee-consumer-wakeup)

Possible directions to consider (none chosen):

- **Embed an idempotent session-end signal in `Dispose()`** before tearing down the wait-primitive and buffers. The plain `using` pattern would then suffice — explicit `Complete()` becomes optional. Trade-off: subtle behavioral shift; consumers relying on the "Dispose without prior Complete" path returning data may need to migrate.
- **Document the contract explicitly** without changing behavior: the public XML doc states the consumer-thread must be quiesced before disposal. Lowest-risk; relies on documentation.
- **Throw on `Dispose` if `Complete` was not called**: stricter contract, surfaces the trap as a clear error rather than `ObjectDisposedException` from the wait-primitive. Breaking change.

Pick a direction before implementing; each has different consumer-migration costs.

## ACCORE-BIN-T-K1F6: Public exposure of the wire-frame fixed overhead
**Priority:** P3 · **Type:** API

The wire-frame header byte-size (currently a private constant on `AsyncPipeWriterOutput`) is needed by consumers calculating transport-buffer sizing — kernel pipe buffer width on `NamedPipeServerStream`, `BufferedStream` wrapper size, etc. Possible directions:

- **Expose a public const** on `AsyncPipeWriterOutput` (e.g. `FrameHeaderSize`).
- **Add a static helper** that returns the chunk-on-wire size given a chunk-data size (or vice versa).
- **Document only**: state the framing overhead in the class XML doc and let consumers hard-code.

Smallest API-surface addition that solves the consumer-side calculation problem. Pick one direction before implementing.

## ACCORE-BIN-T-R8H4: Reset / reopen primitive for a stream after session-end
**Priority:** P3 · **Type:** Feature · **Related:** [`BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-d8l5`](BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-d8l5-session-end-is-irreversible-no-reset-reopen-primitive)

Possible directions for a failover / reconnect scenario (none committed):

- **`Reset()` method**: a one-method primitive that flips the internal session-end flag back to off and clears the buffer. Trade-off: thread-safety contract — must match the disposal contract; partially-completed framing-state must be explicitly reset.
- **Helper-factory wrapping the lifecycle**: a `using`-friendly factory that produces a fresh instance per session, hiding the irreversibility behind a higher-level primitive.
- **Document only**: state that fresh-instance allocation is the supported reconnect pattern; reuse is unsupported.

Each has different thread-safety implications and wire-formatting reentrancy requirements; explore before any code.

## ACCORE-BIN-T-S5N2: ~~Pattern catalogue in the public class summary~~
**Status:** Closed (2026-05-02) · **Priority:** ~~P3~~ · **Type:** ~~Documentation~~ · **Related:** [`BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-j2x7`](BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-j2x7-multi-pattern-documentation-gap-on-the-public-api-surface)

~~Original scope: extend the `AsyncPipeReaderInput` class summary with an explicit producer-consumer pattern catalogue (strictly-sequential single-thread, multi-thread feed+deserialize, push/event-driven, one-shot) with recommended API per pattern.~~

### Resolution (2026-05-02)

Both `AsyncPipeReaderInput` (reader-side) and `AsyncPipeWriterOutput` (writer-side) class summaries now contain a **"When chunked-streaming is the right fit"** catalogue covering the realistic transport scenarios — not just SignalR / NamedPipe-only. The list explicitly enumerates: network transports (TCP/UDP/WebSocket/SSE/HTTP/2), multi-connection servers (SignalR/gRPC/proprietary RPC), message brokers (Kafka/Redis Streams/Service Bus), file streaming (`FileStream`-backed `PipeReader`/`PipeWriter`), in-memory cross-thread `Pipe`, and custom transport adapters. The complementary "When raw `byte[]` is the right fit" section calls out the loopback-IPC / sub-LOH-message / no-GC-pressure case where chunked overhead is visible without commensurate benefit.

The single-class API surface reflects **push pattern only** (the dominant pattern across all listed use cases); the more granular threading-model patterns from the original S5N2 scope (single-thread vs multi-thread feed+deserialize) are now covered by the **explicit "consumer's reader-task drives the transport" framing** in the class summary — applications choose their threading model, the input class is agnostic.

The dedicated `BINARY/CHOOSING_API.md` follow-up (cross-cutting `byte[]` vs `IBufferWriter` vs `AsyncPipeReaderInput`/`AsyncPipeWriterOutput` decision tree as navigable doc-folder content) is **dropped** — the class XML doc-comment surface is sufficient for the IDE-driven discovery flow that NuGet consumers actually use. Re-open as a new TODO if a NuGet consumer reports a discoverability gap.

## ACCORE-BIN-T-G3W8: Transport-buffer alignment best-practice doc section
**Priority:** P3 · **Type:** Documentation · **Related:** [`BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-h4g2`](BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-h4g2-chunk-on-wire-size--chunksize--headersize-caused-page-fragmentation)

A documentation section covering the alignment of the user-space `chunkSize` and the transport-level kernel buffer (general principle, not transport-specific): page-aligned matching avoids fragmentation, mismatched sizes cause two kernel-page transfers per chunk. Reference benchmark archive(s) where empirical numbers exist.

## ACCORE-BIN-T-M2X7: Multi-stream multiplexer support (marker-byte mode distinction)
**Priority:** P3 · **Type:** Feature · **Related:** [`BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-m9p3`](BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-m9p3-no-native-chunk-multiplexing-for-concurrent-writers), [`BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-c4n7`](BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-c4n7-asyncpipereaderinput-single-consumer-constraint)

Enable N parallel logical streams over a single long-lived `Stream` (NamedPipe / NetworkStream / FileStream). Use cases: distributed logging fan-in (many threads → one pipe), telemetry sinks (many event sources → one collector), parallel-dispatch RPC over raw NamedPipe (multiple in-flight calls). NOT needed for SignalR — Kestrel multiplexes at the WebSocket / HTTP-2 layer above and the `AcBinaryHubProtocol` already receives a sequential byte stream.

**Wire-format design (consensus direction):**

Each chunk's first byte (the marker) self-identifies the mode — no session-handshake byte, no out-of-band config, no silent-corruption mismatch risk:

```
Single-mode (UNCHANGED — wire-format-compat with current SignalR protocol):
  [0xC9 = 201][UINT16 size][data]    ← chunk-data marker (current AsyncPipeWriterOutput.ChunkDataMarker)
  [0xCA = 202]                        ← chunk-end marker

Mux-mode (NEW — activates the currently-reserved [0xC8 = 200] byte):
  [0xC8 = 200][streamId][UINT16 size][data]   ← mux-chunk-data marker + 1-byte streamId
  [end-marker — design TBD, see "Open design questions" below]
```

The reader switches on the first byte: `0xC9` → single-mode parse, `0xC8` → mux-mode parse (read streamId, dispatch to per-stream child input). Mixed-mode chunks on the same stream are wire-format-legal (each chunk self-identifies) — useful for migration scenarios, not for typical use.

**Why this design over alternatives** (decisions captured for future maintainers):

- **Why not session-handshake byte at start of stream?** Self-describing chunks survive reconnect / partial-truncation / sender-restart without losing mode-context. The handshake-byte approach has a silent-corruption risk if a receiver misses the handshake (e.g. attaches mid-stream).
- **Why not bit-pack streamId+type+length into one byte?** Length-bit-packing (e.g. "full-chunk flag" omitting the size field) saves ~0.012% wire bytes on large messages but couples producer/receiver chunkSize, breaks debug-readability, and constrains future protocol extensions. ROI doesn't pay back. Length stays `[UINT16 size]` per chunk — robust, debuggable, future-proof.
- **Why `[0xC8]` for mux?** Already documented as reserved/tolerated for future stream-id extension (see `ACCORE-BIN-I-M9P3` cross-reference). Activating it is the natural use of the reserved code-point — no new wire-byte allocation needed.

**Implementation approach (consensus direction):**

Unified class with mode flag — the standard and mux modes share **one code path** distinguished by a `bool _isMux` (or equivalent stream-id container). A handful of branches at the marker-write / marker-parse points cover the difference (~1-2 ns per chunk, negligible vs the ~50–200 µs RT cost). This is preferred over a strategy-generic refactor (`AsyncPipeWriterCore<TFraming>`) because the few extra ns per chunk are invisible in production workloads, while the strategy-generic adds compile-time complexity (struct-strategy boilerplate, generic-constraint gymnastics) without commensurate runtime benefit at the typical 1k–1M ops/sec working ranges.

Per side:

- **Writer side**: `AsyncPipeWriterOutput` (struct) gains an opt-in `streamId` ctor parameter (default = "single-mode, no streamId byte emitted"). When set, the chunk-write loop emits `[0xC8][streamId]` instead of `[0xC9]`. The `IBinaryOutputBase` API surface is unchanged — `AcBinarySerializer` doesn't see any difference. JIT-elimination of the mode-branch is unnecessary because the cost is already negligible.

- **Reader side**: `AsyncPipeReaderInput` (sealed class) gains a multi-stream coordinator overload. Two possible shapes (pick at impl-time):
  - **Inline mode flag**: a `bool _muxMode` ctor flag controls whether the framing-state-machine reads a streamId byte after `[0xC8]`. Single-stream consumer uses one input, multi-stream consumer uses N child inputs accessed via `GetStream(streamId)` on a separate coordinator class.
  - **Coordinator + child instances**: a new `AsyncPipeMuxReaderInput` (sealed class) holds N child `AsyncPipeReaderInput` instances + 1 demux task; the existing core class is untouched. Cleaner separation, more allocations.

**Open design questions** (decide before implementing):

- **End-marker shape in mux-mode**: three candidates, pick one:
  - (a) New marker byte `[0xCB][streamId]` for mux-end (uses next reserved code point)
  - (b) Reuse `[0xCA]` with stream-id suffix in mux context: `[0xCA][streamId]` (parser knows from prior chunks that current frame is mux-mode)
  - (c) Per-chunk "is-final" flag in the size field high bit (eliminates explicit end marker — caps chunk size at 32 KB, currently 16 KB hardlimit so non-shrinking)
- **Stream-id space sizing**: 1-byte `[0..255]` covers most use cases; 2-byte if N > 256 streams expected. Affects per-chunk overhead by 1 vs 2 bytes. The 1-byte choice has a forward-compat path via reserving `0xFF` as "extended id follows".
- **Dispose / lifecycle propagation**: the mux-coordinator's `Dispose()` must tear down all child instances cleanly. Mirror of `ACCORE-BIN-I-W6K4` (disposal trap on the core class) at the coordinator level.
- **Reader-side asymmetry**: the writer is a struct (hot-path generic constraint), the reader is a sealed class. The mode-flag approach is symmetric for the writer (1 byte stream-id field next to existing fields) but on the reader the multi-stream case fundamentally needs N child instances + demux — composition wins there regardless of direction. Decide whether to use one unified class on both sides (writer easy, reader still needs a coordinator) or unified class on writer side + separate coordinator class on reader side.

**Acceptance:**
- Single-mode (standard) wire format byte-equivalent to today's output for any payload (regression test guards against accidental wire changes; SignalR protocol compat preserved).
- Concurrent multi-producer test: N producer threads simultaneously serialise into N stream-id-distinct writers; receiver demultiplexes and verifies each stream's graph independently. No interleaving corruption.
- Microbenchmark: single-stream sequential RT regresses by ≤ ~5 ns/op vs current (a few branch-predicted `if` per chunk; not strategy-generic-grade zero-overhead but production-invisible).
- Wire-format documentation updated in [`BINARY_FORMAT.md`](BINARY_FORMAT.md) with the `[0xC8]` mux-data activation and the chosen end-marker shape.

## ACCORE-BIN-T-A8R5: ASP.NET Core MVC formatters (`AcBinaryInputFormatter` / `AcBinaryOutputFormatter`)
**Priority:** P3 · **Type:** Feature (separate package) · **Related:** none yet

A separate NuGet package `AyCode.Core.AspNetCore` providing zero-friction integration of AcBinary serialization into ASP.NET Core MVC controllers. Single-line activation:
```csharp
builder.Services.AddControllers().AddAcBinaryFormatters();
```

Once registered, controllers using `[FromBody] T model` and `IActionResult` automatically use AcBinary when the client sends `Accept: application/x-acbinary` (content-negotiation handles the JSON-vs-AcBinary fallback transparently).

**Why a separate package**: `AyCode.Core` is layer 0 (no framework dependencies). ASP.NET Core integration would belong in a layer-1 consumer-aware package — typical pattern for serialization libraries that want to avoid forcing ASP.NET Core dependency on every consumer. Layer-correctness preserved.

**Why this specific feature matters**: turns the chunked-streaming infrastructure from a "library primitive that you have to wire up yourself" into a "production-grade REST API content type". The Pipe-streaming path becomes the natural production setup — `Response.BodyWriter` (PipeWriter) and `Request.BodyReader` (PipeReader) are native ASP.NET Core types, so the formatter can drive them directly with `SerializeChunkedFramed` / `AsyncPipeReaderInput.Feed` patterns. Per-connection peak memory bounded by chunk-size; LOH allocation pressure (≥ 85 KB messages) avoided — the chunked-streaming value-proposition surfaces automatically.

**Possible directions** (none fixed; pre-design):

- **InputFormatter shape**: `AcBinaryInputFormatter : InputFormatter` with `SupportedMediaTypes.Add("application/x-acbinary")`. The `ReadRequestBodyAsync` reads from `context.HttpContext.Request.BodyReader` (PipeReader) — either drains to byte[] for simple cases (size-bounded by `[Request].MaxAllowedSize`), or uses the `AsyncPipeReaderInput` + drain-task pattern for low-memory streaming on huge payloads. Decide which is the default; expose the streaming variant as opt-in.

- **OutputFormatter shape**: `AcBinaryOutputFormatter : OutputFormatter` writing to `context.HttpContext.Response.BodyWriter` (PipeWriter). The `WriteResponseBodyAsync` calls `AcBinarySerializer.SerializeChunked(value, pipeWriter, options)` (raw — single-message-per-request, no [201]/[202] framing needed). Optional `FlushPolicy.Coalesced` tuning batches flushes into ~64 KB windows for higher throughput, at the cost of owned-buffer fallback risk under heavy backpressure.

- **Wire-format choice**: raw chunked stream (`SerializeChunked`) is the natural fit for HTTP single-request-single-response. The multi-message framed variant (`SerializeChunkedFramed`) is over-engineered for REST — there's no concept of "next message on this stream" within a single HTTP request. Document this choice clearly.

- **Content-Type negotiation**: `application/x-acbinary` (vendor-specific MIME) vs `application/vnd.aycode.binary` (RFC-style vendor). Pick before publishing — once consumers integrate the MIME type into their `Accept` headers, changing it is a breaking change.

- **Options propagation**: the formatter ctor accepts `AcBinarySerializerOptions?` (nullable, defaults to `Default`). DI registration via `IOptions<AcBinarySerializerOptions>` is also supported. `AddAcBinaryFormatters(options => { options.UseGeneratedCode = true; ... })` builder-pattern overload for declarative configuration.

- **Action result wrapping**: optional `AcBinaryResult : ActionResult` for explicit Content-Type opt-out (when the action wants to force AcBinary even if the client didn't request it). Decide whether to ship.

- **`IAsyncEnumerable<T>` server-streaming**: gRPC-like server-streaming over a single HTTP response. The OutputFormatter can detect `IAsyncEnumerable<T>` and stream items as multi-message chunks (`SerializeChunkedFramed` per item). Big feature, ship in v1 vs v2 — decide.

**Acceptance:**
- New package `AyCode.Core.AspNetCore` published independently with its own README and integration tests (TestServer-based round-trip).
- `[FromBody]` deserialization works on a controller for any `[AcBinarySerializable]` model.
- Action returning a model serialises via AcBinary when `Accept: application/x-acbinary`.
- Falls back transparently to JSON (default ASP.NET formatter) when the client doesn't request AcBinary — coexistence with default formatters.
- README documents content-negotiation, MIME type, wire-format choice, options configuration, performance trade-offs vs default JSON.
- Memory-pressure benchmark (multi-connection load test): demonstrates the bounded-peak-memory property over JSON baseline.

**Sequencing**: implement AFTER `AyCode.Core` reaches its first stable NuGet release (1.0). The formatter package versioning follows `AyCode.Core` — breaking changes there propagate here. Don't ship the formatter package as part of v1.0 — let `AyCode.Core` stabilize first, then add this as a 1.1 / 2.0 optional extension.

## ACCORE-BIN-T-P3F8: Congestion-fallback buffer for the StreamPipeWriter path only
**Priority:** P2 · **Type:** Performance · **Status:** Investigation pending (implementation) · **Related:** `FlushPolicy.Coalesced`, `AsyncPipeWriterOutput.Grow` sequential path, `_serializeFlushAndAcquire`

**Scope — ONLY StreamPipeWriter.** This entry concerns **exclusively** the `PipeWriter.Create(Stream)` (StreamPipeWriter) target — NamedPipe / FileStream / NetworkStream / SslStream / etc. wrapped via the BCL StreamPipeWriter. **All other PipeWriter implementations (Pipe-based, Kestrel transport, custom IBufferWriter) keep their current behaviour unchanged.** The existing slab-pool path is correct and fast for them; touching it has no upside and risks regressions.

**Problem.** On the StreamPipeWriter path the writer auto-sets `_serializeFlushAndAcquire = true` because of the BCL `StreamPipeWriter._tailMemory` reset race: **`Advance` cannot be called while `_lastFlush.IsCompleted == false`**. Every chunk Grow therefore becomes synchronous (Commit → FlushAsync → wait → Acquire). Producer/flush parallelism is lost; per-chunk syscall overhead (kernel context switch + transport write) dominates.

**Established baseline (existing benchmarks).** The NamedPipe transport bench has both a single-shot variant (whole serialized `byte[]` written in one `Stream.WriteAsync`) and a chunked variant (per-chunk `Stream.WriteAsync` due to the sequential constraint). The single-shot variant beats the chunked variant by **at least 30%** wall-clock — driven entirely by the syscall-count difference (1 vs N kernel transitions per payload). That delta is the headroom this entry targets: by lifting the per-chunk synchronous wait, the chunked path can batch multiple chunks into fewer `Stream.WriteAsync` calls (Coalesced window of ~64 KB → ~15× fewer syscalls on a multi-MB payload), closing the gap to the single-shot baseline.

**Approach (sketch).** Keep the current PipeWriter slab-pool path as the default. **Only** when on the StreamPipeWriter target AND the next `Advance` would race with an in-flight flush, fall back to a temporary ArrayPool buffer where the serializer can keep writing. When `_lastFlush.IsCompleted` returns to true, drain that fallback buffer back into the StreamPipeWriter and resume the slab-pool path. This avoids the per-chunk synchronous wait and reduces `Stream.WriteAsync` syscalls.

**Risk: code-complexity blow-up.** Two coexisting buffer worlds in the hot path:
1. **PipeWriter's internal slab** (default, zero-copy)
2. **Owned ArrayPool buffer** (only StreamPipeWriter + congestion)

Both `CommitCurrentChunk` and `Grow` would need branchy semantics. The existing `_ownedBuffer` fallback (for rare non-array-backed memory cases) gives a foretaste of the indirection — promoting it to a hot-path-frequent congestion fallback would amplify the branching across CommitCurrentChunk / AcquireChunk / Flush / GetTotalPosition. **The user's concern: this could turn the writer into ágy-káosz / katyvas.**

**Investigation focus (implementation, not motivation).** The win is established by the single-shot vs chunked NamedPipe bench (≥30% headroom). Open questions are about the **shape of the fix**:
- Containing the fallback path behind a single `if (_isStreamPipeWriter && congested)` branch in 2-3 hot-path methods — vs amplified branching across the writer.
- Reusing the existing `_ownedBuffer` lazy-rent infrastructure (line 461-477) for the congestion case — vs introducing a separate fallback slot.
- Whether to pair this with a Coalesced-mode-only activation (StreamPipeWriter + Coalesced → fallback enabled; StreamPipeWriter + DoubleBuffered → still synchronous), reducing the mode matrix.

**Acceptance.**
- Stream-backed chunked path within ≤10% of the single-shot NamedPipe baseline on a multi-MB payload (currently ≥30% slower)
- `Stream.WriteAsync` syscall count reduced ~10× to ~15× vs current sequential path
- Round-trip tests pass on NamedPipe / FileStream / cross-process / TLS-wrapped streams
- Hot-path branching contained: ≤3 `if (_isStreamPipeWriter ...)` checks across CommitCurrentChunk / Grow / AcquireChunk
- BCL StreamPipeWriter `_tailMemory` race avoided — fallback buffer is owned, no race with the BCL internal state

## ACCORE-BIN-T-Z6N2: `AsyncPipeWriterOutput.Abort()` — partial-chunk padding for mid-stream serialize failure
**Priority:** P3 · **Type:** Robustness · **Related:** SignalR CHUNK_ABORT mechanism ([`SIGNALR_BINARY_PROTOCOL_TODO.md#accore-sbp-t-q3m7`](../../../AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/SIGNALR_BINARY_PROTOCOL_TODO.md#accore-sbp-t-q3m7))

The SignalR-layer CHUNK_ABORT mechanism (`[203]`) lets a mid-stream sender-serialize failure fault-isolate to the affected `requestId` instead of aborting the transport connection. **Edge case not yet covered**: if `AsyncPipeWriterOutput` fails after committing a `[201][UINT16=N]` chunk header but before writing all `N` data bytes, the receiver's `TryParseChunkData` is mid-chunk — the `[203]` byte the protocol layer emits on the host `PipeWriter` is parsed as part of that chunk's data, not as the abort marker. The receiver typically reaches the protocol-violation `InvalidDataException` path (connection aborts; baseline behaviour). The chunked-abort path degrades to baseline only in this specific timing window.

**Why this matters in practice:** rare. Serialize exceptions typically throw mid-data (a property getter dobs while writing field bytes), not mid-header (a 3-byte pre-data reservation, written and `Advance`d in microseconds). Not a correctness bug — the protocol-violation path is safe — but a graceful-fault-isolation gap for the unlucky timing.

**Approach (sketch).** New `AsyncPipeWriterOutput.Abort(byte abortMarker)` instance method:

1. If a `[201][UINT16=N]` chunk header was committed but `dataBytesWrittenSoFar < N`: zero-fill the remaining `N - dataBytesWrittenSoFar` bytes and `Advance` to close the open chunk cleanly. The receiver sees a complete chunk frame; the data tail is garbage but framing stays valid.
2. Emit `abortMarker` (`[203]` for the SignalR layer — kept parameterised so `AsyncPipeWriterOutput` stays SignalR-marker-agnostic).
3. Final `FlushAsync`.

The protocol-layer `WriteMessageChunked` catch then calls `output.Abort(MsgAsyncChunkAbort)` (via a new entry point on `AcBinarySerializer.Serialize` that exposes the in-flight output for finalization) instead of writing directly to the `PipeWriter`. Mid-header window now flows through the graceful abort path.

**Open design questions** (before implementing):
- **API surface for the protocol layer to reach the in-flight `AsyncPipeWriterOutput`**: the current `AcBinarySerializer.Serialize` overload allocates and discards the struct internally. Either (a) the serializer's own catch calls `Abort()` before propagating, (b) a try-with-cleanup-delegate Serialize overload, or (c) the protocol constructs its own `AsyncPipeWriterOutput` and drives the serialize itself. (c) is the biggest refactor — investigate cost.
- **Padding strategy**: zero-fill (natural choice) vs a documented sentinel byte. Zero-fill, document explicitly.
- **Cost on the common (no partial chunk) path**: a couple of field reads to check `dataBytesWrittenSoFar` vs `N`. Should be ≤ 5 ns vs inline `pipeWriter.GetSpan(1) + Advance + SyncFlush` — confirm by microbench.

**Acceptance:**
- New integration test produces the mid-header race deterministically (custom `IGeneratedBinaryWriter` that throws after the header is committed, before the first data byte) and asserts `TryParseChunkData` reaches the `[203]` CHUNK_ABORT branch (graceful), not the `InvalidDataException` branch.
- Existing CHUNK_ABORT mid-data tests pass (common case unchanged).
- Microbenchmark: `Abort()` adds ≤ 5 ns on the common path vs inline `pipeWriter.GetSpan(1) + Advance + SyncFlush`.
