diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerCircularReferenceTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerCircularReferenceTests.cs index 9aa9a19..cb56cc3 100644 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerCircularReferenceTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerCircularReferenceTests.cs @@ -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(); 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>(); Assert.IsNotNull(result); diff --git a/AyCode.Core/Serializers/AcSerializerContextBase.cs b/AyCode.Core/Serializers/AcSerializerContextBase.cs index e60fddc..e2af256 100644 --- a/AyCode.Core/Serializers/AcSerializerContextBase.cs +++ b/AyCode.Core/Serializers/AcSerializerContextBase.cs @@ -22,9 +22,10 @@ public abstract class AcSerializerContextBase /// The options used for this context. Set during Reset. /// 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; /// /// 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 /// 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 diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index a330afb..ba47672 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -22,13 +22,10 @@ public static partial class AcBinaryDeserializer private List? _propertyNames; //private Dictionary? _objectReferences; private Dictionary? _stringCache; - private readonly byte _minStringInternLength; - private readonly bool _useStringCaching; - private readonly int _maxCachedStringLength; /// /// Heap-allocated context class for IId-based reference tracking. - /// Uses AcSerializerContextBase infrastructure. + /// Also holds Options - all options-derived properties delegate to ContextClass.Options. /// public readonly BinaryDeserializationContextClass ContextClass; @@ -36,12 +33,16 @@ public static partial class AcBinaryDeserializer /// /// Convenience property - true if any reference handling is enabled. /// - 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; /// /// 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); } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs index 4af38f9..5361d48 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs @@ -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) diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index b134256..f589d29 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -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); } } + + /// + /// Reads Id value without type marker. The serializer didn't write a marker for IId types. + /// + [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; + } + } /// /// Sets a property to its default value using typed setters to avoid boxing. diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index b3cf9e1..e6dc05a 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -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 } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 5171450..3bec675 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -78,6 +78,11 @@ public static partial class AcBinarySerializer public byte MinStringInternLength => Options.MinStringInternLength; public byte MaxStringInternLength => Options.MaxStringInternLength; public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter; + + /// + /// Cached check for PropertyFilter != null. Set in Reset() to avoid property getter in hot loop. + /// + 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) { diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index a31b3b1..65436c2 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -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); } diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializationContext.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializationContext.cs index b15339e..939a854 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializationContext.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializationContext.cs @@ -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) diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs index 3374d60..b38aff4 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs @@ -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() diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs index bdaa5ac..cc09e5d 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs @@ -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); diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs index 57d2fd0..df7f493 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs @@ -21,11 +21,13 @@ public enum ReferenceHandlingMode : byte /// /// 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. /// OnlyId = 1, /// - /// Full reference handling for all objects (future use). + /// Full reference handling for all objects. /// All = 2 } @@ -45,9 +47,10 @@ public abstract class AcSerializerOptions /// /// 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. /// - public ReferenceHandlingMode ReferenceHandling { get; init; } = ReferenceHandlingMode.OnlyId; + public ReferenceHandlingMode ReferenceHandling { get; set; } = ReferenceHandlingMode.OnlyId; /// /// Maximum depth for serialization/deserialization. @@ -58,6 +61,15 @@ public abstract class AcSerializerOptions /// public byte MaxDepth { get; init; } = byte.MaxValue; + /// + /// 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. + /// + public bool ThrowOnCircularReference { get; init; } = true; + /// /// 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 /// /// Default options instance with reference handling enabled and max depth. /// - public static readonly AcJsonSerializerOptions Default = new(); + public static readonly AcJsonSerializerOptions Default = new() { ReferenceHandling = ReferenceHandlingMode.All }; /// /// Options for shallow serialization (root level only, no references). diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs index 9636dff..feacfb9 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs @@ -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(64, ReferenceEqualityComparer.Instance); _writtenRefs ??= new Dictionary(32, ReferenceEqualityComparer.Instance); @@ -85,7 +88,6 @@ public static partial class AcToonSerializer } _registeredTypes ??= new HashSet(16); - base.Reset(options); } public void Clear() diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs index d77f2a5..00f93d5 100644 --- a/AyCode.Core/Serializers/TypeMetadataBase.cs +++ b/AyCode.Core/Serializers/TypeMetadataBase.cs @@ -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.Id); + private static List 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(); 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); } diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs index 8354620..a1c09b7 100644 --- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -57,6 +57,12 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat RefIdGetter = metadata.TypedIdGetter ?? HashCodeGetter; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool UseTypeReferenceHandling(ReferenceHandlingMode referenceHandling) + { + return referenceHandling != ReferenceHandlingMode.None && (Metadata.IsIId || referenceHandling == ReferenceHandlingMode.All); + } + /// /// Resets tracking state for reuse between serializations. /// Does not deallocate - just clears for reuse.