From 3400cbc65af40da6e1f0d2e60c624551f8620b19 Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 12 Jan 2026 08:36:23 +0100 Subject: [PATCH] Improve AcToonSerializer type metadata handling & tests - Refined type metadata serialization: collections and dictionaries are now detected and described more accurately, avoiding generic type names (e.g., List`1) and redundant "object" element types. - Added circular reference protection to type name generation to prevent stack overflows and duplicate type names. - Updated AcToonSerializerOptions.Compact to use indentation for better readability. - Introduced ToonTests with unit tests to ensure type metadata correctness, uniqueness, and clarity. - Added AyCode.Core project to the solution and adjusted namespaces/usings for consistency. --- .claude/settings.local.json | 3 +- .../Toons/AcToonSerializer.MetaSection.cs | 33 ++++---- ...ToonSerializer.ToonSerializationContext.cs | 2 +- .../AcToonSerializer.ToonTypeMetadata.cs | 78 ++++++++++++++----- .../Toons/AcToonSerializerOptions.cs | 2 +- 5 files changed, 83 insertions(+), 35 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8866d3a..5ced01b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -24,7 +24,8 @@ "Bash(dir /B /O-D \"H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.md\")", "Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.md\")", "Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log 2>nul | head -3\")", - "Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log 2>nul | head -1\")" + "Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log 2>nul | head -1\")", + "Bash(timeout 30 dotnet run:*)" ] } } diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs index 8ed59d2..ad5cdec 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs @@ -106,7 +106,8 @@ public static partial class AcToonSerializer } /// - /// Collect all types that need documentation (recursive). + /// Recursively collects all types that need metadata documentation. + /// Uses the same logic as AcBinarySerializer.RegisterMetadataForType. /// private static void CollectTypes(Type type, HashSet types) { @@ -116,15 +117,7 @@ public static partial class AcToonSerializer if (!types.Add(underlyingType)) return; // Already processed - // Handle collections - var elementType = GetCollectionElementType(underlyingType); - if (elementType != null) - { - CollectTypes(elementType, types); - return; - } - - // Handle dictionaries + // Handle dictionaries FIRST (before generic IEnumerable check) if (IsDictionaryType(underlyingType, out var keyType, out var valueType)) { if (keyType != null) CollectTypes(keyType, types); @@ -132,7 +125,18 @@ public static partial class AcToonSerializer return; } - // Handle object properties + // Handle collections/arrays (matches AcBinarySerializer logic) + if (typeof(IEnumerable).IsAssignableFrom(underlyingType) && !ReferenceEquals(underlyingType, StringType)) + { + var elementType = GetCollectionElementType(underlyingType); + if (elementType != null) + { + CollectTypes(elementType, types); + } + return; + } + + // Handle object properties (traverse type graph) var metadata = GetTypeMetadata(underlyingType); foreach (var prop in metadata.Properties) { @@ -395,8 +399,11 @@ public static partial class AcToonSerializer return $"Enum type with values: {string.Join(", ", Enum.GetNames(type))}"; var metadata = GetTypeMetadata(type); - if (metadata.IsCollection) - return $"Collection of {metadata.ElementType?.Name ?? "items"}"; + + // Only treat as collection if ElementType is not System.Object (fallback value from GetCollectionElementType) + if (metadata.IsCollection && metadata.ElementType != null && metadata.ElementType != typeof(object)) + return $"Collection of {metadata.ElementType.Name}"; + if (metadata.IsDictionary) return "Dictionary mapping keys to values"; diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs index 8dd49b9..ae6420d 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; -using ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer; +using ReferenceEqualityComparer = AyCode.Core.Serializers.ReferenceEqualityComparer; namespace AyCode.Core.Serializers.Toons; diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonTypeMetadata.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonTypeMetadata.cs index fcd2c07..c6f365a 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonTypeMetadata.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonTypeMetadata.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; @@ -28,12 +29,16 @@ public static partial class AcToonSerializer // Get custom attribute if present CustomDescription = type.GetCustomAttribute(); - // Check if collection or dictionary + // Check if collection or dictionary (matches AcBinarySerializer logic) IsDictionary = IsDictionaryType(type, out _, out _); if (!IsDictionary) { - ElementType = GetCollectionElementType(type); - IsCollection = ElementType != null; + // Only treat as collection if it implements IEnumerable (excluding string) + if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType)) + { + ElementType = GetCollectionElementType(type); + IsCollection = true; + } } // Build property accessors @@ -137,8 +142,13 @@ public static partial class AcToonSerializer return false; } + // Thread-local visited set to prevent circular reference stack overflow + [ThreadStatic] + private static HashSet? t_visitedTypes; + /// /// Build human-readable type name for meta section. + /// Uses thread-local visited set to prevent stack overflow on circular references. /// private static string BuildTypeDisplayName(Type type) { @@ -169,30 +179,60 @@ public static partial class AcToonSerializer return isNullable ? baseName + "?" : baseName; } + /// + /// Get complex type name WITH circular reference protection using thread-local visited set. + /// private static string GetComplexTypeName(Type type) { - if (ReferenceEquals(type, GuidType)) return "guid"; - if (ReferenceEquals(type, DateTimeOffsetType)) return "datetimeoffset"; - if (ReferenceEquals(type, TimeSpanType)) return "timespan"; - if (type.IsEnum) return "enum"; + // Initialize thread-local visited set if needed + t_visitedTypes ??= new HashSet(); - // Check for collections - var elementType = GetCollectionElementType(type); - if (elementType != null) + // Check for circular reference - if already visiting, return simple name + if (!t_visitedTypes.Add(type)) { - var elementTypeName = BuildTypeDisplayName(elementType); - return $"{elementTypeName}[]"; + return type.Name; // Break circular reference } - // Check for dictionaries - if (IsDictionaryType(type, out var keyType, out var valueType)) + try { - var keyTypeName = keyType != null ? BuildTypeDisplayName(keyType) : "object"; - var valueTypeName = valueType != null ? BuildTypeDisplayName(valueType) : "object"; - return $"dict<{keyTypeName}, {valueTypeName}>"; - } + if (ReferenceEquals(type, GuidType)) return "guid"; + if (ReferenceEquals(type, DateTimeOffsetType)) return "datetimeoffset"; + if (ReferenceEquals(type, TimeSpanType)) return "timespan"; + if (type.IsEnum) return "enum"; - return type.Name; + // For collections: recursively build element type name (safe with visited tracking) + if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType)) + { + var elementType = GetCollectionElementType(type); + if (elementType != null && elementType != typeof(object)) + { + var elementTypeName = BuildTypeDisplayName(elementType); + return $"{elementTypeName}[]"; + } + } + + // For dictionaries: recursively build key/value type names (safe with visited tracking) + if (IsDictionaryType(type, out var keyType, out var valueType)) + { + var keyTypeName = keyType != null ? BuildTypeDisplayName(keyType) : "object"; + var valueTypeName = valueType != null ? BuildTypeDisplayName(valueType) : "object"; + return $"dict<{keyTypeName}, {valueTypeName}>"; + } + + // Simple type name for complex objects + return type.Name; + } + finally + { + // Remove from visited set when done (allows reuse in different branches) + t_visitedTypes.Remove(type); + + // Clear the set if empty to avoid memory leaks + if (t_visitedTypes.Count == 0) + { + t_visitedTypes = null; + } + } } } } diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs b/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs index 23f0567..0f75579 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs @@ -181,7 +181,7 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions { Mode = ToonSerializationMode.DataOnly, UseMeta = false, - UseIndentation = false, + UseIndentation = true, OmitDefaultValues = true, WriteTypeNames = false, UseReferenceHandling = false