78 lines
4.7 KiB
Markdown
78 lines
4.7 KiB
Markdown
# MVC — AcBinary formatters
|
|
|
|
> ⚠️ **TEMPORARILY DISABLED** — formatter sources block-commented (`/* ... */`) and `Microsoft.AspNetCore.App` FrameworkReference removed from `AyCode.Services.csproj` (downstream net10.0 Hybrid client conflict on `Microsoft.AspNetCore.Mvc` namespace). Re-enable when split into a separate NuGet package / solution. Description below documents the intended state.
|
|
|
|
ASP.NET Core MVC `InputFormatter` / `OutputFormatter` pair for the AcBinary wire format. Works in controller-based MVC and Minimal API on .NET 9+. The wire payload is the raw `byte[]` produced by `AcBinarySerializer.Serialize(value, opts)` — bit-compatible with the single-shot byte[] API; no MVC-specific envelope.
|
|
|
|
> **Code:** `AyCode.Services/Mvc/` (`AcBinaryInputFormatter`, `AcBinaryOutputFormatter`, `AcBinaryMvcBuilderExtensions`)
|
|
> **Binary serializer:** `../../../AyCode.Core/AyCode.Core/docs/BINARY/README.md`
|
|
|
|
## Registration
|
|
|
|
```csharp
|
|
// Program.cs
|
|
builder.Services.AddControllers()
|
|
.AddAcBinaryFormatters(opts => {
|
|
opts.UseGeneratedCode = true;
|
|
});
|
|
```
|
|
|
|
`AddAcBinaryFormatters` inserts both formatters at **index 0** of `MvcOptions.InputFormatters` / `OutputFormatters` — AcBinary wins content-negotiation when the client's `Accept` header allows.
|
|
|
|
## Media Type
|
|
|
|
`application/vnd.acbinary` (vendor tree, registered with the IANA pattern but not yet IANA-listed). The same media type is sent on both request (`Content-Type`) and response.
|
|
|
|
Override via `SupportedMediaTypes.Add(...)` on a custom formatter instance if a project-specific type is needed.
|
|
|
|
## Request flow (InputFormatter)
|
|
|
|
```
|
|
HttpContext.Request.Body (Stream)
|
|
→ PipeReader.Create(Body) (PipeReader)
|
|
→ drain-loop on calling thread:
|
|
while (true) {
|
|
result = await reader.ReadAsync(ct);
|
|
foreach (segment in result.Buffer) input.Feed(segment.Span);
|
|
reader.AdvanceTo(result.Buffer.End);
|
|
if (result.IsCompleted) break;
|
|
}
|
|
input.Complete();
|
|
↑ background Task.Run feeds AcBinaryDeserializer.Deserialize(input, ModelType, opts)
|
|
→ ModelType instance → InputFormatterResult.Success
|
|
```
|
|
|
|
The drain-loop is **inline** in the formatter — the serializer surface ends at `AsyncPipeReaderInput`. Any I/O-specific draining (PipeReader, NamedPipe, FileStream, custom transport) is the consumer's responsibility.
|
|
|
|
## Response flow (OutputFormatter)
|
|
|
|
```
|
|
HttpContext.Response.Body (Stream)
|
|
→ PipeWriter.Create(Body) (PipeWriter)
|
|
→ AcBinarySerializer.SerializeChunked(value, ObjectType, writer, opts)
|
|
(raw mode — pure AcBinary bytes, no [201][UINT16] framing)
|
|
→ await pipeWriter.CompleteAsync()
|
|
```
|
|
|
|
`SerializeChunked` (not `SerializeChunkedFramed`) — the wire is a single self-contained AcBinary blob, identical to `Serialize(value, opts) → byte[]`. No multiplexed framing on the HTTP body.
|
|
|
|
## Error model
|
|
|
|
Deserialization failure → `ModelState.TryAddModelError(ModelName, ex.Message)` → `InputFormatterResult.Failure()`. ASP.NET pipeline emits `400 Bad Request` with `application/problem+json` (RFC 7807) — **not** an AcBinary-encoded error. The client reads the error body as JSON.
|
|
|
|
`OperationCanceledException` (when `RequestAborted` is signalled) is rethrown so the pipeline aborts the response cleanly.
|
|
|
|
## Cancellation
|
|
|
|
`HttpContext.RequestAborted` flows into both formatters. The InputFormatter passes it to `PipeReader.ReadAsync` and `Task.Run`; the OutputFormatter calls `cancellationToken.ThrowIfCancellationRequested()` after `CompleteAsync`. Mid-request abort releases all pooled resources via the `using` and `finally` blocks.
|
|
|
|
## What the formatter does NOT include
|
|
|
|
- **No Stream-async API in the binary core** — `AcBinarySerializer` has no `SerializeAsync(Stream, T)` method. The formatter is the wrapper. (See `BINARY_TODO.md#accore-bin-t-t8k3` — parked.)
|
|
- **No options thread-safety guard** — `AcBinarySerializerOptions` is currently mutable; if registered as a DI singleton with `Configure<>` it is fine because `Configure` is read-only at runtime, but raw-shared mutable instances across concurrent requests are unsafe. (See `BINARY_ISSUES.md#accore-bin-i-l8n5` and `BINARY_TODO.md#accore-bin-t-b7h4`.)
|
|
- **No OpenAPI metadata helpers** — `Microsoft.AspNetCore.OpenApi` / `Swashbuckle.AspNetCore` pick up `SupportedMediaTypes` automatically; no extra integration needed.
|
|
|
|
## Future work
|
|
|
|
The formatter currently lives in `AyCode.Services` (alongside the SignalR transport). The intent is to extract it into its own NuGet package — `AyCode.AspNetCore.Mvc.Formatters.AcBinary` — when the binary serializer is moved to a dedicated solution. No code change required at extraction time; only project-file split.
|