Files
lmxopcua/code-reviews/Driver.OpcUaClient.Contracts/findings.md
T
Joseph Doherty 9244e506c9 review(Driver.OpcUaClient.Contracts): first review; fix TryResolve nullability + [Range] guards
First review at 7286d320. -001 (Medium): TryResolve session param -> ISession? matching the
null guard + out-contract doc. -003: [Range] on MaxDiscoveredNodes/MaxBrowseDepth, drop dead
[Display]. -002 (NamespaceMap pulls full SDK into a DTO project) Open. Surfaced cross-module:
the OpcUaClient.Browser serializer lacks JsonStringEnumConverter (enum-as-int bug).
2026-06-19 12:22:53 -04:00

6.2 KiB

Code Review — Driver.OpcUaClient.Contracts

Field Value
Module src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts
Reviewer Claude Code
Review date 2026-06-19
Commit reviewed a19b0f86
Status Reviewed
Open findings 1

Checklist coverage

# Category Result
1 Correctness & logic bugs Driver.OpcUaClient.Contracts-001
2 OtOpcUa conventions Driver.OpcUaClient.Contracts-002
3 Concurrency & thread safety No issues found
4 Error handling & resilience No issues found
5 Security No issues found
6 Performance & resource management No issues found
7 Design-document adherence Driver.OpcUaClient.Contracts-002
8 Code organization & conventions Driver.OpcUaClient.Contracts-003
9 Testing coverage No test project — no issues to record
10 Documentation & comments No issues found

Findings

Driver.OpcUaClient.Contracts-001

Field Value
Severity Medium
Category Correctness & logic bugs
Location src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/NamespaceMap.cs:118-134
Status Resolved

Description: TryResolve accepts currentSession as a non-nullable ISession parameter but contains a runtime null guard (if (currentSession is null || …) return false). The non-nullable annotation tells callers that null is invalid, yet the implementation silently returns false on null — masking the bug that prompted the guard. More practically: on the driver's reconnect path _namespaceMap is rebuilt under _probeLock, but TryResolve is called outside that lock (at the read/write call-site). A session handed to TryResolve concurrently with a session swap could be non-null at the call site but torn-down inside the call; the silent false return gives the caller no indication that resolution failed due to a racing session swap versus a genuinely unknown NodeId. The out NodeId nodeId parameter contract also does not document that callers must use NodeId.IsNull(nodeId) (OPC UA SDK idiom) rather than nodeId == null (C# null) to test the out-value — no current caller makes this mistake, but the distinction is non-obvious.

Recommendation: Change the parameter to ISession? currentSession (nullable) so the annotation matches reality and the compiler guides callers. Add XML-doc on the out nodeId parameter specifying NodeId.IsNull(nodeId) is the correct test.

Resolution: Resolved 2026-06-19, verified by build (no test project). Changed ISession currentSession to ISession? currentSession so the nullable annotation matches the runtime guard. Expanded XML doc on currentSession to explain the defensive-null contract and on nodeId to specify NodeId.IsNull(nodeId) as the correct test. Browser and runtime driver both build clean (0 errors, 0 warnings) with TreatWarningsAsErrors=true.


Driver.OpcUaClient.Contracts-002

Field Value
Severity Low
Category OtOpcUa conventions / Design-document adherence
Location src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts.csproj:9
Status Open

Description: The Contracts project carries a direct PackageReference to OPCFoundation.NetStandard.Opc.Ua.Client solely to support NamespaceMap. This makes the Contracts project not a lightweight DTO/enum assembly: every consumer (AdminUI serializers, future test helpers, config-only tooling) that references Contracts for OpcUaClientDriverOptions and its enums also transitively receives the full OPC UA client SDK. Both OpcUaClientDriver and OpcUaClientDriverBrowser already reference the SDK directly and could host NamespaceMap without any new dependency. Moving NamespaceMap to the runtime driver (or to a dedicated shared helper project referenced by both driver and browser) would restore the Contracts project to its intended SDK-free role.

Recommendation: Move NamespaceMap out of Contracts into ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient (or an internal shared helper). Remove the OPCFoundation.NetStandard.Opc.Ua.Client reference from the Contracts .csproj. NamespaceMap is not part of the public wire contract; it is an implementation detail of the driver's namespace-stability mechanism.

Resolution: (empty until closed — deferred: refactor affects driver and browser project references; no wire or public-contract change, but requires updating ProjectReference entries in two .csproj files and adjusting the Browser's using directive)


Driver.OpcUaClient.Contracts-003

Field Value
Severity Low
Category Code organization & conventions
Location src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/OpcUaClientDriverOptions.cs:131-138, :174-176
Status Resolved

Description: Two inconsistency gaps in the options class:

  1. [Range(1, 60)] is applied only to ProbeTimeoutSeconds. MaxDiscoveredNodes and MaxBrowseDepth have meaningful minimum bounds (zero or negative values produce silent misbehaviour: MaxDiscoveredNodes = 0 halts all discovery, MaxBrowseDepth = 0 skips all browsing). No [Range] guards exist for these knobs.

  2. The [Display(Name = …, Description = …, GroupName = "Diagnostics")] attribute on ProbeTimeoutSeconds is unused dead metadata: the AdminUI page binds the field directly via InputNumber @bind-Value and does not read [Display] attributes. No UI scaffold, reflection-based form generator, or API documentation tool in the codebase consumes it. Having a [Display] on one property of 18 properties creates a false impression that more of the class is attributed.

Recommendation: Add [Range(1, int.MaxValue)] to MaxDiscoveredNodes and MaxBrowseDepth. Remove the [Display] from ProbeTimeoutSeconds (retaining [Range(1, 60)] which is enforced by the AdminUI's DataAnnotationsValidator).

Resolution: Resolved 2026-06-19, verified by build (no test project). Added [Range(1, int.MaxValue)] to MaxDiscoveredNodes and MaxBrowseDepth. Removed the dead [Display(…)] attribute from ProbeTimeoutSeconds; [Range(1, 60)] retained. All three projects (Contracts, Driver, Browser) build clean with TreatWarningsAsErrors=true.