diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
index dbc9369..f0739da 100644
--- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
+++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
@@ -43,7 +43,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
// ────────────────────────────────────────────────────────────────────────────────────────────
// UsePropertyFilter const removed — replaced by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]`
// attribute flag, propagated through SerializableClassInfo.EnablePropertyFilter to EmitProp/EmitScanProp.
- private const bool UsePolymorphType = false;
+ private const bool UsePolymorphType = true;
private static readonly DiagnosticDescriptor CircularReferenceWarning = new(
id: "ACBIN001",
diff --git a/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs b/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs
index b637060..2bbd17c 100644
--- a/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs
+++ b/AyCode.Core/Serializers/Attributes/AcBinarySerializableAttribute.cs
@@ -31,26 +31,22 @@ public sealed class AcBinarySerializableAttribute : Attribute
/// per-property filtering for this specific type.
///
public bool EnablePropertyFilterFeature { get; }
+ public bool EnablePolymorphDetectFeature { get; }
public AcBinarySerializableAttribute() : this(true)
{
}
- public AcBinarySerializableAttribute(bool enableAllFeatures)
- {
- EnableMetadataFeature = enableAllFeatures;
- EnableIdTrackingFeature = enableAllFeatures;
- EnableRefHandlingFeature = enableAllFeatures;
- EnableInternStringFeature = enableAllFeatures;
- EnablePropertyFilterFeature = enableAllFeatures;
- }
+ public AcBinarySerializableAttribute(bool enableAllFeatures) : this(enableAllFeatures, enableAllFeatures, enableAllFeatures, enableAllFeatures, enableAllFeatures, enableAllFeatures)
+ { }
- public AcBinarySerializableAttribute(bool enableMetadataFeature, bool enableIdTrackingFeature, bool enableRefHandlingFeature, bool enableInternStringFeature, bool enablePropertyFilterFeature)
+ public AcBinarySerializableAttribute(bool enableMetadataFeature, bool enableIdTrackingFeature, bool enableRefHandlingFeature, bool enableInternStringFeature, bool enablePropertyFilterFeature, bool enablePolymorphDetectFeature)
{
EnableMetadataFeature = enableMetadataFeature;
EnableIdTrackingFeature = enableIdTrackingFeature;
EnableRefHandlingFeature = enableRefHandlingFeature;
EnableInternStringFeature = enableInternStringFeature;
EnablePropertyFilterFeature = enablePropertyFilterFeature;
+ EnablePolymorphDetectFeature = enablePolymorphDetectFeature;
}
}
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
index fbebd50..3c2c01d 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
@@ -34,10 +34,10 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
};
///
- /// Options for shallow serialization (root level only).
+ /// Options for flat serialization (root level only).
/// Returns a new instance each time to prevent shared state corruption.
///
- public static AcBinarySerializerOptions ShallowCopy => new()
+ public static AcBinarySerializerOptions FlatCopy => new()
{
MaxDepth = 0,
// Truncate preserves the original "root + Null nested" semantic; under default Throw the preset
diff --git a/AyCode.Core/Serializers/Binaries/README.md b/AyCode.Core/Serializers/Binaries/README.md
index b7690d7..18b3791 100644
--- a/AyCode.Core/Serializers/Binaries/README.md
+++ b/AyCode.Core/Serializers/Binaries/README.md
@@ -93,7 +93,7 @@ Key wire-format options: `WireMode` (Compact/Fast), `ReferenceHandling` (None/On
`ReferenceHandling=None` + `UseStringInterning=None` = no scan pass (single-phase, fastest).
-Presets: `Default`, `FastMode`, `ShallowCopy`, `WasmOptimized`. Details: `docs/BINARY/BINARY_OPTIONS.md`.
+Presets: `Default`, `FastMode`, `FlatCopy`, `WasmOptimized`. Details: `docs/BINARY/BINARY_OPTIONS.md`.
## Dependencies
diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs
index ad13141..387f856 100644
--- a/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs
+++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs
@@ -16,10 +16,10 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
public static AcJsonSerializerOptions Default => new() { ReferenceHandling = ReferenceHandlingMode.All };
///
- /// Options for shallow serialization (root level only, no references).
+ /// Options for flat serialization (root level only, no references).
/// Returns a new instance each time to prevent shared state corruption.
///
- public static AcJsonSerializerOptions ShallowCopy => new() { MaxDepth = 0, ReferenceHandling = ReferenceHandlingMode.None };
+ public static AcJsonSerializerOptions FlatCopy => new() { MaxDepth = 0, ReferenceHandling = ReferenceHandlingMode.None, MaxDepthBehavior = MaxDepthBehavior.Truncate};
///
/// Creates options with specified max depth.
diff --git a/AyCode.Core/Serializers/Jsons/README.md b/AyCode.Core/Serializers/Jsons/README.md
index d8b2437..2c58881 100644
--- a/AyCode.Core/Serializers/Jsons/README.md
+++ b/AyCode.Core/Serializers/Jsons/README.md
@@ -40,7 +40,7 @@ Custom JSON serialization/deserialization built on `System.Text.Json`'s `Utf8Jso
| `MaxDepth` | Maximum object graph depth |
| `ThrowOnCircularReference` | Throw vs silently handle circular refs |
-**Presets:** `Default` (with refs), `ShallowCopy`, `WithMaxDepth`, `WithoutReferenceHandling`.
+**Presets:** `Default` (with refs), `FlatCopy`, `WithMaxDepth`, `WithoutReferenceHandling`.
## Dependencies
diff --git a/AyCode.Core/docs/BINARY/BINARY_ISSUES.md b/AyCode.Core/docs/BINARY/BINARY_ISSUES.md
index 346930f..75360ac 100644
--- a/AyCode.Core/docs/BINARY/BINARY_ISSUES.md
+++ b/AyCode.Core/docs/BINARY/BINARY_ISSUES.md
@@ -196,7 +196,7 @@ The shallow-serialization use case is a legitimate, common pattern (client edits
- `Truncate` — the previous `WriteByte(Null)` behavior, now explicit opt-in for shallow serialization (delta updates, view-model projections, partial DB-update flows). The wire `Null` at the truncation boundary is the developer's contract decision — endpoint protocol dictates what nested null means. Works with any persistence layer (Dapper, ADO.NET, Cosmos DB, MongoDB, Redis, EF Core, etc.).
- `Disable` — skip the depth check entirely (max perf, dev guarantees cycle-free graph).
-The check moved from "every object/collection write" (with rewind) to "before any marker byte is written" (in `WriteObject` runtime + `WriteObjectFullMarker*` SGen). The `ShallowCopy` preset was updated to explicitly set `MaxDepthBehavior = Truncate` to preserve its original "root + Null nested" semantic. See `BINARY_OPTIONS.md` `MaxDepth + MaxDepthBehavior` section for full details.
+The check moved from "every object/collection write" (with rewind) to "before any marker byte is written" (in `WriteObject` runtime + `WriteObjectFullMarker*` SGen). The `FlatCopy` preset was updated to explicitly set `MaxDepthBehavior = Truncate` to preserve its original "root + Null nested" semantic. See `BINARY_OPTIONS.md` `MaxDepth + MaxDepthBehavior` section for full details.
### ACCORE-BIN-I-W3F4: PropertyFilter + UseMetadata=false silently corrupts via index drift
diff --git a/AyCode.Core/docs/BINARY/BINARY_OPTIONS.md b/AyCode.Core/docs/BINARY/BINARY_OPTIONS.md
index 04a7af7..48442ac 100644
--- a/AyCode.Core/docs/BINARY/BINARY_OPTIONS.md
+++ b/AyCode.Core/docs/BINARY/BINARY_OPTIONS.md
@@ -146,14 +146,14 @@ delegate PropertyInfo? PropertyMapperDelegate(PropertyInfo sourceProperty, Type
|--------|----------|----------|-----------------|-------------|----------|------------------|-------------|-------|
| `Default` | Compact | false | Attribute | All | 255 | Throw | None | — |
| `FastMode` | Compact | false | None | None | 255 | Throw | None | No scan pass |
-| `ShallowCopy` | 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) |
+| `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 to `ReferenceHandling=All`
-- `FastMode` / `ShallowCopy` — single-phase (no scan pass) since both interning and refs are disabled
+- `FastMode` / `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
diff --git a/AyCode.Core/docs/BINARY/BINARY_WHYUSE.md b/AyCode.Core/docs/BINARY/BINARY_WHYUSE.md
index c242b18..5b956a9 100644
--- a/AyCode.Core/docs/BINARY/BINARY_WHYUSE.md
+++ b/AyCode.Core/docs/BINARY/BINARY_WHYUSE.md
@@ -36,12 +36,49 @@ Measured production payload: ~10 MB / ~7900 orders (full graph `Order → OrderI
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