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;