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:
parent
3400cbc65a
commit
9312298032
|
|
@ -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:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue