Configuration is now adopted across all three sister apps (local branches), so flip the status lines in CLAUDE.md, components/configuration/GAPS.md, and the lib README/CLAUDE.md from 'not adopted' to adopted (also corrects 27->42 tests). Adds the brainstorm design doc + bite-sized implementation plan (+tasks.json) under docs/plans/ that drove the adoption.
9.4 KiB
Configuration validation — gaps & adoption backlog
Divergence of each project from spec/SPEC.md, and the ordered backlog to adopt
the shared ZB.MOM.WW.Configuration library. The library is BUILT @ 0.1.0 (42 tests) at
../../ZB.MOM.WW.Configuration/ and was ADOPTED across all three
apps on 2026-06-01 — published to the Gitea feed, then consumed on each repo's local default branch
(merged, not yet pushed to remotes). The adoption items below are now largely closed: MxGateway +
ScadaBridge migrated to OptionsValidatorBase/AddValidatedOptions behaviour-preservingly (validator
messages byte-identical), ScadaBridge's StartupValidator → ConfigPreflight, and OtOpcUa gained
net-new Ldap/OpcUa validators (plus a follow-on pass: real Security:Ldap binding, ValidateOnStart
wired for ScadaBridge Cluster/HealthMonitoring, and assorted hardening).
Status legend: ⛔ gap · 🟡 partial · ✅ matches.
Divergence vs spec
§1 IValidateOptions base — failure accumulation (everyone hand-rolls it)
| OtOpcUa | MxAccessGateway | ScadaBridge | |
|---|---|---|---|
Uses OptionsValidatorBase |
⛔ n/a (no validators) | ⛔ hand-rolled | ⛔ hand-rolled ×4 |
Private List<string> accumulation |
n/a | 🟡 GatewayOptionsValidator |
🟡 four validators |
| Aggregates ALL failures | n/a | ✅ (yes, manually) | ✅ (yes, manually) |
MxGateway and ScadaBridge already accumulate all failures correctly — they just open-code the
plumbing (the List<string>, the Count == 0 ? Success : Fail tail, and in MxGateway the
AddIfBlank/AddIfNotPositive helpers). OtOpcUa has no validators at all.
→ Gap B1: MxGateway: GatewayOptionsValidator → OptionsValidatorBase<GatewayOptions>.
→ Gap B2: ScadaBridge: four *OptionsValidator → OptionsValidatorBase<T>.
→ Gap B3: OtOpcUa: optionally add OptionsValidatorBase subclasses for Ldap/OpcUa (no
existing validators to migrate — additive only).
§2 Rule primitives (re-implemented as private helpers)
| Primitive | OtOpcUa | MxAccessGateway | ScadaBridge |
|---|---|---|---|
| required-string | ⛔ none | 🟡 AddIfBlank |
🟡 inline IsNullOrWhiteSpace |
| port range | ⛔ none | 🟡 inline Port check |
🟡 inline (in StartupValidator) |
positive TimeSpan |
⛔ none | n/a | 🟡 inline <= TimeSpan.Zero ×7 |
| one-of-set | ⛔ none | 🟡 inline enum/string checks | 🟡 inline HashSet.Contains |
| min-count | ⛔ none | n/a | 🟡 inline Count < n |
The same five rules recur as private helpers / inline checks across both heavy consumers. The
shared ValidationBuilder primitives (Required, Port, HostPort, PositiveTimeSpan, OneOf,
MinCount) plus RequireThat/Add replace them with identical wording (the internal Checks
seam).
→ Gap P1: MxGateway/ScadaBridge: re-express inline checks/helpers as ValidationBuilder
primitives; keep app-specific rules (.exe path, heartbeat ordering, seed-node topology) as
RequireThat/Add.
§3 DI wiring — AddValidatedOptions (everyone open-codes the triple)
| OtOpcUa | MxAccessGateway | ScadaBridge | |
|---|---|---|---|
bind + register-validator + ValidateOnStart in one call |
⛔ bare .Bind(), no validate |
⛔ AddGatewayConfiguration open-codes it |
⛔ per-module AddXxx open-codes it ×4 |
MxGateway's AddGatewayConfiguration (AddOptions().BindConfiguration().ValidateOnStart() +
AddSingleton<IValidateOptions...>) and ScadaBridge's four module extensions all spell out exactly
what AddValidatedOptions<TOptions, TValidator>(config, sectionPath) collapses into one line.
OtOpcUa binds with bare .Bind() and never validates.
→ Gap W1: MxGateway: AddGatewayConfiguration → AddValidatedOptions.
→ Gap W2: ScadaBridge: four module AddXxx → AddValidatedOptions.
→ Gap W3: OtOpcUa: replace bare .Bind() (Program.cs:99, OtOpcUaServerHostedService.cs:63)
with AddValidatedOptions if validators are added (B3).
§4 Pre-host preflight — ConfigPreflight (only ScadaBridge has the concern)
| OtOpcUa | MxAccessGateway | ScadaBridge | |
|---|---|---|---|
| Pre-host raw-config validation | ⛔ none | ⛔ none (single host, no pre-Akka stage) | ✅ StartupValidator (open-coded) |
Message byte-compatible with ConfigPreflight |
n/a | n/a | ✅ confirmed |
ScadaBridge's StartupValidator is the reason ConfigPreflight exists. Its thrown
InvalidOperationException message is byte-identical to ConfigPreflight.ThrowIfInvalid()
("Configuration validation failed:\n - <field> <reason>", verified against
ConfigPreflight.cs:63–68 and StartupValidator.cs:81–83), so the swap is behaviour-preserving.
→ Gap F1: ScadaBridge: StartupValidator → ConfigPreflight (gated on the byte-compatibility
test). OtOpcUa/MxGateway have no pre-host stage — not applicable.
§5 OtOpcUa has no startup options validation at all (surprise)
OtOpcUa has zero IValidateOptions / ValidateOnStart usages in src/. Its Ldap and
OpcUa sections are bound and trusted; a bad value fails opaquely on first use (the exact failure
mode ScadaBridge's SecurityOptionsValidator was written to prevent). This is an absence, not a
drift — adoption here is additive, optional, and the lowest-stakes of the three.
→ Gap A1: OtOpcUa: add fail-fast validation for Ldap (required server/search-base) and any
OpcUa invariants via OptionsValidatorBase + AddValidatedOptions. Optional; low priority.
§6 Draft validation is out of scope (no gap)
OtOpcUa's draft/generation-content validation (the dormant C# DraftValidator / DraftSnapshot,
plus the live DB sp_ValidateDraft it complements — validating operator config content) is
not options/config validation and is explicitly out of the shared library's scope
(SPEC §0). It is not a gap and requires no change on adoption — listed here only to record
that it was considered and deliberately excluded.
Adoption backlog (ordered)
| # | Item | Projects | Priority | Effort | Risk | Notes |
|---|---|---|---|---|---|---|
| 1 | MxGateway: GatewayOptionsValidator → OptionsValidatorBase; helpers → primitives (Gaps B1, P1) |
MxGateway | P2 | M | Low | One validator (~360 LOC); messages preserved verbatim |
| 2 | MxGateway: AddGatewayConfiguration → AddValidatedOptions (Gap W1) |
MxGateway | P2 | S | Low | Bundles with #1; pass GatewayOptions.SectionName as sectionPath |
| 3 | ScadaBridge: four *OptionsValidator → OptionsValidatorBase; inline checks → primitives (Gaps B2, P1) |
ScadaBridge | P2 | M | Low | Cluster/Security/HealthMonitoring/AuditLog; messages preserved |
| 4 | ScadaBridge: four module AddXxx → AddValidatedOptions (Gap W2) |
ScadaBridge | P2 | S | Low | Bundles with #3; keep HealthMonitoring idempotency guard |
| 5 | ScadaBridge: StartupValidator → ConfigPreflight (Gap F1) |
ScadaBridge | P2 | S | Low | Gated on byte-compatibility test; Program.cs:39 call site unchanged |
| 6 | OtOpcUa: add Ldap/OpcUa validators via OptionsValidatorBase + AddValidatedOptions (Gaps A1, B3, W3) |
OtOpcUa | P3 | S | Low | Additive (no validators today); lowest stakes |
Sequencing: items #1–#2 (MxGateway) and #3–#5 (ScadaBridge) are independent and can land in
either order; each pair lands in its own sister repo once the 0.1.0 nupkg is referenced. Item #6
(OtOpcUa) is optional new work, deferrable indefinitely. No item is a breaking change — every
migration is a behaviour-preserving plumbing swap (the ConfigPreflight swap is the only one that
changes a thrown-message implementation, and it is byte-compatible by construction). There is no
ops-coordination risk (unlike the observability ms→s / Meter-rename items) because no
externally-observed contract (metric label, dashboard, wire format) changes.
Decisions settled (no longer open)
- Draft validation excluded (SETTLED): OtOpcUa's draft/generation-content validation
(DB-side
sp_ValidateDraft; the C#DraftValidator/DraftSnapshotis dormant, nosrc/caller) is config-content validation, not startup options validation, and stays per-project. See SPEC §0 andcurrent-state/otopcua/CURRENT-STATE.md. ConfigPreflightmessage envelope pinned (SETTLED): the library reproduces ScadaBridge'sStartupValidatorenvelope byte-for-byte (InvalidOperationException,"Configuration validation failed:\n - <field> <reason>"), so the migration is behaviour-preserving. Verified inConfigPreflightTests.- Single package, no ASP.NET Core dependency (SETTLED): the library closes over only
Microsoft.Extensions.*abstractions — validators run in plain DI, no framework reference. Seeshared-contract/ZB.MOM.WW.Configuration.md.
Decisions still open
- Filesystem-path validity primitive: MxGateway's
AddIfInvalidPath(valid-path +.exeextension) is currently mapped to a customRequireThat/Addrule. If a second app grows the same need, consider promoting aPath/FilePathprimitive toValidationBuilder— for now it stays app-specific. - No-validator ScadaBridge modules:
Communication,DataConnectionLayer,Transport,Notification*, etc. bind options without validation today. Whether to add validators (and thusAddValidatedOptions) for them is a per-module call, out of scope for the initial adoption.