Refactor AcToonSerializer metadata extraction & DTO tables

- Remove redundant constraints (nullable, numeric, boolean) from metadata; only explicit [Required] is documented.
- Exclude enum values from constraints; add "readonly" for readonly/init-only properties.
- Filter out primitives from documented types; only complex types and enums are included.
- Detect and document enum backing fields with "enum-type" constraint.
- Only output descriptions if explicitly provided; no fallback/inferred text.
- Add "not-mapped" constraint for [NotMapped]/[NotColumn] properties.
- Switch FruitBankHybrid.Shared.Tests.csproj to direct AyCode.Core project reference.
- Add both LinqToDB and DataAnnotations [Table] attributes to DTOs for ORM compatibility.
This commit is contained in:
Loretta 2026-01-13 08:25:28 +01:00
parent 3400cbc65a
commit 9312298032
3 changed files with 203 additions and 66 deletions

View File

@ -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\\*.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 -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:*)" "Bash(timeout 30 dotnet run:*)",
"Bash(dotnet exec vstest:*)"
] ]
} }
} }

View File

@ -17,15 +17,13 @@ public static partial class AcToonSerializer
var underlyingType = Nullable.GetUnderlyingType(type); var underlyingType = Nullable.GetUnderlyingType(type);
var isNullable = underlyingType != null || !type.IsValueType; var isNullable = underlyingType != null || !type.IsValueType;
// Nullable vs Required // Nullable/Required: NE írjuk ki automatikusan, mert redundáns (a típusból látszik)
if (isNullable) // Csak explicit [Required] attribute esetén írjuk ki (azt az ExtractDataAnnotationConstraints kezeli)
constraints.Add("nullable");
else // Numeric/Boolean: NE írjuk ki, mert redundáns (a típusból látszik)
constraints.Add("required"); // Range-eket megtartjuk, mert azok hasznos extra információk
var baseType = underlyingType ?? type; var baseType = underlyingType ?? type;
// Numeric type ranges
var typeCode = Type.GetTypeCode(baseType); var typeCode = Type.GetTypeCode(baseType);
switch (typeCode) switch (typeCode)
{ {
@ -41,37 +39,39 @@ public static partial class AcToonSerializer
case TypeCode.UInt16: case TypeCode.UInt16:
constraints.Add("range: 0-65535"); constraints.Add("range: 0-65535");
break; break;
case TypeCode.Int32:
constraints.Add("numeric");
break;
case TypeCode.UInt32: case TypeCode.UInt32:
constraints.Add("range: 0-4294967295"); constraints.Add("range: 0-4294967295");
break; break;
case TypeCode.Int64: // Int32, Int64, UInt64, Float, Double, Decimal, Boolean: nincs constraint (redundáns lenne)
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;
} }
// Enum values // Enum values: NE írjuk ki a constraint-ben, mert már a type metadata-ban benne van
if (baseType.IsEnum)
return constraints.Count > 0 ? string.Join(", ", constraints) : string.Empty;
}
/// <summary>
/// Detect if property is readonly (no setter or init-only).
/// </summary>
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 // Check for init-only (C# 9+ feature)
var more = Enum.GetNames(baseType).Length > 5 ? "..." : ""; var returnParameter = setMethod.ReturnParameter;
constraints.Add($"enum: {values}{more}"); var requiredModifiers = returnParameter.GetRequiredCustomModifiers();
if (requiredModifiers.Any(x => x.FullName == "System.Runtime.CompilerServices.IsExternalInit"))
{
return true;
}
} }
return string.Join(", ", constraints); return false;
} }
/// <summary> /// <summary>

View File

