Skip to content

fix(json-cppagent): omit empty name attribute from Probe DataItems#140

Draft
ottobolyos wants to merge 24 commits intoTrakHound:masterfrom
ottobolyos:fix/issue-138
Draft

fix(json-cppagent): omit empty name attribute from Probe DataItems#140
ottobolyos wants to merge 24 commits intoTrakHound:masterfrom
ottobolyos:fix/issue-138

Conversation

@ottobolyos
Copy link
Copy Markdown

@ottobolyos ottobolyos commented Apr 25, 2026

Summary

Fixes #138 — JSON-cppagent Probe emitted "name": "" for nested probe elements whose caller did not set Name. The empty-string emission breaks strict cppagent JSON v2 consumers; the optional name attribute should be omitted entirely when unset.

The PR closes the gap on every nested probe DTO that carries an optional Name slot: JsonDataItem, JsonComponent, and JsonComposition — not just the original JsonDataItem surface.

Changes

  • Guard the Name copy in JsonDataItem(IDataItem) of MTConnect.NET-JSON-cppagent/Devices/JsonDataItem.cs with string.IsNullOrEmpty, mirroring the already-correct behavior of MTConnect.NET-XML/Devices/XmlDataItem.cs.
  • Apply the same guard to MTConnect.NET-JSON/Devices/JsonDataItem.cs so the base JSON formatter matches the cppagent variant.
  • Apply the same guard to JsonComponent(IComponent) and JsonComposition(IComposition) — the dime-team's downstream symbol probe surfaced these as still emitting "" even after the JsonDataItem fix landed.
  • Pin the contract at the metadata level: every optional Name property now carries [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] alongside its [JsonPropertyName("name")]. This makes the contract correct under raw JsonSerializer.Serialize(...) (without the agent's JsonFunctions.DefaultOptions), so a downstream consumer reflecting on the property metadata sees the omit-when-null contract directly.
  • Classify DataItem relationships via a pattern-matching switch in the cppagent JsonDataItem ctor so DataItem and Specification relationships fan out to the correct collection.
  • Use a C# 7.3-compatible null check on the relationship classifier so the file compiles on the legacy TFMs (netstandard2.0 / net48).
  • Bind the embedded HTTP server in the integration tests' ClientAgentCommunication suite to loopback to keep the suite stable on multi-NIC hosts.

Tests

NUnit regression coverage for:

  • Null / empty / explicit Name cases on each of JsonDataItem, JsonComponent, JsonComposition for both JSON formatters.
  • The cppagent relationship classifier contract.
  • The metadata-level [JsonIgnore(WhenWritingNull)] attribute presence on each Name property.
  • Raw JsonSerializer.Serialize (not via the agent's options) omits the name key when the source is null.
  • The probe-document empty-name omission end-to-end.

Backward compatibility

Consumers reading the optional name slot through System.Text.Json typed deserialisation see null instead of "" when the source was unset — a strictly more correct shape per the XSD's use="optional" semantic and the cppagent reference behaviour. Producers previously emitting "" continue to work unchanged because the guards apply only at the model→wire boundary; the public POCO setters still accept any string.

@ottobolyos ottobolyos force-pushed the fix/issue-138 branch 4 times, most recently from dce987f to 10e37c5 Compare April 28, 2026 18:10
The fixture previously called `device.AddComponent(heating)`, which
delegates to `Organizers.GetOrganizerType(component.Type)` and, when
the type is a `System` substitution-group member, auto-wraps the
component under a `Systems` organizer (so the wire path becomes
`Components.Systems[0].Components.Heating[0]` rather than
`Components.Heating[0]`).

Whether `Heating` is auto-wrapped depends on whether
`HeatingComponent.TypeId` is listed in `Organizers._systems` — a list
that evolves with the SysML model. The fixture pins the wire-shape
`name`-omission contract on a `DataItem`; that contract is independent
of where the Heating component lives in the organizer tree. Attach
the component directly to `device.Components` to keep the fixture's
JSON path (`Components.Heating[0]`) deterministic regardless of
`Organizers._systems` membership.
The docs/testing/issue-138/ subtree carried phase-by-phase campaign writeups that
referenced internal tooling (CONVENTIONS rule-book, internal section
numbers, extra-files.user/ paths, internal tracker terminology). Those
writeups belong in the campaign's gitignored planning area, not in
the maintainer-facing public docs tree.
Switches JsonDataItemSourceGuardTests off the inline repo-root walker
and onto the shared MTConnect.NET_JSON_cppagent_Tests.TestHelpers.RepoRootLocator
helper landed on feat/issue-133. Future source-grep guards reuse the
helper rather than re-implementing the walk.
Adds regression fixture asserting that JsonDataItem(IDataItem) routes
each IAbstractDataItemRelationship into the matching container bucket on
JsonRelationshipContainer (DataItemRelationships,
SpecificationRelationships) and aggregates duplicates within the same
bucket. Lets the F-P-H6 perf optimization swap the IsAssignableFrom
chain for switch pattern matching without regressing the routing.
Replaces the four-branch typeof(IRelationship).IsAssignableFrom(relationship.GetType())
chain in the JsonDataItem(IDataItem) constructor with a switch-on-type
pattern match in both libraries/MTConnect.NET-JSON-cppagent and
libraries/MTConnect.NET-JSON. Each iteration now does a single is-check
+ cast rather than four reflection probes per element. Behavior is
preserved per JsonDataItemRelationshipCategorizationTests
(11/11 in the cppagent suite).
Adds RED file-content fixture asserting
ClientAgentCommunicationTests.cs constructs HttpServerConfiguration
with Server = "127.0.0.1" so the embedded HTTP server binds to
loopback only. Drives the F-S-L3 hardening that prevents an in-process
integration run from accidentally exposing the test agent on a
non-loopback interface of the dev machine.
Sets HttpServerConfiguration.Server = "127.0.0.1" alongside the existing
Port assignment in the ClientAgentCommunicationTests fixture so the
embedded HTTP server binds to loopback only. Closes the F-S-L3 risk that
an in-process integration run accidentally exposes the test agent on a
non-loopback interface of the dev machine. Greens the F-S-L3 source-grep
regression pin.
The pattern-matching switch in commit 56cb98a used `??=` (null-coalescing
assignment) which is a C# 8.0 feature. MTConnect.NET-JSON multi-targets
net47 / net472 / netstandard2.0 in Release where the default LangVersion
is 7.3, so the Release pack failed with CS8370.

Replace `relationships.X ??= new List<JsonRelationship>()` with the
explicit `if (relationships.X == null) relationships.X = new List<...>()`
on all four cases. Behaviour is identical.

Surfaced via `dotnet pack -c Release` from the integration branch.
Three test files carried internal audit-finding codes in comments
explaining loopback-binding rationale and a relationship-classifier
perf optimisation. Rewrite each comment to make the same point as a
self-contained sentence and drop the codes.
Same omit-when-null fix as the JsonDataItem path (already in this PR)
applied to the sibling probe DTOs. The v2.7 Devices XSD declares
Component/@name and Composition/@name use="optional"; cppagent's
reference printer omits the attribute when the model-side value is
null. JsonComponent and JsonComposition were emitting "name": "" on
operator-authored Devices.xml entries that never set a Name.

Tests pin the wire shape for null / empty / explicit Name sources
across Component and Composition, matching the existing sibling
JsonDataItemEmptyNameOmissionTests pattern.

Closes the remaining surface of TrakHound#138 (the original PR scope covered
JsonDataItem only; Component and Composition were left exposed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The constructor-level guard `if (!string.IsNullOrEmpty(name)) Name = name`
relies on `JsonFunctions.DefaultOptions.DefaultIgnoreCondition =
WhenWritingDefault` for the runtime-correct wire shape. External consumers
that probe the property metadata directly (dime-connector's symbol probe
is the immediate trigger) — or hand the DTO to a fresh JsonSerializer with
no project options — see only `[JsonPropertyName("name")]` and not the
ignore-when-null pin, so they treat the property as always-emit and flag
the DTO as non-compliant with the spec's `use="optional"` attribute.

Pin the contract on the type itself with
`[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]` on:
- JsonDataItem.Name
- JsonComposition.Name
- JsonComponent.Name

The property type stays `string` (non-nullable). Adding `string?` would
propagate through every consumer; the attribute is enough to satisfy the
metadata-level pin.

New TDD pins assert (a) the attribute is present with the correct
condition via reflection, and (b) raw `JsonSerializer.Serialize` with no
project-options omits the `name` key when null, on all three DTOs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JSON-CPPAGENT-MQTT Probe emits "name": "" for DataItems whose Name is set to empty string (XML formatter correctly omits)

1 participant