# Configuration validation (config binding + startup validation) Normalized component for **startup configuration validation** across the three sister projects. **Goal: path to shared code** — converge the apps onto one `IValidateOptions` failure-accumulation base, a shared set of rule primitives, a single bind+validate+`ValidateOnStart` DI helper, and a pre-host raw-config aggregator, extracted as the `ZB.MOM.WW.Configuration` library (single package), while each app keeps its own options classes and domain rules. - The one target: [`spec/SPEC.md`](spec/SPEC.md) - The shared library (paper API): [`shared-contract/ZB.MOM.WW.Configuration.md`](shared-contract/ZB.MOM.WW.Configuration.md) - Divergences + adoption backlog: [`GAPS.md`](GAPS.md) - Current state, per project: [`current-state/`](current-state/) ## Why config validation is a normalization candidate All three apps fail-fast on bad configuration at startup — and all three hand-roll the same plumbing to do it: - **OtOpcUa** has **no startup options validation at all** — `Ldap`/`OpcUa` are bound with bare `.Bind()` and trusted; a bad value fails opaquely on first use. (Its `DraftValidator` is runtime *config-content* validation, a different concern, out of scope.) - **MxAccessGateway** has one large `GatewayOptionsValidator` (~360 LOC, nine sub-validators) with a private `List` accumulator and `AddIfBlank`/`AddIfNotPositive`/`AddIfInvalidPath` helpers, wired through a bespoke `AddGatewayConfiguration` extension. - **ScadaBridge** is the heaviest: **four** per-module `*OptionsValidator` (Cluster / Security / HealthMonitoring / AuditLog), each open-coding the same accumulation, **plus** a raw-config pre-Akka `StartupValidator`. The common core — accumulate-all-failures `IValidateOptions`, reusable rule primitives, `AddValidatedOptions`, and a `ConfigPreflight` that generalizes `StartupValidator` — is genuinely shareable; the **options classes and domain rules stay per-project**. The unifying detail: `ConfigPreflight.ThrowIfInvalid()` reproduces ScadaBridge's `StartupValidator` thrown message **byte-for-byte**, so the heaviest migration is behaviour-preserving. ## Status by project | Project | Options validators today | Pre-host preflight | Failure accumulation | Adoption status | |---|---|---|---|---| | **OtOpcUa** | ⛔ **none** (bare `.Bind()`; `DraftValidator` is out-of-scope runtime validation) | ⛔ none | n/a | Not started (additive, optional) | | **MxAccessGateway** | 🟡 `GatewayOptionsValidator` (hand-rolled `IValidateOptions`) | ⛔ none | 🟡 manual `List` | Not started (follow-on) | | **ScadaBridge** | 🟡 four `*OptionsValidator` (hand-rolled) | ✅ `StartupValidator` (raw config, pre-Akka) | 🟡 manual `List` ×4 | Not started (follow-on; heaviest) | See each project's [`current-state//CURRENT-STATE.md`](current-state/) for the code-verified detail and its adoption plan. ## Normalized vs. left per-project **Normalized (the shared target):** - `OptionsValidatorBase` — abstract `IValidateOptions`; override `protected void Validate(ValidationBuilder, TOptions)`; the base aggregates **all** failures and returns `Success` only when clean. - `ValidationBuilder` rule primitives — `Required`, `Port`, `HostPort`, `PositiveTimeSpan`, `OneOf`, `MinCount`, plus `RequireThat`/`Add` for custom and cross-field rules; consistent `" "` wording via the internal `Checks` seam. - `AddValidatedOptions(IConfiguration, sectionPath)` — bind + register validator + `ValidateOnStart` in one DI call; returns `OptionsBuilder`. - `ConfigPreflight` — fluent pre-host raw-`IConfiguration` aggregator (`For`/`Require`/`RequireValue`/ `RequirePort`/`When`/`ThrowIfInvalid`); generalizes `StartupValidator`, with a byte-compatible thrown message. - The error-handling contract: accumulate ALL failures; two surfacing paths (`OptionsValidationException` at host start via `ValidateOnStart`, vs `ConfigPreflight.ThrowIfInvalid()`'s `InvalidOperationException`); `" "` messages. **Left per-project (not forced together):** - Each app's options classes (`GatewayOptions`, `ClusterOptions`, `SecurityOptions`, `HealthMonitoringOptions`, `AuditLogOptions`, `NodeOptions`, …) and all of their domain rules — worker `.exe` paths, split-brain strategy, Akka heartbeat/threshold ordering, audit retention bounds, gRPC-port-vs-remoting-port topology, etc. - OtOpcUa's draft/generation-content validation (DB-side `sp_ValidateDraft`; the C# `DraftValidator` / `DraftSnapshot` is dormant, no `src/` caller) — config-content validation, **out of scope** entirely. ## Package structure `ZB.MOM.WW.Configuration` ships as a **single package, one DLL** — no third-party packages, no ASP.NET Core framework reference: | Package | Contents | Consumers | |---|---|---| | `ZB.MOM.WW.Configuration` | `OptionsValidatorBase`, `ValidationBuilder`, `ServiceCollectionExtensions.AddValidatedOptions`, `ConfigPreflight`, internal `Checks` | All three (ScadaBridge heaviest) | Dependency closure: `Microsoft.Extensions.{Options, Options.ConfigurationExtensions, Configuration.Abstractions, DependencyInjection.Abstractions}`. ## Component status **Status: Draft. Library BUILT @ 0.1.0; NOT YET ADOPTED by the three apps. Adoption is the backlog** (tracked in [`GAPS.md`](GAPS.md)). Unlike the observability pass, this release carries **no in-pass sister-repo adoption** — it is library-only. The shared library lives at [`~/Desktop/scadaproj/ZB.MOM.WW.Configuration/`](../../ZB.MOM.WW.Configuration/) (.NET 10; single package; 27 tests; `dotnet pack` → 1 nupkg @ 0.1.0). Build/test/pack from `ZB.MOM.WW.Configuration/`: ```bash dotnet test ZB.MOM.WW.Configuration.slnx dotnet pack ZB.MOM.WW.Configuration.slnx -c Release -o ./artifacts ```