diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
index 8d47eed..5984d4f 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
@@ -77,108 +77,196 @@ public static partial class AcBinaryDeserializer
// Non-UseMetadata: properties.Length a target property-k száma (source == target)
var propCount = cacheMap?.Length ?? properties.Length;
- for (int i = 0; i < propCount; i++)
+ if (!context.HasMetadata)
{
- var propInfo = cacheMap != null ? cacheMap[i] : properties[i];
-
- var peekCode = context.PeekByte();
-
- // Nincs megfelelő target property → skip
- if (propInfo == null)
+ // Markerless loop: properties with ExpectedTypeCode read raw values directly.
+ // Properties without ExpectedTypeCode (bool, enum, string, object) use standard marker path.
+ for (int i = 0; i < propCount; i++)
{
- SkipValue(ref context, metadata);
- continue;
- }
+ var propInfo = properties[i];
- // Skip marker - property has default/null value
- if (peekCode == BinaryTypeCode.PropertySkip)
- {
- context.ReadByte(); // consume Skip marker
-
- // Populate mode: overwrite with default (existing object may have non-default values)
- // Deserialize mode: skip write (new object already has defaults from CreateInstance)
- if (!skipDefaultWrite)
+ if (propInfo.ExpectedTypeCode.HasValue)
{
- SetPropertyToDefault(target, propInfo);
- }
- continue;
- }
-
- // Null values - always set
- if (peekCode == BinaryTypeCode.Null)
- {
- context.ReadByte(); // consume Null marker
- propInfo.SetValue(target, null);
- continue;
- }
-
- // Handle collections
- if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
- {
- var existingCollection = propInfo.GetValue(target);
- if (existingCollection is IList existingList)
- {
- context.ReadByte(); // consume Array marker
-
- // Merge mode with IId collection: use merge logic
- if (isMergeMode && propInfo.IsIIdCollection)
- {
- MergeIIdCollection(ref context, existingList, propInfo, nextDepth);
- }
- else
- {
- // Normal populate: replace collection contents
- PopulateListOptimized(ref context, existingList, propInfo, nextDepth);
- }
+ ReadAndSetMarkerlessValue(ref context, target, propInfo);
continue;
}
- }
- // Handle nested complex objects - reuse existing if available
- if ((peekCode == BinaryTypeCode.Object || peekCode == BinaryTypeCode.ObjectWithMetadata) && propInfo.IsComplexType)
+ // Non-markerless properties: standard marker-based read
+ PopulatePropertyWithMarker(ref context, target, propInfo, metadata, nextDepth, isMergeMode, skipDefaultWrite, i, depth);
+ }
+ }
+ else
+ {
+ // UseMetadata=true loop — UNCHANGED, zero extra overhead
+ for (int i = 0; i < propCount; i++)
{
- var existingObj = propInfo.GetValue(target);
- if (existingObj != null)
+ var propInfo = cacheMap != null ? cacheMap[i] : properties[i];
+
+ PopulatePropertyWithMarker(ref context, target, propInfo, metadata, nextDepth, isMergeMode, skipDefaultWrite, i, depth);
+ }
+ }
+ }
+
+ ///
+ /// Standard marker-based property read. Extracted to avoid duplicating logic in both loops.
+ ///
+ private static void PopulatePropertyWithMarker(
+ ref BinaryDeserializationContext context,
+ object target,
+ BinaryPropertySetterBase? propInfo,
+ BinaryDeserializeTypeMetadata metadata,
+ int nextDepth,
+ bool isMergeMode,
+ bool skipDefaultWrite,
+ int propertyIndex,
+ int depth)
+ {
+ var peekCode = context.PeekByte();
+
+ // Nincs megfelelő target property → skip
+ if (propInfo == null)
+ {
+ SkipValue(ref context, metadata);
+ return;
+ }
+
+ // Skip marker - property has default/null value
+ if (peekCode == BinaryTypeCode.PropertySkip)
+ {
+ context.ReadByte(); // consume Skip marker
+
+ // Populate mode: overwrite with default (existing object may have non-default values)
+ // Deserialize mode: skip write (new object already has defaults from CreateInstance)
+ if (!skipDefaultWrite)
+ {
+ SetPropertyToDefault(target, propInfo);
+ }
+ return;
+ }
+
+ // Null values - always set
+ if (peekCode == BinaryTypeCode.Null)
+ {
+ context.ReadByte(); // consume Null marker
+ propInfo.SetValue(target, null);
+ return;
+ }
+
+ // Handle collections
+ if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
+ {
+ var existingCollection = propInfo.GetValue(target);
+ if (existingCollection is IList existingList)
+ {
+ context.ReadByte(); // consume Array marker
+
+ // Merge mode with IId collection: use merge logic
+ if (isMergeMode && propInfo.IsIIdCollection)
{
- // ReadValue kezeli mindkét markert
- var nestedValue = ReadValue(ref context, propInfo.PropertyType, nextDepth);
- if (nestedValue != null)
- {
- var nestedMeta = context.ContextClass.GetWrapper(propInfo.PropertyType).Metadata;
- CopyProperties(nestedValue, existingObj, nestedMeta);
- }
- continue;
+ MergeIIdCollection(ref context, existingList, propInfo, nextDepth);
}
+ else
+ {
+ // Normal populate: replace collection contents
+ PopulateListOptimized(ref context, existingList, propInfo, nextDepth);
+ }
+ return;
}
+ }
- // Default: read value and set (for primitives, strings, new objects)
- var positionBeforeRead = context.Position;
- try
+ // Handle nested complex objects - reuse existing if available
+ if ((peekCode == BinaryTypeCode.Object || peekCode == BinaryTypeCode.ObjectWithMetadata) && propInfo.IsComplexType)
+ {
+ var existingObj = propInfo.GetValue(target);
+ if (existingObj != null)
{
- // Use typed setters for primitives and strings to avoid ReadValue dispatch
- if (propInfo.AccessorType != PropertyAccessorType.Object &&
- TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
- continue;
+ // ReadValue kezeli mindkét markert
+ var nestedValue = ReadValue(ref context, propInfo.PropertyType, nextDepth);
+ if (nestedValue != null)
+ {
+ var nestedMeta = context.ContextClass.GetWrapper(propInfo.PropertyType).Metadata;
+ CopyProperties(nestedValue, existingObj, nestedMeta);
+ }
+ return;
+ }
+ }
- var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
- propInfo.SetValue(target, value);
- }
- catch (InvalidCastException ex)
- {
- var targetType = target.GetType();
- throw new AcBinaryDeserializationException(
- $"Type mismatch for property '{propInfo.Name}' (index {i}) on '{targetType.Name}'. " +
- $"Expected type: '{propInfo.PropertyType.FullName}'. " +
- $"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
- $"Position before read: {positionBeforeRead}, current: {context.Position}. " +
- $"Depth: {depth}. " +
- $"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " +
- $"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " +
- $"Error: {ex.Message}",
- positionBeforeRead,
- propInfo.PropertyType,
- ex);
- }
+ // Default: read value and set (for primitives, strings, new objects)
+ var positionBeforeRead = context.Position;
+ try
+ {
+ // Use typed setters for primitives and strings to avoid ReadValue dispatch
+ if (propInfo.AccessorType != PropertyAccessorType.Object &&
+ TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
+ return;
+
+ var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
+ propInfo.SetValue(target, value);
+ }
+ catch (InvalidCastException ex)
+ {
+ var targetType = target.GetType();
+ throw new AcBinaryDeserializationException(
+ $"Type mismatch for property '{propInfo.Name}' (index {propertyIndex}) on '{targetType.Name}'. " +
+ $"Expected type: '{propInfo.PropertyType.FullName}'. " +
+ $"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
+ $"Position before read: {positionBeforeRead}, current: {context.Position}. " +
+ $"Depth: {depth}. " +
+ $"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " +
+ $"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " +
+ $"Error: {ex.Message}",
+ positionBeforeRead,
+ propInfo.PropertyType,
+ ex);
+ }
+ }
+
+ ///
+ /// Reads a raw value without type marker from stream (markerless mode, UseMetadata=false).
+ /// The property's type is known from metadata — no type code in the stream.
+ /// Only called for non-nullable value types with ExpectedTypeCode set.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void ReadAndSetMarkerlessValue(ref BinaryDeserializationContext context, object target, BinaryPropertySetterBase propInfo)
+ {
+ switch (propInfo.AccessorType)
+ {
+ case PropertyAccessorType.Int32:
+ propInfo.SetInt32(target, context.ReadVarInt());
+ return;
+ case PropertyAccessorType.Int64:
+ propInfo.SetInt64(target, context.ReadVarLong());
+ return;
+ case PropertyAccessorType.Double:
+ propInfo.SetDouble(target, context.ReadDoubleUnsafe());
+ return;
+ case PropertyAccessorType.Single:
+ propInfo.SetSingle(target, context.ReadSingleUnsafe());
+ return;
+ case PropertyAccessorType.Decimal:
+ propInfo.SetDecimal(target, context.ReadDecimalUnsafe());
+ return;
+ case PropertyAccessorType.DateTime:
+ propInfo.SetDateTime(target, context.ReadDateTimeUnsafe());
+ return;
+ case PropertyAccessorType.Guid:
+ propInfo.SetGuid(target, context.ReadGuidUnsafe());
+ return;
+ case PropertyAccessorType.Byte:
+ propInfo.SetByte(target, context.ReadByte());
+ return;
+ case PropertyAccessorType.Int16:
+ propInfo.SetInt16(target, context.ReadInt16Unsafe());
+ return;
+ case PropertyAccessorType.UInt16:
+ propInfo.SetUInt16(target, context.ReadUInt16Unsafe());
+ return;
+ case PropertyAccessorType.UInt32:
+ propInfo.SetUInt32(target, context.ReadVarUInt());
+ return;
+ case PropertyAccessorType.UInt64:
+ propInfo.SetUInt64(target, context.ReadVarULong());
+ return;
}
}
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
index 20f8394..aa4964e 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
@@ -944,27 +944,46 @@ public static partial class AcBinarySerializer
var nextDepth = depth + 1;
var properties = metadata.Properties;
var propCount = properties.Length;
-
- // Single-pass serialization with SKIP markers
- // - No property count needed (fixed property order)
- // - No property indices needed (sequential order)
- // - Single getter call per property
- // - Write value OR skip marker in one operation
var hasPropertyFilter = context.HasPropertyFilter;
- for (var i = 0; i < propCount; i++)
+ if (!context.UseMetadata)
{
- var prop = properties[i];
-
- // Skip if filter says no - write skip marker
- if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
+ // 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.
+ for (var i = 0; i < propCount; i++)
{
- context.WriteByte(BinaryTypeCode.PropertySkip);
- continue;
+ var prop = properties[i];
+
+ if (prop.ExpectedTypeCode.HasValue)
+ {
+ WritePropertyMarkerless(value, prop, context);
+ }
+ else if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
+ {
+ context.WriteByte(BinaryTypeCode.PropertySkip);
+ }
+ else
+ {
+ WritePropertyOrSkip(value, prop, context, nextDepth);
+ }
+ }
+ }
+ else
+ {
+ // UseMetadata=true loop — UNCHANGED, zero extra overhead
+ for (var i = 0; i < propCount; i++)
+ {
+ var prop = properties[i];
+
+ if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
+ {
+ context.WriteByte(BinaryTypeCode.PropertySkip);
+ continue;
+ }
+
+ WritePropertyOrSkip(value, prop, context, nextDepth);
}
-
- // Write property value OR skip marker (single operation, single getter call)
- WritePropertyOrSkip(value, prop, context, nextDepth);
}
}
@@ -1283,6 +1302,55 @@ public static partial class AcBinarySerializer
}
}
+ ///
+ /// Writes a property value without type marker byte (markerless mode, UseMetadata=false).
+ /// All values are written including defaults — no PropertySkip markers.
+ /// Only called for non-nullable value types with ExpectedTypeCode set.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WritePropertyMarkerless(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context)
+ {
+ switch (prop.AccessorType)
+ {
+ case PropertyAccessorType.Int32:
+ context.WriteVarInt(prop.GetInt32(obj));
+ return;
+ case PropertyAccessorType.Int64:
+ context.WriteVarLong(prop.GetInt64(obj));
+ return;
+ case PropertyAccessorType.Double:
+ context.WriteRaw(prop.GetDouble(obj));
+ return;
+ case PropertyAccessorType.Single:
+ context.WriteRaw(prop.GetSingle(obj));
+ return;
+ case PropertyAccessorType.Decimal:
+ context.WriteDecimalBits(prop.GetDecimal(obj));
+ return;
+ case PropertyAccessorType.DateTime:
+ context.WriteDateTimeBits(prop.GetDateTime(obj));
+ return;
+ case PropertyAccessorType.Guid:
+ context.WriteGuidBits(prop.GetGuid(obj));
+ return;
+ case PropertyAccessorType.Byte:
+ context.WriteByte(prop.GetByte(obj));
+ return;
+ case PropertyAccessorType.Int16:
+ context.WriteRaw(prop.GetInt16(obj));
+ return;
+ case PropertyAccessorType.UInt16:
+ context.WriteRaw(prop.GetUInt16(obj));
+ return;
+ case PropertyAccessorType.UInt32:
+ context.WriteVarUInt(prop.GetUInt32(obj));
+ return;
+ case PropertyAccessorType.UInt64:
+ context.WriteVarULong(prop.GetUInt64(obj));
+ return;
+ }
+ }
+
#endregion
#region Specialized Array Writers
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
index 927b822..f58ef46 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; init; } = true;
+ public bool UseMetadata { get; init; } = false;
///
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs
index b68b09a..1450eb4 100644
--- a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs
+++ b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs
@@ -31,6 +31,15 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
///
public Func