From 40fb4950a6667f0f62b0f6604bd447121c01300f Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 23 Jan 2026 23:42:39 +0100 Subject: [PATCH] Refactor AcBinary reference handling and wire format - Unify and clarify object reference tracking for IId and non-IId types - Always write/read Id as a normal property with type marker for IId types - For non-IId types (All mode), use hashcode prefix for reference tracking - Remove special Id prefix logic; all properties use type markers - Centralize hashcode registration logic in deserializer - Improve error handling for missing references - Refactor tests to cover all ReferenceHandlingMode values and verify both data integrity and reference identity - Add hex dump utility for debugging serialized bytes - Make TypeMetadataBase.SourceType public for better diagnostics --- AyCode.Core.Serializers.Console/Program.cs | 49 ++++++ .../AcBinarySerializerIIdReferenceTests.cs | 151 ++++++++++-------- .../AcBinaryDeserializer.CrossType.cs | 2 +- .../Binaries/AcBinaryDeserializer.Populate.cs | 145 +++++++---------- .../Binaries/AcBinaryDeserializer.cs | 148 +++++++++++------ .../Binaries/AcBinarySerializer.cs | 128 +++++++-------- AyCode.Core/Serializers/TypeMetadataBase.cs | 2 +- 7 files changed, 353 insertions(+), 272 deletions(-) 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.