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:
Loretta 2026-01-14 08:00:32 +01:00
parent 18b119c7a8
commit 0bb0b06af4
2 changed files with 238 additions and 0 deletions

View File

@ -0,0 +1,33 @@
namespace FruitBankHybrid.Shared.Tests.TestData;
// Demo entity-k a teszteléshez
public class TestOrder
{
public int Id { get; set; }
public int CustomerId { get; set; }
public TestCustomer? Customer { get; set; }
public List<TestOrderItem> OrderItems { get; set; } = new();
}
public class TestCustomer
{
public int Id { get; set; }
public string Name { get; set; } = "";
public List<TestOrder> Orders { get; set; } = new();
}
public class TestOrderItem
{
public int Id { get; set; }
public int OrderId { get; set; }
public TestOrder? Order { get; set; }
public int ProductId { get; set; }
public TestProduct? Product { get; set; }
}
public class TestProduct
{
public int Id { get; set; }
public string Name { get; set; } = "";
public List<TestOrderItem> OrderItems { get; set; } = new();
}

View File

@ -39,6 +39,7 @@ public sealed class ToonTests
[TestMethod]
public void OrderDtoToToon()
{
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
Console.WriteLine(toon);
@ -101,4 +102,208 @@ public sealed class ToonTests
}
}
}
[TestMethod]
public void ToonTypes_NavigationMetadata_ShouldBeComplete()
{
var toon = AcToonSerializer.SerializeTypeMetadata<Shipping>();
Console.WriteLine(toon);
Console.WriteLine("\n=== NAVIGATION METADATA ELLENŐRZÉS ===\n");
var lines = toon.Split('\n').Select(x => x.TrimEnd()).ToList();
// Ismerten egyirányú kapcsolatok - ezeknél nincs inverse property a másik oldalon
// Customer: NopCommerce domain entity, nincs benne Orders kollekció
// OrderNotes: OrderNote osztályban nincs Order navigation property
// ProductDto: nincs benne OrderItems kollekció
// GenericAttributes: polimorf kapcsolat ExpressionPredicate-tel, nincs inverse ÉS nincs egyértelmű FK
// FONTOS: Csak az inverse-property hiányát engedjük! Az other-key-nek léteznie kell!
var knownUnidirectionalNavigations = new HashSet<string>
{
"Customer",
"OrderNotes",
"ProductDto",
"GenericAttributes",
"ShippingDocumentFile",
"Pallet"
};
// GenericAttributes speciális eset - polimorf, nincs other-key sem
var knownPolymorphicNavigations = new HashSet<string>
{
"GenericAttributes"
};
// FK property-k NEM tartalmazhatnak foreign-key attribútumot
for (int i = 0; i < lines.Count; i++)
{
var line = lines[i].Trim();
if (line.EndsWith("Id: int") || line.EndsWith("Id: int?"))
{
int j = i + 1;
while (j < lines.Count)
{
if (lines[j].StartsWith(" ") && !lines[j].StartsWith(" "))
break;
if (lines[j].StartsWith(" "))
{
var metaLine = lines[j].Trim();
if (metaLine.StartsWith("foreign-key:"))
{
Assert.Fail($"FK property nem tartalmazhat foreign-key attribútumot: {line} -> {metaLine}");
}
}
j++;
}
}
}
Console.WriteLine("✓ FK property-k nem tartalmaznak foreign-key attribútumot\n");
// Számoljuk meg a hiányzó navigation metadatokat
var missingMetadata = new List<string>();
var skippedUnidirectional = new List<string>();
for (int i = 0; i < lines.Count; i++)
{
var line = lines[i].Trim();
// Navigation property-k keresése
if (line.Contains(": ") &&
!line.Contains(": int") && !line.Contains(": string") &&
!line.Contains(": DateTime") && !line.Contains(": decimal") &&
!line.Contains(": bool") && !line.Contains(": Guid") &&
!line.Contains(": double") && !line.Contains(": float") &&
!line.Contains("description:") && !line.Contains("purpose:") &&
!line.Contains("navigation:") && !line.Contains("foreign-key:") &&
!line.Contains("table-name:") && !line.Contains("constraints:") &&
!line.Contains("inverse-property:") && !line.Contains("other-key:") &&
!line.Contains("primary-key:") && !line.Contains("examples:") &&
!line.Contains("@meta") && !line.Contains("@types") &&
!line.Contains("version") && !line.Contains("format") && !line.Contains("source-code-language") &&
!line.Contains("underlying-type:") && !line.Contains("default-value:") && !line.Contains("values:"))
{
var propName = line.Split(':')[0].Trim();
if (string.IsNullOrEmpty(propName) || propName == "types") continue;
// Következő sorok metadatainak összegyűjtése
var metadata = new HashSet<string>();
int j = i + 1;
while (j < lines.Count && lines[j].StartsWith(" ") && lines[j].Trim().Contains(':'))
{
var metaLine = lines[j].Trim();
if (metaLine.StartsWith("navigation:")) metadata.Add("navigation");
if (metaLine.StartsWith("foreign-key:")) metadata.Add("foreign-key");
if (metaLine.StartsWith("inverse-property:")) metadata.Add("inverse-property");
if (metaLine.StartsWith("other-key:")) metadata.Add("other-key");
j++;
}
// Ha van navigation attribútum, ellenőrizzük a szükséges metadatokat
if (metadata.Contains("navigation"))
{
var navLine = lines.Skip(i + 1).FirstOrDefault(x => x.Trim().StartsWith("navigation:"));
if (navLine != null)
{
var isUnidirectional = knownUnidirectionalNavigations.Contains(propName);
var isPolymorphic = knownPolymorphicNavigations.Contains(propName);
if (navLine.Contains("many-to-one"))
{
if (!metadata.Contains("foreign-key"))
missingMetadata.Add($"{propName} (ManyToOne): hiányzik foreign-key");
if (!metadata.Contains("inverse-property"))
{
if (isUnidirectional)
skippedUnidirectional.Add($"{propName} (ManyToOne): egyirányú kapcsolat, nincs inverse");
else
missingMetadata.Add($"{propName} (ManyToOne): hiányzik inverse-property");
}
}
else if (navLine.Contains("one-to-many"))
{
// other-key: polimorf kapcsolatoknál nem kötelező
if (!metadata.Contains("other-key"))
{
if (isPolymorphic)
{
skippedUnidirectional.Add($"{propName} (OneToMany): polimorf kapcsolat, nincs other-key");
}
else
{
// DEBUG: részletes info
Console.WriteLine($"\n[DEBUG] {propName} (OneToMany) - other-key hiányzik!");
// Keressük meg a property típusát a Toon outputban
var propTypePart = line.Split(':').LastOrDefault()?.Trim() ?? "";
Console.WriteLine($" Property type: {propTypePart}");
// Ha List<X> formátum, keressük meg X-et
if (propTypePart.StartsWith("List<") && propTypePart.EndsWith(">"))
{
var elementTypeName = propTypePart.Substring(5, propTypePart.Length - 6);
Console.WriteLine($" Element type: {elementTypeName}");
// Keressük meg az element type definícióját
var elementTypeDefIndex = lines.FindIndex(l => l.Trim().StartsWith($"{elementTypeName}:"));
if (elementTypeDefIndex >= 0)
{
Console.WriteLine($" Element type definition found at line {elementTypeDefIndex}");
// Listázzuk ki az element type property-jeit amik "Id"-re végződnek
for (int k = elementTypeDefIndex + 1; k < lines.Count; k++)
{
var propLine = lines[k];
if (!propLine.StartsWith(" ")) break; // Új típus definíció
if (propLine.StartsWith(" ")) continue; // Metaadat, skip
var trimmed = propLine.Trim();
if (trimmed.EndsWith(": int") && trimmed.Contains("Id"))
{
Console.WriteLine($" FK candidate: {trimmed}");
}
}
}
}
missingMetadata.Add($"{propName} (OneToMany): hiányzik other-key");
}
}
if (!metadata.Contains("inverse-property"))
{
if (isUnidirectional)
skippedUnidirectional.Add($"{propName} (OneToMany): egyirányú kapcsolat, nincs inverse");
else
missingMetadata.Add($"{propName} (OneToMany): hiányzik inverse-property");
}
}
}
}
}
}
if (skippedUnidirectional.Count > 0)
{
Console.WriteLine("EGYIRÁNYÚ/POLIMORF KAPCSOLATOK (nem hiba):");
foreach (var skipped in skippedUnidirectional)
{
Console.WriteLine($" {skipped}");
}
Console.WriteLine();
}
if (missingMetadata.Count > 0)
{
Console.WriteLine("HIÁNYZÓ METAADATOK:");
foreach (var missing in missingMetadata)
{
Console.WriteLine($" - {missing}");
}
Assert.Fail($"Hiányzó navigation metaadatok: {missingMetadata.Count} db");
}
Console.WriteLine("✓ Minden navigation property tartalmazza a szükséges metadatokat");
}
}