@ -2,6 +2,7 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using System.Reflection; using System.Reflection;
using static AyCode.Core.Helpers.JsonUtilities; using static AyCode.Core.Helpers.JsonUtilities;
@ -73,9 +74,17 @@ public static partial class AcToonSerializer
context.Write('='); 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("["); context.Write("[");
var first = true; var first = true;
foreach (var t in typesToDocument) foreach (var t in filteredTypes)
{ {
if (!first) context.Write(", "); if (!first) context.Write(", ");
context.Write($"\"{t.Name}\""); context.Write($"\"{t.Name}\"");
@ -96,7 +105,7 @@ public static partial class AcToonSerializer
context.WriteLine("@types {"); context.WriteLine("@types {");
context.CurrentIndentLevel++; context.CurrentIndentLevel++;
foreach (var t in typesToDocument) foreach (var t in filteredTypes)
{ {
WriteTypeDefinition(t, context); WriteTypeDefinition(t, context);
} }
@ -111,10 +120,18 @@ public static partial class AcToonSerializer
/// </summary> /// </summary>
private static void CollectTypes(Type type, HashSet<Type> types) private static void CollectTypes(Type type, HashSet<Type> types)
{ {
if (IsPrimitiveOrStringFast(type)) return;
var underlyingType = Nullable.GetUnderlyingType(type) ?? type; 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 if (!types.Add(underlyingType)) return; // Already processed
// Handle dictionaries FIRST (before generic IEnumerable check) // Handle dictionaries FIRST (before generic IEnumerable check)
@ -138,12 +155,69 @@ public static partial class AcToonSerializer
// Handle object properties (traverse type graph) // Handle object properties (traverse type graph)
var metadata = GetTypeMetadata(underlyingType); var metadata = GetTypeMetadata(underlyingType);
// Detektáljuk az enum backing field-eket (NotMapped enum property-k)
DetectAndCollectEnumBackingFields(underlyingType, types);
foreach (var prop in metadata.Properties) foreach (var prop in metadata.Properties)
{ {
CollectTypes(prop.PropertyType, types); CollectTypes(prop.PropertyType, types);
} }
} }
/// <summary>
/// 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.
/// </summary>
private static void DetectAndCollectEnumBackingFields(Type type, HashSet<Type> 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);
}
}
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary> /// <summary>
/// Write type definition with property descriptions. /// Write type definition with property descriptions.
/// </summary> /// </summary>
@ -162,11 +236,12 @@ public static partial class AcToonSerializer
var typeDescription = GetFinalTypeDescription(type, metadata); var typeDescription = GetFinalTypeDescription(type, metadata);
context.WriteIndentedLine($"{type.Name}: \"{typeDescription}\""); 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) // Type-level metadata (enhanced mode only)
if (context.Options.UseEnhancedMetadata) if (context.Options.UseEnhancedMetadata)
{ {
context.CurrentIndentLevel++;
// Table name // Table name
var tableName = DetectTableName(type, metadata.CustomDescription); var tableName = DetectTableName(type, metadata.CustomDescription);
if (!string.IsNullOrEmpty(tableName)) if (!string.IsNullOrEmpty(tableName))
@ -180,14 +255,15 @@ public static partial class AcToonSerializer
{ {
context.WriteIndentedLine($"purpose: \"{typePurpose}\""); context.WriteIndentedLine($"purpose: \"{typePurpose}\"");
} }
context.CurrentIndentLevel--;
} }
if (metadata.Properties.Length == 0) return; if (metadata.Properties.Length == 0)
{
context.CurrentIndentLevel--;
return;
}
// Property descriptions // Property descriptions (már indent szint 1-en vagyunk, property-k is ide kerülnek)
context.CurrentIndentLevel++;
foreach (var prop in metadata.Properties) foreach (var prop in metadata.Properties)
{ {
// Get final values with fallback chain + placeholder resolution // Get final values with fallback chain + placeholder resolution
@ -206,7 +282,11 @@ public static partial class AcToonSerializer
// Enhanced format with constraints and purpose // Enhanced format with constraints and purpose
context.WriteIndentedLine($"{prop.Name}: {typeHint}"); context.WriteIndentedLine($"{prop.Name}: {typeHint}");
context.CurrentIndentLevel++; 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)) if (!string.IsNullOrEmpty(purpose))
context.WriteIndentedLine($"purpose: \"{purpose}\""); context.WriteIndentedLine($"purpose: \"{purpose}\"");
if (!string.IsNullOrEmpty(constraints)) if (!string.IsNullOrEmpty(constraints))
@ -234,10 +314,22 @@ public static partial class AcToonSerializer
{ {
// Simple format // Simple format
var relationshipHint = GetRelationshipHint(relationshipMetadata); var relationshipHint = GetRelationshipHint(relationshipMetadata);
var fullDescription = string.IsNullOrEmpty(relationshipHint)
? propDescription // Only write description if there's explicit description OR relationship metadata
: $"{propDescription} ({relationshipHint})"; if (!string.IsNullOrEmpty(propDescription) || !string.IsNullOrEmpty(relationshipHint))
context.WriteIndentedLine($"{prop.Name}: {typeHint} \"{fullDescription}\""); {
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--; context.CurrentIndentLevel--;
@ -366,9 +458,10 @@ public static partial class AcToonSerializer
/// <summary> /// <summary>
/// Get final enum description with fallback chain. /// 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).
/// </summary> /// </summary>
private static string GetFinalEnumDescription(Type enumType, ToonDescriptionAttribute? customDescription) private static string? GetFinalEnumDescription(Type enumType, ToonDescriptionAttribute? customDescription)
{ {
// 1. ToonDescription.Description // 1. ToonDescription.Description
if (!string.IsNullOrWhiteSpace(customDescription?.Description)) if (!string.IsNullOrWhiteSpace(customDescription?.Description))
@ -383,8 +476,8 @@ public static partial class AcToonSerializer
return msDesc.Description; return msDesc.Description;
} }
// 3. Smart inference // 3. No fallback - avoid redundancy (values already listed in values: section)
return $"Enum type with values: {string.Join(", ", Enum.GetNames(enumType))}"; return null;
} }
/// <summary> /// <summary>
@ -439,9 +532,10 @@ public static partial class AcToonSerializer
if (propertyName.EndsWith("Count", StringComparison.OrdinalIgnoreCase) && IsIntegerType(baseType)) if (propertyName.EndsWith("Count", StringComparison.OrdinalIgnoreCase) && IsIntegerType(baseType))
return $"Count of {propertyName.Replace("Count", "")}"; return $"Count of {propertyName.Replace("Count", "")}";
// Collection types // Collection types (csak ha tényleg nem object az elem)
if (GetCollectionElementType(propertyType) != null) var elementType = GetCollectionElementType(propertyType);
return $"Collection of {GetCollectionElementType(propertyType)?.Name ?? "items"} for {declaringType.Name}"; if (elementType != null && elementType != typeof(object))
return $"Collection of {elementType.Name} for {declaringType.Name}";
// Dictionary types // Dictionary types
if (IsDictionaryType(propertyType, out _, out _)) if (IsDictionaryType(propertyType, out _, out _))
@ -522,9 +616,10 @@ public static partial class AcToonSerializer
/// <summary> /// <summary>
/// Get final property description with fallback chain and placeholder resolution. /// 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).
/// </summary> /// </summary>
private static string GetFinalPropertyDescription(ToonPropertyAccessor prop, Type declaringType) private static string? GetFinalPropertyDescription(ToonPropertyAccessor prop, Type declaringType)
{ {
// 1. ToonDescription.Description (if not empty) // 1. ToonDescription.Description (if not empty)
var customDesc = prop.CustomDescription?.Description; var customDesc = prop.CustomDescription?.Description;
@ -545,8 +640,8 @@ public static partial class AcToonSerializer
if (msDesc != null && !string.IsNullOrWhiteSpace(msDesc.Description)) if (msDesc != null && !string.IsNullOrWhiteSpace(msDesc.Description))
return msDesc.Description; return msDesc.Description;
// 3. Smart inference (fallback) // 3. No fallback - return null to avoid redundant output
return GetPropertyDescription(declaringType, prop.Name, prop.PropertyType); return null;
} }
/// <summary> /// <summary>
@ -579,6 +674,21 @@ public static partial class AcToonSerializer
{ {
var customConstraints = prop.CustomDescription?.Constraints; 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)) if (!string.IsNullOrWhiteSpace(customConstraints))
{ {
// Has [#...] placeholder? → Resolve and merge // Has [#...] placeholder? → Resolve and merge
@ -592,23 +702,49 @@ public static partial class AcToonSerializer
{ {
var typeConstraints = ExtractTypeConstraints(prop.PropertyType); var typeConstraints = ExtractTypeConstraints(prop.PropertyType);
var inferredConstraints = GetInferredConstraints(prop.PropertyType, prop.Name); var inferredConstraints = GetInferredConstraints(prop.PropertyType, prop.Name);
result = MergeConstraints(typeConstraints, null, inferredConstraints, resolved);
return MergeConstraints(typeConstraints, null, inferredConstraints, resolved); }
else
{
result = string.Empty;
} }
} }
else else
{ {
// No placeholder → custom wins (replace mode) // 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 // Add readonly constraint if needed
var msConstraints = ExtractDataAnnotationConstraints(prop.PropertyInfo); if (IsReadOnlyProperty(prop.PropertyInfo) && !result.Contains("readonly"))
var typeConstraints2 = ExtractTypeConstraints(prop.PropertyType); {
var inferredConstraints2 = GetInferredConstraints(prop.PropertyType, prop.Name); 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;
} }
/// <summary> /// <summary>