diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 5ced01b..88c7860 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -25,7 +25,8 @@
"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(timeout 30 dotnet run:*)"
+ "Bash(timeout 30 dotnet run:*)",
+ "Bash(dotnet exec vstest:*)"
]
}
}
diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.AttributeExtraction.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.AttributeExtraction.cs
index 4a00c4e..088ec0a 100644
--- a/AyCode.Core/Serializers/Toons/AcToonSerializer.AttributeExtraction.cs
+++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.AttributeExtraction.cs
@@ -17,15 +17,13 @@ public static partial class AcToonSerializer
var underlyingType = Nullable.GetUnderlyingType(type);
var isNullable = underlyingType != null || !type.IsValueType;
- // Nullable vs Required
- if (isNullable)
- constraints.Add("nullable");
- else
- constraints.Add("required");
+ // Nullable/Required: NE írjuk ki automatikusan, mert redundáns (a típusból látszik)
+ // Csak explicit [Required] attribute esetén írjuk ki (azt az ExtractDataAnnotationConstraints kezeli)
+
+ // Numeric/Boolean: NE írjuk ki, mert redundáns (a típusból látszik)
+ // Range-eket megtartjuk, mert azok hasznos extra információk
var baseType = underlyingType ?? type;
-
- // Numeric type ranges
var typeCode = Type.GetTypeCode(baseType);
switch (typeCode)
{
@@ -41,37 +39,39 @@ public static partial class AcToonSerializer
case TypeCode.UInt16:
constraints.Add("range: 0-65535");
break;
- case TypeCode.Int32:
- constraints.Add("numeric");
- break;
case TypeCode.UInt32:
constraints.Add("range: 0-4294967295");
break;
- case TypeCode.Int64:
- constraints.Add("numeric");
- break;
- case TypeCode.UInt64:
- constraints.Add("numeric, non-negative");
- break;
- case TypeCode.Single:
- case TypeCode.Double:
- case TypeCode.Decimal:
- constraints.Add("numeric");
- break;
- case TypeCode.Boolean:
- constraints.Add("boolean: true|false");
- break;
+ // Int32, Int64, UInt64, Float, Double, Decimal, Boolean: nincs constraint (redundáns lenne)
}
- // Enum values
- if (baseType.IsEnum)
+ // Enum values: NE írjuk ki a constraint-ben, mert már a type metadata-ban benne van
+
+ return constraints.Count > 0 ? string.Join(", ", constraints) : string.Empty;
+ }
+
+ ///
+ /// Detect if property is readonly (no setter or init-only).
+ ///
+ private static bool IsReadOnlyProperty(PropertyInfo prop)
+ {
+ // No setter at all → readonly
+ if (!prop.CanWrite) return true;
+
+ // Has setter but it's init-only → readonly after construction
+ var setMethod = prop.GetSetMethod(nonPublic: true);
+ if (setMethod != null)
{
- var values = string.Join("|", Enum.GetNames(baseType).Take(5)); // Limit to 5 values
- var more = Enum.GetNames(baseType).Length > 5 ? "..." : "";
- constraints.Add($"enum: {values}{more}");
+ // Check for init-only (C# 9+ feature)
+ var returnParameter = setMethod.ReturnParameter;
+ var requiredModifiers = returnParameter.GetRequiredCustomModifiers();
+ if (requiredModifiers.Any(x => x.FullName == "System.Runtime.CompilerServices.IsExternalInit"))
+ {
+ return true;
+ }
}
- return string.Join(", ", constraints);
+ return false;
}
///
diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs
index ad5cdec..75d8348 100644
--- a/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs
+++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Linq;
using System.Reflection;
using static AyCode.Core.Helpers.JsonUtilities;
@@ -73,9 +74,17 @@ public static partial class AcToonSerializer
context.Write('=');
}
+ // Szűrés: csak komplex típusok (class/struct) és enum-ok, primitívek nélkül
+ var filteredTypes = typesToDocument
+ .Where(t => t.IsEnum || !IsPrimitiveOrStringFast(t)) // Enum-ok megtartása, primitívek és string kizárása
+ .Where(t => !typeof(System.Collections.IEnumerable).IsAssignableFrom(t) || t == typeof(string))
+ .Where(t => !t.IsGenericType || t.IsGenericTypeDefinition)
+ .Where(t => !t.Name.StartsWith("List`", StringComparison.Ordinal) && !t.Name.StartsWith("ICollection`", StringComparison.Ordinal) && !t.Name.StartsWith("IEnumerable`", StringComparison.Ordinal) && !t.Name.StartsWith("Dictionary`", StringComparison.Ordinal))
+ .ToList();
+
context.Write("[");
var first = true;
- foreach (var t in typesToDocument)
+ foreach (var t in filteredTypes)
{
if (!first) context.Write(", ");
context.Write($"\"{t.Name}\"");
@@ -96,7 +105,7 @@ public static partial class AcToonSerializer
context.WriteLine("@types {");
context.CurrentIndentLevel++;
- foreach (var t in typesToDocument)
+ foreach (var t in filteredTypes)
{
WriteTypeDefinition(t, context);
}
@@ -111,10 +120,18 @@ public static partial class AcToonSerializer
///
private static void CollectTypes(Type type, HashSet types)
{
- if (IsPrimitiveOrStringFast(type)) return;
-
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
+ // Enum-ok: hozzáadjuk és return (nem traversáljuk tovább)
+ if (underlyingType.IsEnum)
+ {
+ types.Add(underlyingType);
+ return;
+ }
+
+ // Primitívek és string: kihagyjuk
+ if (IsPrimitiveOrStringFast(underlyingType)) return;
+
if (!types.Add(underlyingType)) return; // Already processed
// Handle dictionaries FIRST (before generic IEnumerable check)
@@ -138,12 +155,69 @@ public static partial class AcToonSerializer
// Handle object properties (traverse type graph)
var metadata = GetTypeMetadata(underlyingType);
+
+ // Detektáljuk az enum backing field-eket (NotMapped enum property-k)
+ DetectAndCollectEnumBackingFields(underlyingType, types);
+
foreach (var prop in metadata.Properties)
{
CollectTypes(prop.PropertyType, types);
}
}
+ ///
+ /// Detektálja az enum backing field-eket és hozzáadja az enum típusokat a types hash set-hez.
+ /// Logika: Ha van egy enum property (bármilyen) és van egy {PropertyName}Id int property,
+ /// akkor az enum típust hozzáadjuk.
+ ///
+ private static void DetectAndCollectEnumBackingFields(Type type, HashSet types)
+ {
+ var allProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
+
+ foreach (var prop in allProperties)
+ {
+ // Csak enum property-ket vizsgálunk
+ if (!prop.PropertyType.IsEnum) continue;
+
+ // Keressük meg a {PropertyName}Id backing field-et
+ var backingFieldName = $"{prop.Name}Id";
+ var backingField = allProperties.FirstOrDefault(p =>
+ p.Name == backingFieldName &&
+ (p.PropertyType == typeof(int) || p.PropertyType == typeof(int?)));
+
+ if (backingField != null)
+ {
+ // Megtaláltuk az enum backing field párt → hozzáadjuk az enum típust
+ types.Add(prop.PropertyType);
+ }
+ }
+ }
+
+ ///
+ /// Megkeresi a property-hez tartozó enum típust, ha az egy enum backing field.
+ /// Visszaadja az enum típust, ha van, különben null.
+ ///
+ private static Type? FindEnumTypeForBackingField(PropertyInfo backingFieldProperty, Type declaringType)
+ {
+ // Csak int/int? típusú property-k lehetnek backing field-ek
+ if (backingFieldProperty.PropertyType != typeof(int) && backingFieldProperty.PropertyType != typeof(int?))
+ return null;
+
+ // Ha a property neve "...Id"-re végződik, keressük meg az enum property-t
+ if (!backingFieldProperty.Name.EndsWith("Id", StringComparison.Ordinal))
+ return null;
+
+ var enumPropertyName = backingFieldProperty.Name.Substring(0, backingFieldProperty.Name.Length - 2);
+ var allProperties = declaringType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
+
+ var enumProperty = allProperties.FirstOrDefault(p =>
+ p.Name == enumPropertyName &&
+ p.PropertyType.IsEnum);
+
+ // Ha van enum property, visszaadjuk a típusát (függetlenül a NotMapped attribútumtól)
+ return enumProperty?.PropertyType;
+ }
+
///
/// Write type definition with property descriptions.
///
@@ -162,11 +236,12 @@ public static partial class AcToonSerializer
var typeDescription = GetFinalTypeDescription(type, metadata);
context.WriteIndentedLine($"{type.Name}: \"{typeDescription}\"");
+ // Növeljük az indent-et a type body-hoz (type-level metadata ÉS property-k)
+ context.CurrentIndentLevel++;
+
// Type-level metadata (enhanced mode only)
if (context.Options.UseEnhancedMetadata)
{
- context.CurrentIndentLevel++;
-
// Table name
var tableName = DetectTableName(type, metadata.CustomDescription);
if (!string.IsNullOrEmpty(tableName))
@@ -180,14 +255,15 @@ public static partial class AcToonSerializer
{
context.WriteIndentedLine($"purpose: \"{typePurpose}\"");
}
-
- context.CurrentIndentLevel--;
}
- if (metadata.Properties.Length == 0) return;
+ if (metadata.Properties.Length == 0)
+ {
+ context.CurrentIndentLevel--;
+ return;
+ }
- // Property descriptions
- context.CurrentIndentLevel++;
+ // Property descriptions (már indent szint 1-en vagyunk, property-k is ide kerülnek)
foreach (var prop in metadata.Properties)
{
// Get final values with fallback chain + placeholder resolution
@@ -206,7 +282,11 @@ public static partial class AcToonSerializer
// Enhanced format with constraints and purpose
context.WriteIndentedLine($"{prop.Name}: {typeHint}");
context.CurrentIndentLevel++;
- context.WriteIndentedLine($"description: \"{propDescription}\"");
+
+ // Only write description if explicitly provided (not null/empty)
+ if (!string.IsNullOrEmpty(propDescription))
+ context.WriteIndentedLine($"description: \"{propDescription}\"");
+
if (!string.IsNullOrEmpty(purpose))
context.WriteIndentedLine($"purpose: \"{purpose}\"");
if (!string.IsNullOrEmpty(constraints))
@@ -234,10 +314,22 @@ public static partial class AcToonSerializer
{
// Simple format
var relationshipHint = GetRelationshipHint(relationshipMetadata);
- var fullDescription = string.IsNullOrEmpty(relationshipHint)
- ? propDescription
- : $"{propDescription} ({relationshipHint})";
- context.WriteIndentedLine($"{prop.Name}: {typeHint} \"{fullDescription}\"");
+
+ // Only write description if there's explicit description OR relationship metadata
+ if (!string.IsNullOrEmpty(propDescription) || !string.IsNullOrEmpty(relationshipHint))
+ {
+ var fullDescription = string.IsNullOrEmpty(relationshipHint)
+ ? propDescription
+ : string.IsNullOrEmpty(propDescription)
+ ? relationshipHint
+ : $"{propDescription} ({relationshipHint})";
+ context.WriteIndentedLine($"{prop.Name}: {typeHint} \"{fullDescription}\"");
+ }
+ else
+ {
+ // No description and no relationship metadata - just write type hint
+ context.WriteIndentedLine($"{prop.Name}: {typeHint}");
+ }
}
}
context.CurrentIndentLevel--;
@@ -366,9 +458,10 @@ public static partial class AcToonSerializer
///
/// Get final enum description with fallback chain.
- /// Priority: ToonDescription.Description -> Microsoft [Description] -> Smart inference
+ /// Priority: ToonDescription.Description -> Microsoft [Description] -> null (no fallback)
+ /// Returns null if no explicit description to avoid redundancy (values already in values: section).
///
- private static string GetFinalEnumDescription(Type enumType, ToonDescriptionAttribute? customDescription)
+ private static string? GetFinalEnumDescription(Type enumType, ToonDescriptionAttribute? customDescription)
{
// 1. ToonDescription.Description
if (!string.IsNullOrWhiteSpace(customDescription?.Description))
@@ -383,8 +476,8 @@ public static partial class AcToonSerializer
return msDesc.Description;
}
- // 3. Smart inference
- return $"Enum type with values: {string.Join(", ", Enum.GetNames(enumType))}";
+ // 3. No fallback - avoid redundancy (values already listed in values: section)
+ return null;
}
///
@@ -439,9 +532,10 @@ public static partial class AcToonSerializer
if (propertyName.EndsWith("Count", StringComparison.OrdinalIgnoreCase) && IsIntegerType(baseType))
return $"Count of {propertyName.Replace("Count", "")}";
- // Collection types
- if (GetCollectionElementType(propertyType) != null)
- return $"Collection of {GetCollectionElementType(propertyType)?.Name ?? "items"} for {declaringType.Name}";
+ // Collection types (csak ha tényleg nem object az elem)
+ var elementType = GetCollectionElementType(propertyType);
+ if (elementType != null && elementType != typeof(object))
+ return $"Collection of {elementType.Name} for {declaringType.Name}";
// Dictionary types
if (IsDictionaryType(propertyType, out _, out _))
@@ -522,9 +616,10 @@ public static partial class AcToonSerializer
///
/// Get final property description with fallback chain and placeholder resolution.
- /// Priority: ToonDescription (with placeholders) > Microsoft [Description] > Smart inference
+ /// Priority: ToonDescription (with placeholders) > Microsoft [Description] > null (no fallback)
+ /// Returns null if no explicit description is provided (to avoid redundant output).
///
- private static string GetFinalPropertyDescription(ToonPropertyAccessor prop, Type declaringType)
+ private static string? GetFinalPropertyDescription(ToonPropertyAccessor prop, Type declaringType)
{
// 1. ToonDescription.Description (if not empty)
var customDesc = prop.CustomDescription?.Description;
@@ -545,8 +640,8 @@ public static partial class AcToonSerializer
if (msDesc != null && !string.IsNullOrWhiteSpace(msDesc.Description))
return msDesc.Description;
- // 3. Smart inference (fallback)
- return GetPropertyDescription(declaringType, prop.Name, prop.PropertyType);
+ // 3. No fallback - return null to avoid redundant output
+ return null;
}
///
@@ -579,6 +674,21 @@ public static partial class AcToonSerializer
{
var customConstraints = prop.CustomDescription?.Constraints;
+ // --- NotMapped/NotColumn detection ---
+ var notMapped = false;
+ var attrs = prop.PropertyInfo.GetCustomAttributes(inherit: true);
+ foreach (var attr in attrs)
+ {
+ var attrName = attr.GetType().Name;
+ if (attrName == "NotMappedAttribute" || attrName == "NotColumnAttribute")
+ {
+ notMapped = true;
+ break;
+ }
+ }
+
+ string result;
+
if (!string.IsNullOrWhiteSpace(customConstraints))
{
// Has [#...] placeholder? → Resolve and merge
@@ -592,23 +702,49 @@ public static partial class AcToonSerializer
{
var typeConstraints = ExtractTypeConstraints(prop.PropertyType);
var inferredConstraints = GetInferredConstraints(prop.PropertyType, prop.Name);
-
- return MergeConstraints(typeConstraints, null, inferredConstraints, resolved);
+ result = MergeConstraints(typeConstraints, null, inferredConstraints, resolved);
+ }
+ else
+ {
+ result = string.Empty;
}
}
else
{
// No placeholder → custom wins (replace mode)
- return customConstraints;
+ result = customConstraints;
}
}
+ else
+ {
+ // No custom constraint → Fallback chain
+ var msConstraints = ExtractDataAnnotationConstraints(prop.PropertyInfo);
+ var typeConstraints2 = ExtractTypeConstraints(prop.PropertyType);
+ var inferredConstraints2 = GetInferredConstraints(prop.PropertyType, prop.Name);
+ result = MergeConstraints(typeConstraints2, msConstraints, inferredConstraints2, null);
+ }
- // No custom constraint → Fallback chain
- var msConstraints = ExtractDataAnnotationConstraints(prop.PropertyInfo);
- var typeConstraints2 = ExtractTypeConstraints(prop.PropertyType);
- var inferredConstraints2 = GetInferredConstraints(prop.PropertyType, prop.Name);
+ // Add readonly constraint if needed
+ if (IsReadOnlyProperty(prop.PropertyInfo) && !result.Contains("readonly"))
+ {
+ result = string.IsNullOrEmpty(result) ? "readonly" : result + ", readonly";
+ }
- return MergeConstraints(typeConstraints2, msConstraints, inferredConstraints2, null);
+ // Add not-mapped constraint if needed (only once, at the end)
+ if (notMapped && !result.Contains("not-mapped"))
+ {
+ result = string.IsNullOrEmpty(result) ? "not-mapped" : result + ", not-mapped";
+ }
+
+ // Add enum-type constraint if this is an enum backing field
+ var enumType = FindEnumTypeForBackingField(prop.PropertyInfo, declaringType);
+ if (enumType != null && !result.Contains("enum-type:"))
+ {
+ var enumConstraint = $"enum-type: {enumType.Name}";
+ result = string.IsNullOrEmpty(result) ? enumConstraint : result + ", " + enumConstraint;
+ }
+
+ return result;
}
///