Files
scadaproj/components/configuration/current-state/otopcua/CURRENT-STATE.md
T

6.0 KiB
Raw Blame History

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:

  • :99builder.Services.AddOptions<LdapOptions>().Bind(builder.Configuration.GetSection("Ldap")); Bound, not validated — no ValidateOnStart(), no registered IValidateOptions<LdapOptions>. A blank Ldap:Server / Ldap:SearchBase would surface only later, as a low-level LDAP error on the first login (the exact failure mode ScadaBridge's SecurityOptionsValidator exists to prevent).

src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OtOpcUaServerHostedService.cs:

  • :63_configuration.GetSection("OpcUa").Bind(options); — the OpcUa section 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:

  • :14public static class DraftValidator — a managed pre-publish validator (its own doc-comment, :713, frames it as the managed-code complement to the T-SQL sp_ValidateDraft).
  • :24public static IReadOnlyList<ValidationError> Validate(DraftSnapshot draft) — runs seven rule groups (:2834): 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).
  • :206public static IReadOnlyList<ValidationError> ValidateClusterTopology(...) — a second managed guard for cluster topology vs RedundancyMode.
  • It returns every failing rule in one pass — same "surface all errors" philosophy this component normalizes — but over database draft rows (DraftSnapshot), not IConfiguration.

src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftSnapshot.cs:

  • :9public 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 NoneLdapOptions/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 an LdapStartupOptionsValidator : OptionsValidatorBase<LdapOptions> that calls v.Required(o.Server, "Ldap:Server") and v.Required(o.SearchBase, "Ldap:SearchBase") (mirroring ScadaBridge's SecurityOptionsValidator intent), then replace Program.cs:99's AddOptions<LdapOptions>().Bind(...) with AddValidatedOptions<LdapOptions, LdapStartupOptionsValidator>(builder.Configuration, "Ldap").
  • For OpcUa: if any field has a fail-fast invariant (e.g. a required endpoint or a port), add an OptionsValidatorBase<OpcUaOptions> and move the :63 imperative .Bind into AddValidatedOptions at composition time. Skip if the section has no hard invariants.

Keep bespoke (unchanged):

  • DraftValidator and DraftSnapshotout of scope. Runtime draft/snapshot validation, domain rules, ValidationError output, publish-pipeline call site — all stay exactly as they are. Do not fold them into OptionsValidatorBase; 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.