6.0 KiB
Configuration validation — current state: OtOpcUa
Repo: ~/Desktop/OtOpcUa. Stack: .NET 10, OPC UA, gRPC; solution ZB.MOM.WW.OtOpcUa.slnx.
All paths relative to repo root. Verified 2026-06-01.
Headline: OtOpcUa has no startup options validation at all. A repo-wide search for
IValidateOptions and ValidateOnStart returns zero hits in src/. Options are bound with
bare .Bind(...) and never validated. The only "validation" in the configuration namespace is
DraftValidator — but that is runtime draft/snapshot validation of operator config drafts,
not IConfiguration/options validation, and it is out of scope for the shared library.
This makes OtOpcUa the lightest consumer: there is nothing to replace, only an optional opportunity to add the missing startup validation using the shared base.
1. Options binding — no validation
src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs:
:99—builder.Services.AddOptions<LdapOptions>().Bind(builder.Configuration.GetSection("Ldap"));Bound, not validated — noValidateOnStart(), no registeredIValidateOptions<LdapOptions>. A blankLdap:Server/Ldap:SearchBasewould surface only later, as a low-level LDAP error on the first login (the exact failure mode ScadaBridge'sSecurityOptionsValidatorexists to prevent).
src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OtOpcUaServerHostedService.cs:
:63—_configuration.GetSection("OpcUa").Bind(options);— theOpcUasection is bound imperatively inside the hosted service, again with no validation pass.
There is no *OptionsValidator type and no AddValidatedOptions-style helper anywhere in src/.
The repo simply trusts its config sections.
2. DraftValidator / DraftSnapshot — runtime draft validation (OUT OF SCOPE)
src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftValidator.cs:
:14—public static class DraftValidator— a managed pre-publish validator (its own doc-comment,:7–13, frames it as the managed-code complement to the T-SQLsp_ValidateDraft).:24—public static IReadOnlyList<ValidationError> Validate(DraftSnapshot draft)— runs seven rule groups (:28–34): UNS segment regex (:42), path length ≤ 200 (:64), EquipmentUuid immutability (:89), same-cluster namespace binding (:104), reservation pre-flight (:125), EquipmentId derivation (:153), driver/namespace compatibility (:165).:206—public static IReadOnlyList<ValidationError> ValidateClusterTopology(...)— a second managed guard for cluster topology vsRedundancyMode.- It returns every failing rule in one pass — same "surface all errors" philosophy this
component normalizes — but over database draft rows (
DraftSnapshot), notIConfiguration.
src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftSnapshot.cs:
:9—public sealed class DraftSnapshot— the input bag: namespaces, driver instances, equipment, UNS areas/lines, tags, poll groups, plus prior-generation rows for cross-generation invariants. These are domain entities (ZB.MOM.WW.OtOpcUa.Configuration.Entities), not options.
DraftValidator is referenced only by its tests
(tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftValidatorTests.cs) and the publish
pipeline — never from any DI / options registration. It produces ValidationError
(src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/ValidationError.cs), a domain record, not
ValidateOptionsResult.
Why it stays per-project: it validates an operator's configuration content (the equipment hierarchy they are about to publish), with rules that are entirely OtOpcUa domain knowledge (UNS regex, EquipmentId derivation, Galaxy driver/namespace rules). It is not the cross-cutting "validate the host's config section at startup" concern the shared library normalizes. Nothing about it changes on adoption.
3. Summary
| Surface | What exists | Shared-lib relevance |
|---|---|---|
| Startup options validation | None — LdapOptions/OpcUa bound with bare .Bind() |
Gap — could adopt OptionsValidatorBase + AddValidatedOptions |
IValidateOptions / ValidateOnStart |
Zero usages in src/ |
nothing to migrate |
| Pre-host raw-config preflight | None | could adopt ConfigPreflight if pre-host keys emerge |
| Runtime draft validation | DraftValidator + DraftSnapshot (one-pass, all errors) |
out of scope — stays per-project |
Adoption plan → ZB.MOM.WW.Configuration
OtOpcUa is the lightest consumer — adoption is additive, not a replacement, and is entirely optional (no existing validation is wrong, there just isn't any).
Add startup validation for the bound sections (optional, recommended):
- For
Ldap: add anLdapStartupOptionsValidator : OptionsValidatorBase<LdapOptions>that callsv.Required(o.Server, "Ldap:Server")andv.Required(o.SearchBase, "Ldap:SearchBase")(mirroring ScadaBridge'sSecurityOptionsValidatorintent), then replaceProgram.cs:99'sAddOptions<LdapOptions>().Bind(...)withAddValidatedOptions<LdapOptions, LdapStartupOptionsValidator>(builder.Configuration, "Ldap"). - For
OpcUa: if any field has a fail-fast invariant (e.g. a required endpoint or a port), add anOptionsValidatorBase<OpcUaOptions>and move the:63imperative.BindintoAddValidatedOptionsat composition time. Skip if the section has no hard invariants.
Keep bespoke (unchanged):
DraftValidatorandDraftSnapshot— out of scope. Runtime draft/snapshot validation, domain rules,ValidationErroroutput, publish-pipeline call site — all stay exactly as they are. Do not fold them intoOptionsValidatorBase; they are not options validation.
Status: OtOpcUa has no validator to migrate today, so its adoption is purely the new
guarding work above. It is a follow-on (tracked in ../GAPS.md), low priority —
the lowest-stakes of the three because there is no drift to correct, only an absence to optionally
fill once the package is referenced.