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

5.8 KiB
Raw Blame History

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.

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 allLdap/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 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). 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/ (.NET 10; single package; 27 tests; dotnet pack → 1 nupkg @ 0.1.0). Build/test/pack from ZB.MOM.WW.Configuration/:

dotnet test ZB.MOM.WW.Configuration.slnx
dotnet pack ZB.MOM.WW.Configuration.slnx -c Release -o ./artifacts