diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs
index cd61bd8..06d72c3 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs
@@ -333,7 +333,18 @@ public static partial class AcBinaryDeserializer
if (existingObj != null)
{
context.ReadByte(); // consume Object marker
- PopulateObjectNested(ref context, existingObj, propInfo.PropertyType, nextDepth);
+
+ // Handle ref ID if present
+ if (context.HasReferenceHandling)
+ {
+ var refId = context.ReadVarInt();
+ if (refId > 0)
+ {
+ context.RegisterObject(refId, existingObj);
+ }
+ }
+
+ PopulateObjectCore(ref context, existingObj, GetTypeMetadata(propInfo.PropertyType), nextDepth, skipDefaultWrite: false);
return;
}
}
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
index 22e3bed..ab61064 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
@@ -12,11 +12,15 @@ public static partial class AcBinaryDeserializer
{
#region Populate Object Methods
+ ///
+ /// Populate object with automatic mode detection from context.
+ /// Uses IsMergeMode to determine merge behavior for IId collections.
+ ///
private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
{
var metadata = GetTypeMetadata(targetType);
- // Skip ref ID if present
+ // Handle ref ID if present
if (context.HasReferenceHandling)
{
var refId = context.ReadVarInt();
@@ -26,30 +30,32 @@ public static partial class AcBinaryDeserializer
}
}
- PopulateObject(ref context, target, metadata, depth);
+ PopulateObjectCore(ref context, target, metadata, depth, skipDefaultWrite: false);
}
- private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth, bool skipDefaultWrite = false)
+ ///
+ /// Core populate logic shared by all populate paths.
+ ///
+ /// Deserialization context
+ /// Target object to populate
+ /// Type metadata
+ /// Current depth
+ ///
+ /// true = Deserialize mode - object just created, properties already at default, skip writing defaults
+ /// false = Populate mode - existing object, must overwrite properties with default values
+ ///
+ private static void PopulateObjectCore(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth, bool skipDefaultWrite)
{
- // NEW FORMAT: Fixed property order with SKIP markers
- // - No property count (iterate all properties in metadata order)
- // - No property indices (use sequential PropertyIndex = i)
- // - PropertySkip marker indicates default/null value
- //
- // skipDefaultWrite:
- // true = Deserialize mode - object just created, properties already at default, skip writing
- // false = Populate mode - existing object, must overwrite properties with default values
-
var properties = metadata.PropertiesArray;
var nextDepth = depth + 1;
+ var isMergeMode = context.IsMergeMode;
for (int i = 0; i < properties.Length; i++)
{
var propInfo = properties[i];
-
var peekCode = context.PeekByte();
- // OPTIMIZATION: Skip marker - property has default/null value
+ // Skip marker - property has default/null value
if (peekCode == BinaryTypeCode.PropertySkip)
{
context.ReadByte(); // consume Skip marker
@@ -60,11 +66,10 @@ public static partial class AcBinaryDeserializer
{
SetPropertyToDefault(target, propInfo);
}
-
continue;
}
- // OPTIMIZATION: Skip null values early - no GetValue call needed!
+ // Null values - always set
if (peekCode == BinaryTypeCode.Null)
{
context.ReadByte(); // consume Null marker
@@ -72,37 +77,57 @@ public static partial class AcBinaryDeserializer
continue;
}
- // Handle nested complex objects - reuse existing if available
- // ONLY call GetValue if we actually have an Object coming in
- if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType)
- {
- var existingObj = propInfo.GetValue(target);
- if (existingObj != null)
- {
- context.ReadByte(); // consume Object marker
- PopulateObjectNested(ref context, existingObj, propInfo.PropertyType, nextDepth);
- continue;
- }
- }
-
- // Handle collections - reuse existing collection and populate items
- // ONLY call GetValue if we actually have an Array coming in
+ // Handle collections
if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
{
var existingCollection = propInfo.GetValue(target);
if (existingCollection is IList existingList)
{
context.ReadByte(); // consume Array marker
- PopulateListOptimized(ref context, existingList, propInfo, nextDepth);
+
+ // 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);
+ }
continue;
}
}
- // Default: read value and set (for primitives, strings, null cases)
+ // Handle nested complex objects - reuse existing if available
+ if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType)
+ {
+ var existingObj = propInfo.GetValue(target);
+ if (existingObj != null)
+ {
+ context.ReadByte(); // consume Object marker
+
+ // Handle ref ID if present
+ if (context.HasReferenceHandling)
+ {
+ var refId = context.ReadVarInt();
+ if (refId > 0)
+ {
+ context.RegisterObject(refId, existingObj);
+ }
+ }
+
+ // Recursively populate - existing object, don't skip defaults
+ PopulateObjectCore(ref context, existingObj, GetTypeMetadata(propInfo.PropertyType), nextDepth, skipDefaultWrite: false);
+ continue;
+ }
+ }
+
+ // Default: read value and set (for primitives, strings, new objects)
var positionBeforeRead = context.Position;
try
{
- // OPTIMIZATION: Use typed setters for primitives to avoid boxing
+ // Use typed setters for primitives to avoid boxing
if (TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
continue;
@@ -111,12 +136,9 @@ public static partial class AcBinaryDeserializer
}
catch (InvalidCastException ex)
{
- // Only get type info when needed for error message (cold path)
var targetType = target.GetType();
- var targetTypeName = targetType.Name;
-
throw new AcBinaryDeserializationException(
- $"Type mismatch for property '{propInfo.Name}' (index {i}) on '{targetTypeName}'. " +
+ $"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}. " +
@@ -132,91 +154,12 @@ public static partial class AcBinaryDeserializer
}
///
- /// Populate nested object, reusing existing object and recursively updating properties.
+ /// Called from ReadObject for new instances - just calls PopulateObjectCore with skipDefaultWrite=true.
///
- private static void PopulateObjectNested(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth, bool skipDefaultWrite)
{
- var metadata = GetTypeMetadata(targetType);
-
- // Handle ref ID if present
- if (context.HasReferenceHandling)
- {
- var refId = context.ReadVarInt();
- if (refId > 0)
- {
- context.RegisterObject(refId, target);
- }
- }
-
- PopulateObject(ref context, target, metadata, depth);
- }
-
- private static void PopulateObjectMerge(ref BinaryDeserializationContext context, object target, Type targetType, int depth, bool skipDefaultWrite = false)
- {
- var metadata = GetTypeMetadata(targetType);
-
- // Skip ref ID if present
- if (context.HasReferenceHandling)
- {
- var refId = context.ReadVarInt();
- if (refId > 0)
- {
- context.RegisterObject(refId, target);
- }
- }
-
- // NEW FORMAT: Fixed property order with SKIP markers
- var properties = metadata.PropertiesArray;
- var nextDepth = depth + 1;
-
- for (int i = 0; i < properties.Length; i++)
- {
- var propInfo = properties[i];
-
- var peekCode = context.PeekByte();
-
- // OPTIMIZATION: Skip marker - property has default/null value
- if (peekCode == BinaryTypeCode.PropertySkip)
- {
- context.ReadByte(); // consume Skip marker
-
- // Populate mode: overwrite with default (same as PopulateObject)
- // Deserialize mode: skip write (new object already has defaults)
- if (!skipDefaultWrite)
- {
- SetPropertyToDefault(target, propInfo);
- }
-
- continue;
- }
-
- // Handle IId collection merge
- if (propInfo.IsIIdCollection && peekCode == BinaryTypeCode.Array)
- {
- var existingCollection = propInfo.GetValue(target);
- if (existingCollection is IList existingList)
- {
- context.ReadByte(); // consume Array marker
- MergeIIdCollection(ref context, existingList, propInfo, depth);
- continue;
- }
- }
-
- // Handle nested object merge
- if (peekCode == BinaryTypeCode.Object && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
- {
- var existingObj = propInfo.GetValue(target);
- if (existingObj != null)
- {
- context.ReadByte(); // consume Object marker
- PopulateObjectMerge(ref context, existingObj, propInfo.PropertyType, nextDepth);
- continue;
- }
- }
-
- var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
- propInfo.SetValue(target, value);
- }
+ PopulateObjectCore(ref context, target, metadata, depth, skipDefaultWrite);
}
#endregion
@@ -237,7 +180,7 @@ public static partial class AcBinaryDeserializer
var count = (int)context.ReadVarUInt();
var nextDepth = depth + 1;
- // ChainMode: Use cached IId info from element type metadata (no runtime reflection!)
+ // ChainMode: Use cached IId info from element type metadata
BinaryDeserializeTypeMetadata? elementMetadata = null;
if (context.IsChainMode && IsComplexType(elementType))
{
@@ -257,7 +200,6 @@ public static partial class AcBinaryDeserializer
{
if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj))
{
- // Use existing object instead of new one
targetList.Add(existingObj);
continue;
}
@@ -273,31 +215,6 @@ public static partial class AcBinaryDeserializer
}
}
- private static void PopulateListMerge(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth)
- {
- var elementType = GetCollectionElementType(listType) ?? typeof(object);
-
- // Use cached metadata instead of runtime reflection
- if (!IsComplexType(elementType))
- {
- // Not a complex type, just replace
- PopulateList(ref context, targetList, listType, depth);
- return;
- }
-
- var elementMetadata = GetTypeMetadata(elementType);
-
- if (!elementMetadata.IsIId || elementMetadata.IdGetter == null || elementMetadata.IdType == null)
- {
- // No IId, just replace
- PopulateList(ref context, targetList, listType, depth);
- return;
- }
-
- // IId merge logic using cached metadata
- MergeIIdCollectionWithMetadata(ref context, targetList, elementType, elementMetadata, depth);
- }
-
///
/// Optimized list populate that reuses existing items when possible.
///
@@ -338,7 +255,7 @@ public static partial class AcBinaryDeserializer
}
// Populate mode: existing item, must overwrite with defaults
- PopulateObject(ref context, existingItem, elementMetadata, nextDepth, skipDefaultWrite: false);
+ PopulateObjectCore(ref context, existingItem, elementMetadata, nextDepth, skipDefaultWrite: false);
continue;
}
}
@@ -348,12 +265,10 @@ public static partial class AcBinaryDeserializer
if (i < existingCount)
{
- // Replace existing item
existingList[i] = value;
}
else
{
- // Add new item
existingList.Add(value);
}
}
@@ -375,7 +290,8 @@ public static partial class AcBinaryDeserializer
#region Merge Methods
///
- /// IId collection merge using cached property info (for property-based merge).
+ /// IId collection merge using cached property info.
+ /// Matches items by Id, updates existing, adds new, optionally removes orphans.
///
private static void MergeIIdCollection(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth)
{
@@ -430,7 +346,7 @@ public static partial class AcBinaryDeserializer
var newItem = CreateInstance(elementType, elementMetadata);
if (newItem == null) continue;
- // Read ref ID if present
+ // Handle ref ID if present
if (context.HasReferenceHandling)
{
var refId = context.ReadVarInt();
@@ -439,7 +355,7 @@ public static partial class AcBinaryDeserializer
}
// Deserialize mode: new item just created, skip writing defaults
- PopulateObject(ref context, newItem, elementMetadata, nextDepth, skipDefaultWrite: true);
+ PopulateObjectCore(ref context, newItem, elementMetadata, nextDepth, skipDefaultWrite: true);
var itemId = idGetter(newItem);
if (itemId != null && !IsDefaultValue(itemId, idType))
@@ -449,7 +365,7 @@ public static partial class AcBinaryDeserializer
if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
{
- // Copy properties to existing item
+ // Copy properties to existing item (preserves reference)
CopyProperties(newItem, existingItem, elementMetadata);
continue;
}
@@ -461,7 +377,6 @@ public static partial class AcBinaryDeserializer
// Remove orphaned items (items in destination but not in source)
if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
{
- // Find items to remove (those not in sourceIds)
var itemsToRemove = new List