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\\*.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:*)"
]
}
}

View File

@ -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;
}
/// <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
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;
}
/// <summary>

View File

@ -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
/// </summary>
private static void CollectTypes(Type type, HashSet<Type> 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);
}
}
/// <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>
/// Write type definition with property descriptions.
/// </summary>
@ -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
/// <summary>
/// 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>
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;
}
/// <summary>
@ -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
/// <summary>
/// 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>
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;
}
/// <summary>
@ -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;
}
/// <summary>