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.
This commit is contained in:
Loretta 2026-02-20 09:55:21 +01:00
parent dcd9783b3b
commit 3e935cad2f
5 changed files with 218 additions and 110 deletions

View File

@ -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<T>");
@ -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<T>");
@ -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<SerializableClassInfo?> 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);
}
/// <summary>
/// Reads EnableMetadataFeature from a type's [AcBinarySerializable] attribute.
/// Returns true (default) if no attribute or enableAllFeatures=true.
/// </summary>
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; }
/// <summary>FNV-1a hash of each property name, in property order</summary>
public int[] PropertyNameHashes { get; }
public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> 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; }
/// <summary>When false, skip inline metadata and use markerless property write for this type.</summary>
public bool EnableMetadata { get; }
public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> 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; }
/// <summary>FNV-1a hashes of collection element type's properties. Only set when ElementHasGeneratedWriter.</summary>
public int[]? ElementPropertyHashes { get; }
/// <summary>When false, child Complex type skips inline metadata in generated code.</summary>
public bool ChildEnableMetadata { get; }
/// <summary>When false, collection element type skips inline metadata in generated code.</summary>
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

View File

@ -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];

View File

@ -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<TOutput>.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];

View File

@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// allowing the deserializer to match properties by name between different types.
/// Default: false (no overhead)
/// </summary>
public bool UseMetadata { get; set; } = false;
public bool UseMetadata { get; set; } = true;
public bool UseGeneratedCode { get; set; } = true;

View File

@ -130,7 +130,13 @@ public abstract class TypeMetadataBase
/// False for sealed value-like types with only primitives - these never need reference tracking.
/// </summary>
public bool NeedsReferenceTracking { get; protected set; }
/// <summary>
/// When false, this type skips inline metadata and uses markerless property write/read
/// — even when global UseMetadata=true. Read from [AcBinarySerializable] attribute.
/// </summary>
public bool EnableMetadataFeature { get; }
/// <summary>
/// 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<AcBinarySerializableAttribute>(inherit: false);
EnableMetadataFeature = serializableAttr == null || serializableAttr.EnableMetadataFeature;
if (serializableAttr is { EnableIdTrackingFeature: false })
{
IsIId = false;