Refactor Toon serializer: robust navigation/type metadata
- Introduce AcNavigationPropertyInfo for unified, cached relationship metadata per property (primary key, navigation type, FK, other key, inverse, target type) - Refactor relationship detection to use AcNavigationPropertyInfo, replacing ad-hoc logic and old RelationshipMetadata - Add AcSerializerCommon.GetCSharpTypeName for consistent, C#-style type name formatting (handles primitives, generics, nullables, enums, collections) - Use topological sort for @types output to ensure dependency order - Improve enum handling: avoid redundant constraints, use new type name formatter for underlying types - Output navigation, foreign-key, other-key, and inverse-property metadata consistently in meta section - Enhance convention-based detection for inverse properties and "other key", including unidirectional/polymorphic support - Add comprehensive test for navigation metadata completeness and demo test entities - Add "source-code-language: C#" to meta section - Misc: code cleanup, remove unused cache, improve property filtering
This commit is contained in:
parent
9312298032
commit
93d38d427f
|
|
@ -26,7 +26,8 @@
|
|||
"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(dotnet exec vstest:*)"
|
||||
"Bash(dotnet exec vstest:*)",
|
||||
"Bash(dotnet new:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1006,4 +1006,102 @@ public static class AcSerializerCommon
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Type Name Formatting
|
||||
|
||||
/// <summary>
|
||||
/// Gets the C# display name for a type (e.g., "int", "List<string>", "Dictionary<int, string>").
|
||||
/// Handles primitives, nullables, generics, enums, collections, and dictionaries.
|
||||
/// Thread-safe with circular reference protection.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to get the name for.</param>
|
||||
/// <param name="useShortNames">If true, uses C# keywords (int, string). If false, uses full names (Int32, String).</param>
|
||||
/// <returns>C# display name of the type.</returns>
|
||||
public static string GetCSharpTypeName(Type type, bool useShortNames = true)
|
||||
{
|
||||
// Handle nullable types
|
||||
var underlying = Nullable.GetUnderlyingType(type);
|
||||
if (underlying != null)
|
||||
{
|
||||
return GetCSharpTypeName(underlying, useShortNames) + "?";
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (type.IsArray)
|
||||
{
|
||||
var elementType = type.GetElementType()!;
|
||||
return GetCSharpTypeName(elementType, useShortNames) + "[]";
|
||||
}
|
||||
|
||||
// IMPORTANT: Check for enum BEFORE primitive type check!
|
||||
// Enum TypeCode returns the underlying type's code (e.g., Int32),
|
||||
// which would incorrectly return "int" instead of the enum name.
|
||||
if (type.IsEnum)
|
||||
{
|
||||
return type.Name;
|
||||
}
|
||||
|
||||
// Handle primitive types with C# keywords
|
||||
if (useShortNames)
|
||||
{
|
||||
var typeCode = Type.GetTypeCode(type);
|
||||
var primitiveName = typeCode switch
|
||||
{
|
||||
TypeCode.Boolean => "bool",
|
||||
TypeCode.Byte => "byte",
|
||||
TypeCode.SByte => "sbyte",
|
||||
TypeCode.Char => "char",
|
||||
TypeCode.Int16 => "short",
|
||||
TypeCode.UInt16 => "ushort",
|
||||
TypeCode.Int32 => "int",
|
||||
TypeCode.UInt32 => "uint",
|
||||
TypeCode.Int64 => "long",
|
||||
TypeCode.UInt64 => "ulong",
|
||||
TypeCode.Single => "float",
|
||||
TypeCode.Double => "double",
|
||||
TypeCode.Decimal => "decimal",
|
||||
TypeCode.String => "string",
|
||||
_ => null
|
||||
};
|
||||
if (primitiveName != null) return primitiveName;
|
||||
}
|
||||
|
||||
// Handle common BCL types
|
||||
if (type == typeof(object)) return "object";
|
||||
if (type == typeof(void)) return "void";
|
||||
|
||||
// Handle generic types
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var genericDef = type.GetGenericTypeDefinition();
|
||||
var args = type.GetGenericArguments();
|
||||
|
||||
// Special handling for common generic types
|
||||
if (genericDef == typeof(List<>))
|
||||
return $"List<{GetCSharpTypeName(args[0], useShortNames)}>";
|
||||
if (genericDef == typeof(Dictionary<,>))
|
||||
return $"Dictionary<{GetCSharpTypeName(args[0], useShortNames)}, {GetCSharpTypeName(args[1], useShortNames)}>";
|
||||
if (genericDef == typeof(IEnumerable<>))
|
||||
return $"IEnumerable<{GetCSharpTypeName(args[0], useShortNames)}>";
|
||||
if (genericDef == typeof(ICollection<>))
|
||||
return $"ICollection<{GetCSharpTypeName(args[0], useShortNames)}>";
|
||||
if (genericDef == typeof(IList<>))
|
||||
return $"IList<{GetCSharpTypeName(args[0], useShortNames)}>";
|
||||
|
||||
// Generic type with name mangling (e.g., SomeType`2)
|
||||
var baseName = type.Name;
|
||||
var backtickIndex = baseName.IndexOf('`');
|
||||
if (backtickIndex > 0)
|
||||
{
|
||||
baseName = baseName.Substring(0, backtickIndex);
|
||||
}
|
||||
var argNames = string.Join(", ", args.Select(a => GetCSharpTypeName(a, useShortNames)));
|
||||
return $"{baseName}<{argNames}>";
|
||||
}
|
||||
|
||||
// Simple type name (class, struct, enum, etc.)
|
||||
return type.Name;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
using System;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
||||
/// <summary>
|
||||
/// Cached navigation property metadata for efficient relationship information access.
|
||||
/// Contains all relationship details detected once during type metadata construction.
|
||||
/// </summary>
|
||||
public sealed class AcNavigationPropertyInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this property is a primary key.
|
||||
/// </summary>
|
||||
public bool IsPrimaryKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of navigation relationship (ManyToOne, OneToMany, etc.).
|
||||
/// Null if this is not a navigation property.
|
||||
/// </summary>
|
||||
public ToonRelationType? NavigationType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Foreign key property name on the current side.
|
||||
/// For ManyToOne/OneToOne: the FK property in this entity (e.g., "CustomerId").
|
||||
/// For OneToMany: null (FK is on the other side, see OtherKey).
|
||||
/// </summary>
|
||||
public string? ForeignKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Foreign key property name on the other side of the relationship.
|
||||
/// For OneToMany: the FK property in the related entity (e.g., "OrderId" in OrderItem).
|
||||
/// For ManyToOne: null (FK is on this side, see ForeignKey).
|
||||
/// This corresponds to Linq2Db's "OtherKey" concept.
|
||||
/// </summary>
|
||||
public string? OtherKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigation property name on the other side of the relationship.
|
||||
/// For OneToMany: the navigation property in the related entity pointing back (e.g., "Order" in OrderItem).
|
||||
/// For ManyToOne: the collection property in the related entity (e.g., "OrderItems" in Order).
|
||||
/// </summary>
|
||||
public string? InverseProperty { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Target entity type for navigation properties.
|
||||
/// For collections (OneToMany), this is the element type.
|
||||
/// For single references (ManyToOne), this is the property type.
|
||||
/// </summary>
|
||||
public Type? TargetType { get; }
|
||||
|
||||
public AcNavigationPropertyInfo(
|
||||
bool isPrimaryKey = false,
|
||||
ToonRelationType? navigationType = null,
|
||||
string? foreignKey = null,
|
||||
string? otherKey = null,
|
||||
string? inverseProperty = null,
|
||||
Type? targetType = null)
|
||||
{
|
||||
IsPrimaryKey = isPrimaryKey;
|
||||
NavigationType = navigationType;
|
||||
ForeignKey = foreignKey;
|
||||
OtherKey = otherKey;
|
||||
InverseProperty = inverseProperty;
|
||||
TargetType = targetType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Empty navigation info (not a navigation property, not a primary key).
|
||||
/// </summary>
|
||||
public static readonly AcNavigationPropertyInfo Empty = new();
|
||||
}
|
||||
|
|
@ -14,8 +14,8 @@ public static partial class AcToonSerializer
|
|||
{
|
||||
var constraints = new List<string>();
|
||||
|
||||
var underlyingType = Nullable.GetUnderlyingType(type);
|
||||
var isNullable = underlyingType != null || !type.IsValueType;
|
||||
var nullableUnderlying = Nullable.GetUnderlyingType(type);
|
||||
var isNullable = nullableUnderlying != null || !type.IsValueType;
|
||||
|
||||
// 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)
|
||||
|
|
@ -23,7 +23,17 @@ public static partial class AcToonSerializer
|
|||
// 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;
|
||||
var baseType = nullableUnderlying ?? type;
|
||||
|
||||
// Enum property: NE írjunk automatic constraint-et!
|
||||
// A típusból (pl. "TaxDisplayType: TaxDisplayType") már minden információ látszik.
|
||||
// Az underlying type már az enum definíciójában megtalálható a @types szekcióban.
|
||||
// Csak akkor kell constraint, ha explicit attribute-al van megjelölve.
|
||||
if (baseType.IsEnum)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var typeCode = Type.GetTypeCode(baseType);
|
||||
switch (typeCode)
|
||||
{
|
||||
|
|
@ -45,8 +55,6 @@ public static partial class AcToonSerializer
|
|||
// Int32, Int64, UInt64, Float, Double, Decimal, Boolean: nincs constraint (redundáns lenne)
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -326,30 +326,12 @@ public static partial class AcToonSerializer
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get type display name for a type (used in array hints).
|
||||
/// Get C# type display name for a type (used in array hints).
|
||||
/// Uses shared AcSerializerCommon.GetCSharpTypeName for consistency.
|
||||
/// </summary>
|
||||
private static string GetTypeDisplayName(Type type)
|
||||
{
|
||||
var typeCode = Type.GetTypeCode(type);
|
||||
return typeCode switch
|
||||
{
|
||||
TypeCode.Int32 => "int32",
|
||||
TypeCode.Int64 => "int64",
|
||||
TypeCode.Double => "float64",
|
||||
TypeCode.Single => "float32",
|
||||
TypeCode.Decimal => "decimal",
|
||||
TypeCode.Boolean => "bool",
|
||||
TypeCode.String => "string",
|
||||
TypeCode.DateTime => "datetime",
|
||||
TypeCode.Byte => "byte",
|
||||
TypeCode.Int16 => "int16",
|
||||
TypeCode.UInt16 => "uint16",
|
||||
TypeCode.UInt32 => "uint32",
|
||||
TypeCode.UInt64 => "uint64",
|
||||
TypeCode.SByte => "sbyte",
|
||||
TypeCode.Char => "char",
|
||||
_ => type.Name.ToLowerInvariant()
|
||||
};
|
||||
return AcSerializerCommon.GetCSharpTypeName(type, useShortNames: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ public static partial class AcToonSerializer
|
|||
context.CurrentIndentLevel++;
|
||||
context.WriteProperty("version", $"\"{FormatVersion}\"");
|
||||
context.WriteProperty("format", "\"toon\"");
|
||||
context.WriteProperty("source-code-language", "\"C#\"");
|
||||
|
||||
// Write type list
|
||||
context.WriteIndent();
|
||||
|
|
@ -82,9 +83,12 @@ public static partial class AcToonSerializer
|
|||
.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();
|
||||
|
||||
// DAG topological sort: dependencies first
|
||||
var sortedTypes = TopologicalSortTypes(filteredTypes);
|
||||
|
||||
context.Write("[");
|
||||
var first = true;
|
||||
foreach (var t in filteredTypes)
|
||||
foreach (var t in sortedTypes)
|
||||
{
|
||||
if (!first) context.Write(", ");
|
||||
context.Write($"\"{t.Name}\"");
|
||||
|
|
@ -101,11 +105,11 @@ public static partial class AcToonSerializer
|
|||
context.WriteLine();
|
||||
}
|
||||
|
||||
// @types section with descriptions
|
||||
// @types section with descriptions (same sorted order)
|
||||
context.WriteLine("@types {");
|
||||
context.CurrentIndentLevel++;
|
||||
|
||||
foreach (var t in filteredTypes)
|
||||
foreach (var t in sortedTypes)
|
||||
{
|
||||
WriteTypeDefinition(t, context);
|
||||
}
|
||||
|
|
@ -195,14 +199,13 @@ public static partial class AcToonSerializer
|
|||
|
||||
/// <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.
|
||||
/// Visszaadja az enum típust CSAK akkor, ha:
|
||||
/// 1. Van megfelelő nevű enum property (naming convention)
|
||||
/// 2. Az enum property NotMapped/NotColumn/JsonIgnore attribútummal van ellátva
|
||||
/// 3. A backing field típusa megegyezik az enum underlying type-jával (int/int?, byte/byte?, stb.)
|
||||
/// </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;
|
||||
|
|
@ -212,10 +215,36 @@ public static partial class AcToonSerializer
|
|||
|
||||
var enumProperty = allProperties.FirstOrDefault(p =>
|
||||
p.Name == enumPropertyName &&
|
||||
p.PropertyType.IsEnum);
|
||||
(p.PropertyType.IsEnum || (Nullable.GetUnderlyingType(p.PropertyType)?.IsEnum ?? false)));
|
||||
|
||||
// Ha van enum property, visszaadjuk a típusát (függetlenül a NotMapped attribútumtól)
|
||||
return enumProperty?.PropertyType;
|
||||
if (enumProperty == null) return null;
|
||||
|
||||
// Ellenőrizzük, hogy NotMapped/NotColumn/JsonIgnore van-e rajta
|
||||
var isNotMapped = enumProperty.GetCustomAttributes(inherit: true).Any(attr =>
|
||||
{
|
||||
var attrName = attr.GetType().Name;
|
||||
return attrName == "NotMappedAttribute" ||
|
||||
attrName == "NotColumnAttribute" ||
|
||||
attrName == "JsonIgnoreAttribute";
|
||||
});
|
||||
|
||||
// Csak akkor backing field, ha az enum property NotMapped!
|
||||
if (!isNotMapped) return null;
|
||||
|
||||
// Ellenőrizzük, hogy a backing field típusa megegyezik-e az enum underlying type-jával
|
||||
var enumType = Nullable.GetUnderlyingType(enumProperty.PropertyType) ?? enumProperty.PropertyType;
|
||||
var enumUnderlyingType = Enum.GetUnderlyingType(enumType);
|
||||
|
||||
// A backing field lehet nullable vagy non-nullable is
|
||||
var backingUnderlyingType = Nullable.GetUnderlyingType(backingFieldProperty.PropertyType)
|
||||
?? backingFieldProperty.PropertyType;
|
||||
|
||||
// Az underlying type-oknak meg kell egyezniük
|
||||
if (backingUnderlyingType != enumUnderlyingType)
|
||||
return null;
|
||||
|
||||
// Visszaadjuk az enum típust (nullable nélkül)
|
||||
return enumType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -272,8 +301,8 @@ public static partial class AcToonSerializer
|
|||
var constraints = GetFinalPropertyConstraints(prop, type);
|
||||
var examples = GetFinalPropertyExamples(prop);
|
||||
|
||||
// Detect relationship metadata
|
||||
var relationshipMetadata = DetectRelationshipMetadata(prop.PropertyInfo, prop.CustomDescription);
|
||||
// Use cached navigation metadata from property accessor
|
||||
var nav = prop.Navigation;
|
||||
|
||||
var typeHint = prop.TypeDisplayName;
|
||||
|
||||
|
|
@ -294,26 +323,31 @@ public static partial class AcToonSerializer
|
|||
if (!string.IsNullOrEmpty(examples))
|
||||
context.WriteIndentedLine($"examples: \"{examples}\"");
|
||||
|
||||
// Add relationship metadata
|
||||
if (relationshipMetadata.IsPrimaryKey)
|
||||
// Add relationship metadata from cached navigation info
|
||||
if (nav.IsPrimaryKey)
|
||||
context.WriteIndentedLine($"primary-key: true");
|
||||
if (!string.IsNullOrEmpty(relationshipMetadata.ForeignKeyNavigationProperty))
|
||||
context.WriteIndentedLine($"foreign-key: \"{relationshipMetadata.ForeignKeyNavigationProperty}\"");
|
||||
if (relationshipMetadata.NavigationType.HasValue)
|
||||
|
||||
// Only write foreign-key for navigation properties, not for FK properties themselves
|
||||
if (!string.IsNullOrEmpty(nav.ForeignKey) && nav.NavigationType.HasValue)
|
||||
context.WriteIndentedLine($"foreign-key: \"{nav.ForeignKey}\"");
|
||||
|
||||
if (!string.IsNullOrEmpty(nav.OtherKey))
|
||||
context.WriteIndentedLine($"other-key: \"{nav.OtherKey}\"");
|
||||
if (nav.NavigationType.HasValue)
|
||||
{
|
||||
var navType = relationshipMetadata.NavigationType.Value.ToString();
|
||||
var navType = nav.NavigationType.Value.ToString();
|
||||
var navTypeFormatted = string.Concat(navType.Select((c, i) => i > 0 && char.IsUpper(c) ? "-" + char.ToLower(c) : char.ToLower(c).ToString()));
|
||||
context.WriteIndentedLine($"navigation: \"{navTypeFormatted}\"");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(relationshipMetadata.InverseProperty))
|
||||
context.WriteIndentedLine($"inverse-property: \"{relationshipMetadata.InverseProperty}\"");
|
||||
if (!string.IsNullOrEmpty(nav.InverseProperty))
|
||||
context.WriteIndentedLine($"inverse-property: \"{nav.InverseProperty}\"");
|
||||
|
||||
context.CurrentIndentLevel--;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Simple format
|
||||
var relationshipHint = GetRelationshipHint(relationshipMetadata);
|
||||
var relationshipHint = GetRelationshipHint(nav);
|
||||
|
||||
// Only write description if there's explicit description OR relationship metadata
|
||||
if (!string.IsNullOrEmpty(propDescription) || !string.IsNullOrEmpty(relationshipHint))
|
||||
|
|
@ -371,18 +405,7 @@ public static partial class AcToonSerializer
|
|||
|
||||
// Underlying type
|
||||
var underlyingType = Enum.GetUnderlyingType(enumType);
|
||||
var underlyingTypeName = Type.GetTypeCode(underlyingType) switch
|
||||
{
|
||||
TypeCode.Byte => "byte",
|
||||
TypeCode.SByte => "sbyte",
|
||||
TypeCode.Int16 => "int16",
|
||||
TypeCode.UInt16 => "uint16",
|
||||
TypeCode.Int32 => "int32",
|
||||
TypeCode.UInt32 => "uint32",
|
||||
TypeCode.Int64 => "int64",
|
||||
TypeCode.UInt64 => "uint64",
|
||||
_ => underlyingType.Name.ToLower()
|
||||
};
|
||||
var underlyingTypeName = AcSerializerCommon.GetCSharpTypeName(underlyingType, useShortNames: true);
|
||||
context.WriteIndentedLine($"underlying-type: \"{underlyingTypeName}\"");
|
||||
|
||||
// Default value (first enum member)
|
||||
|
|
@ -871,19 +894,22 @@ public static partial class AcToonSerializer
|
|||
/// <summary>
|
||||
/// Get relationship hint string for simple format output.
|
||||
/// </summary>
|
||||
private static string GetRelationshipHint(RelationshipMetadata metadata)
|
||||
private static string GetRelationshipHint(AcNavigationPropertyInfo nav)
|
||||
{
|
||||
var hints = new List<string>();
|
||||
|
||||
if (metadata.IsPrimaryKey)
|
||||
if (nav.IsPrimaryKey)
|
||||
hints.Add("primary-key");
|
||||
|
||||
if (!string.IsNullOrEmpty(metadata.ForeignKeyNavigationProperty))
|
||||
hints.Add($"fk->{metadata.ForeignKeyNavigationProperty}");
|
||||
if (!string.IsNullOrEmpty(nav.ForeignKey))
|
||||
hints.Add($"fk->{nav.ForeignKey}");
|
||||
|
||||
if (metadata.NavigationType.HasValue)
|
||||
if (!string.IsNullOrEmpty(nav.OtherKey))
|
||||
hints.Add($"other-key:{nav.OtherKey}");
|
||||
|
||||
if (nav.NavigationType.HasValue)
|
||||
{
|
||||
var navType = metadata.NavigationType.Value switch
|
||||
var navType = nav.NavigationType.Value switch
|
||||
{
|
||||
ToonRelationType.ManyToOne => "many-to-one",
|
||||
ToonRelationType.OneToMany => "one-to-many",
|
||||
|
|
@ -895,9 +921,136 @@ public static partial class AcToonSerializer
|
|||
hints.Add(navType);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(metadata.InverseProperty))
|
||||
hints.Add($"inverse:{metadata.InverseProperty}");
|
||||
if (!string.IsNullOrEmpty(nav.InverseProperty))
|
||||
hints.Add($"inverse:{nav.InverseProperty}");
|
||||
|
||||
return hints.Count > 0 ? string.Join(", ", hints) : string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Topological sort for types: dependencies appear before dependents.
|
||||
/// Handles circular references by ignoring back-edges.
|
||||
/// </summary>
|
||||
private static List<Type> TopologicalSortTypes(List<Type> types)
|
||||
{
|
||||
// Build dependency graph
|
||||
var graph = new Dictionary<Type, HashSet<Type>>();
|
||||
var inDegree = new Dictionary<Type, int>();
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (!graph.ContainsKey(type))
|
||||
{
|
||||
graph[type] = new HashSet<Type>();
|
||||
inDegree[type] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// For each type, find its dependencies (types referenced in properties)
|
||||
foreach (var type in types)
|
||||
{
|
||||
var dependencies = GetTypeDependencies(type, types);
|
||||
foreach (var dependency in dependencies)
|
||||
{
|
||||
if (graph.ContainsKey(dependency) && dependency != type) // Ignore self-references
|
||||
{
|
||||
// dependency → type edge (dependency must come before type)
|
||||
if (graph[dependency].Add(type))
|
||||
{
|
||||
inDegree[type]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kahn's algorithm for topological sort
|
||||
var queue = new Queue<Type>();
|
||||
var result = new List<Type>();
|
||||
|
||||
// Start with types that have no dependencies (in-degree = 0)
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (inDegree[type] == 0)
|
||||
{
|
||||
queue.Enqueue(type);
|
||||
}
|
||||
}
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
result.Add(current);
|
||||
|
||||
// For each type that depends on current
|
||||
foreach (var dependent in graph[current])
|
||||
{
|
||||
inDegree[dependent]--;
|
||||
if (inDegree[dependent] == 0)
|
||||
{
|
||||
queue.Enqueue(dependent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are cycles, add remaining types in alphabetical order
|
||||
if (result.Count < types.Count)
|
||||
{
|
||||
var remaining = types.Where(t => !result.Contains(t)).OrderBy(t => t.Name);
|
||||
result.AddRange(remaining);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all type dependencies for a given type (types referenced in properties).
|
||||
/// </summary>
|
||||
private static HashSet<Type> GetTypeDependencies(Type type, List<Type> availableTypes)
|
||||
{
|
||||
var dependencies = new HashSet<Type>();
|
||||
|
||||
if (type.IsEnum)
|
||||
{
|
||||
// Enums have no dependencies
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
var metadata = GetTypeMetadata(type);
|
||||
|
||||
foreach (var prop in metadata.Properties)
|
||||
{
|
||||
var propType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
||||
|
||||
// Check if this property type is in our available types
|
||||
if (availableTypes.Contains(propType) && propType != type)
|
||||
{
|
||||
dependencies.Add(propType);
|
||||
}
|
||||
|
||||
// Check for collection element types
|
||||
if (typeof(IEnumerable).IsAssignableFrom(propType) && propType != typeof(string))
|
||||
{
|
||||
var elementType = GetCollectionElementType(propType);
|
||||
if (elementType != null && availableTypes.Contains(elementType) && elementType != type)
|
||||
{
|
||||
dependencies.Add(elementType);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for dictionary key/value types
|
||||
if (IsDictionaryType(propType, out var keyType, out var valueType))
|
||||
{
|
||||
if (keyType != null && availableTypes.Contains(keyType) && keyType != type)
|
||||
{
|
||||
dependencies.Add(keyType);
|
||||
}
|
||||
if (valueType != null && availableTypes.Contains(valueType) && valueType != type)
|
||||
{
|
||||
dependencies.Add(valueType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,89 +7,114 @@ namespace AyCode.Core.Serializers.Toons;
|
|||
public static partial class AcToonSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Relationship metadata for a property.
|
||||
/// </summary>
|
||||
private sealed class RelationshipMetadata
|
||||
{
|
||||
public bool IsPrimaryKey { get; set; }
|
||||
public string? ForeignKeyNavigationProperty { get; set; }
|
||||
public ToonRelationType? NavigationType { get; set; }
|
||||
public string? InverseProperty { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects relationship metadata for a property with fallback chain.
|
||||
/// Detects navigation property metadata for a property with fallback chain.
|
||||
/// Priority: ToonDescription -> EF Core/Linq2Db attributes -> Convention-based detection.
|
||||
/// Returns cached AcNavigationPropertyInfo with all relationship details.
|
||||
/// </summary>
|
||||
private static RelationshipMetadata DetectRelationshipMetadata(PropertyInfo property, ToonDescriptionAttribute? customDescription)
|
||||
internal static AcNavigationPropertyInfo DetectNavigationMetadata(PropertyInfo property, ToonDescriptionAttribute? customDescription)
|
||||
{
|
||||
var metadata = new RelationshipMetadata();
|
||||
|
||||
// 1. IsPrimaryKey - Priority: ToonDescription -> EF Core [Key] -> Linq2Db [PrimaryKey] -> Convention
|
||||
bool isPrimaryKey;
|
||||
if (customDescription?.IsPrimaryKey.HasValue == true)
|
||||
{
|
||||
metadata.IsPrimaryKey = customDescription.IsPrimaryKey.Value;
|
||||
isPrimaryKey = customDescription.IsPrimaryKey.Value;
|
||||
}
|
||||
else if (TryGetEFCoreKey(property) || TryGetLinq2DbPrimaryKey(property))
|
||||
{
|
||||
metadata.IsPrimaryKey = true;
|
||||
isPrimaryKey = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
metadata.IsPrimaryKey = IsConventionPrimaryKey(property);
|
||||
isPrimaryKey = IsConventionPrimaryKey(property);
|
||||
}
|
||||
|
||||
// 2. ForeignKey - Priority: ToonDescription -> EF Core [ForeignKey] -> Linq2Db [Association] -> Convention
|
||||
if (!string.IsNullOrEmpty(customDescription?.ForeignKey))
|
||||
{
|
||||
metadata.ForeignKeyNavigationProperty = customDescription.ForeignKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
var efCoreFk = TryGetEFCoreForeignKey(property);
|
||||
if (!string.IsNullOrEmpty(efCoreFk))
|
||||
{
|
||||
metadata.ForeignKeyNavigationProperty = efCoreFk;
|
||||
}
|
||||
else
|
||||
{
|
||||
metadata.ForeignKeyNavigationProperty = DetectConventionForeignKey(property);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Navigation - Priority: ToonDescription -> EF Core [InverseProperty] -> Linq2Db [Association] -> Convention
|
||||
// 2. Navigation type detection (needed early for FK detection logic)
|
||||
ToonRelationType? navigationType = null;
|
||||
if (customDescription?.Navigation.HasValue == true)
|
||||
{
|
||||
metadata.NavigationType = customDescription.Navigation.Value;
|
||||
navigationType = customDescription.Navigation.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
var linq2dbNav = TryGetLinq2DbNavigation(property);
|
||||
if (linq2dbNav.HasValue)
|
||||
{
|
||||
metadata.NavigationType = linq2dbNav.Value;
|
||||
navigationType = linq2dbNav.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
metadata.NavigationType = DetectConventionNavigation(property);
|
||||
navigationType = DetectConventionNavigation(property);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. InverseProperty - Priority: ToonDescription -> EF Core [InverseProperty]
|
||||
// 3. ForeignKey - Priority: ToonDescription -> EF Core [ForeignKey] -> Linq2Db [Association] -> Convention
|
||||
string? foreignKey = null;
|
||||
if (!string.IsNullOrEmpty(customDescription?.ForeignKey))
|
||||
{
|
||||
foreignKey = customDescription.ForeignKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
var efCoreFk = TryGetEFCoreForeignKey(property);
|
||||
if (!string.IsNullOrEmpty(efCoreFk))
|
||||
{
|
||||
foreignKey = efCoreFk;
|
||||
}
|
||||
else
|
||||
{
|
||||
// For FK properties (int/Guid/etc ending with Id)
|
||||
foreignKey = DetectConventionForeignKey(property);
|
||||
|
||||
// For navigation properties (ManyToOne/OneToOne), find the FK property name
|
||||
if (string.IsNullOrEmpty(foreignKey) &&
|
||||
(navigationType == ToonRelationType.ManyToOne || navigationType == ToonRelationType.OneToOne))
|
||||
{
|
||||
foreignKey = DetectForeignKeyForNavigationProperty(property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. InverseProperty - Priority: ToonDescription -> EF Core [InverseProperty] -> Convention
|
||||
string? inverseProperty = null;
|
||||
if (!string.IsNullOrEmpty(customDescription?.InverseProperty))
|
||||
{
|
||||
metadata.InverseProperty = customDescription.InverseProperty;
|
||||
inverseProperty = customDescription.InverseProperty;
|
||||
}
|
||||
else
|
||||
{
|
||||
var efCoreInverse = TryGetEFCoreInverseProperty(property);
|
||||
if (!string.IsNullOrEmpty(efCoreInverse))
|
||||
{
|
||||
metadata.InverseProperty = efCoreInverse;
|
||||
inverseProperty = efCoreInverse;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: convention-based detection
|
||||
inverseProperty = DetectConventionInverseProperty(property, navigationType);
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
// 5. OtherKey - Detect FK on the other side (for OneToMany)
|
||||
string? otherKey = DetectOtherKey(property, navigationType, inverseProperty);
|
||||
|
||||
// 6. TargetType - Extract target entity type
|
||||
Type? targetType = null;
|
||||
if (navigationType == ToonRelationType.OneToMany || navigationType == ToonRelationType.ManyToMany)
|
||||
{
|
||||
targetType = GetCollectionElementType(property.PropertyType);
|
||||
}
|
||||
else if (navigationType == ToonRelationType.ManyToOne || navigationType == ToonRelationType.OneToOne)
|
||||
{
|
||||
targetType = property.PropertyType;
|
||||
}
|
||||
|
||||
return new AcNavigationPropertyInfo(
|
||||
isPrimaryKey: isPrimaryKey,
|
||||
navigationType: navigationType,
|
||||
foreignKey: foreignKey,
|
||||
otherKey: otherKey,
|
||||
inverseProperty: inverseProperty,
|
||||
targetType: targetType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -432,4 +457,186 @@ public static partial class AcToonSerializer
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Convention-based Inverse Property Detection
|
||||
|
||||
/// <summary>
|
||||
/// Convention: Detect inverse property by looking at the related type's properties.
|
||||
/// For OneToMany: Look for ManyToOne property in element type that points back.
|
||||
/// For ManyToOne: Look for OneToMany collection in related type that points back.
|
||||
/// </summary>
|
||||
private static string? DetectConventionInverseProperty(PropertyInfo property, ToonRelationType? navigationType)
|
||||
{
|
||||
if (!navigationType.HasValue)
|
||||
return null;
|
||||
|
||||
var declaringType = property.DeclaringType;
|
||||
if (declaringType == null)
|
||||
return null;
|
||||
|
||||
if (navigationType == ToonRelationType.OneToMany)
|
||||
{
|
||||
// OneToMany: look for ManyToOne in element type that points back to declaring type
|
||||
var elementType = GetCollectionElementType(property.PropertyType);
|
||||
if (elementType == null)
|
||||
return null;
|
||||
|
||||
var elementProperties = TypeMetadataBase.GetSerializableProperties(elementType);
|
||||
for (int i = 0; i < elementProperties.Length; i++)
|
||||
{
|
||||
if (IsTypeMatch(elementProperties[i].PropertyType, declaringType))
|
||||
return elementProperties[i].Name;
|
||||
}
|
||||
}
|
||||
else if (navigationType == ToonRelationType.ManyToOne)
|
||||
{
|
||||
// ManyToOne: look for OneToMany collection in related type that points back
|
||||
var relatedProperties = TypeMetadataBase.GetSerializableProperties(property.PropertyType);
|
||||
for (int i = 0; i < relatedProperties.Length; i++)
|
||||
{
|
||||
var relatedProp = relatedProperties[i];
|
||||
if (typeof(IEnumerable).IsAssignableFrom(relatedProp.PropertyType) &&
|
||||
relatedProp.PropertyType != typeof(string) &&
|
||||
IsTypeMatch(GetCollectionElementType(relatedProp.PropertyType), declaringType))
|
||||
{
|
||||
return relatedProp.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if two types match (handles different Type instances from different assemblies).
|
||||
/// </summary>
|
||||
private static bool IsTypeMatch(Type? type1, Type? type2)
|
||||
{
|
||||
if (type1 == null || type2 == null) return false;
|
||||
return type1 == type2 || type2.IsAssignableFrom(type1) || type1.FullName == type2.FullName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect the foreign key property name on the other side of the relationship (OtherKey).
|
||||
/// For OneToMany: Order.OrderItems → OrderItem has "OrderId" FK.
|
||||
/// </summary>
|
||||
private static string? DetectOtherKey(PropertyInfo property, ToonRelationType? navigationType, string? inverseProperty)
|
||||
{
|
||||
if (navigationType != ToonRelationType.OneToMany)
|
||||
return null;
|
||||
|
||||
var elementType = GetCollectionElementType(property.PropertyType);
|
||||
if (elementType == null)
|
||||
return null;
|
||||
|
||||
// Use ReflectedType if available (the type we're reflecting on), otherwise DeclaringType
|
||||
var declaringType = property.ReflectedType ?? property.DeclaringType;
|
||||
if (declaringType == null)
|
||||
return null;
|
||||
|
||||
var elementProperties = elementType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
// Strategy 1: FK based on inverse property name
|
||||
if (!string.IsNullOrEmpty(inverseProperty))
|
||||
{
|
||||
var fk = TryFindForeignKeyPropertyDirect(elementProperties, inverseProperty);
|
||||
if (fk != null) return fk;
|
||||
}
|
||||
|
||||
// Strategy 2: FK based on declaring type name (with Dto suffix handling)
|
||||
var fkFromType = TryFindForeignKeyPropertyDirect(elementProperties, declaringType.Name);
|
||||
if (fkFromType != null) return fkFromType;
|
||||
|
||||
// Strategy 3: FK based on element type name prefix
|
||||
// OrderNote.OrderId → "Order" is prefix of "OrderNote", so look for "{prefix}Id"
|
||||
var elementTypeName = elementType.Name;
|
||||
for (int i = 0; i < elementProperties.Length; i++)
|
||||
{
|
||||
var prop = elementProperties[i];
|
||||
if (!prop.Name.EndsWith("Id", StringComparison.Ordinal) || !prop.PropertyType.IsValueType)
|
||||
continue;
|
||||
|
||||
// Extract the prefix: "OrderId" → "Order"
|
||||
var fkPrefix = prop.Name.Substring(0, prop.Name.Length - 2);
|
||||
|
||||
// Check if element type name starts with this prefix: "OrderNote".StartsWith("Order")
|
||||
if (elementTypeName.StartsWith(fkPrefix, StringComparison.Ordinal) && fkPrefix.Length > 0)
|
||||
{
|
||||
return prop.Name;
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 4: FK that has navigation property pointing back
|
||||
for (int i = 0; i < elementProperties.Length; i++)
|
||||
{
|
||||
var prop = elementProperties[i];
|
||||
if (!prop.Name.EndsWith("Id", StringComparison.Ordinal) || !prop.PropertyType.IsValueType)
|
||||
continue;
|
||||
|
||||
var navName = prop.Name.Substring(0, prop.Name.Length - 2);
|
||||
for (int j = 0; j < elementProperties.Length; j++)
|
||||
{
|
||||
if (elementProperties[j].Name == navName && IsTypeMatch(elementProperties[j].PropertyType, declaringType))
|
||||
return prop.Name;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect FK property name for a navigation property.
|
||||
/// Example: Customer navigation → CustomerId FK.
|
||||
/// Also tries target type name: ShippingDocumentFile (type: Files) → FilesId FK.
|
||||
/// </summary>
|
||||
private static string? DetectForeignKeyForNavigationProperty(PropertyInfo property)
|
||||
{
|
||||
var declaringType = property.DeclaringType;
|
||||
if (declaringType == null)
|
||||
return null;
|
||||
|
||||
var allProperties = declaringType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
// Strategy 1: FK based on navigation property name (e.g., Customer → CustomerId)
|
||||
var fk = TryFindForeignKeyPropertyDirect(allProperties, property.Name);
|
||||
if (fk != null)
|
||||
return fk;
|
||||
|
||||
// Strategy 2: FK based on target type name (e.g., ShippingDocumentFile: Files → FilesId)
|
||||
var targetTypeName = property.PropertyType.Name;
|
||||
fk = TryFindForeignKeyPropertyDirect(allProperties, targetTypeName);
|
||||
if (fk != null)
|
||||
return fk;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find FK property by name using direct PropertyInfo array (not filtered by GetSerializableProperties).
|
||||
/// </summary>
|
||||
private static string? TryFindForeignKeyPropertyDirect(PropertyInfo[] properties, string baseName)
|
||||
{
|
||||
// Try "{baseName}Id"
|
||||
var fkName = baseName + "Id";
|
||||
for (int i = 0; i < properties.Length; i++)
|
||||
{
|
||||
if (properties[i].Name == fkName && properties[i].PropertyType.IsValueType)
|
||||
return fkName;
|
||||
}
|
||||
|
||||
// Try without "Dto" suffix: "OrderDto" → "OrderId"
|
||||
if (baseName.Length > 3 && baseName.EndsWith("Dto", StringComparison.Ordinal))
|
||||
{
|
||||
fkName = baseName.Substring(0, baseName.Length - 3) + "Id";
|
||||
for (int i = 0; i < properties.Length; i++)
|
||||
{
|
||||
if (properties[i].Name == fkName && properties[i].PropertyType.IsValueType)
|
||||
return fkName;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -10,8 +10,9 @@ public static partial class AcToonSerializer
|
|||
{
|
||||
/// <summary>
|
||||
/// Cached metadata for a type including properties, type name, and descriptions.
|
||||
/// Uses TypeMetadataBase infrastructure for shared caching across all serializers.
|
||||
/// </summary>
|
||||
private sealed class ToonTypeMetadata
|
||||
private sealed class ToonTypeMetadata : TypeMetadataBase<ToonTypeMetadata>
|
||||
{
|
||||
public string TypeName { get; }
|
||||
public string ShortTypeName { get; }
|
||||
|
|
@ -21,7 +22,7 @@ public static partial class AcToonSerializer
|
|||
public Type? ElementType { get; }
|
||||
public ToonDescriptionAttribute? CustomDescription { get; }
|
||||
|
||||
public ToonTypeMetadata(Type type)
|
||||
public ToonTypeMetadata(Type type) : base(type)
|
||||
{
|
||||
TypeName = type.FullName ?? type.Name;
|
||||
ShortTypeName = type.Name;
|
||||
|
|
@ -29,7 +30,7 @@ public static partial class AcToonSerializer
|
|||
// Get custom attribute if present
|
||||
CustomDescription = type.GetCustomAttribute<ToonDescriptionAttribute>();
|
||||
|
||||
// Check if collection or dictionary (matches AcBinarySerializer logic)
|
||||
// Check if collection or dictionary (use shared JsonUtilities methods)
|
||||
IsDictionary = IsDictionaryType(type, out _, out _);
|
||||
if (!IsDictionary)
|
||||
{
|
||||
|
|
@ -41,14 +42,11 @@ public static partial class AcToonSerializer
|
|||
}
|
||||
}
|
||||
|
||||
// Build property accessors
|
||||
// Build property accessors using shared GetSerializableProperties from base
|
||||
if (!IsCollection && !IsDictionary && !IsPrimitiveOrStringFast(type))
|
||||
{
|
||||
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead &&
|
||||
p.GetIndexParameters().Length == 0 &&
|
||||
!HasJsonIgnoreAttribute(p))
|
||||
.Select(p => new ToonPropertyAccessor(p))
|
||||
var props = GetSerializableProperties(type, requiresRead: true, requiresWrite: false)
|
||||
.Select(p => new ToonPropertyAccessor(p, type))
|
||||
.ToArray();
|
||||
|
||||
Properties = props;
|
||||
|
|
@ -61,56 +59,31 @@ public static partial class AcToonSerializer
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Property accessor with compiled getter for performance.
|
||||
/// Property accessor with compiled getter and cached navigation metadata.
|
||||
/// Extends PropertyAccessorBase for shared functionality and adds Toon-specific metadata.
|
||||
/// </summary>
|
||||
private sealed class ToonPropertyAccessor
|
||||
private sealed class ToonPropertyAccessor : PropertyAccessorBase
|
||||
{
|
||||
public string Name { get; }
|
||||
public Type PropertyType { get; }
|
||||
public PropertyInfo PropertyInfo { get; }
|
||||
public TypeCode PropertyTypeCode { get; }
|
||||
public string TypeDisplayName { get; }
|
||||
public bool IsNullable { get; }
|
||||
|
||||
// Custom attribute metadata
|
||||
public ToonDescriptionAttribute? CustomDescription { get; }
|
||||
public AcNavigationPropertyInfo Navigation { get; }
|
||||
|
||||
private readonly Func<object, object?> _getter;
|
||||
|
||||
public ToonPropertyAccessor(PropertyInfo prop)
|
||||
public ToonPropertyAccessor(PropertyInfo prop, Type declaringType)
|
||||
: base(prop, declaringType)
|
||||
{
|
||||
Name = prop.Name;
|
||||
PropertyType = prop.PropertyType;
|
||||
PropertyInfo = prop;
|
||||
|
||||
var underlyingType = Nullable.GetUnderlyingType(PropertyType);
|
||||
IsNullable = underlyingType != null;
|
||||
|
||||
var actualType = underlyingType ?? PropertyType;
|
||||
PropertyTypeCode = Type.GetTypeCode(actualType);
|
||||
|
||||
// Build type display name for meta section
|
||||
TypeDisplayName = BuildTypeDisplayName(PropertyType);
|
||||
|
||||
// Get custom attribute if present
|
||||
CustomDescription = prop.GetCustomAttribute<ToonDescriptionAttribute>();
|
||||
|
||||
// Compile getter for fast access
|
||||
_getter = CreateCompiledGetter(prop.DeclaringType!, prop);
|
||||
// Detect and cache navigation metadata once
|
||||
Navigation = DetectNavigationMetadata(prop, CustomDescription);
|
||||
}
|
||||
|
||||
private static Func<object, object?> CreateCompiledGetter(Type declaringType, PropertyInfo prop)
|
||||
{
|
||||
var objParam = Expression.Parameter(typeof(object), "obj");
|
||||
var castExpr = Expression.Convert(objParam, declaringType);
|
||||
var propAccess = Expression.Property(castExpr, prop);
|
||||
var boxed = Expression.Convert(propAccess, typeof(object));
|
||||
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object? GetValue(object obj) => _getter(obj);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if value is default/null without boxing value types.
|
||||
/// </summary>
|
||||
|
|
@ -142,97 +115,13 @@ public static partial class AcToonSerializer
|
|||
return false;
|
||||
}
|
||||
|
||||
// Thread-local visited set to prevent circular reference stack overflow
|
||||
[ThreadStatic]
|
||||
private static HashSet<Type>? t_visitedTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Build human-readable type name for meta section.
|
||||
/// Uses thread-local visited set to prevent stack overflow on circular references.
|
||||
/// Build human-readable C# type name for meta section.
|
||||
/// Uses shared AcSerializerCommon.GetCSharpTypeName for consistency.
|
||||
/// </summary>
|
||||
private static string BuildTypeDisplayName(Type type)
|
||||
{
|
||||
var underlying = Nullable.GetUnderlyingType(type);
|
||||
var isNullable = underlying != null;
|
||||
var actualType = underlying ?? type;
|
||||
|
||||
var baseName = Type.GetTypeCode(actualType) switch
|
||||
{
|
||||
TypeCode.Int32 => "int32",
|
||||
TypeCode.Int64 => "int64",
|
||||
TypeCode.Double => "float64",
|
||||
TypeCode.Decimal => "decimal",
|
||||
TypeCode.Single => "float32",
|
||||
TypeCode.Boolean => "bool",
|
||||
TypeCode.String => "string",
|
||||
TypeCode.DateTime => "datetime",
|
||||
TypeCode.Byte => "byte",
|
||||
TypeCode.Int16 => "int16",
|
||||
TypeCode.UInt16 => "uint16",
|
||||
TypeCode.UInt32 => "uint32",
|
||||
TypeCode.UInt64 => "uint64",
|
||||
TypeCode.SByte => "sbyte",
|
||||
TypeCode.Char => "char",
|
||||
_ => GetComplexTypeName(actualType)
|
||||
};
|
||||
|
||||
return isNullable ? baseName + "?" : baseName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get complex type name WITH circular reference protection using thread-local visited set.
|
||||
/// </summary>
|
||||
private static string GetComplexTypeName(Type type)
|
||||
{
|
||||
// Initialize thread-local visited set if needed
|
||||
t_visitedTypes ??= new HashSet<Type>();
|
||||
|
||||
// Check for circular reference - if already visiting, return simple name
|
||||
if (!t_visitedTypes.Add(type))
|
||||
{
|
||||
return type.Name; // Break circular reference
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (ReferenceEquals(type, GuidType)) return "guid";
|
||||
if (ReferenceEquals(type, DateTimeOffsetType)) return "datetimeoffset";
|
||||
if (ReferenceEquals(type, TimeSpanType)) return "timespan";
|
||||
if (type.IsEnum) return "enum";
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
return AcSerializerCommon.GetCSharpTypeName(type, useShortNames: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
|
@ -18,8 +17,6 @@ namespace AyCode.Core.Serializers.Toons;
|
|||
/// </summary>
|
||||
public static partial class AcToonSerializer
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, ToonTypeMetadata> TypeMetadataCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Format version for Toon serialization.
|
||||
/// Incremented when breaking changes are made to format.
|
||||
|
|
@ -323,9 +320,13 @@ public static partial class AcToonSerializer
|
|||
|
||||
#region Type Metadata
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates ToonTypeMetadata using TypeMetadataBase infrastructure.
|
||||
/// This uses the shared GlobalMetadataCache and ThreadLocal cache for optimal performance.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ToonTypeMetadata GetTypeMetadata(Type type)
|
||||
=> TypeMetadataCache.GetOrAdd(type, static t => new ToonTypeMetadata(t));
|
||||
=> ToonTypeMetadata.GetOrCreateMetadata(type, static t => new ToonTypeMetadata(t));
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue