diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
index 54d42f1..b401f03 100644
--- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
+++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs
@@ -290,7 +290,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
if (valid.Count == 0) return;
foreach (var ci in valid)
+ {
context.AddSource($"{ci.ClassName}_GeneratedWriter.g.cs", SourceText.From(GenWriter(ci), Encoding.UTF8));
+ context.AddSource($"{ci.ClassName}_GeneratedReader.g.cs", SourceText.From(GenReader(ci), Encoding.UTF8));
+ }
context.AddSource("AcBinaryGeneratedWriters_Init.g.cs", SourceText.From(GenInit(valid), Encoding.UTF8));
}
@@ -846,10 +849,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
///
/// Emits inline object write for a Complex property that has a generated writer.
- /// Writes marker bytes + inline metadata (UseMetadata) + calls child GeneratedWriter.WriteProperties.
- /// IId types: guard ReferenceHandling != None (tracked in OnlyId + All).
- /// Non-IId types: guard ReferenceHandling == All (tracked only in All mode).
- /// No fallback to WriteObjectGenerated — handles both UseMetadata=true and false inline.
+ /// Compile-time ChildNeedsRefScan eliminates TryConsumeWritePlanEntry when scan never tracks child.
+ /// !ChildNeedsRefScan + !ChildEnableMetadata → ZERO branches: just Object + WriteProperties.
///
private static void EmitDirectObjectWrite(StringBuilder sb, PropInfo p, string a, string i)
{
@@ -872,10 +873,32 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{");
- if (!p.ChildEnableMetadata)
+ if (!p.ChildNeedsRefScan)
{
- // Child type has EnableMetadataFeature=false — no metadata, always Object marker
- // Inline ref tracking still needed for IId/All mode
+ // Compile-time proven: scan never tracks child → TryConsumeWritePlanEntry always false
+ if (!p.ChildEnableMetadata)
+ {
+ // No ref, no metadata → ZERO branches: always Object
+ sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
+ sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});");
+ }
+ else
+ {
+ // No ref, but metadata possible → UseMetadata branch only
+ sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.TypeNameForTypeof})));");
+ sb.AppendLine($"{i} if (context.UseMetadata)");
+ sb.AppendLine($"{i} {{");
+ sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
+ EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{p.Name}", i + " ");
+ sb.AppendLine($"{i} }}");
+ sb.AppendLine($"{i} else");
+ sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
+ sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});");
+ }
+ }
+ else if (!p.ChildEnableMetadata)
+ {
+ // Ref tracking possible, no metadata — Object or ObjectRefFirst/ObjectRef
var refGuard = p.IsIId
? "context.ReferenceHandling != ReferenceHandlingMode.None"
: "context.ReferenceHandling == ReferenceHandlingMode.All";
@@ -901,10 +924,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
}
else
{
- // UseMetadata: register type for first/repeated tracking via slot (no GetWrapper needed)
+ // Full path: ref tracking + metadata
sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.TypeNameForTypeof})));");
-
- // Inline ref tracking: guard depends on IId vs non-IId to match scan pass behavior.
var refGuard = p.IsIId
? "context.ReferenceHandling != ReferenceHandlingMode.None"
: "context.ReferenceHandling == ReferenceHandlingMode.All";
@@ -917,7 +938,6 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{");
- // RefFirst: UseMetadata → ObjectWithMetadataRefFirst + metadata, else → ObjectRefFirst
sb.AppendLine($"{i} if (context.UseMetadata)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);");
@@ -934,7 +954,6 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{");
- // No ref tracking: UseMetadata → ObjectWithMetadata + metadata, else → Object
sb.AppendLine($"{i} if (context.UseMetadata)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
@@ -1021,75 +1040,98 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} if ({e} == null) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}");
sb.AppendLine($"{i} if (nextDepth_{p.Name} > context.MaxDepth) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}");
- // Inline ref tracking
- var elemRefGuard = p.ElementIsIId
- ? "context.ReferenceHandling != ReferenceHandlingMode.None"
- : "context.ReferenceHandling == ReferenceHandlingMode.All";
-
- if (!p.ElementEnableMetadata)
+ if (!p.ElementNeedsRefScan)
{
- // Element type has EnableMetadataFeature=false — no metadata, always Object marker
- sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))");
- sb.AppendLine($"{i} {{");
- sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)");
- sb.AppendLine($"{i} {{");
- sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);");
- sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
- sb.AppendLine($"{i} }}");
- sb.AppendLine($"{i} else");
- sb.AppendLine($"{i} {{");
- sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);");
- sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
- sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
- sb.AppendLine($"{i} }}");
- sb.AppendLine($"{i} }}");
- sb.AppendLine($"{i} else");
- sb.AppendLine($"{i} {{");
- sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
- sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
- sb.AppendLine($"{i} }}");
+ // Compile-time proven: scan never tracks element → TryConsumeWritePlanEntry always false
+ if (!p.ElementEnableMetadata)
+ {
+ // No ref, no metadata → ZERO branches per element: always Object
+ sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
+ sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
+ }
+ else
+ {
+ // No ref, but metadata possible → UseMetadata branch only
+ sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.ElementFullTypeName})));");
+ sb.AppendLine($"{i} if (context.UseMetadata)");
+ sb.AppendLine($"{i} {{");
+ sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
+ EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " ");
+ sb.AppendLine($"{i} }}");
+ sb.AppendLine($"{i} else");
+ sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
+ sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
+ }
}
else
{
- // UseMetadata: register element type for first/repeated tracking
- sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.ElementFullTypeName})));");
+ // Inline ref tracking
+ var elemRefGuard = p.ElementIsIId
+ ? "context.ReferenceHandling != ReferenceHandlingMode.None"
+ : "context.ReferenceHandling == ReferenceHandlingMode.All";
- sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))");
- sb.AppendLine($"{i} {{");
- sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)");
- sb.AppendLine($"{i} {{");
- sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);");
- sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
- sb.AppendLine($"{i} }}");
- sb.AppendLine($"{i} else");
- sb.AppendLine($"{i} {{");
- // RefFirst: UseMetadata → ObjectWithMetadataRefFirst + metadata, else → ObjectRefFirst
- sb.AppendLine($"{i} if (context.UseMetadata)");
- sb.AppendLine($"{i} {{");
- sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);");
- sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
- EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " ");
- sb.AppendLine($"{i} }}");
- sb.AppendLine($"{i} else");
- sb.AppendLine($"{i} {{");
- sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);");
- sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
- sb.AppendLine($"{i} }}");
- sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
- sb.AppendLine($"{i} }}");
- sb.AppendLine($"{i} }}");
- sb.AppendLine($"{i} else");
- sb.AppendLine($"{i} {{");
- // No ref tracking: UseMetadata → ObjectWithMetadata + metadata, else → Object
- sb.AppendLine($"{i} if (context.UseMetadata)");
- sb.AppendLine($"{i} {{");
- sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
- EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " ");
- sb.AppendLine($"{i} }}");
- sb.AppendLine($"{i} else");
- sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
- sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
- sb.AppendLine($"{i} }}");
+ if (!p.ElementEnableMetadata)
+ {
+ // Ref tracking possible, no metadata — Object or ObjectRefFirst/ObjectRef
+ sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))");
+ sb.AppendLine($"{i} {{");
+ sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)");
+ sb.AppendLine($"{i} {{");
+ sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);");
+ sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
+ sb.AppendLine($"{i} }}");
+ sb.AppendLine($"{i} else");
+ sb.AppendLine($"{i} {{");
+ sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);");
+ sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
+ sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
+ sb.AppendLine($"{i} }}");
+ sb.AppendLine($"{i} }}");
+ sb.AppendLine($"{i} else");
+ sb.AppendLine($"{i} {{");
+ sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
+ sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
+ sb.AppendLine($"{i} }}");
+ }
+ else
+ {
+ // Full path: ref tracking + metadata
+ sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.ElementFullTypeName})));");
+ sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))");
+ sb.AppendLine($"{i} {{");
+ sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)");
+ sb.AppendLine($"{i} {{");
+ sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);");
+ sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
+ sb.AppendLine($"{i} }}");
+ sb.AppendLine($"{i} else");
+ sb.AppendLine($"{i} {{");
+ sb.AppendLine($"{i} if (context.UseMetadata)");
+ sb.AppendLine($"{i} {{");
+ sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);");
+ sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
+ EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " ");
+ sb.AppendLine($"{i} }}");
+ sb.AppendLine($"{i} else");
+ sb.AppendLine($"{i} {{");
+ sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);");
+ sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
+ sb.AppendLine($"{i} }}");
+ sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
+ sb.AppendLine($"{i} }}");
+ sb.AppendLine($"{i} }}");
+ sb.AppendLine($"{i} else");
+ sb.AppendLine($"{i} {{");
+ sb.AppendLine($"{i} if (context.UseMetadata)");
+ sb.AppendLine($"{i} {{");
+ sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
+ EmitInlineMetadata(sb, p.ElementTypeNameHash, p.ElementPropertyHashes!, $"isFirstMeta_e_{p.Name}", i + " ");
+ sb.AppendLine($"{i} }}");
+ sb.AppendLine($"{i} else");
+ sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
+ sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
+ sb.AppendLine($"{i} }}");
+ }
}
sb.AppendLine($"{i} }}");
@@ -1201,6 +1243,430 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
}
}
+ #region Reader Code Generation
+
+ ///
+ /// Generates the IGeneratedBinaryReader implementation for a type.
+ /// Phase 1: handles markerless path (no UseMetadata). UseMetadata/ChainMode → runtime fallback.
+ /// Eliminates: GetWrapper dictionary lookup, CreateInstance delegate, property setter delegates,
+ /// AccessorType switch dispatch, ReadValue dispatch table.
+ ///
+ private static string GenReader(SerializableClassInfo ci)
+ {
+ var sb = new StringBuilder(4096);
+ sb.AppendLine("// ");
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine("using System.Runtime.CompilerServices;");
+ sb.AppendLine("using AyCode.Core.Serializers.Binaries;");
+ sb.AppendLine();
+ if (!string.IsNullOrEmpty(ci.Namespace))
+ sb.AppendLine($"namespace {ci.Namespace};");
+ sb.AppendLine();
+ sb.AppendLine($"internal sealed class {ci.ClassName}_GeneratedReader : IGeneratedBinaryReader");
+ sb.AppendLine("{");
+ sb.AppendLine($" internal static readonly {ci.ClassName}_GeneratedReader Instance = new();");
+ sb.AppendLine();
+
+ // ReadObject — IGeneratedBinaryReader implementation
+ sb.AppendLine(" public object? ReadObject(AcBinaryDeserializer.BinaryDeserializationContext context, int depth, int cacheIndex)");
+ sb.AppendLine(" where TInput : struct, IBinaryInputBase");
+ sb.AppendLine(" {");
+ sb.AppendLine($" var obj = new {ci.FullTypeName}();");
+ sb.AppendLine();
+ sb.AppendLine(" if (cacheIndex >= 0)");
+ sb.AppendLine(" context.RegisterInternedValueAt(cacheIndex, obj);");
+ sb.AppendLine();
+
+ // Emit property reads — markerless for primitive types, markered for the rest
+ foreach (var p in ci.Properties)
+ {
+ sb.AppendLine();
+ EmitReadProp(sb, p, " ", ci.EnableMetadata);
+ }
+
+ sb.AppendLine();
+ sb.AppendLine(" return obj;");
+ sb.AppendLine(" }");
+ sb.AppendLine("}");
+ return sb.ToString();
+ }
+
+ ///
+ /// Emits inline read code for a single property.
+ /// Markerless types: read raw value directly (no type code in stream).
+ /// Markered types: read type code byte, then dispatch.
+ /// Mirrors the serializer's EmitProp symmetry.
+ ///
+ private static void EmitReadProp(StringBuilder sb, PropInfo p, string i, bool enableMetadata)
+ {
+ var a = $"obj.{p.Name}";
+
+ // Markerless types: read raw value directly — mirrors EmitMarkerless in writer
+ if (IsMarkerless(p.TypeKind))
+ {
+ if (p.TypeKind == PropertyTypeKind.Enum)
+ sb.AppendLine($"{i}{{ var ev = context.ReadVarInt(); {a} = Unsafe.As(ref ev); }}");
+ else
+ EmitReadMarkerless(sb, p.TypeKind, a, i);
+ return;
+ }
+
+ // Markered types: read type code, then dispatch
+ var tc = $"tc_{p.Name}";
+ sb.AppendLine($"{i}var {tc} = context.ReadByte();");
+
+ // PropertySkip → leave default
+ sb.AppendLine($"{i}if ({tc} != BinaryTypeCode.PropertySkip)");
+ sb.AppendLine($"{i}{{");
+
+ // Nullable value types
+ if (IsNullableVTKind(p.TypeKind))
+ {
+ sb.AppendLine($"{i} if ({tc} == BinaryTypeCode.Null) {{ /* null */ }}");
+ sb.AppendLine($"{i} else");
+ sb.AppendLine($"{i} {{");
+ EmitReadMarkeredValue(sb, Underlying(p.TypeKind), a, tc, i + " ", p, nullable: true);
+ sb.AppendLine($"{i} }}");
+ }
+ else
+ {
+ switch (p.TypeKind)
+ {
+ case PropertyTypeKind.String:
+ EmitReadString(sb, a, tc, i + " ");
+ break;
+
+ case PropertyTypeKind.Complex:
+ EmitReadComplex(sb, p, a, tc, i + " ");
+ break;
+
+ case PropertyTypeKind.Collection:
+ EmitReadCollection(sb, p, a, tc, i + " ");
+ break;
+
+ default:
+ // Unknown markered type (char, sbyte, etc.) — rewind + runtime fallback
+ sb.AppendLine($"{i} context._position--;");
+ if (p.IsNullable)
+ sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1);");
+ else
+ sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1)!;");
+ break;
+ }
+ }
+
+ sb.AppendLine($"{i}}}");
+ }
+
+ ///
+ /// Emits raw value read — no type code in stream. Mirrors EmitMarkerless exactly.
+ ///
+ private static void EmitReadMarkerless(StringBuilder sb, PropertyTypeKind k, string a, string i)
+ {
+ switch (k)
+ {
+ case PropertyTypeKind.Int32: sb.AppendLine($"{i}{a} = context.ReadVarInt();"); break;
+ case PropertyTypeKind.Int64: sb.AppendLine($"{i}{a} = context.ReadVarLong();"); break;
+ case PropertyTypeKind.Double: sb.AppendLine($"{i}{a} = context.ReadDoubleUnsafe();"); break;
+ case PropertyTypeKind.Single: sb.AppendLine($"{i}{a} = context.ReadSingleUnsafe();"); break;
+ case PropertyTypeKind.Decimal: sb.AppendLine($"{i}{a} = context.ReadDecimalUnsafe();"); break;
+ case PropertyTypeKind.DateTime: sb.AppendLine($"{i}{a} = context.ReadDateTimeUnsafe();"); break;
+ case PropertyTypeKind.Guid: sb.AppendLine($"{i}{a} = context.ReadGuidUnsafe();"); break;
+ case PropertyTypeKind.Byte: sb.AppendLine($"{i}{a} = context.ReadByte();"); break;
+ case PropertyTypeKind.Int16: sb.AppendLine($"{i}{a} = context.ReadInt16Unsafe();"); break;
+ case PropertyTypeKind.UInt16: sb.AppendLine($"{i}{a} = context.ReadUInt16Unsafe();"); break;
+ case PropertyTypeKind.UInt32: sb.AppendLine($"{i}{a} = context.ReadVarUInt();"); break;
+ case PropertyTypeKind.UInt64: sb.AppendLine($"{i}{a} = context.ReadVarULong();"); break;
+ case PropertyTypeKind.TimeSpan: sb.AppendLine($"{i}{a} = new System.TimeSpan(context.ReadRaw());"); break;
+ case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}{a} = context.ReadDateTimeOffsetUnsafe();"); break;
+ case PropertyTypeKind.Boolean: sb.AppendLine($"{i}{a} = context.ReadByte() != 0;"); break;
+ }
+ }
+
+ ///
+ /// Emits inline string read from type code. Handles all string wire formats.
+ ///
+ private static void EmitReadString(StringBuilder sb, string a, string tc, string i)
+ {
+ // FixStr is the hot path — most strings are short (1-31 bytes, encoded in the type code itself)
+ sb.AppendLine($"{i}if (BinaryTypeCode.IsFixStr({tc}))");
+ sb.AppendLine($"{i}{{");
+ sb.AppendLine($"{i} var flen = BinaryTypeCode.DecodeFixStrLength({tc});");
+ sb.AppendLine($"{i} {a} = flen == 0 ? string.Empty : context.ReadStringUtf8(flen);");
+ sb.AppendLine($"{i}}}");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.String)");
+ sb.AppendLine($"{i}{{");
+ sb.AppendLine($"{i} var slen = (int)context.ReadVarUInt();");
+ sb.AppendLine($"{i} {a} = slen == 0 ? string.Empty : context.ReadStringUtf8(slen);");
+ sb.AppendLine($"{i}}}");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.StringInternFirst)");
+ sb.AppendLine($"{i}{{");
+ sb.AppendLine($"{i} context.DisableStringCaching();");
+ sb.AppendLine($"{i} var sci = (int)context.ReadVarUInt();");
+ sb.AppendLine($"{i} var slen2 = (int)context.ReadVarUInt();");
+ sb.AppendLine($"{i} var sv = slen2 == 0 ? string.Empty : context.ReadStringUtf8(slen2);");
+ sb.AppendLine($"{i} context.RegisterInternedValueAt(sci, sv);");
+ sb.AppendLine($"{i} {a} = sv;");
+ sb.AppendLine($"{i}}}");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.StringInterned)");
+ sb.AppendLine($"{i} {a} = context.GetInternedString((int)context.ReadVarUInt());");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Null) {a} = null;");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.StringEmpty) {a} = string.Empty;");
+ }
+
+ ///
+ /// Emits inline read for a Complex property.
+ /// SGen reader only runs in non-metadata mode → ObjectWithMetadata never appears.
+ /// Compile-time ChildNeedsRefScan eliminates ObjectRefFirst/ObjectRef when provably unused.
+ /// Non-nullable + no ref → ZERO branches (tc consumed but ignored).
+ /// No SGen → runtime fallback via ReadValueGenerated.
+ ///
+ private static void EmitReadComplex(StringBuilder sb, PropInfo p, string a, string tc, string i)
+ {
+ if (!p.HasGeneratedWriter)
+ {
+ // No SGen reader — runtime fallback (rewind + ReadValueGenerated)
+ if (p.IsNullable)
+ {
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;");
+ sb.AppendLine($"{i}else");
+ sb.AppendLine($"{i}{{");
+ sb.AppendLine($"{i} context._position--;");
+ sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1);");
+ sb.AppendLine($"{i}}}");
+ }
+ else
+ {
+ sb.AppendLine($"{i}context._position--;");
+ sb.AppendLine($"{i}{a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1)!;");
+ }
+ return;
+ }
+
+ var reader = p.WriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader");
+ var cast = $"({p.TypeNameForTypeof})";
+ var nd = "depth + 1";
+
+ if (!p.ChildNeedsRefScan)
+ {
+ // Compile-time proven: child never tracked → only Object (+ Null for nullable) in stream
+ if (p.IsNullable)
+ {
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {{ /* null */ }}");
+ sb.AppendLine($"{i}else {a} = {cast}{reader}.Instance.ReadObject(context, {nd}, -1)!;");
+ }
+ else
+ {
+ // ZERO branches — tc is always Object, just call ReadObject
+ sb.AppendLine($"{i}{a} = {cast}{reader}.Instance.ReadObject(context, {nd}, -1)!;");
+ }
+ }
+ else
+ {
+ // Ref tracking possible — Object/ObjectRefFirst/ObjectRef dispatch
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Object)");
+ sb.AppendLine($"{i} {a} = {cast}{reader}.Instance.ReadObject(context, {nd}, -1)!;");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.ObjectRefFirst)");
+ sb.AppendLine($"{i} {a} = {cast}{reader}.Instance.ReadObject(context, {nd}, (int)context.ReadVarUInt())!;");
+ if (p.IsNullable)
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Null) {{ /* null */ }}");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.ObjectRef)");
+ sb.AppendLine($"{i} {a} = {cast}context.GetInternedObject((int)context.ReadVarUInt())!;");
+ }
+ }
+
+ ///
+ /// Emits inline read for a Collection property.
+ /// Complex element with SGen + known collection kind → inline Array loop with direct element reader calls.
+ /// Else → runtime fallback via ReadValueGenerated.
+ ///
+ private static void EmitReadCollection(StringBuilder sb, PropInfo p, string a, string tc, string i)
+ {
+ // Check if we can inline: need SGen element reader + known collection shape + Array marker
+ if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.CollectionKind != null)
+ {
+ EmitReadCollectionInline(sb, p, a, tc, i);
+ return;
+ }
+
+ // Runtime fallback
+ if (p.IsNullable)
+ {
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;");
+ sb.AppendLine($"{i}else");
+ sb.AppendLine($"{i}{{");
+ sb.AppendLine($"{i} context._position--;");
+ sb.AppendLine($"{i} {a} = ({p.TypeNameForTypeof}?)AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1);");
+ sb.AppendLine($"{i}}}");
+ }
+ else
+ {
+ sb.AppendLine($"{i}context._position--;");
+ sb.AppendLine($"{i}{a} = ({p.TypeNameForTypeof})AcBinaryDeserializer.ReadValueGenerated(context, typeof({p.TypeNameForTypeof}), depth + 1)!;");
+ }
+ }
+
+ ///
+ /// Emits inline collection read: Array marker already consumed as tc.
+ /// Reads count + loops with direct element reader calls.
+ /// Eliminates per-element: ReadValue dispatch, ReadObjectCore dict lookup, Activator.CreateInstance.
+ ///
+ private static void EmitReadCollectionInline(StringBuilder sb, PropInfo p, string a, string tc, string i)
+ {
+ var reader = p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader");
+ var elemType = p.ElementFullTypeName!;
+ var elemCast = $"({elemType})";
+ var s = p.Name;
+
+ // Null check
+ if (p.IsNullable)
+ {
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Null) {a} = null;");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Array)");
+ }
+ else
+ {
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Array)");
+ }
+
+ sb.AppendLine($"{i}{{");
+ sb.AppendLine($"{i} var cnt_{s} = (int)context.ReadVarUInt();");
+ sb.AppendLine($"{i} var nd_{s} = depth + 1;");
+
+ // Create collection based on kind
+ if (p.CollectionKind == "Array")
+ {
+ sb.AppendLine($"{i} var col_{s} = new {elemType}[cnt_{s}];");
+ sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)");
+ sb.AppendLine($"{i} {{");
+ EmitReadCollectionElement(sb, reader, elemCast, $"ri_{s}", s, i + " ", isArray: true, p.ElementNeedsRefScan);
+ sb.AppendLine($"{i} }}");
+ }
+ else // List, Counted — all use List with Add
+ {
+ sb.AppendLine($"{i} var col_{s} = new System.Collections.Generic.List<{elemType}>(cnt_{s});");
+ sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)");
+ sb.AppendLine($"{i} {{");
+ EmitReadCollectionElement(sb, reader, elemCast, $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan);
+ sb.AppendLine($"{i} }}");
+ }
+
+ sb.AppendLine($"{i} {a} = col_{s};");
+ sb.AppendLine($"{i}}}");
+ }
+
+ ///
+ /// Emits per-element read inside collection loop.
+ /// SGen reader = non-metadata mode → no ObjectWithMetadata fallback.
+ /// !needsRefScan → only Object/Null possible → 1 branch per element.
+ ///
+ private static void EmitReadCollectionElement(StringBuilder sb, string reader, string elemCast, string indexVar, string propSuffix, string i, bool isArray, bool needsRefScan)
+ {
+ var etc = $"etc_{propSuffix}";
+ sb.AppendLine($"{i}var {etc} = context.ReadByte();");
+
+ var assignNull = isArray ? $"col_{propSuffix}[{indexVar}] = null!;" : $"col_{propSuffix}.Add(null!);";
+ var assignExpr = isArray ? $"col_{propSuffix}[{indexVar}] = " : $"col_{propSuffix}.Add(";
+ var assignEnd = isArray ? ";" : ");";
+
+ if (!needsRefScan)
+ {
+ // No ref tracking → only Object or Null in stream — 1 branch
+ sb.AppendLine($"{i}if ({etc} == BinaryTypeCode.Null) {{ {assignNull} }}");
+ sb.AppendLine($"{i}else {assignExpr}{elemCast}{reader}.Instance.ReadObject(context, nd_{propSuffix}, -1)!{assignEnd}");
+ }
+ else
+ {
+ // Object hot path first, then ref markers
+ sb.AppendLine($"{i}if ({etc} == BinaryTypeCode.Object)");
+ sb.AppendLine($"{i} {assignExpr}{elemCast}{reader}.Instance.ReadObject(context, nd_{propSuffix}, -1)!{assignEnd}");
+ sb.AppendLine($"{i}else if ({etc} == BinaryTypeCode.ObjectRefFirst)");
+ sb.AppendLine($"{i} {assignExpr}{elemCast}{reader}.Instance.ReadObject(context, nd_{propSuffix}, (int)context.ReadVarUInt())!{assignEnd}");
+ sb.AppendLine($"{i}else if ({etc} == BinaryTypeCode.Null) {{ {assignNull} }}");
+ sb.AppendLine($"{i}else if ({etc} == BinaryTypeCode.ObjectRef)");
+ sb.AppendLine($"{i} {assignExpr}{elemCast}context.GetInternedObject((int)context.ReadVarUInt())!{assignEnd}");
+ }
+ }
+
+ ///
+ /// Emits markered value read for primitive types (with type code already read).
+ /// Handles TinyInt encoding for integer types.
+ ///
+ private static void EmitReadMarkeredValue(StringBuilder sb, PropertyTypeKind k, string a, string tc, string i, PropInfo p, bool nullable)
+ {
+ var assign = nullable ? $"{a} = " : $"{a} = ";
+ switch (k)
+ {
+ case PropertyTypeKind.Int32:
+ sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}BinaryTypeCode.DecodeTinyInt({tc});");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int32) {assign}context.ReadVarInt();");
+ break;
+ case PropertyTypeKind.Int64:
+ sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}BinaryTypeCode.DecodeTinyInt({tc});");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int32) {assign}context.ReadVarInt();");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int64) {assign}context.ReadVarLong();");
+ break;
+ case PropertyTypeKind.Boolean:
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.True) {assign}true;");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.False) {assign}false;");
+ break;
+ case PropertyTypeKind.Double:
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Float64) {assign}context.ReadDoubleUnsafe();");
+ break;
+ case PropertyTypeKind.Single:
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Float32) {assign}context.ReadSingleUnsafe();");
+ break;
+ case PropertyTypeKind.Decimal:
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Decimal) {assign}context.ReadDecimalUnsafe();");
+ break;
+ case PropertyTypeKind.DateTime:
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.DateTime) {assign}context.ReadDateTimeUnsafe();");
+ break;
+ case PropertyTypeKind.Guid:
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Guid) {assign}context.ReadGuidUnsafe();");
+ break;
+ case PropertyTypeKind.Byte:
+ sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(byte)BinaryTypeCode.DecodeTinyInt({tc});");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt8) {assign}context.ReadByte();");
+ break;
+ case PropertyTypeKind.Int16:
+ sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(short)BinaryTypeCode.DecodeTinyInt({tc});");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.Int16) {assign}context.ReadInt16Unsafe();");
+ break;
+ case PropertyTypeKind.UInt16:
+ sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(ushort)BinaryTypeCode.DecodeTinyInt({tc});");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt16) {assign}context.ReadUInt16Unsafe();");
+ break;
+ case PropertyTypeKind.UInt32:
+ sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(uint)BinaryTypeCode.DecodeTinyInt({tc});");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt32) {assign}context.ReadVarUInt();");
+ break;
+ case PropertyTypeKind.UInt64:
+ sb.AppendLine($"{i}if (BinaryTypeCode.IsTinyInt({tc})) {assign}(ulong)BinaryTypeCode.DecodeTinyInt({tc});");
+ sb.AppendLine($"{i}else if ({tc} == BinaryTypeCode.UInt64) {assign}context.ReadVarULong();");
+ break;
+ case PropertyTypeKind.Enum:
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.Enum)");
+ sb.AppendLine($"{i}{{");
+ sb.AppendLine($"{i} var eb = context.ReadByte();");
+ sb.AppendLine($"{i} int ev;");
+ sb.AppendLine($"{i} if (BinaryTypeCode.IsTinyInt(eb)) ev = BinaryTypeCode.DecodeTinyInt(eb);");
+ sb.AppendLine($"{i} else ev = context.ReadVarInt();");
+ sb.AppendLine($"{i} {assign}({p.TypeNameForTypeof})(object)ev;");
+ sb.AppendLine($"{i}}}");
+ sb.AppendLine($"{i}else if (BinaryTypeCode.IsTinyInt({tc})) {assign}({p.TypeNameForTypeof})(object)BinaryTypeCode.DecodeTinyInt({tc});");
+ break;
+ case PropertyTypeKind.TimeSpan:
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.TimeSpan) {assign}context.ReadTimeSpanUnsafe();");
+ break;
+ case PropertyTypeKind.DateTimeOffset:
+ sb.AppendLine($"{i}if ({tc} == BinaryTypeCode.DateTimeOffset) {assign}context.ReadDateTimeOffsetUnsafe();");
+ break;
+ }
+ }
+
+ #endregion
+
private static string GenInit(List classes)
{
var sb = new StringBuilder(512);
@@ -1220,7 +1686,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
var writerRef = string.IsNullOrEmpty(ci.Namespace)
? $"{ci.ClassName}_GeneratedWriter"
: $"{ci.Namespace}.{ci.ClassName}_GeneratedWriter";
+ var readerRef = string.IsNullOrEmpty(ci.Namespace)
+ ? $"{ci.ClassName}_GeneratedReader"
+ : $"{ci.Namespace}.{ci.ClassName}_GeneratedReader";
sb.AppendLine($" AcBinarySerializer.RegisterGeneratedWriter(typeof({ci.FullTypeName}), {writerRef}.Instance);");
+ sb.AppendLine($" AcBinaryDeserializer.RegisterGeneratedReader(typeof({ci.FullTypeName}), {readerRef}.Instance);");
}
sb.AppendLine(" }");
sb.AppendLine("}");
diff --git a/AyCode.Core.Tests/TestModels/SharedTestModels.cs b/AyCode.Core.Tests/TestModels/SharedTestModels.cs
index 11129a1..0d4f97e 100644
--- a/AyCode.Core.Tests/TestModels/SharedTestModels.cs
+++ b/AyCode.Core.Tests/TestModels/SharedTestModels.cs
@@ -55,7 +55,7 @@ public enum TestUserRole
/// Implements IId<int> for semantic $id/$ref serialization.
///
[MemoryPackable]
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
[MessagePackObject]
public partial class SharedTag : IId
{
@@ -80,7 +80,7 @@ public partial class SharedTag : IId
/// Shared category - for hierarchical cross-reference testing.
///
[MemoryPackable]
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
[MessagePackObject]
public partial class SharedCategory : IId
{
@@ -106,7 +106,7 @@ public partial class SharedCategory : IId
/// Shared user reference - appears in many places to test $ref deduplication.
///
[MemoryPackable]
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
[MessagePackObject]
public partial class SharedUser : IId
{
@@ -136,7 +136,7 @@ public partial class SharedUser : IId
/// User preferences - non-IId nested object
///
[MemoryPackable]
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
[MessagePackObject]
public partial class UserPreferences
{
@@ -162,7 +162,7 @@ public partial class UserPreferences
/// Does NOT implement IId, so uses standard Newtonsoft reference tracking.
///
[MemoryPackable]
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
[MessagePackObject]
public partial class MetadataInfo
{
@@ -190,7 +190,7 @@ public partial class MetadataInfo
/// Level 1: Main order - root of the hierarchy
///
[MemoryPackable]
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
[MessagePackObject]
public partial class TestOrder : IId
{
@@ -250,7 +250,7 @@ public partial class TestOrder : IId
/// Level 2: Order item with pallets
///
[MemoryPackable]
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
[MessagePackObject]
public partial class TestOrderItem : IId
{
@@ -290,7 +290,7 @@ public partial class TestOrderItem : IId
/// Level 3: Pallet containing measurements
///
[MemoryPackable]
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
[MessagePackObject]
public partial class TestPallet : IId
{
@@ -333,7 +333,7 @@ public partial class TestPallet : IId
/// Level 4: Measurement with multiple points
///
[MemoryPackable]
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
[MessagePackObject]
public partial class TestMeasurement : IId
{
@@ -368,7 +368,7 @@ public partial class TestMeasurement : IId
/// Level 5: Deepest level - measurement point
///
[MemoryPackable]
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
[MessagePackObject]
public partial class TestMeasurementPoint : IId
{
@@ -402,7 +402,7 @@ public partial class TestMeasurementPoint : IId
///
/// Order with Guid Id - for testing Guid-based IId
///
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
public class TestGuidOrder : IId
{
public Guid Id { get; set; }
@@ -414,7 +414,7 @@ public class TestGuidOrder : IId
///
/// Item with Guid Id
///
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
public class TestGuidItem : IId
{
public Guid Id { get; set; }
@@ -430,7 +430,7 @@ public class TestGuidItem : IId
/// Simulates NopCommerce GenericAttribute - stores key-value pairs where DateTime values
/// are stored as strings in the database.
///
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
public class TestGenericAttribute
{
public int Id { get; set; }
@@ -442,7 +442,7 @@ public class TestGenericAttribute
/// DTO with GenericAttributes collection - simulates OrderDto with string-stored DateTime values.
/// This reproduces the production bug where Binary serialization was thought to corrupt DateTime strings.
///
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
public class TestDtoWithGenericAttributes : IId
{
public int Id { get; set; }
@@ -453,7 +453,7 @@ public class TestDtoWithGenericAttributes : IId
///
/// Order with nullable collections for null vs empty testing
///
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
public class TestOrderWithNullableCollections
{
public int Id { get; set; }
@@ -466,7 +466,7 @@ public class TestOrderWithNullableCollections
/// Class with all primitive types for WASM/serialization testing
///
[MemoryPackable]
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
public partial class PrimitiveTestClass
{
public int IntValue { get; set; }
@@ -489,7 +489,7 @@ public partial class PrimitiveTestClass
/// Class with extended primitive types for full serializer coverage.
/// Includes DateTimeOffset, TimeSpan, Dictionary, null properties.
///
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
public class ExtendedPrimitiveTestClass
{
public int Id { get; set; }
@@ -519,7 +519,7 @@ public class ExtendedPrimitiveTestClass
///
/// Class with array of objects containing null items for WriteNull coverage
///
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
public class ObjectWithNullItems
{
public int Id { get; set; }
@@ -534,7 +534,7 @@ public class ObjectWithNullItems
/// "Server-side" DTO with extra properties that the "client" doesn't know about.
/// Used to test SkipValue functionality when deserializing unknown properties.
///
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
public class ServerCustomerDto : IId
{
public int Id { get; set; }
@@ -567,7 +567,7 @@ public class ServerCustomerDto : IId
/// the deserializer must skip unknown properties correctly
/// while still maintaining string intern table consistency.
///
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
public class ClientCustomerDto : IId
{
public int Id { get; set; }
@@ -581,7 +581,7 @@ public class ClientCustomerDto : IId
/// Server DTO with nested objects that client doesn't know about.
/// Tests skipping complex nested structures.
///
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
public class ServerOrderWithExtras : IId
{
public int Id { get; set; }
@@ -602,7 +602,7 @@ public class ServerOrderWithExtras : IId
///
/// Client version of the order - doesn't have Customer/RelatedCustomers properties.
///
-[AcBinarySerializable(true)]
+[AcBinarySerializable(false)]
public class ClientOrderSimple : IId
{
public int Id { get; set; }
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
index 2c84df0..58f0139 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
@@ -29,6 +29,32 @@ public static partial class AcBinaryDeserializer
{
private static readonly ConcurrentDictionary TypeConversionCache = new();
+ ///
+ /// Thread-safe registry for generated readers. Looked up in ReadObjectCore to bypass runtime path.
+ ///
+ internal static class GeneratedReaderRegistry
+ {
+ private static readonly ConcurrentDictionary Readers = new();
+
+ internal static void Register(Type type, IGeneratedBinaryReader reader) => Readers[type] = reader;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static IGeneratedBinaryReader? TryGet(Type type) =>
+ Readers.TryGetValue(type, out var reader) ? reader : null;
+ }
+
+ ///
+ /// Registers a source-generated binary reader for the specified type.
+ /// Once registered, ReadObjectCore bypasses the runtime wrapper/property loop
+ /// and calls the generated reader directly.
+ ///
+ internal static void RegisterGeneratedReader(Type type, IGeneratedBinaryReader reader)
+ {
+ ArgumentNullException.ThrowIfNull(type);
+ ArgumentNullException.ThrowIfNull(reader);
+ GeneratedReaderRegistry.Register(type, reader);
+ }
+
///
/// ThreadLocal cache for type conversion info.
///
@@ -827,6 +853,17 @@ public static partial class AcBinaryDeserializer
return false;
}
+ ///
+ /// Bridge for generated readers to call ReadValue for unknown/complex/collection property types.
+ /// Reads typeCode + dispatches via TypeReaderTable — same as runtime ReadValue.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static object? ReadValueGenerated(BinaryDeserializationContext context, Type targetType, int depth)
+ where TInput : struct, IBinaryInputBase
+ {
+ return ReadValue(context, targetType, depth);
+ }
+
///
/// Optimized value reader using FrozenDictionary dispatch table.
///
@@ -1088,6 +1125,16 @@ public static partial class AcBinaryDeserializer
return ReadDictionaryAsObject(context, keyType!, valueType!, depth);
}
+ // SGen fast path: generated reader bypasses GetWrapper + CreateInstance + PopulateObject entirely.
+ // Only when not in UseMetadata mode (cross-type CacheMap not known at compile time)
+ // and not in ChainMode (needs post-read identity tracking).
+ if (!context.HasMetadata && !context.IsChainMode)
+ {
+ var generatedReader = GeneratedReaderRegistry.TryGet(targetType);
+ if (generatedReader != null)
+ return generatedReader.ReadObject(context, depth, cacheIndex);
+ }
+
var wrapper = context.GetWrapper(targetType);
return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex);
}
diff --git a/AyCode.Core/Serializers/Binaries/IGeneratedBinaryReader.cs b/AyCode.Core/Serializers/Binaries/IGeneratedBinaryReader.cs
new file mode 100644
index 0000000..5e63aee
--- /dev/null
+++ b/AyCode.Core/Serializers/Binaries/IGeneratedBinaryReader.cs
@@ -0,0 +1,29 @@
+namespace AyCode.Core.Serializers.Binaries;
+
+///
+/// Interface for source-generated binary property readers.
+/// Implementations bypass the runtime property loop, wrapper lookup, and delegate-based setters.
+/// Each generated reader handles all properties of a specific type using direct property access.
+///
+/// Performance gains over runtime path:
+/// - No GetWrapper() dictionary lookup per object (~20-50ns saved)
+/// - No property setter delegate calls (~5-8ns/property saved)
+/// - No AccessorType switch dispatch (~2-3ns/property saved)
+/// - No boxing for value type properties (direct obj.Prop = context.ReadXxx())
+/// - No ReadValue dispatch table for known property types
+///
+internal interface IGeneratedBinaryReader
+{
+ ///
+ /// Creates a new instance and reads all properties from the stream.
+ /// Handles both markerless (no UseMetadata) and markered wire formats.
+ /// UseMetadata=true falls back to runtime path (cross-type CacheMap not known at compile time).
+ ///
+ /// The deserialization context (owns buffer, position, options).
+ /// Current depth in the object graph.
+ /// -1 = not cached, 0+ = register at this cache index for ref tracking.
+ /// Input strategy (ArrayBinaryInput or SequenceBinaryInput).
+ /// The deserialized object, or null if creation failed.
+ object? ReadObject(AcBinaryDeserializer.BinaryDeserializationContext context, int depth, int cacheIndex)
+ where TInput : struct, IBinaryInputBase;
+}