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:
parent
de2727ac8a
commit
40fb4950a6
|
|
@ -680,6 +680,19 @@ public static class Program
|
||||||
sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
||||||
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
|
// CSV-like data for easy import
|
||||||
sb.AppendLine("=== RAW DATA (CSV) ===");
|
sb.AppendLine("=== RAW DATA (CSV) ===");
|
||||||
sb.AppendLine("TestData,Serializer,Size,SerializeMs,DeserializeMs,RoundTripMs");
|
sb.AppendLine("TestData,Serializer,Size,SerializeMs,DeserializeMs,RoundTripMs");
|
||||||
|
|
@ -758,5 +771,41 @@ public static class Program
|
||||||
System.Console.WriteLine($"\n✓ Results saved to: {filePath}");
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
using AyCode.Core.Serializers.Binaries;
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
|
|
@ -29,11 +30,9 @@ public class AcBinarySerializerIIdReferenceTests
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Counts occurrences of ObjectRef (0x1B = 27) in binary data.
|
/// Counts occurrences of ObjectRef (0x1B = 27) in binary data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static int CountObjectRefs(byte[] binary)
|
private static int CountObjectRefs(byte[] binary, bool writeBinaryToConsole = true)
|
||||||
{
|
{
|
||||||
Console.WriteLine();
|
if (writeBinaryToConsole) WriteBinaryToConsole(binary);
|
||||||
Console.WriteLine( BitConverter.ToString(binary));
|
|
||||||
Console.WriteLine();
|
|
||||||
|
|
||||||
var count = 0;
|
var count = 0;
|
||||||
for (var i = 0; i < binary.Length; i++)
|
for (var i = 0; i < binary.Length; i++)
|
||||||
|
|
@ -44,6 +43,13 @@ public class AcBinarySerializerIIdReferenceTests
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void WriteBinaryToConsole(byte[] binary)
|
||||||
|
{
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine(BitConverter.ToString(binary));
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Counts occurrences of a string in binary data (UTF8).
|
/// Counts occurrences of a string in binary data (UTF8).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -73,80 +79,87 @@ public class AcBinarySerializerIIdReferenceTests
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SCENARIO 1: Same instance referenced multiple times.
|
/// SCENARIO 1: Same instance referenced multiple times.
|
||||||
/// Validates: ObjectRef present + data integrity after deserialize + reference identity.
|
/// Tests all ReferenceHandling modes: None, OnlyId, All
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void SameInstance_SerializeAndDeserialize()
|
public void SameInstance_SerializeAndDeserialize()
|
||||||
{
|
{
|
||||||
// Arrange: SAME instance used 4 times
|
var modes = new[]
|
||||||
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,
|
ReferenceHandlingMode.None,
|
||||||
OrderNumber = "ORD-001",
|
ReferenceHandlingMode.OnlyId,
|
||||||
PrimaryTag = sharedTag,
|
ReferenceHandlingMode.All
|
||||||
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 } }
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
foreach (var mode in modes)
|
||||||
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++)
|
|
||||||
{
|
{
|
||||||
Assert.IsNotNull(result.Items[i].Tag, $"Items[{i}].Tag is null - data lost!");
|
Console.WriteLine($"\n========== ReferenceHandling: {mode} ==========");
|
||||||
Assert.AreEqual(1, result.Items[i].Tag!.Id, $"Items[{i}].Tag.Id incorrect");
|
|
||||||
Assert.AreEqual("ImportantTag", result.Items[i].Tag.Name, $"Items[{i}].Tag.Name incorrect");
|
|
||||||
|
|
||||||
Assert.IsNotNull(result.Items[i].Assignee, $"Items[{i}].Assignee is null - data lost!");
|
// 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 };
|
||||||
|
|
||||||
|
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
|
#endregion
|
||||||
|
|
|
||||||
|
|
@ -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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,24 @@ namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
||||||
public static partial class AcBinaryDeserializer
|
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
|
#region Populate Object Methods
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|
@ -20,56 +38,52 @@ public static partial class AcBinaryDeserializer
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Populate object with automatic mode detection from context.
|
/// Populate object with automatic mode detection from context.
|
||||||
/// Uses IsMergeMode to determine merge behavior for IId collections.
|
/// 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>
|
/// </summary>
|
||||||
private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
|
private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
|
||||||
{
|
{
|
||||||
var wrapper = context.ContextClass.GetWrapper(targetType);
|
var wrapper = context.ContextClass.GetWrapper(targetType);
|
||||||
var metadata = wrapper.Metadata;
|
PopulateObjectCore(ref context, target, wrapper, depth, skipDefaultWrite: false);
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Core populate logic shared by all populate paths.
|
/// 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>
|
/// </summary>
|
||||||
/// <param name="context">Deserialization context</param>
|
private static void PopulateObjectCore(
|
||||||
/// <param name="target">Target object to populate</param>
|
ref BinaryDeserializationContext context,
|
||||||
/// <param name="metadata">Type metadata</param>
|
object target,
|
||||||
/// <param name="depth">Current depth</param>
|
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
|
||||||
/// <param name="skipDefaultWrite">
|
int depth,
|
||||||
/// true = Deserialize mode - object just created, properties already at default, skip writing defaults
|
bool skipDefaultWrite)
|
||||||
/// false = Populate mode - existing object, must overwrite properties with default values
|
{
|
||||||
/// </param>
|
// Handle hashcode for Non-IId types - ONE place for this logic!
|
||||||
private static void PopulateObjectCore(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth, bool skipDefaultWrite)
|
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 properties = metadata.PropertiesArray;
|
||||||
var nextDepth = depth + 1;
|
var nextDepth = depth + 1;
|
||||||
var isMergeMode = context.IsMergeMode;
|
var isMergeMode = context.IsMergeMode;
|
||||||
|
|
||||||
var startIndex = 0;
|
// All properties start from index 0 - Id is included with normal type markers
|
||||||
// For IId types with reference handling: Id property has no type marker (value only)
|
for (int i = 0; i < properties.Length; i++)
|
||||||
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++)
|
|
||||||
{
|
{
|
||||||
var propInfo = properties[i];
|
var propInfo = properties[i];
|
||||||
var peekCode = context.PeekByte();
|
var peekCode = context.PeekByte();
|
||||||
|
|
@ -126,20 +140,8 @@ public static partial class AcBinaryDeserializer
|
||||||
{
|
{
|
||||||
context.ReadByte(); // consume Object marker
|
context.ReadByte(); // consume Object marker
|
||||||
|
|
||||||
var wrapper = context.ContextClass.GetWrapper(propInfo.PropertyType);
|
var nestedWrapper = context.ContextClass.GetWrapper(propInfo.PropertyType);
|
||||||
|
PopulateObjectCore(ref context, existingObj, nestedWrapper, nextDepth, skipDefaultWrite: false);
|
||||||
// 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);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -177,12 +179,13 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth, bool skipDefaultWrite)
|
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
|
#endregion
|
||||||
|
|
@ -253,19 +256,7 @@ public static partial class AcBinaryDeserializer
|
||||||
if (existingItem != null)
|
if (existingItem != null)
|
||||||
{
|
{
|
||||||
context.ReadByte(); // consume Object marker
|
context.ReadByte(); // consume Object marker
|
||||||
|
PopulateObjectCore(ref context, existingItem, wrapper, nextDepth, skipDefaultWrite: false);
|
||||||
// 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);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -357,16 +348,8 @@ public static partial class AcBinaryDeserializer
|
||||||
var newItem = CreateInstance(elementType, elementMetadata);
|
var newItem = CreateInstance(elementType, elementMetadata);
|
||||||
if (newItem == null) continue;
|
if (newItem == null) continue;
|
||||||
|
|
||||||
// Handle ref ID if present
|
// PopulateObjectCore handles hashcode reading for Non-IId types
|
||||||
if (context.ContextClass.UseTypeReferenceHandling(elementMetadata))
|
PopulateObjectCore(ref context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
|
||||||
{
|
|
||||||
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);
|
|
||||||
|
|
||||||
var itemId = idGetter(newItem);
|
var itemId = idGetter(newItem);
|
||||||
if (itemId != null && !IsDefaultValue(itemId, idType))
|
if (itemId != null && !IsDefaultValue(itemId, idType))
|
||||||
|
|
@ -469,16 +452,8 @@ public static partial class AcBinaryDeserializer
|
||||||
var newItem = CreateInstance(elementType, elementMetadata);
|
var newItem = CreateInstance(elementType, elementMetadata);
|
||||||
if (newItem == null) continue;
|
if (newItem == null) continue;
|
||||||
|
|
||||||
// Handle ref ID if present
|
// PopulateObjectCore handles hashcode reading for Non-IId types
|
||||||
if (context.ContextClass.UseTypeReferenceHandling(elementMetadata))
|
PopulateObjectCore(ref context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
|
||||||
{
|
|
||||||
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);
|
|
||||||
|
|
||||||
var itemId = idGetter(newItem);
|
var itemId = idGetter(newItem);
|
||||||
if (itemId != null && !IsDefaultValue(itemId, idType))
|
if (itemId != null && !IsDefaultValue(itemId, idType))
|
||||||
|
|
|
||||||
|
|
@ -904,57 +904,90 @@ public static partial class AcBinaryDeserializer
|
||||||
#region Object Reading
|
#region Object Reading
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads an ObjectRef - looks up previously registered object by (Type, IId) in ContextClass.
|
/// Reads an ObjectRef - looks up previously registered object.
|
||||||
/// The wire format depends on IdAccessorType:
|
/// Wire format:
|
||||||
/// - Int32: VarInt (1-5 bytes)
|
/// - IId types: [ObjectRef][Id érték] - lookup by Id
|
||||||
/// - Int64: VarLong (1-10 bytes)
|
/// - Non-IId types: [ObjectRef][hashcode] - lookup by hashcode
|
||||||
/// - Guid: Raw 16 bytes
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static object? ReadObjectRef(ref BinaryDeserializationContext context, Type targetType, int depth)
|
private static object? ReadObjectRef(ref BinaryDeserializationContext context, Type targetType, int depth)
|
||||||
{
|
{
|
||||||
var wrapper = context.ContextClass.GetWrapper(targetType);
|
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;
|
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
|
return metadata.IdAccessorType switch
|
||||||
{
|
{
|
||||||
AcSerializerCommon.IdAccessorType.Int32 => ReadObjectRefInt32(ref context, ref wrapper),
|
AcSerializerCommon.IdAccessorType.Int32 => ReadObjectRefInt32(ref context, ref wrapper),
|
||||||
AcSerializerCommon.IdAccessorType.Int64 => ReadObjectRefInt64(ref context, ref wrapper),
|
AcSerializerCommon.IdAccessorType.Int64 => ReadObjectRefInt64(ref context, ref wrapper),
|
||||||
AcSerializerCommon.IdAccessorType.Guid => ReadObjectRefGuid(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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Non-IId: [ObjectRef][hashcode] - lookup by hashcode
|
||||||
|
var hashcode = context.ReadVarInt();
|
||||||
|
if (context.ContextClass.TryGetValue(wrapper, hashcode, out var instance))
|
||||||
|
return instance;
|
||||||
|
|
||||||
return null;
|
throw new AcBinaryDeserializationException(
|
||||||
|
$"ObjectRef hashcode {hashcode} not found for type '{metadata.SourceType.Name}'",
|
||||||
|
context.Position, metadata.SourceType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static object? ReadObjectRefInt32(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
private static object? ReadObjectRefInt32(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
||||||
{
|
{
|
||||||
var id = context.ReadVarInt();
|
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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static object? ReadObjectRefInt64(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
private static object? ReadObjectRefInt64(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
||||||
{
|
{
|
||||||
var id = context.ReadVarLong();
|
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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static object? ReadObjectRefGuid(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
private static object? ReadObjectRefGuid(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
||||||
{
|
{
|
||||||
var id = context.ReadGuidUnsafe(); // 16 bytes raw!
|
var id = context.ReadGuidUnsafe();
|
||||||
return context.ContextClass.TryGetValue(wrapper, id, out var instance) ? instance : throw new Exception("ReadObjectRefGuid");;
|
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)
|
private static object? ReadObject(ref BinaryDeserializationContext context, Type targetType, int depth)
|
||||||
{
|
{
|
||||||
// Handle dictionary types
|
// Handle dictionary types
|
||||||
|
|
@ -966,48 +999,62 @@ public static partial class AcBinaryDeserializer
|
||||||
var wrapper = context.ContextClass.GetWrapper(targetType);
|
var wrapper = context.ContextClass.GetWrapper(targetType);
|
||||||
var metadata = wrapper.Metadata;
|
var metadata = wrapper.Metadata;
|
||||||
|
|
||||||
object? instance = null;
|
object? instance;
|
||||||
|
|
||||||
if (context.ContextClass.UseTypeReferenceHandling(metadata))
|
if (context.ContextClass.UseTypeReferenceHandling(metadata))
|
||||||
{
|
{
|
||||||
switch (metadata.IdAccessorType)
|
if (metadata.IsIId)
|
||||||
{
|
{
|
||||||
case AcSerializerCommon.IdAccessorType.Int32:
|
// IId: [Object][props 0-tól...]
|
||||||
var intId = context.ReadVarInt();
|
// Create → Populate (Id beolvasódik) → Register by Id
|
||||||
if (context.ContextClass.TryGetValue(wrapper, intId, out instance)) return instance;
|
instance = CreateInstance(targetType, metadata);
|
||||||
|
if (instance == null) return null;
|
||||||
|
|
||||||
instance = CreateInstance(targetType, metadata);
|
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
|
||||||
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);
|
// Register by Id after populate (Id is now set)
|
||||||
if (instance == null) return null;
|
switch (metadata.IdAccessorType)
|
||||||
context.ContextClass.TryGetOrStoreLong(wrapper, instance, longId);
|
{
|
||||||
break;
|
case AcSerializerCommon.IdAccessorType.Int32:
|
||||||
case AcSerializerCommon.IdAccessorType.Guid:
|
var intId = metadata.GetIdInt32(instance);
|
||||||
var guidId = context.ReadGuidUnsafe();
|
context.ContextClass.TryGetOrStoreInt32(wrapper, instance, intId);
|
||||||
if (context.ContextClass.TryGetValue(wrapper, guidId, out instance)) return instance;
|
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();
|
||||||
|
|
||||||
instance = CreateInstance(targetType, metadata);
|
// TryGetValue - if already exists, return (shouldn't happen for Object, only ObjectRef)
|
||||||
if (instance == null) return null;
|
if (context.ContextClass.TryGetValue(wrapper, hashcode, out instance))
|
||||||
context.ContextClass.TryGetOrStoreGuid(wrapper, instance, guidId);
|
return instance;
|
||||||
break;
|
|
||||||
default:
|
// Create + Register by hashcode
|
||||||
throw new Exception($"metadata.IdAccessorType not valid: {metadata.IdAccessorType}");
|
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
|
else
|
||||||
{
|
{
|
||||||
|
// Ref=Off: [Object][props 0-tól...]
|
||||||
instance = CreateInstance(targetType, metadata);
|
instance = CreateInstance(targetType, metadata);
|
||||||
if (instance == null) return null;
|
if (instance == null) return null;
|
||||||
}
|
|
||||||
|
|
||||||
// Deserialize mode: object just created, skip writing defaults (already at default)
|
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
|
||||||
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
|
}
|
||||||
|
|
||||||
// ChainMode: Register/retrieve from chain tracker (separate from reference handling)
|
// ChainMode: Register/retrieve from chain tracker (separate from reference handling)
|
||||||
// Note: For ChainMode, we need IdGetter OR typed getters (IdAccessorType != None)
|
// 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)
|
private static void SkipObject(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData)
|
||||||
{
|
{
|
||||||
// Skip ref ID if present
|
// Wire format:
|
||||||
if (context.ContextClass.UseTypeReferenceHandling(metaData))
|
// - 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();
|
context.ReadVarInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -641,85 +641,79 @@ public static partial class AcBinarySerializer
|
||||||
var wrapper = context.GetWrapper(type);
|
var wrapper = context.GetWrapper(type);
|
||||||
var metadata = wrapper.Metadata;
|
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))
|
if (context.UseTypeReferenceHandling(metadata))
|
||||||
{
|
{
|
||||||
switch (metadata.IdAccessorType)
|
if (metadata.IsIId)
|
||||||
{
|
{
|
||||||
case AcSerializerCommon.IdAccessorType.Int32:
|
// IId típus: track by Id, ObjectRef writes Id
|
||||||
if (!context.TryTrack(wrapper, value, out int intId))
|
switch (metadata.IdAccessorType)
|
||||||
{
|
{
|
||||||
// Already seen › write reference
|
case AcSerializerCommon.IdAccessorType.Int32:
|
||||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
if (!context.TryTrack(wrapper, value, out int intId))
|
||||||
context.WriteVarInt(intId);
|
{
|
||||||
return;
|
// Already seen ? ObjectRef + Id
|
||||||
}
|
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||||
// First occurrence › write object with refId
|
context.WriteVarInt(intId);
|
||||||
context.WriteByte(BinaryTypeCode.Object);
|
return;
|
||||||
context.WriteVarInt(intId);
|
}
|
||||||
break;
|
// First occurrence ? Object (no extra data, Id in props)
|
||||||
|
context.WriteByte(BinaryTypeCode.Object);
|
||||||
|
break;
|
||||||
|
|
||||||
case AcSerializerCommon.IdAccessorType.Int64:
|
case AcSerializerCommon.IdAccessorType.Int64:
|
||||||
if (!context.TryTrack(wrapper, value, out long longId))
|
if (!context.TryTrack(wrapper, value, out long longId))
|
||||||
{
|
{
|
||||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||||
context.WriteVarLong(longId);
|
context.WriteVarLong(longId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
context.WriteByte(BinaryTypeCode.Object);
|
context.WriteByte(BinaryTypeCode.Object);
|
||||||
context.WriteVarLong(longId);
|
break;
|
||||||
break;
|
|
||||||
|
|
||||||
case AcSerializerCommon.IdAccessorType.Guid:
|
case AcSerializerCommon.IdAccessorType.Guid:
|
||||||
if (!context.TryTrack(wrapper, value, out Guid guidId))
|
if (!context.TryTrack(wrapper, value, out Guid guidId))
|
||||||
{
|
{
|
||||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||||
context.WriteGuidBits(guidId);
|
context.WriteGuidBits(guidId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
context.WriteByte(BinaryTypeCode.Object);
|
context.WriteByte(BinaryTypeCode.Object);
|
||||||
context.WriteGuidBits(guidId);
|
break;
|
||||||
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
|
else
|
||||||
{
|
{
|
||||||
// No reference handling for this type - just write object marker
|
// No reference handling - just write object marker
|
||||||
context.WriteByte(BinaryTypeCode.Object);
|
context.WriteByte(BinaryTypeCode.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write all properties (startIndex=0, including Id for IId types)
|
||||||
|
|
||||||
|
|
||||||
// Write properties
|
|
||||||
var nextDepth = depth + 1;
|
var nextDepth = depth + 1;
|
||||||
var properties = metadata.Properties;
|
var properties = metadata.Properties;
|
||||||
var propCount = properties.Length;
|
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
|
// Single-pass serialization with SKIP markers
|
||||||
// - No property count needed (fixed property order)
|
// - No property count needed (fixed property order)
|
||||||
|
|
@ -728,7 +722,7 @@ public static partial class AcBinarySerializer
|
||||||
// - Write value OR skip marker in one operation
|
// - Write value OR skip marker in one operation
|
||||||
var hasPropertyFilter = context.HasPropertyFilter;
|
var hasPropertyFilter = context.HasPropertyFilter;
|
||||||
|
|
||||||
for (var i = startIndex; i < propCount; i++)
|
for (var i = 0; i < propCount; i++)
|
||||||
{
|
{
|
||||||
var prop = properties[i];
|
var prop = properties[i];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ public abstract class TypeMetadataBase
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The type this metadata is for. Stored to avoid repeated reflection.
|
/// The type this metadata is for. Stored to avoid repeated reflection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected Type SourceType { get; }
|
public Type SourceType { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Readable properties (CanRead=true) for this type. Used by serializers.
|
/// Readable properties (CanRead=true) for this type. Used by serializers.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue