diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs
index aeffeb1..3722462 100644
--- a/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs
+++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs
@@ -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
///
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
diff --git a/AyCode.Core/Serializers/AcSerializerCommon.cs b/AyCode.Core/Serializers/AcSerializerCommon.cs
index 6e59f49..1e50ad2 100644
--- a/AyCode.Core/Serializers/AcSerializerCommon.cs
+++ b/AyCode.Core/Serializers/AcSerializerCommon.cs
@@ -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
///
public enum IdAccessorType : byte
{
+ None = 0,
/// Id is int (most common).
Int32 = 1,
/// Id is long.
@@ -618,28 +620,64 @@ public static class AcSerializerCommon
}
///
- /// 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.
///
/// The ID type (int, long, Guid)
public sealed class IdentityMap : IIdentityMap where TId : notnull
{
- private readonly HashSet _seenIds;
+ private readonly Dictionary _tracked;
public IdentityMap()
{
- _seenIds = new HashSet(EqualityComparer.Default);
+ _tracked = new Dictionary(EqualityComparer.Default);
}
///
- /// 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.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public bool TryAdd(TId id)
+ public bool TryAddKey(TId key)
{
- return _seenIds.Add(id);
+ return _tracked.TryAdd(key, null);
+ }
+
+ ///
+ /// 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).
+ ///
+ [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;
+ }
+
+ ///
+ /// 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.
+ ///
+ [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;
+ }
+
+ ///
+ /// Tries to get the value for a key (ObjectRef lookup).
+ /// Returns true if found, false if not.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool TryGetValue(TId key, out object? value)
+ {
+ return _tracked.TryGetValue(key, out value);
}
///
@@ -647,180 +685,10 @@ public static class AcSerializerCommon
///
public void Reset()
{
- _seenIds.Clear();
+ _tracked.Clear();
}
}
-
- ///
- /// 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.
- ///
- 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
-
- ///
- /// 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.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public bool TryGetOriginalObject(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(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(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(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(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);
- }
- ///
- /// Registers an IId object.
- ///
- public void Register(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(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(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(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(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;
- }
-
- ///
- /// Clears all tracking data for reuse.
- ///
- public void Reset()
- {
- _int32Cache?.Clear();
- _int64Cache?.Clear();
- _guidCache?.Clear();
- _objectCache?.Clear();
- }
- }
-
-
#endregion
#region Chain Reference Tracking
diff --git a/AyCode.Core/Serializers/AcSerializerContextBase.cs b/AyCode.Core/Serializers/AcSerializerContextBase.cs
index 051a9da..f84ea3f 100644
--- a/AyCode.Core/Serializers/AcSerializerContextBase.cs
+++ b/AyCode.Core/Serializers/AcSerializerContextBase.cs
@@ -90,7 +90,7 @@ public abstract class AcSerializerContextBase where TMetadata : TypeM
map = new AcSerializerCommon.IdentityMap();
wrapper.IdentityMap = map;
}
- return map.TryAdd(refId);
+ return map.TryAddKey(refId);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -138,7 +138,7 @@ public abstract class AcSerializerContextBase where TMetadata : TypeM
map = new AcSerializerCommon.IdentityMap();
wrapper.IdentityMap = map;
}
- return map.TryAdd(refId);
+ return map.TryAddKey(refId);
}
#endregion
@@ -164,7 +164,64 @@ public abstract class AcSerializerContextBase where TMetadata : TypeM
map = new AcSerializerCommon.IdentityMap();
wrapper.IdentityMap = map;
}
- return map.TryAdd(refId);
+ return map.TryAddKey(refId);
+ }
+
+ #endregion
+
+ #region Deserialization API - TryGetOrStore
+
+ ///
+ /// 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!
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public object TryGetOrStoreInt32(TypeMetadataWrapper wrapper, object newObj, int id)
+ {
+ if (id == 0) return newObj; // Default Id - no tracking
+
+ var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap;
+ if (map == null)
+ {
+ map = new AcSerializerCommon.IdentityMap();
+ wrapper.IdentityMap = map;
+ }
+ return map.TryGetOrAddValue(id, newObj);
+ }
+
+ ///
+ /// For deserialization: checks if an object with this Id was already seen (long version).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public object TryGetOrStoreLong(TypeMetadataWrapper wrapper, object newObj, long id)
+ {
+ if (id == 0) return newObj;
+
+ var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap;
+ if (map == null)
+ {
+ map = new AcSerializerCommon.IdentityMap();
+ wrapper.IdentityMap = map;
+ }
+ return map.TryGetOrAddValue(id, newObj);
+ }
+
+ ///
+ /// For deserialization: checks if an object with this Id was already seen (Guid version).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public object TryGetOrStoreGuid(TypeMetadataWrapper wrapper, object newObj, Guid id)
+ {
+ if (id == Guid.Empty) return newObj;
+
+ var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap;
+ if (map == null)
+ {
+ map = new AcSerializerCommon.IdentityMap();
+ wrapper.IdentityMap = map;
+ }
+ return map.TryGetOrAddValue(id, newObj);
}
#endregion
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs
index 0df74c8..e47603c 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs
@@ -11,6 +11,7 @@ public static partial class AcBinaryDeserializer
{
///
/// Binary deserialization context. Public for generated serializers.
+ /// Uses composition with BinaryDeserializationContextClass for IId-based tracking.
///
internal ref struct BinaryDeserializationContext
{
@@ -23,6 +24,12 @@ public static partial class AcBinaryDeserializer
private readonly byte _minStringInternLength;
private readonly bool _useStringCaching;
private readonly int _maxCachedStringLength;
+
+ ///
+ /// Heap-allocated context class for IId-based reference tracking.
+ /// Uses AcSerializerContextBase infrastructure.
+ ///
+ public readonly BinaryDeserializationContextClass ContextClass;
public bool HasMetadata { get; private set; }
public bool HasReferenceHandling { get; private set; }
@@ -44,11 +51,16 @@ public static partial class AcBinaryDeserializer
public readonly bool IsChainMode => ChainTracker != null;
public BinaryDeserializationContext(ReadOnlySpan data)
- : this(data, AcBinarySerializerOptions.Default)
+ : this(data, AcBinarySerializerOptions.Default, new BinaryDeserializationContextClass())
{
}
public BinaryDeserializationContext(ReadOnlySpan data, AcBinarySerializerOptions options)
+ : this(data, options, new BinaryDeserializationContextClass())
+ {
+ }
+
+ public BinaryDeserializationContext(ReadOnlySpan 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
///
/// 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).
///
[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
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs
new file mode 100644
index 0000000..0798333
--- /dev/null
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace AyCode.Core.Serializers.Binaries;
+
+public static partial class AcBinaryDeserializer
+{
+ ///
+ /// 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.
+ ///
+ internal sealed class BinaryDeserializationContextClass : AcSerializerContextBase
+ {
+ ///
+ /// Factory for creating BinaryDeserializeTypeMetadata instances.
+ ///
+ protected override Func MetadataFactory
+ => static t => new BinaryDeserializeTypeMetadata(t);
+
+ ///
+ /// 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).
+ ///
+ [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
+ };
+ }
+ }
+}
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
index 2b93581..74149c6 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
@@ -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,35 +894,101 @@ public static partial class AcBinaryDeserializer
return context.ReadBytes(length);
}
+
+
#endregion
#region Object Reading
+ ///
+ /// 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
+ ///
+ 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 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 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 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
if (IsDictionaryType(targetType, out var keyType, out var valueType))
{
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)