diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs
index d946dd2..a7b1e34 100644
--- a/AyCode.Core.Serializers.Console/Program.cs
+++ b/AyCode.Core.Serializers.Console/Program.cs
@@ -680,6 +680,19 @@ public static class Program
sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
sb.AppendLine();
+ // Serialized bytes for Large test data (AcBinary Default)
+ var largeTestData = testDataSets.FirstOrDefault(t => t.Name.StartsWith("Large"));
+ if (largeTestData != null)
+ {
+ sb.AppendLine("=== SERIALIZED BYTES: Large (5x5x5x10) - AcBinary (Default) ===");
+ var serializedBytes = AcBinarySerializer.Serialize(largeTestData.Order, AcBinarySerializerOptions.Default);
+ sb.AppendLine($"Size: {serializedBytes.Length:N0} bytes");
+ sb.AppendLine();
+ sb.AppendLine("Hex dump:");
+ sb.AppendLine(FormatHexDump(serializedBytes));
+ sb.AppendLine();
+ }
+
// CSV-like data for easy import
sb.AppendLine("=== RAW DATA (CSV) ===");
sb.AppendLine("TestData,Serializer,Size,SerializeMs,DeserializeMs,RoundTripMs");
@@ -758,5 +771,41 @@ public static class Program
System.Console.WriteLine($"\n✓ Results saved to: {filePath}");
}
+ ///
+ /// Formats byte array as hex dump with offset, hex values, and ASCII representation.
+ ///
+ private static string FormatHexDump(byte[] bytes, int bytesPerLine = 16)
+ {
+ var sb = new StringBuilder();
+ for (var i = 0; i < bytes.Length; i += bytesPerLine)
+ {
+ // Offset
+ sb.Append($"{i:X8} ");
+
+ // Hex bytes
+ for (var j = 0; j < bytesPerLine; j++)
+ {
+ if (i + j < bytes.Length)
+ sb.Append($"{bytes[i + j]:X2} ");
+ else
+ sb.Append(" ");
+
+ if (j == 7) sb.Append(' '); // Extra space in middle
+ }
+
+ sb.Append(" |");
+
+ // ASCII representation
+ for (var j = 0; j < bytesPerLine && i + j < bytes.Length; j++)
+ {
+ var b = bytes[i + j];
+ sb.Append(b is >= 32 and < 127 ? (char)b : '.');
+ }
+
+ sb.AppendLine("|");
+ }
+ return sb.ToString();
+ }
+
#endregion
}
diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs
index 3722462..64cbfcb 100644
--- a/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs
+++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Binaries;
+using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -29,11 +30,9 @@ public class AcBinarySerializerIIdReferenceTests
///
/// Counts occurrences of ObjectRef (0x1B = 27) in binary data.
///
- private static int CountObjectRefs(byte[] binary)
+ private static int CountObjectRefs(byte[] binary, bool writeBinaryToConsole = true)
{
- Console.WriteLine();
- Console.WriteLine( BitConverter.ToString(binary));
- Console.WriteLine();
+ if (writeBinaryToConsole) WriteBinaryToConsole(binary);
var count = 0;
for (var i = 0; i < binary.Length; i++)
@@ -44,6 +43,13 @@ public class AcBinarySerializerIIdReferenceTests
return count;
}
+ private static void WriteBinaryToConsole(byte[] binary)
+ {
+ Console.WriteLine();
+ Console.WriteLine(BitConverter.ToString(binary));
+ Console.WriteLine();
+ }
+
///
/// Counts occurrences of a string in binary data (UTF8).
///
@@ -73,80 +79,87 @@ public class AcBinarySerializerIIdReferenceTests
///
/// SCENARIO 1: Same instance referenced multiple times.
- /// Validates: ObjectRef present + data integrity after deserialize + reference identity.
+ /// Tests all ReferenceHandling modes: None, OnlyId, All
///
[TestMethod]
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, 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 } }
- ]
+ var modes = new[]
+ {
+ ReferenceHandlingMode.None,
+ ReferenceHandlingMode.OnlyId,
+ ReferenceHandlingMode.All
};
- // Act
- var binary = order.ToBinary();
- var result = binary.BinaryTo();
-
- // Assert 1: Binary should have ObjectRef entries (reference handling is active)
- var objectRefCount = CountObjectRefs(binary);
- Console.WriteLine($"Binary size: {binary.Length} bytes");
- Console.WriteLine($"ObjectRef count: {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
- Assert.IsNotNull(result, "Deserialized result is null");
- Assert.IsNotNull(result.PrimaryTag, "PrimaryTag is null - data lost!");
- Assert.AreEqual(1, result.PrimaryTag.Id, "PrimaryTag.Id incorrect");
- Assert.AreEqual("ImportantTag", result.PrimaryTag.Name, "PrimaryTag.Name incorrect");
- Assert.AreEqual("#FF0000", result.PrimaryTag.Color, "PrimaryTag.Color incorrect");
-
- Assert.IsNotNull(result.Items, "Items is null");
- Assert.AreEqual(3, result.Items.Count, "Items count incorrect");
-
- for (var i = 0; i < 3; i++)
+ foreach (var mode in modes)
{
- 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");
+ Console.WriteLine($"\n========== ReferenceHandling: {mode} ==========");
+
+ // Arrange: SAME instance used multiple times
+ var userPreferences = new UserPreferences();
+ var sharedTag = new SharedTag { Id = 1, Name = "ImportantTag", Color = "#FF0000" };
+ var sharedUser = new SharedUser { Id = 1, Preferences = userPreferences };
- Assert.IsNotNull(result.Items[i].Assignee, $"Items[{i}].Assignee is null - data lost!");
+ var order = new TestOrder
+ {
+ Id = 1,
+ OrderNumber = "ORD-001",
+ PrimaryTag = sharedTag,
+ Owner = sharedUser,
+ Items =
+ [
+ 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 } }
+ ]
+ };
+
+ var options = new AcBinarySerializerOptions { ReferenceHandling = mode };
+
+ // Act
+ var binary = AcBinarySerializer.Serialize(order, options);
+ WriteBinaryToConsole(binary);
+ var result = binary.BinaryTo(); // Options from header
+
+ var objectRefCount = CountObjectRefs(binary, false);
+ Console.WriteLine($"Binary size: {binary.Length} bytes");
+ Console.WriteLine($"ObjectRef count: {objectRefCount}");
+
+ // Assert based on mode
+ switch (mode)
+ {
+ case ReferenceHandlingMode.None:
+ Assert.AreEqual(0, objectRefCount, $"[{mode}] Should have 0 ObjectRefs");
+ break;
+
+ case ReferenceHandlingMode.OnlyId:
+ // sharedTag (Id=1) 4x → 3 ObjectRefs, sharedUser (Id=1) 2x → 1 ObjectRef = 4 total
+ Assert.IsTrue(objectRefCount >= 4, $"[{mode}] Expected at least 4 ObjectRefs, found {objectRefCount}");
+ // IId types should have reference identity
+ Assert.AreSame(result.PrimaryTag, result.Items[0].Tag, $"[{mode}] Tag reference identity failed");
+ Assert.AreSame(result.Owner, result.Items[0].Assignee, $"[{mode}] User reference identity failed");
+ break;
+
+ case ReferenceHandlingMode.All:
+ // IId types + Non-IId (UserPreferences) should have ObjectRefs
+ Assert.IsTrue(objectRefCount >= 4, $"[{mode}] Expected at least 4 ObjectRefs, found {objectRefCount}");
+ Assert.AreSame(result.PrimaryTag, result.Items[0].Tag, $"[{mode}] Tag reference identity failed");
+ Assert.AreSame(result.Owner, result.Items[0].Assignee, $"[{mode}] User reference identity failed");
+ // Non-IId should also have reference identity in All mode
+ Assert.AreSame(result.Owner.Preferences, result.Items[0].Assignee.Preferences,
+ $"[{mode}] UserPreferences reference identity failed - Non-IId should work in All mode!");
+ break;
+ }
+
+ // Data integrity - always check
+ Assert.IsNotNull(result, $"[{mode}] Deserialized result is null");
+ Assert.IsNotNull(result.PrimaryTag, $"[{mode}] PrimaryTag is null");
+ Assert.AreEqual(1, result.PrimaryTag.Id, $"[{mode}] PrimaryTag.Id incorrect");
+ Assert.AreEqual("ImportantTag", result.PrimaryTag.Name, $"[{mode}] PrimaryTag.Name incorrect");
+ Assert.AreEqual(3, result.Items.Count, $"[{mode}] Items count incorrect");
+
+ Console.WriteLine($"[{mode}] PASSED ✓");
}
-
- // Assert 3: Reference identity - all should be same object reference
- Assert.AreSame(result.PrimaryTag, result.Items[0].Tag,
- "Item[0].Tag should be same reference as PrimaryTag");
- Assert.AreSame(result.PrimaryTag, result.Items[1].Tag,
- "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
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs
index 5361d48..00f2766 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs
@@ -345,7 +345,7 @@ public static partial class AcBinaryDeserializer
}
}
- PopulateObjectCore(ref context, existingObj, wrapper.Metadata, nextDepth, skipDefaultWrite: false);
+ PopulateObjectCore(ref context, existingObj, wrapper, nextDepth, skipDefaultWrite: false);
return;
}
}
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
index f589d29..3006412 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
@@ -11,6 +11,24 @@ namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinaryDeserializer
{
+ #region Helper Methods
+
+ ///
+ /// Reads hashcode prefix and registers object for Non-IId types with RefHandling=All.
+ /// IId types have no prefix - their Id is in the properties.
+ /// Call this AFTER consuming the Object marker, BEFORE PopulateObjectCore.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void ReadAndRegisterHashcodeIfNeeded(ref BinaryDeserializationContext context, TypeMetadataWrapper wrapper, object instance)
+ {
+ if (wrapper.Metadata.IsIId || !context.ContextClass.UseTypeReferenceHandling(wrapper.Metadata)) return;
+
+ var hashcode = context.ReadVarInt();
+ context.ContextClass.TryGetOrStoreInt32(wrapper, instance, hashcode);
+ }
+
+ #endregion
+
#region Populate Object Methods
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -20,56 +38,52 @@ public static partial class AcBinaryDeserializer
///
/// Populate object with automatic mode detection from context.
/// Uses IsMergeMode to determine merge behavior for IId collections.
+ /// Wire format:
+ /// - IId types: [Object][props 0-tl...] - no refId prefix, Id is in props
+ /// - Non-IId + All: [Object][hashcode][props 0-tl...] - hashcode prefix
+ /// - Ref=Off: [Object][props 0-tl...] - no prefix
///
private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
{
var wrapper = context.ContextClass.GetWrapper(targetType);
- var metadata = wrapper.Metadata;
-
- // Handle ref ID if present
- if (context.ContextClass.UseTypeReferenceHandling(metadata))
- {
- var refId = context.ReadVarInt();
- if (refId > 0)
- {
- context.RegisterObject(wrapper, refId, target);
- }
- }
-
- PopulateObjectCore(ref context, target, metadata, depth, skipDefaultWrite: false);
+ PopulateObjectCore(ref context, target, wrapper, depth, skipDefaultWrite: false);
}
///
/// Core populate logic shared by all populate paths.
+ /// Handles hashcode reading for Non-IId types internally.
+ /// Wire format: All properties are written WITH type markers (including Id for IId types).
///
- /// Deserialization context
- /// Target object to populate
- /// Type metadata
- /// Current depth
- ///
- /// true = Deserialize mode - object just created, properties already at default, skip writing defaults
- /// false = Populate mode - existing object, must overwrite properties with default values
- ///
- private static void PopulateObjectCore(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth, bool skipDefaultWrite)
+ private static void PopulateObjectCore(
+ ref BinaryDeserializationContext context,
+ object target,
+ TypeMetadataWrapper wrapper,
+ int depth,
+ bool skipDefaultWrite)
+ {
+ // Handle hashcode for Non-IId types - ONE place for this logic!
+ ReadAndRegisterHashcodeIfNeeded(ref context, wrapper, target);
+
+ PopulateObjectProperties(ref context, target, wrapper.Metadata, depth, skipDefaultWrite);
+ }
+
+ ///
+ /// Populates object properties only - does NOT read hashcode prefix.
+ /// Used by ReadObject where hashcode is already handled separately.
+ ///
+ private static void PopulateObjectProperties(
+ ref BinaryDeserializationContext context,
+ object target,
+ BinaryDeserializeTypeMetadata metadata,
+ int depth,
+ bool skipDefaultWrite)
{
var properties = metadata.PropertiesArray;
var nextDepth = depth + 1;
var isMergeMode = context.IsMergeMode;
- var startIndex = 0;
- // For IId types with reference handling: Id property has no type marker (value only)
- var skipIdMarker = metadata.IsIId && context.ContextClass.UseTypeReferenceHandling(metadata);
-
- if (skipIdMarker)
- {
- startIndex = 1;
-
- // Id property: read value WITHOUT type marker (serializer didn't write one)
- // For IId types, Id is always at index 0 (sorted first)
- ReadIdValueWithoutMarker(ref context, target, properties[0], metadata.IdAccessorType);
- }
-
- for (int i = startIndex; i < properties.Length; i++)
+ // All properties start from index 0 - Id is included with normal type markers
+ for (int i = 0; i < properties.Length; i++)
{
var propInfo = properties[i];
var peekCode = context.PeekByte();
@@ -126,20 +140,8 @@ public static partial class AcBinaryDeserializer
{
context.ReadByte(); // consume Object marker
- var wrapper = context.ContextClass.GetWrapper(propInfo.PropertyType);
-
- // Handle ref ID if present
- if (context.ContextClass.UseTypeReferenceHandling(wrapper.Metadata))
- {
- var refId = context.ReadVarInt();
- if (refId > 0)
- {
- context.RegisterObject(wrapper, refId, existingObj);
- }
- }
-
- // Recursively populate - existing object, don't skip defaults
- PopulateObjectCore(ref context, existingObj, wrapper.Metadata, nextDepth, skipDefaultWrite: false);
+ var nestedWrapper = context.ContextClass.GetWrapper(propInfo.PropertyType);
+ PopulateObjectCore(ref context, existingObj, nestedWrapper, nextDepth, skipDefaultWrite: false);
continue;
}
}
@@ -177,12 +179,13 @@ public static partial class AcBinaryDeserializer
}
///
- /// Called from ReadObject for new instances - just calls PopulateObjectCore with skipDefaultWrite=true.
+ /// Called from ReadObject for new instances.
+ /// Note: For Non-IId types, hashcode is already read and registered in ReadObject before this call.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth, bool skipDefaultWrite)
{
- PopulateObjectCore(ref context, target, metadata, depth, skipDefaultWrite);
+ PopulateObjectProperties(ref context, target, metadata, depth, skipDefaultWrite);
}
#endregion
@@ -253,19 +256,7 @@ public static partial class AcBinaryDeserializer
if (existingItem != null)
{
context.ReadByte(); // consume Object marker
-
- // Handle ref ID if present
- if (context.ContextClass.UseTypeReferenceHandling(elementMetadata))
- {
- var refId = context.ReadVarInt();
- if (refId > 0)
- {
- context.RegisterObject(wrapper, refId, existingItem);
- }
- }
-
- // Populate mode: existing item, must overwrite with defaults
- PopulateObjectCore(ref context, existingItem, elementMetadata, nextDepth, skipDefaultWrite: false);
+ PopulateObjectCore(ref context, existingItem, wrapper, nextDepth, skipDefaultWrite: false);
continue;
}
}
@@ -357,16 +348,8 @@ public static partial class AcBinaryDeserializer
var newItem = CreateInstance(elementType, elementMetadata);
if (newItem == null) continue;
- // Handle ref ID if present
- if (context.ContextClass.UseTypeReferenceHandling(elementMetadata))
- {
- var refId = context.ReadVarInt();
- if (refId > 0)
- context.RegisterObject(wrapper, refId, newItem);
- }
-
- // Deserialize mode: new item just created, skip writing defaults
- PopulateObjectCore(ref context, newItem, elementMetadata, nextDepth, skipDefaultWrite: true);
+ // PopulateObjectCore handles hashcode reading for Non-IId types
+ PopulateObjectCore(ref context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
var itemId = idGetter(newItem);
if (itemId != null && !IsDefaultValue(itemId, idType))
@@ -469,16 +452,8 @@ public static partial class AcBinaryDeserializer
var newItem = CreateInstance(elementType, elementMetadata);
if (newItem == null) continue;
- // Handle ref ID if present
- if (context.ContextClass.UseTypeReferenceHandling(elementMetadata))
- {
- var refId = context.ReadVarInt();
- if (refId > 0)
- context.RegisterObject(wrapper, refId, newItem);
- }
-
- // Deserialize mode: new item just created, skip writing defaults
- PopulateObjectCore(ref context, newItem, elementMetadata, nextDepth, skipDefaultWrite: true);
+ // PopulateObjectCore handles hashcode reading for Non-IId types
+ PopulateObjectCore(ref context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
var itemId = idGetter(newItem);
if (itemId != null && !IsDefaultValue(itemId, idType))
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
index e6dc05a..bd7768c 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
@@ -904,57 +904,90 @@ public static partial class AcBinaryDeserializer
#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
+ /// Reads an ObjectRef - looks up previously registered object.
+ /// Wire format:
+ /// - IId types: [ObjectRef][Id érték] - lookup by Id
+ /// - Non-IId types: [ObjectRef][hashcode] - lookup by hashcode
///
private static object? ReadObjectRef(ref BinaryDeserializationContext context, Type targetType, int depth)
{
var wrapper = context.ContextClass.GetWrapper(targetType);
- return ReadObjectRef(ref context, ref wrapper, depth);
+ return ReadObjectRef(ref context, ref wrapper);
}
- private static object? ReadObjectRef(ref BinaryDeserializationContext context, ref TypeMetadataWrapper wrapper, int depth)
+
+ private static object? ReadObjectRef(ref BinaryDeserializationContext context, ref TypeMetadataWrapper wrapper)
{
var metadata = wrapper.Metadata;
- if (metadata.IdAccessorType != AcSerializerCommon.IdAccessorType.None)
+ if (metadata.IsIId)
{
- // Read ID based on type and lookup in IdentityMap
+ // IId: [ObjectRef][Id érték] - lookup by Id
return metadata.IdAccessorType switch
{
AcSerializerCommon.IdAccessorType.Int32 => ReadObjectRefInt32(ref context, ref wrapper),
AcSerializerCommon.IdAccessorType.Int64 => ReadObjectRefInt64(ref context, ref wrapper),
AcSerializerCommon.IdAccessorType.Guid => ReadObjectRefGuid(ref context, ref wrapper),
- _ => throw new Exception("metadata.IdAccessorType not valid")
+ _ => throw new AcBinaryDeserializationException(
+ $"IId type '{metadata.SourceType.Name}' must have valid IdAccessorType",
+ context.Position, metadata.SourceType)
};
}
-
- return null;
+ else
+ {
+ // Non-IId: [ObjectRef][hashcode] - lookup by hashcode
+ var hashcode = context.ReadVarInt();
+ if (context.ContextClass.TryGetValue(wrapper, hashcode, out var instance))
+ return instance;
+
+ throw new AcBinaryDeserializationException(
+ $"ObjectRef hashcode {hashcode} not found for type '{metadata.SourceType.Name}'",
+ context.Position, metadata.SourceType);
+ }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObjectRefInt32(ref BinaryDeserializationContext context, ref TypeMetadataWrapper wrapper)
{
var id = context.ReadVarInt();
- return context.ContextClass.TryGetValue(wrapper, id, out var instance) ? instance : throw new Exception("ReadObjectRefInt32");
+ if (context.ContextClass.TryGetValue(wrapper, id, out var instance))
+ return instance;
+
+ throw new AcBinaryDeserializationException(
+ $"ObjectRef Id {id} not found for type '{wrapper.Metadata.SourceType.Name}'",
+ context.Position, wrapper.Metadata.SourceType);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObjectRefInt64(ref BinaryDeserializationContext context, ref TypeMetadataWrapper wrapper)
{
var id = context.ReadVarLong();
- return context.ContextClass.TryGetValue(wrapper, id, out var instance) ? instance : throw new Exception("ReadObjectRefInt64");;
+ if (context.ContextClass.TryGetValue(wrapper, id, out var instance))
+ return instance;
+
+ throw new AcBinaryDeserializationException(
+ $"ObjectRef Id {id} not found for type '{wrapper.Metadata.SourceType.Name}'",
+ context.Position, wrapper.Metadata.SourceType);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObjectRefGuid(ref BinaryDeserializationContext context, ref TypeMetadataWrapper wrapper)
{
- var id = context.ReadGuidUnsafe(); // 16 bytes raw!
- return context.ContextClass.TryGetValue(wrapper, id, out var instance) ? instance : throw new Exception("ReadObjectRefGuid");;
+ var id = context.ReadGuidUnsafe();
+ if (context.ContextClass.TryGetValue(wrapper, id, out var instance))
+ return instance;
+
+ throw new AcBinaryDeserializationException(
+ $"ObjectRef Id {id} not found for type '{wrapper.Metadata.SourceType.Name}'",
+ context.Position, wrapper.Metadata.SourceType);
}
+ ///
+ /// Reads an Object.
+ /// Wire format:
+ /// - IId types: [Object][props 0-tól...] - Id a props-ban, populate után register
+ /// - Non-IId + All: [Object][hashcode][props 0-tól...] - hashcode előre olvasva
+ /// - Ref=Off: [Object][props 0-tól...] - semmi extra
+ ///
private static object? ReadObject(ref BinaryDeserializationContext context, Type targetType, int depth)
{
// Handle dictionary types
@@ -966,48 +999,62 @@ public static partial class AcBinaryDeserializer
var wrapper = context.ContextClass.GetWrapper(targetType);
var metadata = wrapper.Metadata;
- object? instance = null;
+ object? instance;
if (context.ContextClass.UseTypeReferenceHandling(metadata))
{
- switch (metadata.IdAccessorType)
+ if (metadata.IsIId)
{
- case AcSerializerCommon.IdAccessorType.Int32:
- var intId = context.ReadVarInt();
- if (context.ContextClass.TryGetValue(wrapper, intId, out instance)) return instance;
-
- instance = CreateInstance(targetType, metadata);
- if (instance == null) return null;
- context.ContextClass.TryGetOrStoreInt32(wrapper, instance, intId);
- break;
- case AcSerializerCommon.IdAccessorType.Int64:
- var longId = context.ReadVarLong();
- if (context.ContextClass.TryGetValue(wrapper, longId, out instance)) return instance;
-
- instance = CreateInstance(targetType, metadata);
- if (instance == null) return null;
- context.ContextClass.TryGetOrStoreLong(wrapper, instance, longId);
- break;
- case AcSerializerCommon.IdAccessorType.Guid:
- var guidId = context.ReadGuidUnsafe();
- if (context.ContextClass.TryGetValue(wrapper, guidId, out instance)) return instance;
-
- instance = CreateInstance(targetType, metadata);
- if (instance == null) return null;
- context.ContextClass.TryGetOrStoreGuid(wrapper, instance, guidId);
- break;
- default:
- throw new Exception($"metadata.IdAccessorType not valid: {metadata.IdAccessorType}");
+ // IId: [Object][props 0-tól...]
+ // Create → Populate (Id beolvasódik) → Register by Id
+ instance = CreateInstance(targetType, metadata);
+ if (instance == null) return null;
+
+ PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
+
+ // Register by Id after populate (Id is now set)
+ switch (metadata.IdAccessorType)
+ {
+ case AcSerializerCommon.IdAccessorType.Int32:
+ var intId = metadata.GetIdInt32(instance);
+ context.ContextClass.TryGetOrStoreInt32(wrapper, instance, intId);
+ break;
+ case AcSerializerCommon.IdAccessorType.Int64:
+ var longId = metadata.GetIdInt64(instance);
+ context.ContextClass.TryGetOrStoreLong(wrapper, instance, longId);
+ break;
+ case AcSerializerCommon.IdAccessorType.Guid:
+ var guidId = metadata.GetIdGuid(instance);
+ context.ContextClass.TryGetOrStoreGuid(wrapper, instance, guidId);
+ break;
+ }
+ }
+ else
+ {
+ // Non-IId + All: [Object][hashcode][props 0-tól...]
+ var hashcode = context.ReadVarInt();
+
+ // TryGetValue - if already exists, return (shouldn't happen for Object, only ObjectRef)
+ if (context.ContextClass.TryGetValue(wrapper, hashcode, out instance))
+ return instance;
+
+ // Create + Register by hashcode
+ instance = CreateInstance(targetType, metadata);
+ if (instance == null) return null;
+ context.ContextClass.TryGetOrStoreInt32(wrapper, instance, hashcode);
+
+ // Populate
+ PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
}
}
else
{
+ // Ref=Off: [Object][props 0-tól...]
instance = CreateInstance(targetType, metadata);
if (instance == null) return null;
+
+ PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
}
-
- // Deserialize mode: object just created, skip writing defaults (already at default)
- PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
// ChainMode: Register/retrieve from chain tracker (separate from reference handling)
// Note: For ChainMode, we need IdGetter OR typed getters (IdAccessorType != None)
@@ -1406,9 +1453,12 @@ public static partial class AcBinaryDeserializer
private static void SkipObject(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData)
{
- // Skip ref ID if present
- if (context.ContextClass.UseTypeReferenceHandling(metaData))
+ // Wire format:
+ // - IId: [Object][props 0-tól...] - no hashcode
+ // - Non-IId + All: [Object][hashcode][props 0-tól...] - hashcode present
+ if (context.ContextClass.UseTypeReferenceHandling(metaData) && !metaData.IsIId)
{
+ // Non-IId: skip hashcode
context.ReadVarInt();
}
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
index 65436c2..8b5f8f4 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
@@ -641,85 +641,79 @@ public static partial class AcBinarySerializer
var wrapper = context.GetWrapper(type);
var metadata = wrapper.Metadata;
- // Single-pass reference tracking - type-specific check
+ // Wire format:
+ // - IId types: [Object][props 0-tl...] - Id a props-ban, nincs extra
+ // - Non-IId + All: [Object][hashcode][props 0-tl...] - hashcode elre
+ // - Ref=Off: [Object][props 0-tl...] - semmi extra
+ // ObjectRef format:
+ // - IId: [ObjectRef][Id rtk]
+ // - Non-IId: [ObjectRef][hashcode]
+
if (context.UseTypeReferenceHandling(metadata))
{
- switch (metadata.IdAccessorType)
+ if (metadata.IsIId)
{
- case AcSerializerCommon.IdAccessorType.Int32:
- if (!context.TryTrack(wrapper, value, out int intId))
- {
- // Already seen write reference
- context.WriteByte(BinaryTypeCode.ObjectRef);
- context.WriteVarInt(intId);
- return;
- }
- // First occurrence write object with refId
- context.WriteByte(BinaryTypeCode.Object);
- context.WriteVarInt(intId);
- break;
-
- case AcSerializerCommon.IdAccessorType.Int64:
- if (!context.TryTrack(wrapper, value, out long longId))
- {
- context.WriteByte(BinaryTypeCode.ObjectRef);
- context.WriteVarLong(longId);
- return;
- }
- context.WriteByte(BinaryTypeCode.Object);
- context.WriteVarLong(longId);
- break;
-
- case AcSerializerCommon.IdAccessorType.Guid:
- if (!context.TryTrack(wrapper, value, out Guid guidId))
- {
- context.WriteByte(BinaryTypeCode.ObjectRef);
- context.WriteGuidBits(guidId);
- return;
- }
- context.WriteByte(BinaryTypeCode.Object);
- context.WriteGuidBits(guidId);
- break;
+ // IId tpus: track by Id, ObjectRef writes Id
+ switch (metadata.IdAccessorType)
+ {
+ case AcSerializerCommon.IdAccessorType.Int32:
+ if (!context.TryTrack(wrapper, value, out int intId))
+ {
+ // Already seen ? ObjectRef + Id
+ context.WriteByte(BinaryTypeCode.ObjectRef);
+ context.WriteVarInt(intId);
+ return;
+ }
+ // First occurrence ? Object (no extra data, Id in props)
+ context.WriteByte(BinaryTypeCode.Object);
+ break;
+
+ case AcSerializerCommon.IdAccessorType.Int64:
+ if (!context.TryTrack(wrapper, value, out long longId))
+ {
+ context.WriteByte(BinaryTypeCode.ObjectRef);
+ context.WriteVarLong(longId);
+ return;
+ }
+ context.WriteByte(BinaryTypeCode.Object);
+ break;
+
+ case AcSerializerCommon.IdAccessorType.Guid:
+ if (!context.TryTrack(wrapper, value, out Guid guidId))
+ {
+ context.WriteByte(BinaryTypeCode.ObjectRef);
+ context.WriteGuidBits(guidId);
+ return;
+ }
+ context.WriteByte(BinaryTypeCode.Object);
+ break;
+ }
+ }
+ else
+ {
+ // Non-IId + RefHandling=All: track by hashcode (IdAccessorType=Int32, RefIdGetter=GetHashCode)
+ if (!context.TryTrack(wrapper, value, out int hashcode))
+ {
+ // Already seen ? ObjectRef + hashcode
+ context.WriteByte(BinaryTypeCode.ObjectRef);
+ context.WriteVarInt(hashcode);
+ return;
+ }
+ // First occurrence ? Object + hashcode + props
+ context.WriteByte(BinaryTypeCode.Object);
+ context.WriteVarInt(hashcode);
}
}
else
{
- // No reference handling for this type - just write object marker
+ // No reference handling - just write object marker
context.WriteByte(BinaryTypeCode.Object);
}
-
-
-
- // Write properties
+ // Write all properties (startIndex=0, including Id for IId types)
var nextDepth = depth + 1;
var properties = metadata.Properties;
var propCount = properties.Length;
- var startIndex = 0;
- // For IId types with reference handling: skip type marker for Id property (value only)
- // The deserializer knows the Id type from metadata, so marker is redundant
- var skipIdMarker = metadata.IsIId && context.UseTypeReferenceHandling(metadata);
-
- if (skipIdMarker)
- {
- startIndex = 1;
- var prop = properties[0];
-
- // Id property: write value WITHOUT type marker (deserializer knows type from metadata)
- // For IId types, Id is always at index 0 (sorted first)
- switch (metadata.IdAccessorType)
- {
- case AcSerializerCommon.IdAccessorType.Int32:
- context.WriteVarInt(prop.GetInt32(value));
- break;
- case AcSerializerCommon.IdAccessorType.Int64:
- context.WriteVarLong(prop.GetInt64(value));
- break;
- case AcSerializerCommon.IdAccessorType.Guid:
- context.WriteGuidBits(prop.GetGuid(value));
- break;
- }
- }
// Single-pass serialization with SKIP markers
// - No property count needed (fixed property order)
@@ -728,7 +722,7 @@ public static partial class AcBinarySerializer
// - Write value OR skip marker in one operation
var hasPropertyFilter = context.HasPropertyFilter;
- for (var i = startIndex; i < propCount; i++)
+ for (var i = 0; i < propCount; i++)
{
var prop = properties[i];
diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs
index 00f93d5..81299eb 100644
--- a/AyCode.Core/Serializers/TypeMetadataBase.cs
+++ b/AyCode.Core/Serializers/TypeMetadataBase.cs
@@ -45,7 +45,7 @@ public abstract class TypeMetadataBase
///
/// The type this metadata is for. Stored to avoid repeated reflection.
///
- protected Type SourceType { get; }
+ public Type SourceType { get; }
///
/// Readable properties (CanRead=true) for this type. Used by serializers.