775 lines
31 KiB
C#
775 lines
31 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Runtime.CompilerServices;
|
||
using AyCode.Core.Helpers;
|
||
using AyCode.Core.Serializers;
|
||
using static AyCode.Core.Helpers.JsonUtilities;
|
||
|
||
namespace AyCode.Core.Serializers.Binaries;
|
||
|
||
public static partial class AcBinaryDeserializer
|
||
{
|
||
#region Helper Methods
|
||
|
||
// ReadAndRegisterHashcodeIfNeeded removed - new format uses position-based footer for all reference tracking.
|
||
// No hashcode prefix in wire format anymore.
|
||
|
||
#endregion
|
||
|
||
/// <summary>
|
||
/// Loop-invariant state for PopulatePropertyWithMarker. Built once per object, passed by ref.
|
||
/// Reduces call-site overhead from 9 parameters to 3 (ref state + propInfo + propertyIndex).
|
||
/// </summary>
|
||
private ref struct PopulateState<TInput> where TInput : struct, IBinaryInputBase
|
||
{
|
||
public BinaryDeserializationContext<TInput> Context;
|
||
public object Target;
|
||
public TypeMetadataWrapper<BinaryDeserializeTypeMetadata> ParentWrapper;
|
||
public BinaryDeserializeTypeMetadata Metadata;
|
||
public int NextDepth;
|
||
public int Depth;
|
||
public bool IsMergeMode;
|
||
public bool SkipDefaultWrite;
|
||
}
|
||
|
||
#region Populate Object Methods
|
||
|
||
/// <summary>
|
||
/// Resolves a property type wrapper using PropertyTypeWrappers cache.
|
||
/// Falls back to GetWrapper on cache miss and populates the cache.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static TypeMetadataWrapper<BinaryDeserializeTypeMetadata> ResolvePropertyWrapper<TInput>(
|
||
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> parentWrapper,
|
||
int complexPropertyIndex,
|
||
Type propertyType,
|
||
BinaryDeserializationContext<TInput> context)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var cached = parentWrapper.GetPropertyTypeWrapper(complexPropertyIndex, propertyType);
|
||
if (cached != null)
|
||
return cached;
|
||
|
||
var resolved = context.GetWrapper(propertyType);
|
||
parentWrapper.SetPropertyTypeWrapper(complexPropertyIndex, resolved);
|
||
return resolved;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type)
|
||
=> null;//MetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t));
|
||
|
||
/// <summary>
|
||
/// Populate object with automatic mode detection from context.
|
||
/// Uses IsMergeMode to determine merge behavior for IId collections.
|
||
/// Wire format:
|
||
/// - IId types: [Object][props 0-t<>l...] - no refId prefix, Id is in props
|
||
/// - Non-IId + All: [Object][hashcode][props 0-t<>l...] - hashcode prefix
|
||
/// - Ref=Off: [Object][props 0-t<>l...] - no prefix
|
||
/// </summary>
|
||
private static void PopulateObject<TInput>(BinaryDeserializationContext<TInput> context, object target, Type targetType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var wrapper = context.GetWrapper(targetType);
|
||
|
||
// UseMetadata: CacheMap is built in ReadObjectWithMetadata or ReadInlineMetadataForPopulate
|
||
PopulateObjectCore(context, target, wrapper, depth, skipDefaultWrite: false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Core populate logic shared by all populate paths.
|
||
/// Wire format: All properties are written WITH type markers (including Id for IId types).
|
||
/// No hashcode prefix - position-based footer handles reference tracking.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void PopulateObjectCore<TInput>(
|
||
BinaryDeserializationContext<TInput> context,
|
||
object target,
|
||
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
|
||
int depth,
|
||
bool skipDefaultWrite)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
PopulateObjectPropertiesIndexed(context, target, wrapper, depth, skipDefaultWrite);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Unified property populate for both UseMetadata and non-UseMetadata modes.
|
||
/// UseMetadata=true: cacheMap[i] gives the setter (null → skip).
|
||
/// UseMetadata=false: properties[i] gives the setter directly.
|
||
/// </summary>
|
||
private static void PopulateObjectPropertiesIndexed<TInput>(BinaryDeserializationContext<TInput> context, object target, TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int depth, bool skipDefaultWrite)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
// No depth safety net on deserialize: wire format is linear + finite, the serializer-side counter
|
||
// already prevents pathological depth in well-formed payloads. Malicious wire is out of scope.
|
||
|
||
var metadata = wrapper.Metadata;
|
||
var properties = metadata.PropertiesArray;
|
||
var cacheMap = wrapper.CacheMap;
|
||
|
||
// UseMetadata: cacheMap.Length a source property-k száma
|
||
// Non-UseMetadata: properties.Length a target property-k száma (source == target)
|
||
var propCount = cacheMap?.Length ?? properties.Length;
|
||
|
||
var state = new PopulateState<TInput>
|
||
{
|
||
Context = context,
|
||
Target = target,
|
||
ParentWrapper = wrapper,
|
||
Metadata = metadata,
|
||
NextDepth = depth + 1,
|
||
Depth = depth,
|
||
IsMergeMode = context.IsMergeMode,
|
||
SkipDefaultWrite = skipDefaultWrite
|
||
};
|
||
|
||
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];
|
||
|
||
if (propInfo.ExpectedTypeCode.HasValue)
|
||
{
|
||
ReadAndSetMarkerlessValue(context, target, propInfo);
|
||
continue;
|
||
}
|
||
|
||
// Non-markerless properties: standard marker-based read
|
||
PopulatePropertyWithMarker(ref state, propInfo, i);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// UseMetadata=true loop — UNCHANGED, zero extra overhead
|
||
for (int i = 0; i < propCount; i++)
|
||
{
|
||
var propInfo = cacheMap != null ? cacheMap[i] : properties[i];
|
||
|
||
PopulatePropertyWithMarker(ref state, propInfo, i);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Standard marker-based property read. Extracted to avoid duplicating logic in both loops.
|
||
/// Loop-invariant state is passed via ref PopulateState to reduce call-site overhead.
|
||
/// </summary>
|
||
private static void PopulatePropertyWithMarker<TInput>(
|
||
ref PopulateState<TInput> state,
|
||
BinaryPropertySetterBase? propInfo,
|
||
int propertyIndex)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var context = state.Context;
|
||
var target = state.Target;
|
||
|
||
// Nincs megfelelő target property → skip (SkipValue reads its own marker byte)
|
||
if (propInfo == null)
|
||
{
|
||
SkipValue(context, state.Metadata);
|
||
return;
|
||
}
|
||
|
||
// FastWire markerless string-property fast-path — int32 sentinel header (-1 null / 0 empty / N>0
|
||
// content). Wire-symmetric with `WriteStringGenerated` / `WritePropertyOrSkip` String case via
|
||
// `WriteStringUtf16Markerless`. Skips the typeCode-read entirely; reader-writer pair eliminates
|
||
// 1 byte per content string in FastWire mode. Condition order: bool field-load (`FastWire`)
|
||
// first → cheap short-circuit in Compact mode (most-common case in many deployments) and
|
||
// branch-predictor-stable in FastWire mode (constant for the entire Deserialize). The
|
||
// `AccessorType == String` enum-compare (2 instructions: load + cmp) only runs when needed.
|
||
if (context.FastWire && propInfo.AccessorType == PropertyAccessorType.String)
|
||
{
|
||
propInfo.SetValue(target, context.ReadStringUtf16Markerless());
|
||
return;
|
||
}
|
||
|
||
// Read marker once — eliminates redundant PeekByte + ReadByte boundary checks.
|
||
// All branches below receive the already-consumed typeCode.
|
||
var typeCode = context.ReadByte();
|
||
|
||
// Skip marker - property has default/null value
|
||
if (typeCode == BinaryTypeCode.PropertySkip)
|
||
{
|
||
// Populate mode: overwrite with default (existing object may have non-default values)
|
||
// Deserialize mode: skip write (new object already has defaults from CreateInstance)
|
||
if (!state.SkipDefaultWrite)
|
||
{
|
||
SetPropertyToDefault(target, propInfo);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Null values - always set
|
||
if (typeCode == BinaryTypeCode.Null)
|
||
{
|
||
propInfo.SetValue(target, null);
|
||
return;
|
||
}
|
||
|
||
var nextDepth = state.NextDepth;
|
||
|
||
// Handle collections
|
||
if (typeCode == BinaryTypeCode.Array && propInfo.IsCollection)
|
||
{
|
||
var existingCollection = propInfo.GetValue(target);
|
||
if (existingCollection is IList existingList)
|
||
{
|
||
// Merge mode with IId collection: use merge logic
|
||
if (state.IsMergeMode && propInfo.IsIIdCollection)
|
||
{
|
||
MergeIIdCollection(context, existingList, propInfo, nextDepth);
|
||
}
|
||
else
|
||
{
|
||
// Normal populate: replace collection contents
|
||
PopulateListOptimized(context, existingList, propInfo, nextDepth);
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Handle nested complex objects - reuse existing if available
|
||
if ((typeCode == BinaryTypeCode.Object || typeCode == BinaryTypeCode.ObjectWithMetadata
|
||
|| typeCode == BinaryTypeCode.ObjectRefFirst || typeCode == BinaryTypeCode.ObjectWithMetadataRefFirst) && propInfo.IsComplexType)
|
||
{
|
||
var existingObj = propInfo.GetValue(target);
|
||
if (existingObj != null)
|
||
{
|
||
// Marker already consumed → rewind so ReadValue can read it
|
||
context._position--;
|
||
var nestedValue = ReadValue(context, propInfo.PropertyType);
|
||
if (nestedValue != null)
|
||
{
|
||
var complexIdx = propInfo.ComplexPropertyIndex;
|
||
var parentWrapper = state.ParentWrapper;
|
||
var nestedWrapper = complexIdx >= 0
|
||
? ResolvePropertyWrapper(parentWrapper, complexIdx, propInfo.PropertyType, context)
|
||
: context.GetWrapper(propInfo.PropertyType);
|
||
CopyProperties(nestedValue, existingObj, nestedWrapper.Metadata);
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Default: read value and set (for primitives, strings, new objects)
|
||
var positionBeforeRead = context.Position - 1; // marker already consumed
|
||
try
|
||
{
|
||
// Use typed setters for primitives and strings to avoid ReadValue dispatch.
|
||
// typeCode is already consumed — TryReadAndSetTypedValue skips its internal ReadByte.
|
||
if (propInfo.AccessorType != PropertyAccessorType.Object &&
|
||
TryReadAndSetTypedValue(context, target, propInfo, typeCode))
|
||
return;
|
||
|
||
// Complex property with Object marker: use pre-cached wrapper to skip GetWrapper lookup
|
||
var complexIdx = propInfo.ComplexPropertyIndex;
|
||
if (complexIdx >= 0 && typeCode == BinaryTypeCode.Object)
|
||
{
|
||
// Marker already consumed — go straight to ReadObjectCoreWithWrapper
|
||
var propWrapper = ResolvePropertyWrapper(state.ParentWrapper, complexIdx, propInfo.PropertyType, context);
|
||
var value = ReadObjectCoreWithWrapper(context, propWrapper, cacheIndex: -1);
|
||
propInfo.SetValue(target, value);
|
||
}
|
||
else
|
||
{
|
||
// Marker already consumed → rewind so ReadValue can read it
|
||
context._position--;
|
||
var value = ReadValue(context, propInfo.PropertyType);
|
||
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}'. " +
|
||
$"TypeCode read: {typeCode} (0x{typeCode:X2}). " +
|
||
$"Position before read: {positionBeforeRead}, current: {context.Position}. " +
|
||
$"Depth: {state.Depth}. " +
|
||
$"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " +
|
||
$"All target properties: [{string.Join(", ", state.Metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " +
|
||
$"Error: {ex.Message}",
|
||
positionBeforeRead,
|
||
propInfo.PropertyType,
|
||
ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void ReadAndSetMarkerlessValue<TInput>(BinaryDeserializationContext<TInput> context, object target, BinaryPropertySetterBase propInfo)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
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;
|
||
case PropertyAccessorType.Boolean:
|
||
propInfo.SetBoolean(target, context.ReadByte() != 0);
|
||
return;
|
||
case PropertyAccessorType.Enum:
|
||
propInfo.SetEnumAsInt32(target, context.ReadVarInt());
|
||
return;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Called from ReadObject/ReadObjectWithMetadata for new instances.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void PopulateObject<TInput>(BinaryDeserializationContext<TInput> context, object target, TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, bool skipDefaultWrite)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
// Bridge: PopulateObjectPropertiesIndexed still has int depth in signature (other callers use it).
|
||
// Pass 0 as placeholder; depth-cleanup of PopulateObjectPropertiesIndexed + its other callers is pending.
|
||
PopulateObjectPropertiesIndexed(context, target, wrapper, 0, skipDefaultWrite);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Populate List Methods
|
||
|
||
private static void PopulateList<TInput>(BinaryDeserializationContext<TInput> context, IList targetList, Type listType, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var elementType = GetCollectionElementType(listType) ?? typeof(object);
|
||
|
||
var acObservable = targetList as IAcObservableCollection;
|
||
acObservable?.BeginUpdate();
|
||
|
||
try
|
||
{
|
||
targetList.Clear();
|
||
|
||
var count = (int)context.ReadVarUInt();
|
||
var nextDepth = depth + 1;
|
||
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
// ReadValue handles ChainMode internally (ReadObject returns cached instance)
|
||
var value = ReadValue(context, elementType);
|
||
targetList.Add(value);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
acObservable?.EndUpdate();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized list populate that reuses existing items when possible.
|
||
/// </summary>
|
||
private static void PopulateListOptimized<TInput>(BinaryDeserializationContext<TInput> context, IList existingList, BinaryPropertySetterBase propInfo, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var elementType = propInfo.ElementType ?? typeof(object);
|
||
var count = (int)context.ReadVarUInt();
|
||
var nextDepth = depth + 1;
|
||
|
||
var acObservable = existingList as IAcObservableCollection;
|
||
acObservable?.BeginUpdate();
|
||
|
||
try
|
||
{
|
||
var existingCount = existingList.Count;
|
||
|
||
// Early exit if empty source - just clear destination
|
||
if (count == 0)
|
||
{
|
||
existingList.Clear();
|
||
return;
|
||
}
|
||
|
||
var wrapper = context.GetWrapper(elementType);
|
||
var elementMetadata = wrapper.Metadata.IsComplexType ? wrapper.Metadata : null;
|
||
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
// Read marker once — eliminates redundant PeekByte + ReadByte boundary checks
|
||
var typeCode = context.ReadByte();
|
||
|
||
// ObjectRefFirst: read cacheIndex before processing the object body
|
||
var cacheIndex = -1;
|
||
if (typeCode == BinaryTypeCode.ObjectRefFirst && elementMetadata != null)
|
||
{
|
||
cacheIndex = (int)context.ReadVarUInt();
|
||
// Treat as Object from here on — same wire body, just with cacheIndex prefix
|
||
typeCode = BinaryTypeCode.Object;
|
||
}
|
||
|
||
// If we have an existing item at this index and the incoming is an object, reuse it
|
||
if (i < existingCount && typeCode == BinaryTypeCode.Object && elementMetadata != null)
|
||
{
|
||
var existingItem = existingList[i];
|
||
if (existingItem != null)
|
||
{
|
||
if (cacheIndex >= 0)
|
||
context.RegisterInternedValueAt(cacheIndex, existingItem);
|
||
PopulateObjectPropertiesIndexed(context, existingItem, wrapper, nextDepth, skipDefaultWrite: false);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Read new value — use pre-resolved wrapper for Object elements to skip GetWrapper dictionary lookup
|
||
object? value;
|
||
if (typeCode == BinaryTypeCode.Object && elementMetadata != null)
|
||
{
|
||
value = ReadObjectCoreWithWrapper(context, wrapper, cacheIndex: cacheIndex);
|
||
}
|
||
else
|
||
{
|
||
// Marker already consumed → rewind so ReadValue can read it
|
||
context._position--;
|
||
value = ReadValue(context, elementType);
|
||
}
|
||
|
||
if (i < existingCount)
|
||
{
|
||
existingList[i] = value;
|
||
}
|
||
else
|
||
{
|
||
existingList.Add(value);
|
||
}
|
||
}
|
||
|
||
// Remove extra items if new list is shorter
|
||
while (existingList.Count > count)
|
||
{
|
||
existingList.RemoveAt(existingList.Count - 1);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
acObservable?.EndUpdate();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Merge Methods
|
||
|
||
/// <summary>
|
||
/// IId collection merge using cached property info.
|
||
/// Matches items by Id, updates existing, adds new, optionally removes orphans.
|
||
/// </summary>
|
||
private static void MergeIIdCollection<TInput>(BinaryDeserializationContext<TInput> context, IList existingList, BinaryPropertySetterBase propInfo, int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var elementType = propInfo.ElementType!;
|
||
var idGetter = propInfo.ElementIdGetter!;
|
||
var idType = propInfo.ElementIdType!;
|
||
|
||
var count = existingList.Count;
|
||
var acObservable = existingList as IAcObservableCollection;
|
||
acObservable?.BeginUpdate();
|
||
|
||
try
|
||
{
|
||
// Build lookup dictionary with capacity hint
|
||
Dictionary<object, object>? existingById = null;
|
||
if (count > 0)
|
||
{
|
||
existingById = new Dictionary<object, object>(count);
|
||
for (var idx = 0; idx < count; idx++)
|
||
{
|
||
var item = existingList[idx];
|
||
if (item != null)
|
||
{
|
||
var id = idGetter(item);
|
||
if (id != null && !IsDefaultValue(id, idType))
|
||
existingById[id] = item;
|
||
}
|
||
}
|
||
}
|
||
|
||
var arrayCount = (int)context.ReadVarUInt();
|
||
var nextDepth = depth + 1;
|
||
var wrapper = context.GetWrapper(elementType);
|
||
var elementMetadata = wrapper.Metadata;
|
||
|
||
// Track which IDs we see in source (for orphan removal)
|
||
HashSet<object>? sourceIds = context.RemoveOrphanedItems && existingById != null
|
||
? new HashSet<object>(arrayCount)
|
||
: null;
|
||
|
||
for (int i = 0; i < arrayCount; i++)
|
||
{
|
||
// Read marker once — eliminates redundant PeekByte + ReadByte boundary checks
|
||
var itemCode = context.ReadByte();
|
||
|
||
// Read or create the new item
|
||
object? newItem;
|
||
if (itemCode == BinaryTypeCode.Object)
|
||
{
|
||
// Fast path: use pre-resolved wrapper, skip GetWrapper dictionary lookup
|
||
newItem = CreateInstance(elementType, elementMetadata);
|
||
if (newItem == null) continue;
|
||
PopulateObjectPropertiesIndexed(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
|
||
}
|
||
else
|
||
{
|
||
// Marker already consumed → rewind so ReadValue can read it
|
||
context._position--;
|
||
newItem = ReadValue(context, elementType);
|
||
if (newItem == null) continue;
|
||
}
|
||
|
||
var itemId = idGetter(newItem);
|
||
if (itemId != null && !IsDefaultValue(itemId, idType))
|
||
{
|
||
// Track this ID as seen in source
|
||
sourceIds?.Add(itemId);
|
||
|
||
if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
|
||
{
|
||
// Copy properties to existing item (preserves reference)
|
||
CopyProperties(newItem, existingItem, elementMetadata);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
existingList.Add(newItem);
|
||
}
|
||
|
||
// Remove orphaned items (items in destination but not in source)
|
||
if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
|
||
{
|
||
var itemsToRemove = new List<object>();
|
||
foreach (var kvp in existingById)
|
||
{
|
||
if (!sourceIds.Contains(kvp.Key))
|
||
{
|
||
itemsToRemove.Add(kvp.Value);
|
||
}
|
||
}
|
||
|
||
foreach (var item in itemsToRemove)
|
||
{
|
||
existingList.Remove(item);
|
||
}
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
acObservable?.EndUpdate();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// IId collection merge using type metadata (for top-level list merge).
|
||
/// </summary>
|
||
private static void MergeIIdCollectionWithMetadata<TInput>(
|
||
BinaryDeserializationContext<TInput> context,
|
||
IList existingList,
|
||
Type elementType,
|
||
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
|
||
int depth)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
var elementMetadata = wrapper.Metadata;
|
||
var idGetter = elementMetadata.IdGetter!;
|
||
var idType = elementMetadata.IdType!;
|
||
|
||
var count = existingList.Count;
|
||
var acObservable = existingList as IAcObservableCollection;
|
||
acObservable?.BeginUpdate();
|
||
|
||
try
|
||
{
|
||
// Build lookup dictionary with capacity hint
|
||
Dictionary<object, object>? existingById = null;
|
||
if (count > 0)
|
||
{
|
||
existingById = new Dictionary<object, object>(count);
|
||
for (var idx = 0; idx < count; idx++)
|
||
{
|
||
var item = existingList[idx];
|
||
if (item != null)
|
||
{
|
||
var id = idGetter(item);
|
||
if (id != null && !IsDefaultValue(id, idType))
|
||
existingById[id] = item;
|
||
}
|
||
}
|
||
}
|
||
|
||
var arrayCount = (int)context.ReadVarUInt();
|
||
var nextDepth = depth + 1;
|
||
|
||
// Track which IDs we see in source (for orphan removal)
|
||
HashSet<object>? sourceIds = context.RemoveOrphanedItems && existingById != null
|
||
? new HashSet<object>(arrayCount)
|
||
: null;
|
||
|
||
for (int i = 0; i < arrayCount; i++)
|
||
{
|
||
// Read marker once — eliminates redundant PeekByte + ReadByte boundary checks
|
||
var itemCode = context.ReadByte();
|
||
|
||
// Read or create the new item
|
||
object? newItem;
|
||
if (itemCode == BinaryTypeCode.Object)
|
||
{
|
||
// Fast path: use pre-resolved wrapper, skip GetWrapper dictionary lookup
|
||
newItem = CreateInstance(elementType, elementMetadata);
|
||
if (newItem == null) continue;
|
||
PopulateObjectPropertiesIndexed(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
|
||
}
|
||
else
|
||
{
|
||
// Marker already consumed → rewind so ReadValue can read it
|
||
context._position--;
|
||
newItem = ReadValue(context, elementType);
|
||
if (newItem == null) continue;
|
||
}
|
||
|
||
var itemId = idGetter(newItem);
|
||
if (itemId != null && !IsDefaultValue(itemId, idType))
|
||
{
|
||
// Track this ID as seen in source
|
||
sourceIds?.Add(itemId);
|
||
|
||
if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
|
||
{
|
||
// Copy properties to existing item (preserves reference)
|
||
CopyProperties(newItem, existingItem, elementMetadata);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
existingList.Add(newItem);
|
||
}
|
||
|
||
// Remove orphaned items (items in destination but not in source)
|
||
if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
|
||
{
|
||
var itemsToRemove = new List<object>();
|
||
foreach (var kvp in existingById)
|
||
{
|
||
if (!sourceIds.Contains(kvp.Key))
|
||
{
|
||
itemsToRemove.Add(kvp.Value);
|
||
}
|
||
}
|
||
|
||
foreach (var item in itemsToRemove)
|
||
{
|
||
existingList.Remove(item);
|
||
}
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
acObservable?.EndUpdate();
|
||
}
|
||
}
|
||
|
||
private static void CopyProperties(object source, object target, BinaryDeserializeTypeMetadata metadata)
|
||
{
|
||
var props = metadata.PropertiesArray;
|
||
for (var i = 0; i < props.Length; i++)
|
||
{
|
||
var prop = props[i];
|
||
var value = prop.GetValue(source);
|
||
if (value != null)
|
||
prop.SetValue(target, value);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Reads Id value without type marker. The serializer didn't write a marker for IId types.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void ReadIdValueWithoutMarker<TInput>(BinaryDeserializationContext<TInput> context, object target, BinaryPropertySetterInfo propInfo, IdAccessorType idType)
|
||
where TInput : struct, IBinaryInputBase
|
||
{
|
||
switch (idType)
|
||
{
|
||
case IdAccessorType.Int32:
|
||
propInfo.SetInt32(target, context.ReadVarInt());
|
||
break;
|
||
case IdAccessorType.Int64:
|
||
propInfo.SetInt64(target, context.ReadVarLong());
|
||
break;
|
||
case IdAccessorType.Guid:
|
||
propInfo.SetGuid(target, context.ReadGuidUnsafe());
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sets a property to its default value using typed setters to avoid boxing.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void SetPropertyToDefault(object target, BinaryPropertySetterBase propInfo)
|
||
=> propInfo.SetToDefault(target);
|
||
|
||
/// <summary>
|
||
/// Determines if a type is a complex type (not primitive, string, or simple value type).
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static bool IsComplexType5(Type type)
|
||
{
|
||
if (type.IsPrimitive) return false;
|
||
if (ReferenceEquals(type, StringType)) return false;
|
||
if (type.IsEnum) return false;
|
||
if (ReferenceEquals(type, GuidType)) return false;
|
||
if (ReferenceEquals(type, DateTimeType)) return false;
|
||
if (ReferenceEquals(type, DecimalType)) return false;
|
||
if (ReferenceEquals(type, TimeSpanType)) return false;
|
||
if (ReferenceEquals(type, DateTimeOffsetType)) return false;
|
||
if (Nullable.GetUnderlyingType(type) != null) return false;
|
||
return true;
|
||
}
|
||
|
||
#endregion
|
||
}
|