# 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().Bind(builder.Configuration.GetSection("Ldap"));` Bound, **not** validated — no `ValidateOnStart()`, no registered `IValidateOptions`. 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` — 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-SQL `sp_ValidateDraft`). In the current tree that complement is **not wired in** — see the no-caller note below. - `:24` — `public static IReadOnlyList 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 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`: - `: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 an `LdapStartupOptionsValidator : OptionsValidatorBase` 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().Bind(...)` with `AddValidatedOptions(builder.Configuration, "Ldap")`. - For `OpcUa`: if any field has a fail-fast invariant (e.g. a required endpoint or a port), add an `OptionsValidatorBase` and move the `:63` imperative `.Bind` into `AddValidatedOptions` at composition time. Skip if the section has no hard invariants. **Keep bespoke (unchanged):** - `DraftValidator` and `DraftSnapshot` — **out of scope**. Draft/generation content validation (enforced DB-side by `sp_ValidateDraft`, with the managed `DraftValidator` as dormant complement code), domain rules, `ValidationError` output — all stay exactly as they are. Do **not** fold them into `OptionsValidatorBase`; they are not options validation. (Whether the unused C# `DraftValidator` should 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`](../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.