From d8a577024ab15186211e164bdfaf1a09d4c50838 Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 31 May 2026 14:52:40 +0200 Subject: [PATCH] Deduplicate property overrides; Shipping carrier nullable - Fix property enumeration to dedupe overridden/shadowed properties, ensuring only the most-derived is included (affects serialization and schema). - Update TOON_ISSUES.md: close duplicate property issue, document root cause and resolution. - Make CargoPartnerId nullable in Shipping and IShipping; clarify ToonDescription for carrier/truck/trailer fields. - Refine ShippingDocument ToonDescription for clarity. - Minor null-safety and style cleanup in GridShipping.razor. --- AyCode.Core/Serializers/TypeMetadataBase.cs | 16 ++++++++++++---- AyCode.Core/docs/TOON/TOON_ISSUES.md | 14 ++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs index 6d974e0..ef5d38c 100644 --- a/AyCode.Core/Serializers/TypeMetadataBase.cs +++ b/AyCode.Core/Serializers/TypeMetadataBase.cs @@ -363,14 +363,21 @@ public abstract class TypeMetadataBase [UnconditionalSuppressMessage("Trimming", "IL2080", Justification = "Same as IL2070 — currentType variable starts DAMs-annotated but loses annotation " + "through the BaseType reassignment in the loop.")] - private static List BuildPropertyList( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, - bool requiresWrite) + private static List BuildPropertyList([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, bool requiresWrite) { // Collect properties from inheritance hierarchy (derived -> base order) // Sorted alphabetically for deterministic property index ordering var allProperties = new List(); + // Dedup by name across inheritance levels. An overridden (or 'new'-shadowed) property is + // declared at EVERY level it appears in, so BindingFlags.DeclaredOnly returns it once per + // level — without this guard the derived AND base PropertyInfo would both be collected + // (duplicate wire field on the runtime path; duplicate @types entry in Toon). The derived->base + // walk means the most-derived declaration is seen first and wins. Mirrors the SGen path + // (AcBinarySourceGenerator.GetAllSerializablePropertySymbols) so the runtime and generated + // property orders stay bit-identical. + var seen = new HashSet(StringComparer.Ordinal); + for (var currentType = type; currentType != null && currentType != typeof(object); currentType = currentType.BaseType) { // Get properties declared at this level only @@ -379,7 +386,8 @@ public abstract class TypeMetadataBase .Where(p => p.CanRead && (!requiresWrite || p.CanWrite) && p.GetIndexParameters().Length == 0 && - !IsUnsupportedPropertyType(p.PropertyType)) + !IsUnsupportedPropertyType(p.PropertyType) && + seen.Add(p.Name)) .OrderBy(static p => p.Name, StringComparer.Ordinal); allProperties.AddRange(levelProperties); diff --git a/AyCode.Core/docs/TOON/TOON_ISSUES.md b/AyCode.Core/docs/TOON/TOON_ISSUES.md index 24d94c6..6245bff 100644 --- a/AyCode.Core/docs/TOON/TOON_ISSUES.md +++ b/AyCode.Core/docs/TOON/TOON_ISSUES.md @@ -85,7 +85,7 @@ None yet. ## ACCORE-TOON-I-P6V5: Property override duplicated on inheritance in `@types` -**Severity:** Minor (schema correctness; LLM may misinterpret) · **Status:** Open · **Area:** Property enumeration — likely `AcToonSerializer.ToonSerializeTypeMetadata.cs` or the shared base in `Serializers/` root. +**Severity:** Minor (schema correctness; LLM may misinterpret) · **Status:** Closed (2026-05-31) · **Area:** `TypeMetadataBase.BuildPropertyList` (shared property enumeration — Binary/JSON/Toon). ### Description When a derived class uses `override` on a property inherited from its base, both the derived and the base version appear in the `@types` schema — two entries for the same logical member, each with its own `business-logic` string. @@ -105,15 +105,17 @@ OrderItemPallet: Only affected when a class uses `override` on a property. Sibling non-overriding classes (`ShippingItemPallet`, `StockTakingItemPallet`) emit the property once as expected. -### Root cause (likely) -`Type.GetProperties(BindingFlags.Public | BindingFlags.Instance)` returns overridden properties twice — once per declaring type in the inheritance chain. The Toon property enumeration does not dedupe on `Name`, so both versions reach the writer. +### Root cause (confirmed) +`TypeMetadataBase.BuildPropertyList` walks the inheritance chain with `BindingFlags.DeclaredOnly` per level and `AddRange`s each level with **no name-dedup**. An `override`d (or `new`-shadowed) property is declared at every level, so it was collected once per level. (Not the flat `GetProperties(Public|Instance)` — that *would* dedupe; the explicit `DeclaredOnly` + `BaseType` walk is what duplicates.) + +Scope is wider than Toon: the shared list feeds **both** the runtime AcBinary path and Toon. The SGen path already deduped via its own `HashSet seen` (`AcBinarySourceGenerator.GetAllSerializablePropertySymbols`). `MeasuringStatus` surfaced only in Toon because Toon reads `ReadableProperties` while AcBinary uses `WritableProperties`, where the readonly `MeasuringStatus` is dropped by the `CanWrite` filter — but any **writable** override was duplicated on the runtime wire too, and diverged from the deduped SGen output on mixed runtime↔SGen paths. ### Fix options - **(a)** Filter in the metadata build step: group by `Name`, keep the most-derived override (lowest inheritance distance from the target type). -- **(b)** Walk the inheritance chain with `BindingFlags.DeclaredOnly` per level, collecting names into a set; skip if already seen. +- **(b)** Walk the inheritance chain with `BindingFlags.DeclaredOnly` per level, collecting names into a set; skip if already seen. ← chosen -### Known workaround -None — the schema is wrong as-is. Downstream consumers must tolerate the duplicate entry. +### Resolution (2026-05-31) +Option (b) in `TypeMetadataBase.BuildPropertyList`: a `HashSet seen` added as the final `.Where` predicate (`seen.Add(p.Name)`). The derived→base walk means the most-derived declaration is seen first and wins; later (base) levels skip the already-seen name. Mirrors the SGen `GetAllSerializablePropertySymbols`, so the runtime and generated property orders stay bit-identical (closes the latent runtime-writable↔SGen wire divergence). Zero behaviour change for non-override types (every name unique → `seen.Add` always true). ### Related TODO None yet.