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.Serializers.Binaries;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using static AyCode.Core.Tests.TestModels.AcSerializerModels;
|
||||
|
||||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
|
@ -54,7 +56,10 @@ public class AcBinarySerializerCircularReferenceTests
|
|||
child.GrandChildren.Add(grandChild);
|
||||
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>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
|
|
@ -125,7 +130,10 @@ public class AcBinarySerializerCircularReferenceTests
|
|||
return parent;
|
||||
}).ToList();
|
||||
|
||||
var binary = parents.ToBinary();
|
||||
var option = AcBinarySerializerOptions.Default;
|
||||
option.ReferenceHandling = ReferenceHandlingMode.All;
|
||||
|
||||
var binary = parents.ToBinary(option);
|
||||
var result = binary.BinaryTo<List<CircularParent>>();
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
|
|||
/// The options used for this context. Set during Reset.
|
||||
/// </summary>
|
||||
public TOptions Options { get; private set; } = null!;
|
||||
|
||||
public byte MaxDepth { get; private set; }
|
||||
public ReferenceHandlingMode ReferenceHandling { get; internal set; }
|
||||
|
||||
public byte MaxDepth => Options.MaxDepth;
|
||||
public ReferenceHandlingMode ReferenceHandling => Options.ReferenceHandling;
|
||||
public bool ThrowOnCircularReference => Options.ThrowOnCircularReference;
|
||||
/// <summary>
|
||||
/// Global shared cache for metadata (thread-safe, shared across all contexts).
|
||||
/// Generic specialization ensures separate cache per TMetadata type.
|
||||
|
|
@ -142,14 +143,12 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
|
|||
/// </summary>
|
||||
public virtual void Reset(TOptions options)
|
||||
{
|
||||
Options = options;
|
||||
|
||||
foreach (var wrapper in _wrappers.Values)
|
||||
{
|
||||
wrapper.ResetTracking();
|
||||
}
|
||||
|
||||
Options = options;
|
||||
MaxDepth = options.MaxDepth;
|
||||
ReferenceHandling = options.ReferenceHandling;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -22,13 +22,10 @@ public static partial class AcBinaryDeserializer
|
|||
private List<string>? _propertyNames;
|
||||
//private Dictionary<int, object>? _objectReferences;
|
||||
private Dictionary<int, string>? _stringCache;
|
||||
private readonly byte _minStringInternLength;
|
||||
private readonly bool _useStringCaching;
|
||||
private readonly int _maxCachedStringLength;
|
||||
|
||||
/// <summary>
|
||||
/// Heap-allocated context class for IId-based reference tracking.
|
||||
/// Uses AcSerializerContextBase infrastructure.
|
||||
/// Also holds Options - all options-derived properties delegate to ContextClass.Options.
|
||||
/// </summary>
|
||||
public readonly BinaryDeserializationContextClass ContextClass;
|
||||
|
||||
|
|
@ -36,12 +33,16 @@ public static partial class AcBinaryDeserializer
|
|||
/// <summary>
|
||||
/// Convenience property - true if any reference handling is enabled.
|
||||
/// </summary>
|
||||
public bool HasReferenceHandling => ContextClass?.ReferenceHandling != ReferenceHandlingMode.None;
|
||||
//public readonly bool HasReferenceHandling => ContextClass.ReferenceHandling != ReferenceHandlingMode.None;
|
||||
public bool IsMergeMode { readonly get; set; }
|
||||
public bool RemoveOrphanedItems { readonly get; set; }
|
||||
public bool IsAtEnd => _position >= _buffer.Length;
|
||||
public int Position => _position;
|
||||
public byte MinStringInternLength => _minStringInternLength;
|
||||
public readonly bool IsAtEnd => _position >= _buffer.Length;
|
||||
public readonly int Position => _position;
|
||||
|
||||
// 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>
|
||||
/// Chain reference tracker for maintaining object identity across chain operations.
|
||||
|
|
@ -76,12 +77,9 @@ public static partial class AcBinaryDeserializer
|
|||
IsMergeMode = false;
|
||||
RemoveOrphanedItems = false;
|
||||
ChainTracker = null;
|
||||
_minStringInternLength = options.MinStringInternLength;
|
||||
_useStringCaching = options.UseStringCaching;
|
||||
_maxCachedStringLength = options.MaxCachedStringLength;
|
||||
ContextClass = contextClass;
|
||||
// Initialize ReferenceHandling from options (will be overwritten by ReadHeader if present in stream)
|
||||
ContextClass.ReferenceHandling = options.ReferenceHandling;
|
||||
// Reset ContextClass with options - this sets Options and clears any previous state
|
||||
ContextClass.Reset(options);
|
||||
}
|
||||
|
||||
public void ReadHeader()
|
||||
|
|
@ -108,11 +106,11 @@ public static partial class AcBinaryDeserializer
|
|||
if (marker == BinaryTypeCode.MetadataHeader)
|
||||
{
|
||||
hasPropertyTable = true;
|
||||
ContextClass.ReferenceHandling = ReferenceHandlingMode.OnlyId; // Legacy: assume OnlyId
|
||||
ContextClass.Options.ReferenceHandling = ReferenceHandlingMode.OnlyId; // Legacy: assume OnlyId
|
||||
}
|
||||
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)
|
||||
{
|
||||
|
|
@ -121,7 +119,7 @@ public static partial class AcBinaryDeserializer
|
|||
// Decode ReferenceHandlingMode from separate bits
|
||||
var hasOnlyId = (flags & BinaryTypeCode.HeaderFlag_RefHandling_OnlyId) != 0;
|
||||
var hasAll = (flags & BinaryTypeCode.HeaderFlag_RefHandling_All) != 0;
|
||||
ContextClass.ReferenceHandling = hasAll ? ReferenceHandlingMode.All
|
||||
ContextClass.Options.ReferenceHandling = hasAll ? ReferenceHandlingMode.All
|
||||
: hasOnlyId ? ReferenceHandlingMode.OnlyId
|
||||
: ReferenceHandlingMode.None;
|
||||
|
||||
|
|
@ -448,7 +446,7 @@ public static partial class AcBinaryDeserializer
|
|||
EnsureAvailable(length);
|
||||
|
||||
// WASM optimization: cache short strings to reduce allocations
|
||||
if (_useStringCaching && length <= _maxCachedStringLength)
|
||||
if (UseStringCaching && length <= MaxCachedStringLength)
|
||||
{
|
||||
return ReadStringUtf8Cached(length);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ public static partial class AcBinaryDeserializer
|
|||
var metadata = wrapper.Metadata;
|
||||
|
||||
// Handle reference ID if present
|
||||
if (context.HasReferenceHandling)
|
||||
if (context.ContextClass.UseTypeReferenceHandling(metadata))
|
||||
{
|
||||
var refId = context.ReadVarInt();
|
||||
if (context.ContextClass.TryGetValue(wrapper, refId, out var instance)) return instance;
|
||||
|
|
@ -270,7 +270,7 @@ public static partial class AcBinaryDeserializer
|
|||
if (destPropIndex == -1)
|
||||
{
|
||||
// No mapping - skip this property
|
||||
SkipValue(ref context);
|
||||
SkipValue(ref context, metadata);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -279,7 +279,7 @@ public static partial class AcBinaryDeserializer
|
|||
if (propInfo == null)
|
||||
{
|
||||
// Destination property not found - skip
|
||||
SkipValue(ref context);
|
||||
SkipValue(ref context, metadata);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -336,7 +336,7 @@ public static partial class AcBinaryDeserializer
|
|||
context.ReadByte(); // consume Object marker
|
||||
|
||||
// Handle ref ID if present
|
||||
if (context.HasReferenceHandling)
|
||||
if (context.ContextClass.UseTypeReferenceHandling(wrapper.Metadata))
|
||||
{
|
||||
var refId = context.ReadVarInt();
|
||||
if (refId > 0)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using AyCode.Core.Helpers;
|
||||
using AyCode.Core.Serializers;
|
||||
using static AyCode.Core.Helpers.JsonUtilities;
|
||||
|
||||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
|
@ -26,7 +27,7 @@ public static partial class AcBinaryDeserializer
|
|||
var metadata = wrapper.Metadata;
|
||||
|
||||
// Handle ref ID if present
|
||||
if (context.HasReferenceHandling)
|
||||
if (context.ContextClass.UseTypeReferenceHandling(metadata))
|
||||
{
|
||||
var refId = context.ReadVarInt();
|
||||
if (refId > 0)
|
||||
|
|
@ -55,7 +56,20 @@ public static partial class AcBinaryDeserializer
|
|||
var nextDepth = depth + 1;
|
||||
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 peekCode = context.PeekByte();
|
||||
|
|
@ -115,7 +129,7 @@ public static partial class AcBinaryDeserializer
|
|||
var wrapper = context.ContextClass.GetWrapper(propInfo.PropertyType);
|
||||
|
||||
// Handle ref ID if present
|
||||
if (context.HasReferenceHandling)
|
||||
if (context.ContextClass.UseTypeReferenceHandling(wrapper.Metadata))
|
||||
{
|
||||
var refId = context.ReadVarInt();
|
||||
if (refId > 0)
|
||||
|
|
@ -241,7 +255,7 @@ public static partial class AcBinaryDeserializer
|
|||
context.ReadByte(); // consume Object marker
|
||||
|
||||
// Handle ref ID if present
|
||||
if (context.HasReferenceHandling)
|
||||
if (context.ContextClass.UseTypeReferenceHandling(elementMetadata))
|
||||
{
|
||||
var refId = context.ReadVarInt();
|
||||
if (refId > 0)
|
||||
|
|
@ -344,7 +358,7 @@ public static partial class AcBinaryDeserializer
|
|||
if (newItem == null) continue;
|
||||
|
||||
// Handle ref ID if present
|
||||
if (context.HasReferenceHandling)
|
||||
if (context.ContextClass.UseTypeReferenceHandling(elementMetadata))
|
||||
{
|
||||
var refId = context.ReadVarInt();
|
||||
if (refId > 0)
|
||||
|
|
@ -456,7 +470,7 @@ public static partial class AcBinaryDeserializer
|
|||
if (newItem == null) continue;
|
||||
|
||||
// Handle ref ID if present
|
||||
if (context.HasReferenceHandling)
|
||||
if (context.ContextClass.UseTypeReferenceHandling(elementMetadata))
|
||||
{
|
||||
var refId = context.ReadVarInt();
|
||||
if (refId > 0)
|
||||
|
|
@ -518,6 +532,26 @@ public static partial class AcBinaryDeserializer
|
|||
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>
|
||||
/// 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;
|
||||
|
||||
if (context.HasReferenceHandling && metadata.IdAccessorType != AcSerializerCommon.IdAccessorType.None)
|
||||
if (context.ContextClass.UseTypeReferenceHandling(metadata))
|
||||
{
|
||||
switch (metadata.IdAccessorType)
|
||||
{
|
||||
|
|
@ -1267,7 +1267,7 @@ public static partial class AcBinaryDeserializer
|
|||
|
||||
#region Skip Value
|
||||
|
||||
private static void SkipValue(ref BinaryDeserializationContext context)
|
||||
private static void SkipValue(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData)
|
||||
{
|
||||
var typeCode = context.ReadByte();
|
||||
|
||||
|
|
@ -1349,16 +1349,16 @@ public static partial class AcBinaryDeserializer
|
|||
if (enumByte == BinaryTypeCode.Int32) context.ReadVarInt();
|
||||
return;
|
||||
case BinaryTypeCode.Object:
|
||||
SkipObject(ref context);
|
||||
SkipObject(ref context, metaData);
|
||||
return;
|
||||
case BinaryTypeCode.ObjectRef:
|
||||
context.ReadVarInt();
|
||||
return;
|
||||
case BinaryTypeCode.Array:
|
||||
SkipArray(ref context);
|
||||
SkipArray(ref context, metaData);
|
||||
return;
|
||||
case BinaryTypeCode.Dictionary:
|
||||
SkipDictionary(ref context);
|
||||
SkipDictionary(ref context, metaData);
|
||||
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
|
||||
if (context.HasReferenceHandling)
|
||||
if (context.ContextClass.UseTypeReferenceHandling(metaData))
|
||||
{
|
||||
context.ReadVarInt();
|
||||
}
|
||||
|
|
@ -1420,22 +1420,22 @@ public static partial class AcBinaryDeserializer
|
|||
"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();
|
||||
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();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
SkipValue(ref context); // key
|
||||
SkipValue(ref context); // value
|
||||
SkipValue(ref context, metaData); // key
|
||||
SkipValue(ref context, metaData); // value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,11 @@ public static partial class AcBinarySerializer
|
|||
public byte MinStringInternLength => Options.MinStringInternLength;
|
||||
public byte MaxStringInternLength => Options.MaxStringInternLength;
|
||||
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;
|
||||
|
||||
|
|
@ -96,11 +101,12 @@ public static partial class AcBinarySerializer
|
|||
|
||||
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);
|
||||
|
||||
_position = 0;
|
||||
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
||||
_initialBufferSize = Math.Max(Options.InitialBufferCapacity, MinBufferSize);
|
||||
HasPropertyFilter = Options.PropertyFilter != null;
|
||||
|
||||
if (_buffer.Length < _initialBufferSize)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -641,8 +641,8 @@ public static partial class AcBinarySerializer
|
|||
var wrapper = context.GetWrapper(type);
|
||||
var metadata = wrapper.Metadata;
|
||||
|
||||
// Single-pass reference tracking
|
||||
if (context.ReferenceHandling != ReferenceHandlingMode.None)
|
||||
// Single-pass reference tracking - type-specific check
|
||||
if (context.UseTypeReferenceHandling(metadata))
|
||||
{
|
||||
switch (metadata.IdAccessorType)
|
||||
{
|
||||
|
|
@ -684,32 +684,61 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
else
|
||||
{
|
||||
// No reference handling - just write object marker
|
||||
// No reference handling for this type - just write object marker
|
||||
context.WriteByte(BinaryTypeCode.Object);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Write properties
|
||||
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)
|
||||
// - No property indices needed (sequential order)
|
||||
// - Single getter call per property
|
||||
// - 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];
|
||||
|
||||
// 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);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Write property value OR skip marker (single operation, single getter call)
|
||||
WritePropertyOrSkip(value, prop, context, nextDepth);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,11 +77,12 @@ public static partial class AcJsonDeserializer
|
|||
|
||||
public override void Reset(AcJsonSerializerOptions options)
|
||||
{
|
||||
// IMPORTANT: base.Reset sets Options first
|
||||
base.Reset(options);
|
||||
|
||||
IsMergeMode = false;
|
||||
ChainTracker = null;
|
||||
_refTracker.Reset();
|
||||
|
||||
base.Reset(options);
|
||||
}
|
||||
|
||||
public void Clear(AcJsonSerializerOptions options)
|
||||
|
|
|
|||
|
|
@ -64,14 +64,15 @@ public static partial class AcJsonSerializer
|
|||
|
||||
public override void Reset(AcJsonSerializerOptions options)
|
||||
{
|
||||
// IMPORTANT: base.Reset sets Options first
|
||||
base.Reset(options);
|
||||
|
||||
_refTracker.Reset();
|
||||
|
||||
if (options.ReferenceHandling != ReferenceHandlingMode.None)
|
||||
if (ReferenceHandling != ReferenceHandlingMode.None)
|
||||
{
|
||||
_refTracker.EnsureInitialized();
|
||||
}
|
||||
|
||||
base.Reset(options);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
|
|
|
|||
|
|
@ -180,9 +180,10 @@ public static partial class AcJsonSerializer
|
|||
var writer = context.Writer;
|
||||
var wrapper = context.GetWrapper(type);
|
||||
var metadata = wrapper.Metadata;
|
||||
|
||||
var useReferenceHandling = context.UseTypeReferenceHandling(metadata);
|
||||
|
||||
// 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.WriteString(RefPropertyEncoded, refId.ToString(CultureInfo.InvariantCulture));
|
||||
|
|
@ -192,7 +193,7 @@ public static partial class AcJsonSerializer
|
|||
|
||||
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));
|
||||
context.MarkAsWrittenForIId(value, metadata, id);
|
||||
|
|
|
|||
|
|
@ -21,11 +21,13 @@ public enum ReferenceHandlingMode : byte
|
|||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
OnlyId = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Full reference handling for all objects (future use).
|
||||
/// Full reference handling for all objects.
|
||||
/// </summary>
|
||||
All = 2
|
||||
}
|
||||
|
|
@ -45,9 +47,10 @@ public abstract class AcSerializerOptions
|
|||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public ReferenceHandlingMode ReferenceHandling { get; init; } = ReferenceHandlingMode.OnlyId;
|
||||
public ReferenceHandlingMode ReferenceHandling { get; set; } = ReferenceHandlingMode.OnlyId;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum depth for serialization/deserialization.
|
||||
|
|
@ -58,6 +61,15 @@ public abstract class AcSerializerOptions
|
|||
/// </summary>
|
||||
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>
|
||||
/// Optional callback for custom property mapping during cross-type operations.
|
||||
/// Used when deserializing/populating with Deserialize<TSource, TDest> or Populate<TSource, TDest>.
|
||||
|
|
@ -85,7 +97,7 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
|
|||
/// <summary>
|
||||
/// Default options instance with reference handling enabled and max depth.
|
||||
/// </summary>
|
||||
public static readonly AcJsonSerializerOptions Default = new();
|
||||
public static readonly AcJsonSerializerOptions Default = new() { ReferenceHandling = ReferenceHandlingMode.All };
|
||||
|
||||
/// <summary>
|
||||
/// Options for shallow serialization (root level only, no references).
|
||||
|
|
|
|||
|
|
@ -74,10 +74,13 @@ public static partial class AcToonSerializer
|
|||
|
||||
public override void Reset(AcToonSerializerOptions options)
|
||||
{
|
||||
// IMPORTANT: base.Reset sets Options first
|
||||
base.Reset(options);
|
||||
|
||||
CurrentIndentLevel = 0;
|
||||
_nextRefId = 1;
|
||||
|
||||
if (options.ReferenceHandling != ReferenceHandlingMode.None)
|
||||
if (ReferenceHandling != ReferenceHandlingMode.None)
|
||||
{
|
||||
_scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
||||
_writtenRefs ??= new Dictionary<object, int>(32, ReferenceEqualityComparer.Instance);
|
||||
|
|
@ -85,7 +88,6 @@ public static partial class AcToonSerializer
|
|||
}
|
||||
|
||||
_registeredTypes ??= new HashSet<Type>(16);
|
||||
base.Reset(options);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AyCode.Core.Helpers;
|
||||
using AyCode.Core.Interfaces;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
|
|
@ -227,14 +228,20 @@ public abstract class TypeMetadataBase
|
|||
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)
|
||||
{
|
||||
return UnfilteredPropertiesGlobalCache.GetOrAdd((type, requiresWrite), static 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)
|
||||
// Then sort alphabetically - ensures consistent ordering for serialization/deserialization
|
||||
// Sort: IId types have Id first, then alphabetical
|
||||
var allProperties = new List<PropertyInfo>();
|
||||
|
||||
for (var currentType = t; currentType != null && currentType != typeof(object); currentType = currentType.BaseType)
|
||||
|
|
@ -246,7 +253,10 @@ public abstract class TypeMetadataBase
|
|||
(!needsWrite || p.CanWrite) &&
|
||||
p.GetIndexParameters().Length == 0 &&
|
||||
!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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,12 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
|||
RefIdGetter = metadata.TypedIdGetter ?? HashCodeGetter;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool UseTypeReferenceHandling(ReferenceHandlingMode referenceHandling)
|
||||
{
|
||||
return referenceHandling != ReferenceHandlingMode.None && (Metadata.IsIId || referenceHandling == ReferenceHandlingMode.All);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets tracking state for reuse between serializations.
|
||||
/// Does not deallocate - just clears for reuse.
|
||||
|
|
|
|||
Loading…
Reference in New Issue