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.
7.1 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-shaped type in the configuration
namespace is the C# DraftValidator — but it is dormant (no live caller in src/) and, by
design, concerns config-generation content, not IConfiguration/options. The enforced pre-publish
draft validation actually runs DB-side in the sp_ValidateDraft stored procedure. Either way,
draft/generation validation is out of scope for the shared options-validation 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 — dormant managed draft validator (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). In the current tree that complement is not wired in — see the no-caller note below.:24—public static IReadOnlyList<ValidationError> Validate(DraftSnapshot draft)— would run 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) — a repo-wide search
finds no live caller in src/ (nothing constructs a DraftSnapshot or calls
DraftValidator.Validate/ValidateClusterTopology), and it is never registered in DI or options.
The enforced pre-publish validation lives DB-side in the sp_ValidateDraft stored procedure
(src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260417215224_StoredProcedures.cs:157+,
called as part of the Status='Draft' → sp_PublishGeneration generation lifecycle); the managed
DraftValidator is currently dormant complement code. When it does run it produces
ValidationError (.../Validation/ValidationError.cs), a domain record, not ValidateOptionsResult.
Why it stays per-project: it (and its live DB counterpart sp_ValidateDraft) 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 |
| Draft/generation validation | DB sp_ValidateDraft (live, in the publish lifecycle) + C# DraftValidator (dormant, no src/ caller) |
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. Draft/generation content validation (enforced DB-side bysp_ValidateDraft, with the managedDraftValidatoras dormant complement code), domain rules,ValidationErroroutput — all stay exactly as they are. Do not fold them intoOptionsValidatorBase; they are not options validation. (Whether the unused C#DraftValidatorshould be revived or removed is an OtOpcUa housekeeping question, unrelated to this component.)
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.