You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
MTConnect.NET-Common's Organizers static class hand-maintains a list of System substitution-group members used by Device.AddComponent() to decide whether to auto-wrap a component under a <Systems> organiser. The list has drifted: it includes components that are not (any longer) System substitution-group members per the current XSD, and omits several System subtypes that ARE substitution-group members per the v2.5 + v2.6 + v2.7 XSDs — most notably Heating. As a result, device.AddComponent(new ProtectiveComponent()) is auto-wrapped under <Systems> (because Protective is in the list) while device.AddComponent(new HeatingComponent()) is left as a direct child of Device (because Heating is NOT in the list). Two components that are equivalent under the XSD substitution-group model are placed at different nesting levels.
Environment
MTConnect.NET-Common at v6.9.0 (official Docker image trakhound/mtconnect.net-agent:6.9.0, published 2025-10-16 — binary-equivalent to v6.9.0.2). The bug is library-level, not tied to any particular host application.
Reproduction wrapper used for the live test: a publicly-available third-party MT.NET wrapper image — any MT.NET client that calls Device.AddComponent() programmatically (rather than loading the Device tree from XML) surfaces the bug identically.
Broker (for the live repro): eclipse-mosquitto:2.0.22.
Verified — 2026-04-20 — source inspection + live reproduction
Live reproduction
Tested via a third-party MT.NET wrapper image (publicly available on Docker Hub) that embeds this library and calls Device.AddComponent() for each XPath path part when building a Device tree from YAML-declared clauses like Device[...]/Heating[...]/...). With a minimal configuration declaring both Heating[name=MainController] and Protective[name=HiLimitController] as peer children of the same Device, the resulting Probe JSON (captured from the MQTT MTConnect/Probe/<uuid> topic) is:
Both components were declared at the same level in the wrapper's configuration. The library auto-wrapped Protective but left Heating at Device level — matching the per-subtype branching the source code analysis predicts. Evidence file: probe-third-party-wrapper.json.
privatestaticreadonlyIEnumerable<string>_systems=newList<string>{ControllerComponent.TypeId,CoolantComponent.TypeId,DielectricComponent.TypeId,ElectricComponent.TypeId,EnclosureComponent.TypeId,EndEffectorComponent.TypeId,FeederComponent.TypeId,HydraulicComponent.TypeId,// ← not a System subtype per v2.5/v2.7 XSDLubricationComponent.TypeId,PneumaticComponent.TypeId,// ← not a System subtype per v2.5/v2.7 XSDProcessPowerComponent.TypeId,ProtectiveComponent.TypeId,WorkEnvelopeComponent.TypeId};
The actual System subtypes (v2.5 XSD — the library's own advertised ceiling)
Per grep "substitutionGroup='System'" MTConnectDevices_2.5.xsd:
Subtype
In _systems?
MT.NET component class exists?
Actuator
Missing
✓ ActuatorComponent.g.cs
Controller
Present
✓
Coolant
Present
✓
Dielectric
Present
✓
Electric
Present
✓
Enclosure
Present
✓
EndEffector
Present
✓
Feeder
Present
✓
Heating
Missing
✓ HeatingComponent.g.cs
Lubrication
Present
✓
ProcessPower
Present
✓
Protective
Present
✓
Vacuum
Missing
✓ VacuumComponent.g.cs
Cooling
Missing
✓ CoolingComponent.g.cs
Pressure
Missing
✓ PressureComponent.g.cs
WorkEnvelope
Present
✓
AirHandler
Missing
✓ AirHandlerComponent.g.cs
v2.7 adds ToolHolder and PinTool (also missing from _systems).
Phantoms in the list (not substitutionGroup='System' in v2.5 or v2.7 XSD):
Hydraulic — no element <xs:element name='Hydraulic' ... substitutionGroup='System'> in current XSDs. Likely an artefact from an older version.
publicvoidAddComponent(IComponentcomponent){
...var organizerType =Organizers.GetOrganizerType(component.Type);if(organizerType!=null&&organizerType!=ControllersComponent.TypeId){if(!components.Any(o =>o.Type==organizerType)){varorganizer=Component.Create(organizerType);if(organizer!=null){organizer.Parent=this;organizer.Id=Component.CreateContainerId(organizer,ComponentIdFormat);organizer.AddComponent(component);// ← Protective gets wrapped herecomponents.Add(organizer);}}else{/* add to existing organizer */}}else{components.Add(component);// ← Heating falls here (type not in list)}
...}
GetOrganizerType(component.Type) returns SystemsComponent.TypeId only if component.Type is in the _systems list. For Protective it returns SystemsComponent.TypeId; for Heating it returns null. The two components therefore land at different tree levels.
Consequence for operators
The MT.NET library ships every System subtype as a C# class (auto-generated .g.cs files under libraries/MTConnect.NET-Common/Devices/Components/). A consumer / wrapper that builds a Device programmatically via AddComponent() — for example:
…ends up with the two components at different depths in the Probe tree: Heating at Device/Components/Heating, Protective at Device/Components/Systems/Components/Protective. No XSD rule justifies the split; both are identical substitution-group members of System under CommonComponent.
Why the XML-load path escapes this
When a Device is loaded from an XML file, the parser preserves the element tree as-written (tested live 2026-04-20 with both forms of the test device XML — see tech-writeup-substitution-group-placement.md). The bug surfaces only on the programmaticAddComponent() path.
Authority
MTConnectDevices_{2.5,2.6,2.7}.xsd — System substitution-group members enumerated via <xs:element name='X' type='XType' substitutionGroup='System'>. Every member is interchangeable under the XSD's substitution-group semantics; asymmetric treatment by a producer breaks that interchangeability guarantee.
Cross-implementation comparison (both XSD-valid forms, both load paths)
Per the XSD, System substitution-group members may appear in two equally-valid forms:
Form A — direct placement under Device/Components: e.g. <Device><Components><Heating/><Protective/></Components></Device>.
Form B — nested under a <Systems> organiser: e.g. <Device><Components><Systems><Components><Heating/><Protective/></Components></Systems></Components></Device>.
The XSD does not prefer one form over the other and it requires uniform treatment of every substitution-group member.
XML-load path — both agents preserve the author's form verbatim
Tested live 2026-04-20 with identical Devices.xml files against MTConnect.NET v6.9.0 and cppagent v2.7.0.7. Both agents preserve Form A input as Form A output, and Form B input as Form B output. Neither auto-wraps during XML load. Good — the bug is not in the XML parser.
Device.AddComponent() consults the stale Organizers._systems list and wraps Protective-type components under a <Systems> organiser while leaving Heating-type components as direct children of Device (see Root cause / The asymmetric list sections above for the exhaustive subtype table). cppagent does not expose an equivalent programmatic API path, so the defect is MT.NET-specific.
Three-way verification summary
Agent
Path
Asymmetric nesting?
Why
MT.NET direct (XML-load)
Devices.xml → Probe
No
XML-load preserves author layout
cppagent direct (XML-load)
Devices.xml → Probe
No
Same
MT.NET programmatic wrapper
AddComponent() per-path → Probe
Yes
Organizers._systems list is incomplete — Heating missing, Protective present
Fix lives entirely in Organizers.cs (list regeneration from XSD) + Device.AddComponent() (decide whether auto-wrap should happen at all — see Suggested fix below).
Suggested fix
Regenerate Organizers._systems from the XSD or from the same source used for the generated *Component.g.cs class files. The hand-maintained list has drifted; if the class files are auto-generated, the organiser mapping should be too.
Remove phantoms (Hydraulic, Pneumatic) unless they are re-introduced in a future XSD.
Add a unit test that iterates every *Component class whose XSD element has substitutionGroup='System' and asserts Organizers.GetOrganizerType(typeId) == SystemsComponent.TypeId.
Consider whether Device.AddComponent() should auto-wrap at all. The XSD permits both forms; respecting the caller's placement (never auto-wrap) is the minimum-surprise alternative. If auto-wrap stays, it must apply uniformly to every substitution-group member.
Apply the same audit to every _auxiliaries, _axes, _interfaces, _materials, _parts, _processes, _resources, _structures list in Organizers.cs.
Impact
Any MTConnect.NET-based wrapper that constructs Device trees programmatically — e.g. from XPath clauses, from a custom DSL, from configuration — inherits the asymmetry silently. See one such wrapper's XPath-to-Device builder seen in the wild for an example that had to add a "deep search" compensation step with an explicit comment "agent may auto-nest components (e.g., under Systems)".
Consumers that walk the Probe tree by substitution-group type end up with per-subtype lookup paths instead of a uniform walk.
The XSD substitution-group contract (members are interchangeable) is violated at the producer side.
Stability across MTConnect versions
Heating has been a substitutionGroup='System' element since v1.5 (verified against MTConnectDevices_1.5.xsd onwards); Actuator, Vacuum, Cooling, Pressure, AirHandler similarly stable across every v2.x XSD. The asymmetric-nesting defect has therefore existed against every MTConnect version the library advertises support for (v2.0 floor through v2.5 ceiling), not just against v2.6 / v2.7.
v2.7 introduces ToolHolder and PinTool as additional System subtypes — also absent from Organizers._systems. The uplift (companion issue Add support for MTConnect Standard v2.6 and v2.7 #133) inherits this defect unless the list is regenerated.
The phantoms (Hydraulic, Pneumatic) appear to be legacy entries from an older MTConnect version where these elements were directly substitution-group members of System; neither appears in v2.5 or later XSDs.
Fix is single-location and applies cleanly across the entire v2.x range.
Related issues
Add support for MTConnect Standard v2.6 and v2.7 #133 — Add MTConnect v2.6 + v2.7 support — the uplift will introduce more System subtypes; any generation-driven approach to Organizers._systems should be rolled in.
Summary
MTConnect.NET-Common'sOrganizersstatic class hand-maintains a list ofSystemsubstitution-group members used byDevice.AddComponent()to decide whether to auto-wrap a component under a<Systems>organiser. The list has drifted: it includes components that are not (any longer)Systemsubstitution-group members per the current XSD, and omits several System subtypes that ARE substitution-group members per the v2.5 + v2.6 + v2.7 XSDs — most notablyHeating. As a result,device.AddComponent(new ProtectiveComponent())is auto-wrapped under<Systems>(becauseProtectiveis in the list) whiledevice.AddComponent(new HeatingComponent())is left as a direct child of Device (becauseHeatingis NOT in the list). Two components that are equivalent under the XSD substitution-group model are placed at different nesting levels.Environment
MTConnect.NET-Commonat v6.9.0 (official Docker imagetrakhound/mtconnect.net-agent:6.9.0, published 2025-10-16 — binary-equivalent to v6.9.0.2). The bug is library-level, not tied to any particular host application.Device.AddComponent()programmatically (rather than loading the Device tree from XML) surfaces the bug identically.eclipse-mosquitto:2.0.22.libraries/MTConnect.NET-Common/Devices/Organizers.cs+libraries/MTConnect.NET-Common/Devices/Device.cs+libraries/MTConnect.NET-Common/Devices/Components/*Component.g.cs.Verified — 2026-04-20 — source inspection + live reproduction
Live reproduction
Tested via a third-party MT.NET wrapper image (publicly available on Docker Hub) that embeds this library and calls
Device.AddComponent()for each XPath path part when building a Device tree from YAML-declared clauses likeDevice[...]/Heating[...]/...). With a minimal configuration declaring bothHeating[name=MainController]andProtective[name=HiLimitController]as peer children of the same Device, the resulting Probe JSON (captured from the MQTTMTConnect/Probe/<uuid>topic) is:{ "Device": { "Components": { "Heating": [ { "id": "…", "name": "MainController", "DataItems": { … } } ], "Systems": [ /* ← auto-created organiser */ { "id": "…_systems", "name": "systems", "Components": { "Protective": [ { "id": "…", "name": "HiLimitController", "DataItems": { … } } ] } } ] } } }Both components were declared at the same level in the wrapper's configuration. The library auto-wrapped
Protectivebut leftHeatingat Device level — matching the per-subtype branching the source code analysis predicts. Evidence file:probe-third-party-wrapper.json.Source inspection
The asymmetric list
libraries/MTConnect.NET-Common/Devices/Organizers.cslines 85-99:The actual System subtypes (v2.5 XSD — the library's own advertised ceiling)
Per
grep "substitutionGroup='System'" MTConnectDevices_2.5.xsd:_systems?ActuatorComponent.g.csHeatingComponent.g.csVacuumComponent.g.csCoolingComponent.g.csPressureComponent.g.csAirHandlerComponent.g.csv2.7 adds
ToolHolderandPinTool(also missing from_systems).Phantoms in the list (not
substitutionGroup='System'in v2.5 or v2.7 XSD):Hydraulic— no element<xs:element name='Hydraulic' ... substitutionGroup='System'>in current XSDs. Likely an artefact from an older version.Pneumatic— same.The buggy code path
libraries/MTConnect.NET-Common/Devices/Device.cslines 317-360:GetOrganizerType(component.Type)returnsSystemsComponent.TypeIdonly ifcomponent.Typeis in the_systemslist. ForProtectiveit returnsSystemsComponent.TypeId; forHeatingit returnsnull. The two components therefore land at different tree levels.Consequence for operators
The MT.NET library ships every
Systemsubtype as a C# class (auto-generated.g.csfiles underlibraries/MTConnect.NET-Common/Devices/Components/). A consumer / wrapper that builds a Device programmatically viaAddComponent()— for example:…ends up with the two components at different depths in the Probe tree: Heating at
Device/Components/Heating, Protective atDevice/Components/Systems/Components/Protective. No XSD rule justifies the split; both are identical substitution-group members ofSystemunderCommonComponent.Why the XML-load path escapes this
When a Device is loaded from an XML file, the parser preserves the element tree as-written (tested live 2026-04-20 with both forms of the test device XML — see
tech-writeup-substitution-group-placement.md). The bug surfaces only on the programmaticAddComponent()path.Authority
MTConnectDevices_{2.5,2.6,2.7}.xsd—Systemsubstitution-group members enumerated via<xs:element name='X' type='XType' substitutionGroup='System'>. Every member is interchangeable under the XSD's substitution-group semantics; asymmetric treatment by a producer breaks that interchangeability guarantee.Cross-implementation comparison (both XSD-valid forms, both load paths)
Per the XSD,
Systemsubstitution-group members may appear in two equally-valid forms:Device/Components: e.g.<Device><Components><Heating/><Protective/></Components></Device>.<Systems>organiser: e.g.<Device><Components><Systems><Components><Heating/><Protective/></Components></Systems></Components></Device>.The XSD does not prefer one form over the other and it requires uniform treatment of every substitution-group member.
XML-load path — both agents preserve the author's form verbatim
Tested live 2026-04-20 with identical
Devices.xmlfiles against MTConnect.NET v6.9.0 and cppagent v2.7.0.7. Both agents preserve Form A input as Form A output, and Form B input as Form B output. Neither auto-wraps during XML load. Good — the bug is not in the XML parser.Programmatic path — MTConnect.NET auto-wraps asymmetrically
Device.AddComponent()consults the staleOrganizers._systemslist and wrapsProtective-type components under a<Systems>organiser while leavingHeating-type components as direct children of Device (seeRoot cause/The asymmetric listsections above for the exhaustive subtype table). cppagent does not expose an equivalent programmatic API path, so the defect is MT.NET-specific.Three-way verification summary
Devices.xml→ ProbeDevices.xml→ ProbeAddComponent()per-path → ProbeOrganizers._systemslist is incomplete —Heatingmissing,ProtectivepresentFix lives entirely in
Organizers.cs(list regeneration from XSD) +Device.AddComponent()(decide whether auto-wrap should happen at all — seeSuggested fixbelow).Suggested fix
Organizers._systemsfrom the XSD or from the same source used for the generated*Component.g.csclass files. The hand-maintained list has drifted; if the class files are auto-generated, the organiser mapping should be too.Hydraulic,Pneumatic) unless they are re-introduced in a future XSD.*Componentclass whose XSD element hassubstitutionGroup='System'and assertsOrganizers.GetOrganizerType(typeId) == SystemsComponent.TypeId.Device.AddComponent()should auto-wrap at all. The XSD permits both forms; respecting the caller's placement (never auto-wrap) is the minimum-surprise alternative. If auto-wrap stays, it must apply uniformly to every substitution-group member._auxiliaries,_axes,_interfaces,_materials,_parts,_processes,_resources,_structureslist inOrganizers.cs.Impact
Stability across MTConnect versions
Heatinghas been asubstitutionGroup='System'element since v1.5 (verified againstMTConnectDevices_1.5.xsdonwards);Actuator,Vacuum,Cooling,Pressure,AirHandlersimilarly stable across every v2.x XSD. The asymmetric-nesting defect has therefore existed against every MTConnect version the library advertises support for (v2.0 floor through v2.5 ceiling), not just against v2.6 / v2.7.ToolHolderandPinToolas additional System subtypes — also absent fromOrganizers._systems. The uplift (companion issue Add support for MTConnect Standard v2.6 and v2.7 #133) inherits this defect unless the list is regenerated.Hydraulic,Pneumatic) appear to be legacy entries from an older MTConnect version where these elements were directly substitution-group members ofSystem; neither appears in v2.5 or later XSDs.Related issues
Systemsubtypes; any generation-driven approach toOrganizers._systemsshould be rolled in.References
libraries/MTConnect.NET-Common/Devices/Organizers.cslibraries/MTConnect.NET-Common/Devices/Device.cslines 317-365libraries/MTConnect.NET-Common/Devices/Components/(auto-generated class files — full set includes every current System subtype)MTConnectDevices_2.7.xsdMTConnectDevices_2.5.xsdMTConnectSysMLModel.xml(substitution-group relationships).