Refactor and unify object/collection deserialization logic
Refactored deserialization to use a unified PopulateObjectCore method, replacing PopulateObjectMerge and PopulateObjectNested to reduce duplication. Improved and centralized IId collection merge logic, with automatic detection and handling in merge mode. Reference ID registration is now consistent for reused objects and collections. Simplified SetPropertyToDefault to set collections to null. Updated documentation and clarified merge mode handling throughout. These changes improve maintainability, consistency, and robustness of deserialization.
This commit is contained in:
parent
fd3487c12b
commit
1f2f06ff8c
|
|
@ -333,7 +333,18 @@ public static partial class AcBinaryDeserializer
|
||||||
if (existingObj != null)
|
if (existingObj != null)
|
||||||
{
|
{
|
||||||
context.ReadByte(); // consume Object marker
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,15 @@ public static partial class AcBinaryDeserializer
|
||||||
{
|
{
|
||||||
#region Populate Object Methods
|
#region Populate Object Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populate object with automatic mode detection from context.
|
||||||
|
/// Uses IsMergeMode to determine merge behavior for IId collections.
|
||||||
|
/// </summary>
|
||||||
private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
|
private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
|
||||||
{
|
{
|
||||||
var metadata = GetTypeMetadata(targetType);
|
var metadata = GetTypeMetadata(targetType);
|
||||||
|
|
||||||
// Skip ref ID if present
|
// Handle ref ID if present
|
||||||
if (context.HasReferenceHandling)
|
if (context.HasReferenceHandling)
|
||||||
{
|
{
|
||||||
var refId = context.ReadVarInt();
|
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)
|
/// <summary>
|
||||||
|
/// Core populate logic shared by all populate paths.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Deserialization context</param>
|
||||||
|
/// <param name="target">Target object to populate</param>
|
||||||
|
/// <param name="metadata">Type metadata</param>
|
||||||
|
/// <param name="depth">Current depth</param>
|
||||||
|
/// <param name="skipDefaultWrite">
|
||||||
|
/// true = Deserialize mode - object just created, properties already at default, skip writing defaults
|
||||||
|
/// false = Populate mode - existing object, must overwrite properties with default values
|
||||||
|
/// </param>
|
||||||
|
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 properties = metadata.PropertiesArray;
|
||||||
var nextDepth = depth + 1;
|
var nextDepth = depth + 1;
|
||||||
|
var isMergeMode = context.IsMergeMode;
|
||||||
|
|
||||||
for (int i = 0; i < properties.Length; i++)
|
for (int i = 0; i < properties.Length; i++)
|
||||||
{
|
{
|
||||||
var propInfo = properties[i];
|
var propInfo = properties[i];
|
||||||
|
|
||||||
var peekCode = context.PeekByte();
|
var peekCode = context.PeekByte();
|
||||||
|
|
||||||
// OPTIMIZATION: Skip marker - property has default/null value
|
// Skip marker - property has default/null value
|
||||||
if (peekCode == BinaryTypeCode.PropertySkip)
|
if (peekCode == BinaryTypeCode.PropertySkip)
|
||||||
{
|
{
|
||||||
context.ReadByte(); // consume Skip marker
|
context.ReadByte(); // consume Skip marker
|
||||||
|
|
@ -60,11 +66,10 @@ public static partial class AcBinaryDeserializer
|
||||||
{
|
{
|
||||||
SetPropertyToDefault(target, propInfo);
|
SetPropertyToDefault(target, propInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// OPTIMIZATION: Skip null values early - no GetValue call needed!
|
// Null values - always set
|
||||||
if (peekCode == BinaryTypeCode.Null)
|
if (peekCode == BinaryTypeCode.Null)
|
||||||
{
|
{
|
||||||
context.ReadByte(); // consume Null marker
|
context.ReadByte(); // consume Null marker
|
||||||
|
|
@ -72,37 +77,57 @@ public static partial class AcBinaryDeserializer
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle nested complex objects - reuse existing if available
|
// Handle collections
|
||||||
// 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
|
|
||||||
if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
|
if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
|
||||||
{
|
{
|
||||||
var existingCollection = propInfo.GetValue(target);
|
var existingCollection = propInfo.GetValue(target);
|
||||||
if (existingCollection is IList existingList)
|
if (existingCollection is IList existingList)
|
||||||
{
|
{
|
||||||
context.ReadByte(); // consume Array marker
|
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;
|
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;
|
var positionBeforeRead = context.Position;
|
||||||
try
|
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))
|
if (TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
@ -111,12 +136,9 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
catch (InvalidCastException ex)
|
catch (InvalidCastException ex)
|
||||||
{
|
{
|
||||||
// Only get type info when needed for error message (cold path)
|
|
||||||
var targetType = target.GetType();
|
var targetType = target.GetType();
|
||||||
var targetTypeName = targetType.Name;
|
|
||||||
|
|
||||||
throw new AcBinaryDeserializationException(
|
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}'. " +
|
$"Expected type: '{propInfo.PropertyType.FullName}'. " +
|
||||||
$"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
|
$"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
|
||||||
$"Position before read: {positionBeforeRead}, current: {context.Position}. " +
|
$"Position before read: {positionBeforeRead}, current: {context.Position}. " +
|
||||||
|
|
@ -132,91 +154,12 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Populate nested object, reusing existing object and recursively updating properties.
|
/// Called from ReadObject for new instances - just calls PopulateObjectCore with skipDefaultWrite=true.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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);
|
PopulateObjectCore(ref context, target, metadata, depth, skipDefaultWrite);
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -237,7 +180,7 @@ public static partial class AcBinaryDeserializer
|
||||||
var count = (int)context.ReadVarUInt();
|
var count = (int)context.ReadVarUInt();
|
||||||
var nextDepth = depth + 1;
|
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;
|
BinaryDeserializeTypeMetadata? elementMetadata = null;
|
||||||
if (context.IsChainMode && IsComplexType(elementType))
|
if (context.IsChainMode && IsComplexType(elementType))
|
||||||
{
|
{
|
||||||
|
|
@ -257,7 +200,6 @@ public static partial class AcBinaryDeserializer
|
||||||
{
|
{
|
||||||
if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj))
|
if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj))
|
||||||
{
|
{
|
||||||
// Use existing object instead of new one
|
|
||||||
targetList.Add(existingObj);
|
targetList.Add(existingObj);
|
||||||
continue;
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optimized list populate that reuses existing items when possible.
|
/// Optimized list populate that reuses existing items when possible.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -338,7 +255,7 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate mode: existing item, must overwrite with defaults
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -348,12 +265,10 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
if (i < existingCount)
|
if (i < existingCount)
|
||||||
{
|
{
|
||||||
// Replace existing item
|
|
||||||
existingList[i] = value;
|
existingList[i] = value;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Add new item
|
|
||||||
existingList.Add(value);
|
existingList.Add(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -375,7 +290,8 @@ public static partial class AcBinaryDeserializer
|
||||||
#region Merge Methods
|
#region Merge Methods
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void MergeIIdCollection(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth)
|
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);
|
var newItem = CreateInstance(elementType, elementMetadata);
|
||||||
if (newItem == null) continue;
|
if (newItem == null) continue;
|
||||||
|
|
||||||
// Read ref ID if present
|
// Handle ref ID if present
|
||||||
if (context.HasReferenceHandling)
|
if (context.HasReferenceHandling)
|
||||||
{
|
{
|
||||||
var refId = context.ReadVarInt();
|
var refId = context.ReadVarInt();
|
||||||
|
|
@ -439,7 +355,7 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deserialize mode: new item just created, skip writing defaults
|
// 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);
|
var itemId = idGetter(newItem);
|
||||||
if (itemId != null && !IsDefaultValue(itemId, idType))
|
if (itemId != null && !IsDefaultValue(itemId, idType))
|
||||||
|
|
@ -449,7 +365,7 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
|
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);
|
CopyProperties(newItem, existingItem, elementMetadata);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -461,7 +377,6 @@ public static partial class AcBinaryDeserializer
|
||||||
// Remove orphaned items (items in destination but not in source)
|
// Remove orphaned items (items in destination but not in source)
|
||||||
if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
|
if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
|
||||||
{
|
{
|
||||||
// Find items to remove (those not in sourceIds)
|
|
||||||
var itemsToRemove = new List<object>();
|
var itemsToRemove = new List<object>();
|
||||||
foreach (var kvp in existingById)
|
foreach (var kvp in existingById)
|
||||||
{
|
{
|
||||||
|
|
@ -471,7 +386,6 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove orphaned items
|
|
||||||
foreach (var item in itemsToRemove)
|
foreach (var item in itemsToRemove)
|
||||||
{
|
{
|
||||||
existingList.Remove(item);
|
existingList.Remove(item);
|
||||||
|
|
@ -485,7 +399,7 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optimized IId collection merge using cached metadata (no runtime reflection).
|
/// IId collection merge using type metadata (for top-level list merge).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void MergeIIdCollectionWithMetadata(
|
private static void MergeIIdCollectionWithMetadata(
|
||||||
ref BinaryDeserializationContext context,
|
ref BinaryDeserializationContext context,
|
||||||
|
|
@ -543,7 +457,7 @@ public static partial class AcBinaryDeserializer
|
||||||
var newItem = CreateInstance(elementType, elementMetadata);
|
var newItem = CreateInstance(elementType, elementMetadata);
|
||||||
if (newItem == null) continue;
|
if (newItem == null) continue;
|
||||||
|
|
||||||
// Read ref ID if present
|
// Handle ref ID if present
|
||||||
if (context.HasReferenceHandling)
|
if (context.HasReferenceHandling)
|
||||||
{
|
{
|
||||||
var refId = context.ReadVarInt();
|
var refId = context.ReadVarInt();
|
||||||
|
|
@ -552,7 +466,7 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deserialize mode: new item just created, skip writing defaults
|
// 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);
|
var itemId = idGetter(newItem);
|
||||||
if (itemId != null && !IsDefaultValue(itemId, idType))
|
if (itemId != null && !IsDefaultValue(itemId, idType))
|
||||||
|
|
@ -562,7 +476,7 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
|
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);
|
CopyProperties(newItem, existingItem, elementMetadata);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -574,7 +488,6 @@ public static partial class AcBinaryDeserializer
|
||||||
// Remove orphaned items (items in destination but not in source)
|
// Remove orphaned items (items in destination but not in source)
|
||||||
if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
|
if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
|
||||||
{
|
{
|
||||||
// Find items to remove (those not in sourceIds)
|
|
||||||
var itemsToRemove = new List<object>();
|
var itemsToRemove = new List<object>();
|
||||||
foreach (var kvp in existingById)
|
foreach (var kvp in existingById)
|
||||||
{
|
{
|
||||||
|
|
@ -584,7 +497,6 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove orphaned items
|
|
||||||
foreach (var item in itemsToRemove)
|
foreach (var item in itemsToRemove)
|
||||||
{
|
{
|
||||||
existingList.Remove(item);
|
existingList.Remove(item);
|
||||||
|
|
@ -611,7 +523,6 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets a property to its default value using typed setters to avoid boxing.
|
/// Sets a property to its default value using typed setters to avoid boxing.
|
||||||
/// For collections and complex objects, clears the collection instead of setting to null.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static void SetPropertyToDefault(object target, BinaryPropertySetterInfo propInfo)
|
private static void SetPropertyToDefault(object target, BinaryPropertySetterInfo propInfo)
|
||||||
|
|
@ -647,18 +558,6 @@ public static partial class AcBinaryDeserializer
|
||||||
return;
|
return;
|
||||||
case PropertyAccessorType.Object:
|
case PropertyAccessorType.Object:
|
||||||
default:
|
default:
|
||||||
// For collections: clear instead of setting to null
|
|
||||||
//if (propInfo.IsCollection)
|
|
||||||
//{
|
|
||||||
// var collection = propInfo.GetValue(target);
|
|
||||||
// if (collection is IList list)
|
|
||||||
// {
|
|
||||||
// list.Clear();
|
|
||||||
// }
|
|
||||||
// // If not IList or null, leave it as-is (readonly collection or null already)
|
|
||||||
// return;
|
|
||||||
//}
|
|
||||||
|
|
||||||
// Reference types and nullable value types: set to null
|
// Reference types and nullable value types: set to null
|
||||||
propInfo.SetValue(target, null);
|
propInfo.SetValue(target, null);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -332,12 +332,25 @@ public static partial class AcBinaryDeserializer
|
||||||
if (typeCode == BinaryTypeCode.Object)
|
if (typeCode == BinaryTypeCode.Object)
|
||||||
{
|
{
|
||||||
context.ReadByte();
|
context.ReadByte();
|
||||||
PopulateObjectMerge(ref context, target, targetType, 0);
|
// Uses unified PopulateObject which checks IsMergeMode internally
|
||||||
|
PopulateObject(ref context, target, targetType, 0);
|
||||||
}
|
}
|
||||||
else if (typeCode == BinaryTypeCode.Array && target is IList targetList)
|
else if (typeCode == BinaryTypeCode.Array && target is IList targetList)
|
||||||
{
|
{
|
||||||
context.ReadByte();
|
context.ReadByte();
|
||||||
PopulateListMerge(ref context, targetList, targetType, 0);
|
// For top-level list merge, check if it's an IId collection
|
||||||
|
var elementType = GetCollectionElementType(targetType);
|
||||||
|
if (elementType != null && IsComplexType(elementType))
|
||||||
|
{
|
||||||
|
var elementMetadata = GetTypeMetadata(elementType);
|
||||||
|
if (elementMetadata.IsIId && elementMetadata.IdGetter != null)
|
||||||
|
{
|
||||||
|
MergeIIdCollectionWithMetadata(ref context, targetList, elementType, elementMetadata, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Non-IId collection, just populate
|
||||||
|
PopulateList(ref context, targetList, targetType, 0);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue