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).
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user