Refactor IId reference tracking for binary serialization

Unifies IId-based reference handling for binary serialization and deserialization. Introduces BinaryDeserializationContextClass for heap-based IId tracking, refactors IdentityMap<TId> for unified object storage, and removes legacy IIdReferenceTracker logic. Updates deserializer to use the new infrastructure for all IId types (int, long, Guid) and correct wire formats. Enhances tests for reference identity and object graph integrity. Improves code clarity and maintainability.
This commit is contained in:
Loretta 2026-01-19 14:37:42 +01:00
parent 09a61539fa
commit dc2526da7e
6 changed files with 275 additions and 274 deletions

View File

@ -1,6 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Tests.TestModels;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace AyCode.Core.Tests.Serialization;
@ -27,6 +31,10 @@ public class AcBinarySerializerIIdReferenceTests
/// </summary>
private static int CountObjectRefs(byte[] binary)
{
Console.WriteLine();
Console.WriteLine( BitConverter.ToString(binary));
Console.WriteLine();
var count = 0;
for (var i = 0; i < binary.Length; i++)
{
@ -71,18 +79,22 @@ public class AcBinarySerializerIIdReferenceTests
public void SameInstance_SerializeAndDeserialize()
{
// Arrange: SAME instance used 4 times
var userPreferences = new UserPreferences();
var sharedTag = new SharedTag { Id = 1, Name = "ImportantTag", Color = "#FF0000" };
var sharedUser = new SharedUser { Id = 1, Preferences = userPreferences };
var order = new TestOrder
{
Id = 1,
OrderNumber = "ORD-001",
PrimaryTag = sharedTag,
Owner = sharedUser,
Items =
[
new TestOrderItem { Id = 1, ProductName = "Product-A", Tag = sharedTag },
new TestOrderItem { Id = 2, ProductName = "Product-B", Tag = sharedTag },
new TestOrderItem { Id = 3, ProductName = "Product-C", Tag = sharedTag }
new TestOrderItem { Id = 1, ProductName = "Product-A", Tag = sharedTag, Assignee = sharedUser },
new TestOrderItem { Id = 2, ProductName = "Product-B", Tag = sharedTag, Assignee = new SharedUser { Id = 2, Preferences = userPreferences }},
new TestOrderItem { Id = 3, ProductName = "Product-C", Tag = sharedTag, Assignee = new SharedUser { Id = 3, Preferences = userPreferences } }
]
};
@ -95,8 +107,8 @@ public class AcBinarySerializerIIdReferenceTests
Console.WriteLine($"Binary size: {binary.Length} bytes");
Console.WriteLine($"ObjectRef count: {objectRefCount}");
Assert.IsTrue(objectRefCount >= 3,
$"Expected at least 3 ObjectRef entries for shared tag, found {objectRefCount}. " +
Assert.IsTrue(objectRefCount >= 6,
$"Expected at least 6 ObjectRef entries for shared tag and user (3 each), found {objectRefCount}. " +
"Reference handling may not be active!");
// Assert 2: Data integrity - ALL tags are present and have correct data
@ -114,6 +126,8 @@ public class AcBinarySerializerIIdReferenceTests
Assert.IsNotNull(result.Items[i].Tag, $"Items[{i}].Tag is null - data lost!");
Assert.AreEqual(1, result.Items[i].Tag!.Id, $"Items[{i}].Tag.Id incorrect");
Assert.AreEqual("ImportantTag", result.Items[i].Tag.Name, $"Items[{i}].Tag.Name incorrect");
Assert.IsNotNull(result.Items[i].Assignee, $"Items[{i}].Assignee is null - data lost!");
}
// Assert 3: Reference identity - all should be same object reference
@ -123,6 +137,16 @@ public class AcBinarySerializerIIdReferenceTests
"Item[1].Tag should be same reference as PrimaryTag");
Assert.AreSame(result.PrimaryTag, result.Items[2].Tag,
"Item[2].Tag should be same reference as PrimaryTag");
Assert.AreSame(result.Owner, result.Items[0].Assignee,
"Item[0].Assignee should be same reference as Owner");
Assert.AreSame(result.Owner.Preferences, result.Items[0].Assignee.Preferences,
"Item[0].Assignee.Preferences should be same reference as Owner.Preferences");
Assert.AreSame(result.Owner.Preferences, result.Items[1].Assignee.Preferences,
"Item[1].Assignee.Preferences should be same reference as Owner.Preferences");
Assert.AreSame(result.Items[1].Assignee.Preferences, result.Items[2].Assignee.Preferences,
"Item[1].Assignee.Preferences should be same reference as Item[2].Assignee.Preferences");
}
#endregion
@ -142,6 +166,7 @@ public class AcBinarySerializerIIdReferenceTests
{
// Arrange: DIFFERENT instances but SAME IId.Id
// CRITICAL: Multiple DIFFERENT TYPES all have Id=1 - must not be confused!
var sharedTag = new SharedTag { Id = 55, Name = "ImportantTag_55", Color = "#FF0000" };
var order = new TestOrder
{
Id = 1,
@ -182,7 +207,7 @@ public class AcBinarySerializerIIdReferenceTests
// Assert 1: Check if ObjectRef is used (IId-based deduplication active)
var objectRefCount = CountObjectRefs(binary);
Console.WriteLine($"Binary size: {binary.Length} bytes");
Console.WriteLine($"\nBinary size: {binary.Length} bytes (WithRef)");
Console.WriteLine($"ObjectRef count: {objectRefCount}");
// Assert 3: Reference identity - same TYPE with same Id should be same reference

View File

@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using AyCode.Core.Helpers;
using AyCode.Core.Serializers.Expressions;
using AyCode.Core.Serializers.Jsons;
@ -597,6 +598,7 @@ public static class AcSerializerCommon
/// </summary>
public enum IdAccessorType : byte
{
None = 0,
/// <summary>Id is int (most common).</summary>
Int32 = 1,
/// <summary>Id is long.</summary>
@ -618,28 +620,64 @@ public static class AcSerializerCommon
}
/// <summary>
/// Generic identity map for tracking IId values during serialization.
/// Uses JIT-optimized EqualityComparer for maximum performance with common ID types.
/// Generic identity map for tracking IId values during serialization/deserialization.
/// Uses Dictionary internally for unified tracking + object storage.
/// </summary>
/// <typeparam name="TId">The ID type (int, long, Guid)</typeparam>
public sealed class IdentityMap<TId> : IIdentityMap where TId : notnull
{
private readonly HashSet<TId> _seenIds;
private readonly Dictionary<TId, object?> _tracked;
public IdentityMap()
{
_seenIds = new HashSet<TId>(EqualityComparer<TId>.Default);
_tracked = new Dictionary<TId, object?>(EqualityComparer<TId>.Default);
}
/// <summary>
/// Tries to add an ID to the tracking set.
/// Returns true if this is the first occurrence (ID was added).
/// Tries to add a key to tracking (serialization).
/// Returns true if first occurrence (key was added).
/// Returns false if already seen.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryAdd(TId id)
public bool TryAddKey(TId key)
{
return _seenIds.Add(id);
return _tracked.TryAdd(key, null);
}
/// <summary>
/// Checks if key exists and returns existing value, or adds new key (deserialization).
/// Returns true if first occurrence (key was added, out = null).
/// Returns false if already seen (out = existing value).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetOrAddKey(TId key, out object? existing)
{
ref var slot = ref CollectionsMarshal.GetValueRefOrAddDefault(_tracked, key, out var exists);
existing = exists ? slot : null;
return !exists;
}
/// <summary>
/// Gets existing value or adds new value for key (deserialization with object storage).
/// Returns the existing value if key was seen before, or stores and returns newValue.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrAddValue(TId key, object newValue)
{
ref var slot = ref CollectionsMarshal.GetValueRefOrAddDefault(_tracked, key, out var exists);
if (exists && slot != null) return slot;
slot = newValue;
return newValue;
}
/// <summary>
/// Tries to get the value for a key (ObjectRef lookup).
/// Returns true if found, false if not.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValue(TId key, out object? value)
{
return _tracked.TryGetValue(key, out value);
}
/// <summary>
@ -647,180 +685,10 @@ public static class AcSerializerCommon
/// </summary>
public void Reset()
{
_seenIds.Clear();
_tracked.Clear();
}
}
/// <summary>
/// IId-based reference tracking for serialization.
/// Supplements (not replaces) the ReferenceEquals-based SerializationReferenceTracker.
/// Tracks objects by (Type, Id) key to detect same IId across different instances.
/// Uses typed dictionaries for int/long/Guid to avoid boxing overhead.
/// Stores the original object to enable marking it as multi-referenced when a duplicate is found.
/// </summary>
public sealed class IIdReferenceTracker
{
// Typed dictionaries for common Id types (no boxing!)
// Value is the first object registered with that (Type, Id) key
private Dictionary<(Type, int), object>? _int32Cache;
private Dictionary<(Type, long), object>? _int64Cache;
private Dictionary<(Type, Guid), object>? _guidCache;
private Dictionary<(Type, object), object>? _objectCache; // Fallback for exotic types
/// <summary>
/// Tries to get existing object for an IId.
/// Returns true if same (Type, Id) was already tracked.
/// The out parameter contains the ORIGINAL object that was first registered.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetOriginalObject<TMetadata>(object obj, TMetadata metadata, out object? originalObject)
where TMetadata : TypeMetadataBase
{
originalObject = null;
if (!metadata.IsIId) return false;
return metadata.IdAccessorType switch
{
IdAccessorType.Int32 => TryGetInt32Original(obj, metadata, out originalObject),
IdAccessorType.Int64 => TryGetInt64Original(obj, metadata, out originalObject),
IdAccessorType.Guid => TryGetGuidOriginal(obj, metadata, out originalObject),
_ => false
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryGetInt32Original<TMetadata>(object obj, TMetadata metadata, out object? originalObject)
where TMetadata : TypeMetadataBase
{
originalObject = null;
if (_int32Cache == null) return false;
var id = metadata.GetIdInt32(obj);
if (id == 0) return false; // Skip default Id
var key = (obj.GetType(), id);
return _int32Cache.TryGetValue(key, out originalObject);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryGetInt64Original<TMetadata>(object obj, TMetadata metadata, out object? originalObject)
where TMetadata : TypeMetadataBase
{
originalObject = null;
if (_int64Cache == null) return false;
var id = metadata.GetIdInt64(obj);
if (id == 0) return false; // Skip default Id
var key = (obj.GetType(), id);
return _int64Cache.TryGetValue(key, out originalObject);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryGetGuidOriginal<TMetadata>(object obj, TMetadata metadata, out object? originalObject)
where TMetadata : TypeMetadataBase
{
originalObject = null;
if (_guidCache == null) return false;
var id = metadata.GetIdGuid(obj);
if (id == Guid.Empty) return false; // Skip default Id
var key = (obj.GetType(), id);
return _guidCache.TryGetValue(key, out originalObject);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryGetObjectOriginal<TMetadata>(object obj, TMetadata metadata, out object? originalObject)
where TMetadata : TypeMetadataBase
{
originalObject = null;
if (_objectCache == null || metadata.IdGetter == null) return false;
var id = metadata.IdGetter(obj);
if (id == null) return false;
var key = (obj.GetType(), id);
return _objectCache.TryGetValue(key, out originalObject);
}
/// <summary>
/// Registers an IId object.
/// </summary>
public void Register<TMetadata>(object obj, TMetadata metadata)
where TMetadata : TypeMetadataBase
{
if (!metadata.IsIId) return;
switch (metadata.IdAccessorType)
{
case IdAccessorType.Int32:
RegisterInt32(obj, metadata);
break;
case IdAccessorType.Int64:
RegisterInt64(obj, metadata);
break;
case IdAccessorType.Guid:
RegisterGuid(obj, metadata);
break;
}
}
private void RegisterInt32<TMetadata>(object obj, TMetadata metadata)
where TMetadata : TypeMetadataBase
{
var id = metadata.GetIdInt32(obj);
if (id == 0) return; // Skip default Id
_int32Cache ??= new Dictionary<(Type, int), object>(64);
_int32Cache[(obj.GetType(), id)] = obj;
}
private void RegisterInt64<TMetadata>(object obj, TMetadata metadata)
where TMetadata : TypeMetadataBase
{
var id = metadata.GetIdInt64(obj);
if (id == 0) return; // Skip default Id
_int64Cache ??= new Dictionary<(Type, long), object>(64);
_int64Cache[(obj.GetType(), id)] = obj;
}
private void RegisterGuid<TMetadata>(object obj, TMetadata metadata)
where TMetadata : TypeMetadataBase
{
var id = metadata.GetIdGuid(obj);
if (id == Guid.Empty) return; // Skip default Id
_guidCache ??= new Dictionary<(Type, Guid), object>(64);
_guidCache[(obj.GetType(), id)] = obj;
}
private void RegisterObject<TMetadata>(object obj, TMetadata metadata)
where TMetadata : TypeMetadataBase
{
if (metadata.IdGetter == null) return;
var id = metadata.IdGetter(obj);
if (id == null) return;
_objectCache ??= new Dictionary<(Type, object), object>(64);
_objectCache[(obj.GetType(), id)] = obj;
}
/// <summary>
/// Clears all tracking data for reuse.
/// </summary>
public void Reset()
{
_int32Cache?.Clear();
_int64Cache?.Clear();
_guidCache?.Clear();
_objectCache?.Clear();
}
}
#endregion
#region Chain Reference Tracking

View File

@ -90,7 +90,7 @@ public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeM
map = new AcSerializerCommon.IdentityMap<int>();
wrapper.IdentityMap = map;
}
return map.TryAdd(refId);
return map.TryAddKey(refId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -138,7 +138,7 @@ public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeM
map = new AcSerializerCommon.IdentityMap<long>();
wrapper.IdentityMap = map;
}
return map.TryAdd(refId);
return map.TryAddKey(refId);
}
#endregion
@ -164,7 +164,64 @@ public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeM
map = new AcSerializerCommon.IdentityMap<Guid>();
wrapper.IdentityMap = map;
}
return map.TryAdd(refId);
return map.TryAddKey(refId);
}
#endregion
#region Deserialization API - TryGetOrStore
/// <summary>
/// For deserialization: checks if an object with this Id was already seen.
/// If yes, returns the existing object. If no, stores this object and returns it.
/// Uses IdentityMap.TryGetOrAddValue - single Dictionary operation!
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreInt32(TypeMetadataWrapper<TMetadata> wrapper, object newObj, int id)
{
if (id == 0) return newObj; // Default Id - no tracking
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<int>;
if (map == null)
{
map = new AcSerializerCommon.IdentityMap<int>();
wrapper.IdentityMap = map;
}
return map.TryGetOrAddValue(id, newObj);
}
/// <summary>
/// For deserialization: checks if an object with this Id was already seen (long version).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreLong(TypeMetadataWrapper<TMetadata> wrapper, object newObj, long id)
{
if (id == 0) return newObj;
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<long>;
if (map == null)
{
map = new AcSerializerCommon.IdentityMap<long>();
wrapper.IdentityMap = map;
}
return map.TryGetOrAddValue(id, newObj);
}
/// <summary>
/// For deserialization: checks if an object with this Id was already seen (Guid version).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreGuid(TypeMetadataWrapper<TMetadata> wrapper, object newObj, Guid id)
{
if (id == Guid.Empty) return newObj;
var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap<Guid>;
if (map == null)
{
map = new AcSerializerCommon.IdentityMap<Guid>();
wrapper.IdentityMap = map;
}
return map.TryGetOrAddValue(id, newObj);
}
#endregion

View File

@ -11,6 +11,7 @@ public static partial class AcBinaryDeserializer
{
/// <summary>
/// Binary deserialization context. Public for generated serializers.
/// Uses composition with BinaryDeserializationContextClass for IId-based tracking.
/// </summary>
internal ref struct BinaryDeserializationContext
{
@ -24,6 +25,12 @@ public static partial class AcBinaryDeserializer
private readonly bool _useStringCaching;
private readonly int _maxCachedStringLength;
/// <summary>
/// Heap-allocated context class for IId-based reference tracking.
/// Uses AcSerializerContextBase infrastructure.
/// </summary>
public readonly BinaryDeserializationContextClass ContextClass;
public bool HasMetadata { get; private set; }
public bool HasReferenceHandling { get; private set; }
public bool IsMergeMode { readonly get; set; }
@ -44,11 +51,16 @@ public static partial class AcBinaryDeserializer
public readonly bool IsChainMode => ChainTracker != null;
public BinaryDeserializationContext(ReadOnlySpan<byte> data)
: this(data, AcBinarySerializerOptions.Default)
: this(data, AcBinarySerializerOptions.Default, new BinaryDeserializationContextClass())
{
}
public BinaryDeserializationContext(ReadOnlySpan<byte> data, AcBinarySerializerOptions options)
: this(data, options, new BinaryDeserializationContextClass())
{
}
public BinaryDeserializationContext(ReadOnlySpan<byte> data, AcBinarySerializerOptions options, BinaryDeserializationContextClass contextClass)
{
_buffer = data;
_position = 0;
@ -64,6 +76,7 @@ public static partial class AcBinaryDeserializer
_minStringInternLength = options.MinStringInternLength;
_useStringCaching = options.UseStringCaching;
_maxCachedStringLength = options.MaxCachedStringLength;
ContextClass = contextClass;
}
public void ReadHeader()
@ -551,89 +564,17 @@ public static partial class AcBinaryDeserializer
return ReadStringUtf8(byteLength);
}
#region IId Reference Cache
// Typed caches for IId-based deduplication (no boxing for int/long/Guid)
private Dictionary<(Type, int), object>? _iidCacheInt32;
private Dictionary<(Type, long), object>? _iidCacheLong;
private Dictionary<(Type, Guid), object>? _iidCacheGuid;
#region IId Reference Cache - Delegates to ContextClass
/// <summary>
/// After PopulateObject, checks if we should reuse an existing IId object.
/// Delegates to ContextClass which uses AcSerializerContextBase infrastructure.
/// Returns the object to use (either the new one or an existing cached one).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object GetOrRegisterIIdObject(object newObj, BinaryDeserializeTypeMetadata metadata)
{
if (!metadata.IsIId) return newObj;
return metadata.IdAccessorType switch
{
AcSerializerCommon.IdAccessorType.Int32 => GetOrRegisterInt32(newObj, metadata),
AcSerializerCommon.IdAccessorType.Int64 => GetOrRegisterInt64(newObj, metadata),
AcSerializerCommon.IdAccessorType.Guid => GetOrRegisterGuid(newObj, metadata),
_ => newObj // Object fallback - don't cache (boxing overhead)
};
}
private object GetOrRegisterInt32(object newObj, BinaryDeserializeTypeMetadata metadata)
{
var id = metadata.GetIdInt32(newObj);
if (id == 0) return newObj; // Default Id → no caching
var key = (newObj.GetType(), id);
_iidCacheInt32 ??= new Dictionary<(Type, int), object>(64);
// If already exists → return existing
if (_iidCacheInt32.TryGetValue(key, out var existing))
{
return existing;
}
// New → register and return
_iidCacheInt32[key] = newObj;
return newObj;
}
private object GetOrRegisterInt64(object newObj, BinaryDeserializeTypeMetadata metadata)
{
var id = metadata.GetIdInt64(newObj);
if (id == 0) return newObj; // Default Id → no caching
var key = (newObj.GetType(), id);
_iidCacheLong ??= new Dictionary<(Type, long), object>(64);
// If already exists → return existing
if (_iidCacheLong.TryGetValue(key, out var existing))
{
return existing;
}
// New → register and return
_iidCacheLong[key] = newObj;
return newObj;
}
private object GetOrRegisterGuid(object newObj, BinaryDeserializeTypeMetadata metadata)
{
var id = metadata.GetIdGuid(newObj);
if (id == Guid.Empty) return newObj; // Default Id → no caching
var key = (newObj.GetType(), id);
_iidCacheGuid ??= new Dictionary<(Type, Guid), object>(64);
// If already exists → return existing
if (_iidCacheGuid.TryGetValue(key, out var existing))
{
return existing;
}
// New → register and return
_iidCacheGuid[key] = newObj;
return newObj;
return ContextClass.GetOrRegisterIIdObject(newObj, metadata);
}
#endregion

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinaryDeserializer
{
/// <summary>
/// Heap-allocated context class for binary deserialization.
/// Inherits from AcSerializerContextBase for unified metadata caching and IId-based reference tracking.
/// Used in composition with the ref struct BinaryDeserializationContext.
/// </summary>
internal sealed class BinaryDeserializationContextClass : AcSerializerContextBase<BinaryDeserializeTypeMetadata>
{
/// <summary>
/// Factory for creating BinaryDeserializeTypeMetadata instances.
/// </summary>
protected override Func<Type, BinaryDeserializeTypeMetadata> MetadataFactory
=> static t => new BinaryDeserializeTypeMetadata(t);
/// <summary>
/// After PopulateObject, checks if we should reuse an existing IId object.
/// Uses the unified tracking from AcSerializerContextBase (IdentityMap).
/// Returns the object to use (either the new one or an existing cached one).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object GetOrRegisterIIdObject(object newObj, BinaryDeserializeTypeMetadata metadata)
{
if (!metadata.IsIId) return newObj;
var wrapper = GetWrapper(newObj.GetType());
return metadata.IdAccessorType switch
{
AcSerializerCommon.IdAccessorType.Int32 => TryGetOrStoreInt32(wrapper, newObj, metadata.GetIdInt32(newObj)),
AcSerializerCommon.IdAccessorType.Int64 => TryGetOrStoreLong(wrapper, newObj, metadata.GetIdInt64(newObj)),
AcSerializerCommon.IdAccessorType.Guid => TryGetOrStoreGuid(wrapper, newObj, metadata.GetIdGuid(newObj)),
_ => newObj
};
}
}
}

View File

@ -1,3 +1,4 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
@ -66,7 +67,7 @@ public static partial class AcBinaryDeserializer
RegisterReader(BinaryTypeCode.Guid, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadGuidUnsafe());
RegisterReader(BinaryTypeCode.Enum, static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadEnumValue(ref ctx, type));
RegisterReader(BinaryTypeCode.Object, ReadObject);
RegisterReader(BinaryTypeCode.ObjectRef, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetReferencedObject(ctx.ReadVarInt()));
RegisterReader(BinaryTypeCode.ObjectRef, ReadObjectRef);
RegisterReader(BinaryTypeCode.Array, ReadArray);
RegisterReader(BinaryTypeCode.Dictionary, ReadDictionary);
RegisterReader(BinaryTypeCode.ByteArray, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadByteArray(ref ctx));
@ -893,17 +894,84 @@ public static partial class AcBinaryDeserializer
return context.ReadBytes(length);
}
#endregion
#region Object Reading
/// <summary>
/// Reads an ObjectRef - looks up previously registered object by (Type, IId) in ContextClass.
/// The wire format depends on IdAccessorType:
/// - Int32: VarInt (1-5 bytes)
/// - Int64: VarLong (1-10 bytes)
/// - Guid: Raw 16 bytes
/// </summary>
private static object? ReadObjectRef(ref BinaryDeserializationContext context, Type targetType, int depth)
{
var metadata = GetTypeMetadata(targetType);
if (metadata.IsIId)
{
var wrapper = context.ContextClass.GetWrapper(targetType);
var map = wrapper.IdentityMap;
// Read ID based on type and lookup in IdentityMap
return metadata.IdAccessorType switch
{
AcSerializerCommon.IdAccessorType.Int32 => ReadObjectRefInt32(ref context, map),
AcSerializerCommon.IdAccessorType.Int64 => ReadObjectRefInt64(ref context, map),
AcSerializerCommon.IdAccessorType.Guid => ReadObjectRefGuid(ref context, map),
_ => context.GetReferencedObject(context.ReadVarInt())
};
}
// Non-IId: sequential int refId
return context.GetReferencedObject(context.ReadVarInt());
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObjectRefInt32(ref BinaryDeserializationContext context, AcSerializerCommon.IIdentityMap? map)
{
var id = context.ReadVarInt();
if (map is AcSerializerCommon.IdentityMap<int> intMap && intMap.TryGetValue(id, out var obj))
return obj;
return context.GetReferencedObject(id);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObjectRefInt64(ref BinaryDeserializationContext context, AcSerializerCommon.IIdentityMap? map)
{
var id = context.ReadVarLong(); // VarLong, not VarInt!
if (map is AcSerializerCommon.IdentityMap<long> longMap && longMap.TryGetValue(id, out var obj))
return obj;
return null; // Long IIds don't use flat reference tracking
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObjectRefGuid(ref BinaryDeserializationContext context, AcSerializerCommon.IIdentityMap? map)
{
var id = context.ReadGuidUnsafe(); // 16 bytes raw!
if (map is AcSerializerCommon.IdentityMap<Guid> guidMap && guidMap.TryGetValue(id, out var obj))
return obj;
return null; // Guid IIds don't use flat reference tracking
}
private static object? ReadObject(ref BinaryDeserializationContext context, Type targetType, int depth)
{
// Read reference ID if present
int refId = -1;
var metadata = GetTypeMetadata(targetType);
// Read reference ID based on IdAccessorType (different wire format for each type)
object? readId = null;
if (context.HasReferenceHandling)
{
refId = context.ReadVarInt();
readId = metadata.IdAccessorType switch
{
AcSerializerCommon.IdAccessorType.Int32 => context.ReadVarInt(),
AcSerializerCommon.IdAccessorType.Int64 => context.ReadVarLong(),
AcSerializerCommon.IdAccessorType.Guid => context.ReadGuidUnsafe(),
_ => throw new Exception($"metadata.IdAccessorType not valid: {metadata.IdAccessorType}")
};
}
// Handle dictionary types
@ -912,16 +980,15 @@ public static partial class AcBinaryDeserializer
return ReadDictionaryAsObject(ref context, keyType!, valueType!, depth);
}
var metadata = GetTypeMetadata(targetType);
// Create instance
var instance = CreateInstance(targetType, metadata);
if (instance == null) return null;
// Register reference
if (refId > 0)
// Register reference for flat lookup (int refId only)
if (readId is int intRefId)
{
context.RegisterObject(refId, instance);
if (intRefId == 0) throw new Exception("intRefId == 0");
context.RegisterObject(intRefId, instance);
}
// Deserialize mode: object just created, skip writing defaults (already at default)