# Configuration validation โ€” gaps & adoption backlog Divergence of each project from [`spec/SPEC.md`](spec/SPEC.md), and the ordered backlog to adopt the shared `ZB.MOM.WW.Configuration` library. The library is **BUILT @ 0.1.0** (27 tests) at [`../../ZB.MOM.WW.Configuration/`](../../ZB.MOM.WW.Configuration/) but **NOT YET ADOPTED** by any app โ€” so every item below is an *adoption* item, not a library-build item. This mirrors the Auth / UI-Theme / Health pattern: the shared library is built first; adoption is opt-in and tracked here, not forced. (Unlike the observability pass, there is **no in-pass sister-repo adoption** in this release.) 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` 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`, the `Count == 0 ? Success : Fail` tail, and in MxGateway the `AddIfBlank`/`AddIfNotPositive` helpers). OtOpcUa has **no validators at all**. โ†’ **Gap B1:** MxGateway: `GatewayOptionsValidator` โ†’ `OptionsValidatorBase`. โ†’ **Gap B2:** ScadaBridge: four `*OptionsValidator` โ†’ `OptionsValidatorBase`. โ†’ **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`) and ScadaBridge's four module extensions all spell out exactly what `AddValidatedOptions(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 - "`, 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`/`DraftSnapshot` is dormant, no `src/` caller) is config-content validation, not startup options validation, and stays per-project. See SPEC ยง0 and [`current-state/otopcua/CURRENT-STATE.md`](current-state/otopcua/CURRENT-STATE.md). - **`ConfigPreflight` message envelope pinned (SETTLED):** the library reproduces ScadaBridge's `StartupValidator` envelope byte-for-byte (`InvalidOperationException`, `"Configuration validation failed:\n - "`), so the migration is behaviour-preserving. Verified in `ConfigPreflightTests`. - **Single package, no ASP.NET Core dependency (SETTLED):** the library closes over only `Microsoft.Extensions.*` abstractions โ€” validators run in plain DI, no framework reference. See [`shared-contract/ZB.MOM.WW.Configuration.md`](shared-contract/ZB.MOM.WW.Configuration.md). ## Decisions still open - **Filesystem-path validity primitive:** MxGateway's `AddIfInvalidPath` (valid-path + `.exe` extension) is currently mapped to a custom `RequireThat`/`Add` rule. If a second app grows the same need, consider promoting a `Path`/`FilePath` primitive to `ValidationBuilder` โ€” 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 thus `AddValidatedOptions`) for them is a per-module call, out of scope for the initial adoption.