Improve reference handling for serializers and IId types
- Make ReferenceHandlingMode type-aware; OnlyId fully supported for binary, All is default for JSON - ReferenceHandling is now settable; add ThrowOnCircularReference option - Always sort Id property first for IId types to optimize tracking - Serialize/deserialize IId.Id without type marker when reference handling is enabled - Contexts now delegate options-derived properties to Options - Improve skip logic and property filter performance in binary serializer - Update tests to explicitly set ReferenceHandlingMode.All - Refactor internal APIs for clarity, safety, and efficiency
This commit is contained in:
parent
852ab53af3
commit
de2727ac8a
|
|
@ -1,4 +1,6 @@
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using static AyCode.Core.Tests.TestModels.AcSerializerModels;
|
using static AyCode.Core.Tests.TestModels.AcSerializerModels;
|
||||||
|
|
||||||
namespace AyCode.Core.Tests.Serialization;
|
namespace AyCode.Core.Tests.Serialization;
|
||||||
|
|
@ -54,7 +56,10 @@ public class AcBinarySerializerCircularReferenceTests
|
||||||
child.GrandChildren.Add(grandChild);
|
child.GrandChildren.Add(grandChild);
|
||||||
parent.Children.Add(child);
|
parent.Children.Add(child);
|
||||||
|
|
||||||
var binary = parent.ToBinary();
|
var option = AcBinarySerializerOptions.Default;
|
||||||
|
option.ReferenceHandling = ReferenceHandlingMode.All;
|
||||||
|
|
||||||
|
var binary = parent.ToBinary(option);
|
||||||
var result = binary.BinaryTo<CircularParent>();
|
var result = binary.BinaryTo<CircularParent>();
|
||||||
|
|
||||||
Assert.IsNotNull(result);
|
Assert.IsNotNull(result);
|
||||||
|
|
@ -125,7 +130,10 @@ public class AcBinarySerializerCircularReferenceTests
|
||||||
return parent;
|
return parent;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
var binary = parents.ToBinary();
|
var option = AcBinarySerializerOptions.Default;
|
||||||
|
option.ReferenceHandling = ReferenceHandlingMode.All;
|
||||||
|
|
||||||
|
var binary = parents.ToBinary(option);
|
||||||
var result = binary.BinaryTo<List<CircularParent>>();
|
var result = binary.BinaryTo<List<CircularParent>>();
|
||||||
|
|
||||||
Assert.IsNotNull(result);
|
Assert.IsNotNull(result);
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,10 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
|
||||||
/// The options used for this context. Set during Reset.
|
/// The options used for this context. Set during Reset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TOptions Options { get; private set; } = null!;
|
public TOptions Options { get; private set; } = null!;
|
||||||
|
|
||||||
public byte MaxDepth { get; private set; }
|
public byte MaxDepth => Options.MaxDepth;
|
||||||
public ReferenceHandlingMode ReferenceHandling { get; internal set; }
|
public ReferenceHandlingMode ReferenceHandling => Options.ReferenceHandling;
|
||||||
|
public bool ThrowOnCircularReference => Options.ThrowOnCircularReference;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Global shared cache for metadata (thread-safe, shared across all contexts).
|
/// Global shared cache for metadata (thread-safe, shared across all contexts).
|
||||||
/// Generic specialization ensures separate cache per TMetadata type.
|
/// Generic specialization ensures separate cache per TMetadata type.
|
||||||
|
|
@ -142,14 +143,12 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Reset(TOptions options)
|
public virtual void Reset(TOptions options)
|
||||||
{
|
{
|
||||||
|
Options = options;
|
||||||
|
|
||||||
foreach (var wrapper in _wrappers.Values)
|
foreach (var wrapper in _wrappers.Values)
|
||||||
{
|
{
|
||||||
wrapper.ResetTracking();
|
wrapper.ResetTracking();
|
||||||
}
|
}
|
||||||
|
|
||||||
Options = options;
|
|
||||||
MaxDepth = options.MaxDepth;
|
|
||||||
ReferenceHandling = options.ReferenceHandling;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,10 @@ public static partial class AcBinaryDeserializer
|
||||||
private List<string>? _propertyNames;
|
private List<string>? _propertyNames;
|
||||||
//private Dictionary<int, object>? _objectReferences;
|
//private Dictionary<int, object>? _objectReferences;
|
||||||
private Dictionary<int, string>? _stringCache;
|
private Dictionary<int, string>? _stringCache;
|
||||||
private readonly byte _minStringInternLength;
|
|
||||||
private readonly bool _useStringCaching;
|
|
||||||
private readonly int _maxCachedStringLength;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Heap-allocated context class for IId-based reference tracking.
|
/// Heap-allocated context class for IId-based reference tracking.
|
||||||
/// Uses AcSerializerContextBase infrastructure.
|
/// Also holds Options - all options-derived properties delegate to ContextClass.Options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly BinaryDeserializationContextClass ContextClass;
|
public readonly BinaryDeserializationContextClass ContextClass;
|
||||||
|
|
||||||
|
|
@ -36,12 +33,16 @@ public static partial class AcBinaryDeserializer
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convenience property - true if any reference handling is enabled.
|
/// Convenience property - true if any reference handling is enabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasReferenceHandling => ContextClass?.ReferenceHandling != ReferenceHandlingMode.None;
|
//public readonly bool HasReferenceHandling => ContextClass.ReferenceHandling != ReferenceHandlingMode.None;
|
||||||
public bool IsMergeMode { readonly get; set; }
|
public bool IsMergeMode { readonly get; set; }
|
||||||
public bool RemoveOrphanedItems { readonly get; set; }
|
public bool RemoveOrphanedItems { readonly get; set; }
|
||||||
public bool IsAtEnd => _position >= _buffer.Length;
|
public readonly bool IsAtEnd => _position >= _buffer.Length;
|
||||||
public int Position => _position;
|
public readonly int Position => _position;
|
||||||
public byte MinStringInternLength => _minStringInternLength;
|
|
||||||
|
// Options-derived properties - delegate to ContextClass.Options
|
||||||
|
public readonly byte MinStringInternLength => ContextClass.Options.MinStringInternLength;
|
||||||
|
public readonly bool UseStringCaching => ContextClass.Options.UseStringCaching;
|
||||||
|
public readonly int MaxCachedStringLength => ContextClass.Options.MaxCachedStringLength;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Chain reference tracker for maintaining object identity across chain operations.
|
/// Chain reference tracker for maintaining object identity across chain operations.
|
||||||
|
|
@ -76,12 +77,9 @@ public static partial class AcBinaryDeserializer
|
||||||
IsMergeMode = false;
|
IsMergeMode = false;
|
||||||
RemoveOrphanedItems = false;
|
RemoveOrphanedItems = false;
|
||||||
ChainTracker = null;
|
ChainTracker = null;
|
||||||
_minStringInternLength = options.MinStringInternLength;
|
|
||||||
_useStringCaching = options.UseStringCaching;
|
|
||||||
_maxCachedStringLength = options.MaxCachedStringLength;
|
|
||||||
ContextClass = contextClass;
|
ContextClass = contextClass;
|
||||||
// Initialize ReferenceHandling from options (will be overwritten by ReadHeader if present in stream)
|
// Reset ContextClass with options - this sets Options and clears any previous state
|
||||||
ContextClass.ReferenceHandling = options.ReferenceHandling;
|
ContextClass.Reset(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReadHeader()
|
public void ReadHeader()
|
||||||
|
|
@ -108,11 +106,11 @@ public static partial class AcBinaryDeserializer
|
||||||
if (marker == BinaryTypeCode.MetadataHeader)
|
if (marker == BinaryTypeCode.MetadataHeader)
|
||||||
{
|
{
|
||||||
hasPropertyTable = true;
|
hasPropertyTable = true;
|
||||||
ContextClass.ReferenceHandling = ReferenceHandlingMode.OnlyId; // Legacy: assume OnlyId
|
ContextClass.Options.ReferenceHandling = ReferenceHandlingMode.OnlyId; // Legacy: assume OnlyId
|
||||||
}
|
}
|
||||||
else if (marker == BinaryTypeCode.NoMetadataHeader)
|
else if (marker == BinaryTypeCode.NoMetadataHeader)
|
||||||
{
|
{
|
||||||
ContextClass.ReferenceHandling = ReferenceHandlingMode.OnlyId; // Legacy: assume OnlyId
|
ContextClass.Options.ReferenceHandling = ReferenceHandlingMode.OnlyId; // Legacy: assume OnlyId
|
||||||
}
|
}
|
||||||
else if ((marker & 0xF0) == BinaryTypeCode.HeaderFlagsBase)
|
else if ((marker & 0xF0) == BinaryTypeCode.HeaderFlagsBase)
|
||||||
{
|
{
|
||||||
|
|
@ -121,7 +119,7 @@ public static partial class AcBinaryDeserializer
|
||||||
// Decode ReferenceHandlingMode from separate bits
|
// Decode ReferenceHandlingMode from separate bits
|
||||||
var hasOnlyId = (flags & BinaryTypeCode.HeaderFlag_RefHandling_OnlyId) != 0;
|
var hasOnlyId = (flags & BinaryTypeCode.HeaderFlag_RefHandling_OnlyId) != 0;
|
||||||
var hasAll = (flags & BinaryTypeCode.HeaderFlag_RefHandling_All) != 0;
|
var hasAll = (flags & BinaryTypeCode.HeaderFlag_RefHandling_All) != 0;
|
||||||
ContextClass.ReferenceHandling = hasAll ? ReferenceHandlingMode.All
|
ContextClass.Options.ReferenceHandling = hasAll ? ReferenceHandlingMode.All
|
||||||
: hasOnlyId ? ReferenceHandlingMode.OnlyId
|
: hasOnlyId ? ReferenceHandlingMode.OnlyId
|
||||||
: ReferenceHandlingMode.None;
|
: ReferenceHandlingMode.None;
|
||||||
|
|
||||||
|
|
@ -448,7 +446,7 @@ public static partial class AcBinaryDeserializer
|
||||||
EnsureAvailable(length);
|
EnsureAvailable(length);
|
||||||
|
|
||||||
// WASM optimization: cache short strings to reduce allocations
|
// WASM optimization: cache short strings to reduce allocations
|
||||||
if (_useStringCaching && length <= _maxCachedStringLength)
|
if (UseStringCaching && length <= MaxCachedStringLength)
|
||||||
{
|
{
|
||||||
return ReadStringUtf8Cached(length);
|
return ReadStringUtf8Cached(length);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@ public static partial class AcBinaryDeserializer
|
||||||
var metadata = wrapper.Metadata;
|
var metadata = wrapper.Metadata;
|
||||||
|
|
||||||
// Handle reference ID if present
|
// Handle reference ID if present
|
||||||
if (context.HasReferenceHandling)
|
if (context.ContextClass.UseTypeReferenceHandling(metadata))
|
||||||
{
|
{
|
||||||
var refId = context.ReadVarInt();
|
var refId = context.ReadVarInt();
|
||||||
if (context.ContextClass.TryGetValue(wrapper, refId, out var instance)) return instance;
|
if (context.ContextClass.TryGetValue(wrapper, refId, out var instance)) return instance;
|
||||||
|
|
@ -270,7 +270,7 @@ public static partial class AcBinaryDeserializer
|
||||||
if (destPropIndex == -1)
|
if (destPropIndex == -1)
|
||||||
{
|
{
|
||||||
// No mapping - skip this property
|
// No mapping - skip this property
|
||||||
SkipValue(ref context);
|
SkipValue(ref context, metadata);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -279,7 +279,7 @@ public static partial class AcBinaryDeserializer
|
||||||
if (propInfo == null)
|
if (propInfo == null)
|
||||||
{
|
{
|
||||||
// Destination property not found - skip
|
// Destination property not found - skip
|
||||||
SkipValue(ref context);
|
SkipValue(ref context, metadata);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -336,7 +336,7 @@ public static partial class AcBinaryDeserializer
|
||||||
context.ReadByte(); // consume Object marker
|
context.ReadByte(); // consume Object marker
|
||||||
|
|
||||||
// Handle ref ID if present
|
// Handle ref ID if present
|
||||||
if (context.HasReferenceHandling)
|
if (context.ContextClass.UseTypeReferenceHandling(wrapper.Metadata))
|
||||||
{
|
{
|
||||||
var refId = context.ReadVarInt();
|
var refId = context.ReadVarInt();
|
||||||
if (refId > 0)
|
if (refId > 0)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using AyCode.Core.Helpers;
|
using AyCode.Core.Helpers;
|
||||||
|
using AyCode.Core.Serializers;
|
||||||
using static AyCode.Core.Helpers.JsonUtilities;
|
using static AyCode.Core.Helpers.JsonUtilities;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Binaries;
|
namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
@ -26,7 +27,7 @@ public static partial class AcBinaryDeserializer
|
||||||
var metadata = wrapper.Metadata;
|
var metadata = wrapper.Metadata;
|
||||||
|
|
||||||
// Handle ref ID if present
|
// Handle ref ID if present
|
||||||
if (context.HasReferenceHandling)
|
if (context.ContextClass.UseTypeReferenceHandling(metadata))
|
||||||
{
|
{
|
||||||
var refId = context.ReadVarInt();
|
var refId = context.ReadVarInt();
|
||||||
if (refId > 0)
|
if (refId > 0)
|
||||||
|
|
@ -55,7 +56,20 @@ public static partial class AcBinaryDeserializer
|
||||||
var nextDepth = depth + 1;
|
var nextDepth = depth + 1;
|
||||||
var isMergeMode = context.IsMergeMode;
|
var isMergeMode = context.IsMergeMode;
|
||||||
|
|
||||||
for (int i = 0; i < properties.Length; i++)
|
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++)
|
||||||
{
|
{
|
||||||
var propInfo = properties[i];
|
var propInfo = properties[i];
|
||||||
var peekCode = context.PeekByte();
|
var peekCode = context.PeekByte();
|
||||||
|
|
@ -115,7 +129,7 @@ public static partial class AcBinaryDeserializer
|
||||||
var wrapper = context.ContextClass.GetWrapper(propInfo.PropertyType);
|
var wrapper = context.ContextClass.GetWrapper(propInfo.PropertyType);
|
||||||
|
|
||||||
// Handle ref ID if present
|
// Handle ref ID if present
|
||||||
if (context.HasReferenceHandling)
|
if (context.ContextClass.UseTypeReferenceHandling(wrapper.Metadata))
|
||||||
{
|
{
|
||||||
var refId = context.ReadVarInt();
|
var refId = context.ReadVarInt();
|
||||||
if (refId > 0)
|
if (refId > 0)
|
||||||
|
|
@ -241,7 +255,7 @@ public static partial class AcBinaryDeserializer
|
||||||
context.ReadByte(); // consume Object marker
|
context.ReadByte(); // consume Object marker
|
||||||
|
|
||||||
// Handle ref ID if present
|
// Handle ref ID if present
|
||||||
if (context.HasReferenceHandling)
|
if (context.ContextClass.UseTypeReferenceHandling(elementMetadata))
|
||||||
{
|
{
|
||||||
var refId = context.ReadVarInt();
|
var refId = context.ReadVarInt();
|
||||||
if (refId > 0)
|
if (refId > 0)
|
||||||
|
|
@ -344,7 +358,7 @@ public static partial class AcBinaryDeserializer
|
||||||
if (newItem == null) continue;
|
if (newItem == null) continue;
|
||||||
|
|
||||||
// Handle ref ID if present
|
// Handle ref ID if present
|
||||||
if (context.HasReferenceHandling)
|
if (context.ContextClass.UseTypeReferenceHandling(elementMetadata))
|
||||||
{
|
{
|
||||||
var refId = context.ReadVarInt();
|
var refId = context.ReadVarInt();
|
||||||
if (refId > 0)
|
if (refId > 0)
|
||||||
|
|
@ -456,7 +470,7 @@ public static partial class AcBinaryDeserializer
|
||||||
if (newItem == null) continue;
|
if (newItem == null) continue;
|
||||||
|
|
||||||
// Handle ref ID if present
|
// Handle ref ID if present
|
||||||
if (context.HasReferenceHandling)
|
if (context.ContextClass.UseTypeReferenceHandling(elementMetadata))
|
||||||
{
|
{
|
||||||
var refId = context.ReadVarInt();
|
var refId = context.ReadVarInt();
|
||||||
if (refId > 0)
|
if (refId > 0)
|
||||||
|
|
@ -518,6 +532,26 @@ public static partial class AcBinaryDeserializer
|
||||||
prop.SetValue(target, value);
|
prop.SetValue(target, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads Id value without type marker. The serializer didn't write a marker for IId types.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static void ReadIdValueWithoutMarker(ref BinaryDeserializationContext context, object target, BinaryPropertySetterInfo propInfo, AcSerializerCommon.IdAccessorType idType)
|
||||||
|
{
|
||||||
|
switch (idType)
|
||||||
|
{
|
||||||
|
case AcSerializerCommon.IdAccessorType.Int32:
|
||||||
|
propInfo.SetInt32(target, context.ReadVarInt());
|
||||||
|
break;
|
||||||
|
case AcSerializerCommon.IdAccessorType.Int64:
|
||||||
|
propInfo.SetInt64(target, context.ReadVarLong());
|
||||||
|
break;
|
||||||
|
case AcSerializerCommon.IdAccessorType.Guid:
|
||||||
|
propInfo.SetGuid(target, context.ReadGuidUnsafe());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets a property to its default value using typed setters to avoid boxing.
|
/// Sets a property to its default value using typed setters to avoid boxing.
|
||||||
|
|
|
||||||
|
|
@ -968,7 +968,7 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
object? instance = null;
|
object? instance = null;
|
||||||
|
|
||||||
if (context.HasReferenceHandling && metadata.IdAccessorType != AcSerializerCommon.IdAccessorType.None)
|
if (context.ContextClass.UseTypeReferenceHandling(metadata))
|
||||||
{
|
{
|
||||||
switch (metadata.IdAccessorType)
|
switch (metadata.IdAccessorType)
|
||||||
{
|
{
|
||||||
|
|
@ -1267,7 +1267,7 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
#region Skip Value
|
#region Skip Value
|
||||||
|
|
||||||
private static void SkipValue(ref BinaryDeserializationContext context)
|
private static void SkipValue(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData)
|
||||||
{
|
{
|
||||||
var typeCode = context.ReadByte();
|
var typeCode = context.ReadByte();
|
||||||
|
|
||||||
|
|
@ -1349,16 +1349,16 @@ public static partial class AcBinaryDeserializer
|
||||||
if (enumByte == BinaryTypeCode.Int32) context.ReadVarInt();
|
if (enumByte == BinaryTypeCode.Int32) context.ReadVarInt();
|
||||||
return;
|
return;
|
||||||
case BinaryTypeCode.Object:
|
case BinaryTypeCode.Object:
|
||||||
SkipObject(ref context);
|
SkipObject(ref context, metaData);
|
||||||
return;
|
return;
|
||||||
case BinaryTypeCode.ObjectRef:
|
case BinaryTypeCode.ObjectRef:
|
||||||
context.ReadVarInt();
|
context.ReadVarInt();
|
||||||
return;
|
return;
|
||||||
case BinaryTypeCode.Array:
|
case BinaryTypeCode.Array:
|
||||||
SkipArray(ref context);
|
SkipArray(ref context, metaData);
|
||||||
return;
|
return;
|
||||||
case BinaryTypeCode.Dictionary:
|
case BinaryTypeCode.Dictionary:
|
||||||
SkipDictionary(ref context);
|
SkipDictionary(ref context, metaData);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1404,10 +1404,10 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SkipObject(ref BinaryDeserializationContext context)
|
private static void SkipObject(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData)
|
||||||
{
|
{
|
||||||
// Skip ref ID if present
|
// Skip ref ID if present
|
||||||
if (context.HasReferenceHandling)
|
if (context.ContextClass.UseTypeReferenceHandling(metaData))
|
||||||
{
|
{
|
||||||
context.ReadVarInt();
|
context.ReadVarInt();
|
||||||
}
|
}
|
||||||
|
|
@ -1420,22 +1420,22 @@ public static partial class AcBinaryDeserializer
|
||||||
"Unable to determine property count without type metadata.");
|
"Unable to determine property count without type metadata.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SkipArray(ref BinaryDeserializationContext context)
|
private static void SkipArray(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData)
|
||||||
{
|
{
|
||||||
var count = (int)context.ReadVarUInt();
|
var count = (int)context.ReadVarUInt();
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
SkipValue(ref context);
|
SkipValue(ref context, metaData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SkipDictionary(ref BinaryDeserializationContext context)
|
private static void SkipDictionary(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData)
|
||||||
{
|
{
|
||||||
var count = (int)context.ReadVarUInt();
|
var count = (int)context.ReadVarUInt();
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
SkipValue(ref context); // key
|
SkipValue(ref context, metaData); // key
|
||||||
SkipValue(ref context); // value
|
SkipValue(ref context, metaData); // value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,11 @@ public static partial class AcBinarySerializer
|
||||||
public byte MinStringInternLength => Options.MinStringInternLength;
|
public byte MinStringInternLength => Options.MinStringInternLength;
|
||||||
public byte MaxStringInternLength => Options.MaxStringInternLength;
|
public byte MaxStringInternLength => Options.MaxStringInternLength;
|
||||||
public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter;
|
public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cached check for PropertyFilter != null. Set in Reset() to avoid property getter in hot loop.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasPropertyFilter { get; private set; }
|
||||||
|
|
||||||
public int Position => _position;
|
public int Position => _position;
|
||||||
|
|
||||||
|
|
@ -96,11 +101,12 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
public override void Reset(AcBinarySerializerOptions options)
|
public override void Reset(AcBinarySerializerOptions options)
|
||||||
{
|
{
|
||||||
// Reset wrapper tracking state from base class (IId tracking)
|
// IMPORTANT: base.Reset sets Options first, so derived code can use Options-derived properties
|
||||||
base.Reset(options);
|
base.Reset(options);
|
||||||
|
|
||||||
_position = 0;
|
_position = 0;
|
||||||
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
_initialBufferSize = Math.Max(Options.InitialBufferCapacity, MinBufferSize);
|
||||||
|
HasPropertyFilter = Options.PropertyFilter != null;
|
||||||
|
|
||||||
if (_buffer.Length < _initialBufferSize)
|
if (_buffer.Length < _initialBufferSize)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -641,8 +641,8 @@ 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
|
// Single-pass reference tracking - type-specific check
|
||||||
if (context.ReferenceHandling != ReferenceHandlingMode.None)
|
if (context.UseTypeReferenceHandling(metadata))
|
||||||
{
|
{
|
||||||
switch (metadata.IdAccessorType)
|
switch (metadata.IdAccessorType)
|
||||||
{
|
{
|
||||||
|
|
@ -684,32 +684,61 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// No reference handling - just write object marker
|
// No reference handling for this type - just write object marker
|
||||||
context.WriteByte(BinaryTypeCode.Object);
|
context.WriteByte(BinaryTypeCode.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Write properties
|
// 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)
|
||||||
// - No property indices needed (sequential order)
|
// - No property indices needed (sequential order)
|
||||||
// - Single getter call per property
|
// - Single getter call per property
|
||||||
// - Write value OR skip marker in one operation
|
// - Write value OR skip marker in one operation
|
||||||
|
var hasPropertyFilter = context.HasPropertyFilter;
|
||||||
|
|
||||||
for (var i = 0; i < propCount; i++)
|
for (var i = startIndex; i < propCount; i++)
|
||||||
{
|
{
|
||||||
var prop = properties[i];
|
var prop = properties[i];
|
||||||
|
|
||||||
// Skip if filter says no - write skip marker
|
// Skip if filter says no - write skip marker
|
||||||
if (context.PropertyFilter != null && !context.ShouldSerializeProperty(value, prop))
|
if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
|
||||||
{
|
{
|
||||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write property value OR skip marker (single operation, single getter call)
|
// Write property value OR skip marker (single operation, single getter call)
|
||||||
WritePropertyOrSkip(value, prop, context, nextDepth);
|
WritePropertyOrSkip(value, prop, context, nextDepth);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,11 +77,12 @@ public static partial class AcJsonDeserializer
|
||||||
|
|
||||||
public override void Reset(AcJsonSerializerOptions options)
|
public override void Reset(AcJsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
|
// IMPORTANT: base.Reset sets Options first
|
||||||
|
base.Reset(options);
|
||||||
|
|
||||||
IsMergeMode = false;
|
IsMergeMode = false;
|
||||||
ChainTracker = null;
|
ChainTracker = null;
|
||||||
_refTracker.Reset();
|
_refTracker.Reset();
|
||||||
|
|
||||||
base.Reset(options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear(AcJsonSerializerOptions options)
|
public void Clear(AcJsonSerializerOptions options)
|
||||||
|
|
|
||||||
|
|
@ -64,14 +64,15 @@ public static partial class AcJsonSerializer
|
||||||
|
|
||||||
public override void Reset(AcJsonSerializerOptions options)
|
public override void Reset(AcJsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
|
// IMPORTANT: base.Reset sets Options first
|
||||||
|
base.Reset(options);
|
||||||
|
|
||||||
_refTracker.Reset();
|
_refTracker.Reset();
|
||||||
|
|
||||||
if (options.ReferenceHandling != ReferenceHandlingMode.None)
|
if (ReferenceHandling != ReferenceHandlingMode.None)
|
||||||
{
|
{
|
||||||
_refTracker.EnsureInitialized();
|
_refTracker.EnsureInitialized();
|
||||||
}
|
}
|
||||||
|
|
||||||
base.Reset(options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
|
|
|
||||||
|
|
@ -180,9 +180,10 @@ public static partial class AcJsonSerializer
|
||||||
var writer = context.Writer;
|
var writer = context.Writer;
|
||||||
var wrapper = context.GetWrapper(type);
|
var wrapper = context.GetWrapper(type);
|
||||||
var metadata = wrapper.Metadata;
|
var metadata = wrapper.Metadata;
|
||||||
|
var useReferenceHandling = context.UseTypeReferenceHandling(metadata);
|
||||||
|
|
||||||
// Use IId-aware reference handling
|
// Use IId-aware reference handling
|
||||||
if (context.ReferenceHandling != ReferenceHandlingMode.None && context.TryGetExistingRefForIId(value, metadata, out var refId))
|
if (useReferenceHandling && context.TryGetExistingRefForIId(value, metadata, out var refId))
|
||||||
{
|
{
|
||||||
writer.WriteStartObject();
|
writer.WriteStartObject();
|
||||||
writer.WriteString(RefPropertyEncoded, refId.ToString(CultureInfo.InvariantCulture));
|
writer.WriteString(RefPropertyEncoded, refId.ToString(CultureInfo.InvariantCulture));
|
||||||
|
|
@ -192,7 +193,7 @@ public static partial class AcJsonSerializer
|
||||||
|
|
||||||
writer.WriteStartObject();
|
writer.WriteStartObject();
|
||||||
|
|
||||||
if (context.ReferenceHandling != ReferenceHandlingMode.None && context.ShouldWriteIdForIId(value, metadata, out var id))
|
if (useReferenceHandling && context.ShouldWriteIdForIId(value, metadata, out var id))
|
||||||
{
|
{
|
||||||
writer.WriteString(IdPropertyEncoded, id.ToString(CultureInfo.InvariantCulture));
|
writer.WriteString(IdPropertyEncoded, id.ToString(CultureInfo.InvariantCulture));
|
||||||
context.MarkAsWrittenForIId(value, metadata, id);
|
context.MarkAsWrittenForIId(value, metadata, id);
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,13 @@ public enum ReferenceHandlingMode : byte
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reference handling only for IId objects - uses semantic Id for deduplication.
|
/// Reference handling only for IId objects - uses semantic Id for deduplication.
|
||||||
|
/// NOTE: Not fully implemented for JSON serializer - use All instead.
|
||||||
|
/// Binary serializer supports this mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
OnlyId = 1,
|
OnlyId = 1,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Full reference handling for all objects (future use).
|
/// Full reference handling for all objects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
All = 2
|
All = 2
|
||||||
}
|
}
|
||||||
|
|
@ -45,9 +47,10 @@ public abstract class AcSerializerOptions
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reference handling mode for circular/shared references.
|
/// Reference handling mode for circular/shared references.
|
||||||
/// Default: OnlyId (handles IId objects)
|
/// Default: OnlyId (JSON serializer requires All mode, OnlyId not yet implemented)
|
||||||
|
/// Note: Binary serializer supports OnlyId mode for IId-only tracking.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ReferenceHandlingMode ReferenceHandling { get; init; } = ReferenceHandlingMode.OnlyId;
|
public ReferenceHandlingMode ReferenceHandling { get; set; } = ReferenceHandlingMode.OnlyId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maximum depth for serialization/deserialization.
|
/// Maximum depth for serialization/deserialization.
|
||||||
|
|
@ -58,6 +61,15 @@ public abstract class AcSerializerOptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte MaxDepth { get; init; } = byte.MaxValue;
|
public byte MaxDepth { get; init; } = byte.MaxValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Throw exception on circular reference detection for non-IId types.
|
||||||
|
/// When true: Tracks all objects and throws InvalidOperationException on circular references.
|
||||||
|
/// When false: No tracking for non-IId types (faster, but circular refs may cause MaxDepth truncation).
|
||||||
|
/// Default: true (production safety)
|
||||||
|
/// Note: IId types are always tracked when ReferenceHandling != None.
|
||||||
|
/// </summary>
|
||||||
|
public bool ThrowOnCircularReference { get; init; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional callback for custom property mapping during cross-type operations.
|
/// Optional callback for custom property mapping during cross-type operations.
|
||||||
/// Used when deserializing/populating with Deserialize<TSource, TDest> or Populate<TSource, TDest>.
|
/// Used when deserializing/populating with Deserialize<TSource, TDest> or Populate<TSource, TDest>.
|
||||||
|
|
@ -85,7 +97,7 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default options instance with reference handling enabled and max depth.
|
/// Default options instance with reference handling enabled and max depth.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly AcJsonSerializerOptions Default = new();
|
public static readonly AcJsonSerializerOptions Default = new() { ReferenceHandling = ReferenceHandlingMode.All };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Options for shallow serialization (root level only, no references).
|
/// Options for shallow serialization (root level only, no references).
|
||||||
|
|
|
||||||
|
|
@ -74,10 +74,13 @@ public static partial class AcToonSerializer
|
||||||
|
|
||||||
public override void Reset(AcToonSerializerOptions options)
|
public override void Reset(AcToonSerializerOptions options)
|
||||||
{
|
{
|
||||||
|
// IMPORTANT: base.Reset sets Options first
|
||||||
|
base.Reset(options);
|
||||||
|
|
||||||
CurrentIndentLevel = 0;
|
CurrentIndentLevel = 0;
|
||||||
_nextRefId = 1;
|
_nextRefId = 1;
|
||||||
|
|
||||||
if (options.ReferenceHandling != ReferenceHandlingMode.None)
|
if (ReferenceHandling != ReferenceHandlingMode.None)
|
||||||
{
|
{
|
||||||
_scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
_scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
||||||
_writtenRefs ??= new Dictionary<object, int>(32, ReferenceEqualityComparer.Instance);
|
_writtenRefs ??= new Dictionary<object, int>(32, ReferenceEqualityComparer.Instance);
|
||||||
|
|
@ -85,7 +88,6 @@ public static partial class AcToonSerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
_registeredTypes ??= new HashSet<Type>(16);
|
_registeredTypes ??= new HashSet<Type>(16);
|
||||||
base.Reset(options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AyCode.Core.Helpers;
|
using AyCode.Core.Helpers;
|
||||||
|
using AyCode.Core.Interfaces;
|
||||||
using AyCode.Core.Serializers.Jsons;
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
@ -227,14 +228,20 @@ public abstract class TypeMetadataBase
|
||||||
return requiresWrite ? WritableProperties : ReadableProperties;
|
return requiresWrite ? WritableProperties : ReadableProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Id properties are always at index 0 (sorted first) in UnfilteredPropertiesGlobalCache!
|
||||||
|
public const string IdPropertyName = nameof(IId<int>.Id);
|
||||||
|
|
||||||
private static List<PropertyInfo> GetUnfilteredProperties(Type type, bool requiresWrite)
|
private static List<PropertyInfo> GetUnfilteredProperties(Type type, bool requiresWrite)
|
||||||
{
|
{
|
||||||
return UnfilteredPropertiesGlobalCache.GetOrAdd((type, requiresWrite), static key =>
|
return UnfilteredPropertiesGlobalCache.GetOrAdd((type, requiresWrite), static key =>
|
||||||
{
|
{
|
||||||
var (t, needsWrite) = key;
|
var (t, needsWrite) = key;
|
||||||
|
|
||||||
|
// Check if type implements IId - if so, Id property will be first
|
||||||
|
var isIId = GetIdInfo(t).IsId;
|
||||||
|
|
||||||
// Collect properties from inheritance hierarchy (derived -> base order)
|
// Collect properties from inheritance hierarchy (derived -> base order)
|
||||||
// Then sort alphabetically - ensures consistent ordering for serialization/deserialization
|
// Sort: IId types have Id first, then alphabetical
|
||||||
var allProperties = new List<PropertyInfo>();
|
var allProperties = new List<PropertyInfo>();
|
||||||
|
|
||||||
for (var currentType = t; currentType != null && currentType != typeof(object); currentType = currentType.BaseType)
|
for (var currentType = t; currentType != null && currentType != typeof(object); currentType = currentType.BaseType)
|
||||||
|
|
@ -246,7 +253,10 @@ public abstract class TypeMetadataBase
|
||||||
(!needsWrite || p.CanWrite) &&
|
(!needsWrite || p.CanWrite) &&
|
||||||
p.GetIndexParameters().Length == 0 &&
|
p.GetIndexParameters().Length == 0 &&
|
||||||
!IsUnsupportedPropertyType(p.PropertyType))
|
!IsUnsupportedPropertyType(p.PropertyType))
|
||||||
.OrderBy(static p => p.Name, StringComparer.Ordinal); // Alphabetical within level
|
// IId: Id first (0), then alphabetical (1)
|
||||||
|
// Non-IId: all alphabetical
|
||||||
|
.OrderBy(p => isIId && p.Name == IdPropertyName ? 0 : 1)
|
||||||
|
.ThenBy(static p => p.Name, StringComparer.Ordinal);
|
||||||
|
|
||||||
allProperties.AddRange(levelProperties);
|
allProperties.AddRange(levelProperties);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,12 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
||||||
RefIdGetter = metadata.TypedIdGetter ?? HashCodeGetter;
|
RefIdGetter = metadata.TypedIdGetter ?? HashCodeGetter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool UseTypeReferenceHandling(ReferenceHandlingMode referenceHandling)
|
||||||
|
{
|
||||||
|
return referenceHandling != ReferenceHandlingMode.None && (Metadata.IsIId || referenceHandling == ReferenceHandlingMode.All);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resets tracking state for reuse between serializations.
|
/// Resets tracking state for reuse between serializations.
|
||||||
/// Does not deallocate - just clears for reuse.
|
/// Does not deallocate - just clears for reuse.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue