fbf0f23e76
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.
100 lines
5.8 KiB
Markdown
100 lines
5.8 KiB
Markdown
# 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
|
||
```
|