Files
scadaproj/components/configuration/README.md
T
Joseph Doherty fbf0f23e76 docs(config): correct OtOpcUa draft-validation description
The C# DraftValidator/DraftSnapshot has NO live caller in OtOpcUa src/ (verified
repo-wide) — it is dormant complement code. The enforced pre-publish draft
validation runs DB-side in the sp_ValidateDraft stored procedure (Status='Draft'
-> sp_PublishGeneration lifecycle). Reframe across current-state/SPEC/GAPS/README/
CLAUDE.md from 'runtime draft validation' + a false publish-pipeline caller to
'dormant managed validator; enforcement is DB-side'. Out-of-scope conclusion
for ZB.MOM.WW.Configuration is unchanged.
2026-06-01 10:13:29 -04:00

100 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<string>` 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<string>` | Not started (follow-on) |
| **ScadaBridge** | 🟡 four `*OptionsValidator` (hand-rolled) | ✅ `StartupValidator` (raw config, pre-Akka) | 🟡 manual `List<string>` ×4 | Not started (follow-on; heaviest) |
See each project's [`current-state/<project>/CURRENT-STATE.md`](current-state/) for the
code-verified detail and its adoption plan.
## Normalized vs. left per-project
**Normalized (the shared target):**
- `OptionsValidatorBase<TOptions>` — abstract `IValidateOptions<TOptions>`; 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
`"<field> <reason>"` wording via the internal `Checks` seam.
- `AddValidatedOptions<TOptions, TValidator>(IConfiguration, sectionPath)` — bind + register
validator + `ValidateOnStart` in one DI call; returns `OptionsBuilder<TOptions>`.
- `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`); `"<field> <reason>"` 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<TOptions>`, `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
```