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
This commit is contained in:
Loretta 2026-01-23 23:42:39 +01:00
parent de2727ac8a
commit 40fb4950a6
7 changed files with 353 additions and 272 deletions

View File

@ -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}");
}
/// <summary>
/// Formats byte array as hex dump with offset, hex values, and ASCII representation.
/// </summary>
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
}

View File

@ -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
/// <summary>
/// Counts occurrences of ObjectRef (0x1B = 27) in binary data.
/// </summary>
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();
}
/// <summary>
/// Counts occurrences of a string in binary data (UTF8).
/// </summary>
@ -73,80 +79,87 @@ public class AcBinarySerializerIIdReferenceTests
/// <summary>
/// SCENARIO 1: Same instance referenced multiple times.
/// Validates: ObjectRef present + data integrity after deserialize + reference identity.
/// Tests all ReferenceHandling modes: None, OnlyId, All
/// </summary>
[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<TestOrder>();
// 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<TestOrder>(); // 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

View File

@ -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;
}
}

View File

@ -11,6 +11,24 @@ namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinaryDeserializer
{
#region Helper Methods
/// <summary>
/// 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.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ReadAndRegisterHashcodeIfNeeded(ref BinaryDeserializationContext context, TypeMetadataWrapper<BinaryDeserializeTypeMetadata> 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
/// <summary>
/// Populate object with automatic mode detection from context.
/// Uses IsMergeMode to determine merge behavior for IId collections.
/// Wire format:
/// - IId types: [Object][props 0-tól...] - no refId prefix, Id is in props
/// - Non-IId + All: [Object][hashcode][props 0-tól...] - hashcode prefix
/// - Ref=Off: [Object][props 0-tól...] - no prefix
/// </summary>
private static void PopulateObject(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);
}
/// <summary>
/// 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).
/// </summary>
/// <param name="context">Deserialization context</param>
/// <param name="target">Target object to populate</param>
/// <param name="metadata">Type metadata</param>
/// <param name="depth">Current depth</param>
/// <param name="skipDefaultWrite">
/// true = Deserialize mode - object just created, properties already at default, skip writing defaults
/// false = Populate mode - existing object, must overwrite properties with default values
/// </param>
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<BinaryDeserializeTypeMetadata> 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);
}
/// <summary>
/// Populates object properties only - does NOT read hashcode prefix.
/// Used by ReadObject where hashcode is already handled separately.
/// </summary>
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
}
/// <summary>
/// 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.
/// </summary>
[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))

View File

@ -904,57 +904,90 @@ public static partial class AcBinaryDeserializer
#region Object Reading
/// <summary>
/// Reads an ObjectRef - looks up previously registered object by (Type, IId) in ContextClass.
/// The wire format depends on IdAccessorType:
/// - Int32: VarInt (1-5 bytes)
/// - Int64: VarLong (1-10 bytes)
/// - Guid: Raw 16 bytes
/// 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
/// </summary>
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<BinaryDeserializeTypeMetadata> wrapper, int depth)
private static object? ReadObjectRef(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> 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<BinaryDeserializeTypeMetadata> 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<BinaryDeserializeTypeMetadata> 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<BinaryDeserializeTypeMetadata> 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);
}
/// <summary>
/// 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
/// </summary>
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();
}

View File

@ -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-tól...] - Id a props-ban, nincs extra
// - Non-IId + All: [Object][hashcode][props 0-tól...] - hashcode elõre
// - Ref=Off: [Object][props 0-tól...] - semmi extra
// ObjectRef format:
// - IId: [ObjectRef][Id érték]
// - 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 típus: 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];

View File

@ -45,7 +45,7 @@ public abstract class TypeMetadataBase
/// <summary>
/// The type this metadata is for. Stored to avoid repeated reflection.
/// </summary>
protected Type SourceType { get; }
public Type SourceType { get; }
/// <summary>
/// Readable properties (CanRead=true) for this type. Used by serializers.