From 3e935cad2f39a8d36753493df947cd127300a1e8 Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 20 Feb 2026 09:55:21 +0100 Subject: [PATCH] Per-type metadata control for binary serialization Adds EnableMetadataFeature to [AcBinarySerializable], allowing types to opt out of inline metadata even when global UseMetadata is enabled. Source generator, serializer, and deserializer now respect this flag for child and element types. Default UseMetadata is set to true. Enables fine-grained control over serialization overhead and compatibility. --- .../AcBinarySourceGenerator.cs | 296 ++++++++++++------ .../Binaries/AcBinaryDeserializer.Populate.cs | 3 +- .../Binaries/AcBinarySerializer.cs | 18 +- .../Binaries/AcBinarySerializerOptions.cs | 2 +- AyCode.Core/Serializers/TypeMetadataBase.cs | 9 +- 5 files changed, 218 insertions(+), 110 deletions(-) diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index 8542828..d65ed3e 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -47,6 +47,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator var enableIdTracking = true; var enableRefHandling = true; var enableInternString = true; + var enableMetadata = true; var binarySerializableAttr = typeSymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == AttributeName); if (binarySerializableAttr != null) @@ -58,10 +59,12 @@ public class AcBinarySourceGenerator : IIncrementalGenerator enableIdTracking = all; enableRefHandling = all; enableInternString = all; + enableMetadata = all; } else if (binarySerializableAttr.ConstructorArguments.Length == 4) { // Four bool ctor: (metadata, idTracking, refHandling, internString) + enableMetadata = (bool)binarySerializableAttr.ConstructorArguments[0].Value!; enableIdTracking = (bool)binarySerializableAttr.ConstructorArguments[1].Value!; enableRefHandling = (bool)binarySerializableAttr.ConstructorArguments[2].Value!; enableInternString = (bool)binarySerializableAttr.ConstructorArguments[3].Value!; @@ -110,6 +113,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator var kind = GetKind(p.Type); bool hasGenWriter = false; bool propTypeIsIId = false; + bool propEnableMetadata = true; string? writerClassName = null; string? propIdTypeName = null; int childTypeNameHash = 0; @@ -127,6 +131,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator if (hasGenWriter) { + // Read child type's EnableMetadataFeature + propEnableMetadata = ReadEnableMetadata(resolvedType); var iidIface = resolvedType.AllInterfaces.FirstOrDefault(i => i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); @@ -153,6 +159,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator PropertyTypeKind elemKind = PropertyTypeKind.Unknown; bool elemHasGenWriter = false; bool elemIsIId = false; + bool elemEnableMetadata = true; string? elemWriterClassName = null; string? elemIdTypeName = null; string? collKind = null; @@ -197,6 +204,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator a.AttributeClass?.ToDisplayString() == AttributeName); if (elemHasGenWriter) { + // Read element type's EnableMetadataFeature + elemEnableMetadata = ReadEnableMetadata(resolvedElem); var elemIidIface = resolvedElem.AllInterfaces.FirstOrDefault(ifc => ifc.IsGenericType && ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId"); @@ -228,7 +237,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName, propIdTypeName, elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName, childTypeNameHash, childPropertyHashes, - elementTypeNameHash, elementPropertyHashes)); + elementTypeNameHash, elementPropertyHashes, + propEnableMetadata, elemEnableMetadata)); } } @@ -253,7 +263,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator var className = BuildFlatName(typeSymbol); var typeNameHash = ComputeFnvHash(typeSymbol.Name); var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray(); - return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes); + return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata); } private static void Execute(ImmutableArray classes, SourceProductionContext context) @@ -299,7 +309,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator foreach (var p in ci.Properties) { sb.AppendLine(); - EmitProp(sb, p, " ", ci.FullTypeName); + EmitProp(sb, p, " ", ci.FullTypeName, ci.EnableMetadata); } sb.AppendLine(" }"); @@ -406,25 +416,33 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine(" }"); } - private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName) + private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enableMetadata) { var a = $"obj.{p.Name}"; // Markerless types: write raw value only, no type marker, no PropertySkip // Matches runtime WritePropertyMarkerless — these have ExpectedTypeCode // NEVER filtered (runtime doesn't filter markerless properties either) - // UseMetadata=true: markerless path NOT available — must use markered path (EmitSkip) - // to match runtime WritePropertyOrSkip behavior (every property gets a type marker byte) + // When EnableMetadataFeature=false: always markerless (no UseMetadata branch needed) + // When EnableMetadataFeature=true: UseMetadata=true uses markered path (EmitSkip) if (IsMarkerless(p.TypeKind)) { - sb.AppendLine($"{i}if (context.UseMetadata)"); - sb.AppendLine($"{i}{{"); - EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i + " "); - sb.AppendLine($"{i}}}"); - sb.AppendLine($"{i}else"); - sb.AppendLine($"{i}{{"); - EmitMarkerless(sb, p.TypeKind, a, i + " "); - sb.AppendLine($"{i}}}"); + if (!enableMetadata) + { + // Per-type metadata disabled — always markerless, no branch + EmitMarkerless(sb, p.TypeKind, a, i); + } + else + { + sb.AppendLine($"{i}if (context.UseMetadata)"); + sb.AppendLine($"{i}{{"); + EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i + " "); + sb.AppendLine($"{i}}}"); + sb.AppendLine($"{i}else"); + sb.AppendLine($"{i}{{"); + EmitMarkerless(sb, p.TypeKind, a, i + " "); + sb.AppendLine($"{i}}}"); + } return; } @@ -759,49 +777,79 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} else"); sb.AppendLine($"{i} {{"); - // UseMetadata: register type for first/repeated tracking via slot (no GetWrapper needed) - sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && context.RegisterMetadataTypeBySlot({writer}.s_metadataSlot);"); + if (!p.ChildEnableMetadata) + { + // Child type has EnableMetadataFeature=false — no metadata, always Object marker + // Inline ref tracking still needed for IId/All mode + var refGuard = p.IsIId + ? "context.ReferenceHandling != ReferenceHandlingMode.None" + : "context.ReferenceHandling == ReferenceHandlingMode.All"; + sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{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)pe_{p.Name}.CacheMapIndex);"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); + 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({a}, context, {nextDepth});"); + sb.AppendLine($"{i} }}"); + } + else + { + // UseMetadata: register type for first/repeated tracking via slot (no GetWrapper needed) + sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && context.RegisterMetadataTypeBySlot({writer}.s_metadataSlot);"); - // 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"; - sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))"); - sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)"); - sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);"); - sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{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)pe_{p.Name}.CacheMapIndex);"); - EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{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)pe_{p.Name}.CacheMapIndex);"); - sb.AppendLine($"{i} }}"); - sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); - 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.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});"); - sb.AppendLine($"{i} }}"); + // 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"; + sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)pe_{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)pe_{p.Name}.CacheMapIndex);"); + EmitInlineMetadata(sb, p.ChildTypeNameHash, p.ChildPropertyHashes!, $"isFirstMeta_{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)pe_{p.Name}.CacheMapIndex);"); + sb.AppendLine($"{i} }}"); + sb.AppendLine($"{i} {writer}.Instance.WriteProperties({a}, context, {nextDepth});"); + 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.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});"); + sb.AppendLine($"{i} }}"); + } sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); @@ -878,49 +926,76 @@ 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; }}"); - // UseMetadata: register element type for first/repeated tracking - sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && context.RegisterMetadataTypeBySlot({writer}.s_metadataSlot);"); - // 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) + { + // 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} }}"); + } + else + { + // UseMetadata: register element type for first/repeated tracking + sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && context.RegisterMetadataTypeBySlot({writer}.s_metadataSlot);"); + + 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} }}"); + } sb.AppendLine($"{i} }}"); sb.AppendLine($"{i}}}"); @@ -1077,6 +1152,22 @@ public class AcBinarySourceGenerator : IIncrementalGenerator return string.Join("_", parts); } + /// + /// Reads EnableMetadataFeature from a type's [AcBinarySerializable] attribute. + /// Returns true (default) if no attribute or enableAllFeatures=true. + /// + private static bool ReadEnableMetadata(ITypeSymbol type) + { + var attr = type.GetAttributes().FirstOrDefault(a => + a.AttributeClass?.ToDisplayString() == AttributeName); + if (attr == null) return true; + if (attr.ConstructorArguments.Length == 1) + return (bool)attr.ConstructorArguments[0].Value!; + if (attr.ConstructorArguments.Length == 4) + return (bool)attr.ConstructorArguments[0].Value!; + return true; + } + #region FNV-1a Hash (compile-time) private static int ComputeFnvHash(string value) @@ -1222,8 +1313,10 @@ internal sealed class SerializableClassInfo public int TypeNameHash { get; } /// FNV-1a hash of each property name, in property order public int[] PropertyNameHashes { get; } - public SerializableClassInfo(string ns, string cn, string ftn, List p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes) - { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; } + /// When false, skip inline metadata and use markerless property write for this type. + public bool EnableMetadata { get; } + public SerializableClassInfo(string ns, string cn, string ftn, List p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata) + { Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; } } internal sealed class PropInfo @@ -1278,13 +1371,18 @@ internal sealed class PropInfo public int ElementTypeNameHash { get; } /// FNV-1a hashes of collection element type's properties. Only set when ElementHasGeneratedWriter. public int[]? ElementPropertyHashes { get; } + /// When false, child Complex type skips inline metadata in generated code. + public bool ChildEnableMetadata { get; } + /// When false, collection element type skips inline metadata in generated code. + public bool ElementEnableMetadata { get; } public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable, bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null, string? idTypeName = null, PropertyTypeKind elementKind = PropertyTypeKind.Unknown, bool elementHasGenWriter = false, bool elementIsIId = false, string? elementWriterClassName = null, string? elementIdTypeName = null, string? collectionKind = null, string? elementFullTypeName = null, int childTypeNameHash = 0, int[]? childPropertyHashes = null, - int elementTypeNameHash = 0, int[]? elementPropertyHashes = null) + int elementTypeNameHash = 0, int[]? elementPropertyHashes = null, + bool childEnableMetadata = true, bool elementEnableMetadata = true) { Name = n; TypeName = tn; @@ -1306,6 +1404,8 @@ internal sealed class PropInfo ChildPropertyHashes = childPropertyHashes; ElementTypeNameHash = elementTypeNameHash; ElementPropertyHashes = elementPropertyHashes; + ChildEnableMetadata = childEnableMetadata; + ElementEnableMetadata = elementEnableMetadata; // Mirror runtime _interningFlags computation from BinaryPropertyAccessorBase int flags = 0; if (stringInternAttr == true) flags |= (1 << 1); // Attribute bit diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index f8046d5..545e3c8 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -128,10 +128,11 @@ public static partial class AcBinaryDeserializer SkipDefaultWrite = skipDefaultWrite }; - if (!context.HasMetadata) + if (!context.HasMetadata || !metadata.EnableMetadataFeature) { // Markerless loop: properties with ExpectedTypeCode read raw values directly. // Properties without ExpectedTypeCode (bool, enum, string, object) use standard marker path. + // Also used when EnableMetadataFeature=false on the type (per-type metadata opt-out). for (int i = 0; i < propCount; i++) { var propInfo = properties[i]; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 89f1a80..0aaf039 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -1038,15 +1038,14 @@ public static partial class AcBinarySerializer { var metadata = wrapper.Metadata; - // Wire format: - // - UseMetadata=false: [Object][props...] - // - UseMetadata=true, első: [ObjectWithMetadata][propNameHash (4b)][propCount (VarUInt)][hash0..N][props...] - // - UseMetadata=true, ismételt: [ObjectWithMetadata][propNameHash (4b)][props...] - // ObjectRef: [ObjectRef][cacheIndex] + // Per-type metadata flag: when EnableMetadataFeature=false on [AcBinarySerializable], + // skip inline metadata and use markerless property write — even when global UseMetadata=true. + // Deserializer must have the same attribute on the type (developer responsibility). + var useMetaForType = context.UseMetadata && metadata.EnableMetadataFeature; // UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking) var isFirstMetadataOccurrence = false; - if (context.UseMetadata) + if (useMetaForType) { isFirstMetadataOccurrence = BinarySerializationContext.RegisterMetadataType(wrapper); } @@ -1076,7 +1075,7 @@ public static partial class AcBinarySerializer // Marker kiírása: // - Cached object first occurrence: ObjectRefFirst/ObjectWithMetadataRefFirst + cacheIndex // - Non-cached: Object/ObjectWithMetadata - if (context.UseMetadata) + if (useMetaForType) { if (cachedObjectCacheIndex >= 0) { @@ -1122,12 +1121,13 @@ public static partial class AcBinarySerializer return; } } - - if (!context.UseMetadata) + + if (!useMetaForType) { // Markerless loop: no extra branching per property for the common case. // Properties with ExpectedTypeCode write raw values (no type marker, no skip). // Properties without ExpectedTypeCode (bool, enum, string, object) use the standard path. + // Also used when EnableMetadataFeature=false on the type (per-type metadata opt-out). for (var i = 0; i < propCount; i++) { var prop = properties[i]; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index 6b01f75..e0fff73 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// allowing the deserializer to match properties by name between different types. /// Default: false (no overhead) /// - public bool UseMetadata { get; set; } = false; + public bool UseMetadata { get; set; } = true; public bool UseGeneratedCode { get; set; } = true; diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs index 5c9123d..b95eab6 100644 --- a/AyCode.Core/Serializers/TypeMetadataBase.cs +++ b/AyCode.Core/Serializers/TypeMetadataBase.cs @@ -130,7 +130,13 @@ public abstract class TypeMetadataBase /// False for sealed value-like types with only primitives - these never need reference tracking. /// public bool NeedsReferenceTracking { get; protected set; } - + + /// + /// When false, this type skips inline metadata and uses markerless property write/read + /// — even when global UseMetadata=true. Read from [AcBinarySerializable] attribute. + /// + public bool EnableMetadataFeature { get; } + /// /// True if this type is a primitive, string, enum, Guid, DateTime, etc. /// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path. @@ -215,6 +221,7 @@ public abstract class TypeMetadataBase } var serializableAttr = type.GetCustomAttribute(inherit: false); + EnableMetadataFeature = serializableAttr == null || serializableAttr.EnableMetadataFeature; if (serializableAttr is { EnableIdTrackingFeature: false }) { IsIId = false;