# Configuration validation — current state: MxAccessGateway Repo: `~/Desktop/MxAccessGateway` (`mxaccessgw`). Stack: .NET 10 gateway (x64) + .NET 4.8 worker (x86), gRPC; solution `src/MxGateway.sln`. All paths relative to repo root. Verified 2026-06-01. MxGateway has **one large, well-structured options validator** for a single composite `GatewayOptions`, wired through a bespoke DI extension. It is the textbook hand-rolled version of exactly what the shared library normalizes: a private `List` accumulator, a stack of `AddIfXxx` helper methods, and an `AddOptions().BindConfiguration().ValidateOnStart()` registration — all of which collapse onto `OptionsValidatorBase` + `AddValidatedOptions` with the domain rules left untouched. ## 1. `GatewayOptionsValidator` — hand-rolled `IValidateOptions` `src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs`: - `:6` — `public sealed class GatewayOptionsValidator : IValidateOptions` — implements the interface directly (no shared base). - `:17–34` — `Validate(string? name, GatewayOptions options)`: creates `List failures` (`:19`), dispatches to **nine** sub-validators (`:21–29`), and returns the `failures.Count == 0 ? Success : Fail(failures)` tail (`:31–33`). This is precisely the accumulate-all-then-decide convention the base owns. - Sub-validators (each takes `(section options, List failures)` and `failures.Add(...)`s): - `:36` `ValidateAuthentication` — `Enum.IsDefined` on `Mode`; conditional required `SqlitePath` / `PepperSecretName` when `Mode == ApiKey`. - `:61` `ValidateLdap` — short-circuits when `!Enabled` (`:63`); seven required-string checks (`:68–89`), `Port` positivity (`:90`), and a cross-field `UseTls`/`AllowInsecureLdap` rule (`:92`). - `:98` `ValidateWorker` — required `ExecutablePath` (`:100`), valid-path + `.exe`-extension checks (`:101–110`), `Enum.IsDefined` on architecture (`:120`), eight positive-int checks (`:125–152`), and a cross-field `HeartbeatGraceSeconds >= HeartbeatIntervalSeconds` rule (`:154`), plus a `MaxMessageBytes` range (`:160`). - `:167` `ValidateSessions` — five positive-int checks (`:169–185`) + an "unsupported feature" guard (`:187`). - `:194` `ValidateEvents` — `QueueCapacity` positivity (`:196`) + `Enum.IsDefined` on policy (`:198`). - `:204` `ValidateDashboard` — `GroupToRole` map shape (`:211–224`) + interval/limit bounds (`:226–237`). - `:240` `ValidateAlarms` — short-circuits when `!Enabled` (`:242`); a "need expression or area" rule (`:251`) + a canonical-prefix rule (`:258`). - `:269` `ValidateTls` — `ValidityYears` range (`:271`), required non-blank cert path + valid path (`:278–285`), non-blank DNS-name entries (`:287`). - `:296` `ValidateProtocol` — exact `WorkerProtocolVersion` match (`:298`) + `MaxGrpcMessageBytes` range (`:304`). - **Private helpers that duplicate the shared primitives** (`:311–358`): - `:311` `AddIfBlank` → maps to `ValidationBuilder.Required`. - `:319` `AddIfNotPositive` → maps to `RequireThat(value > 0, ...)`. - `:327` `AddIfNegative` → maps to `RequireThat(value >= 0, ...)`. - `:335` `AddIfInvalidPath` → app-specific; stays as a `RequireThat`/`Add` custom rule (filesystem-path validity is not a shared primitive). Every failure message is the gateway's own (`"MxGateway:
: ..."`) — these are **domain rules and stay per-project**; only the accumulation plumbing and the trivial helpers move to the base. ## 2. DI wiring — `AddGatewayConfiguration` `src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayConfigurationServiceCollectionExtensions.cs`: - `:10–21` — `AddGatewayConfiguration(this IServiceCollection services)`: - `:12–15` — `services.AddOptions().BindConfiguration(GatewayOptions.SectionName).ValidateOnStart();` - `:17` — `services.AddSingleton, GatewayOptionsValidator>();` - `:18` — also registers `IGatewayConfigurationProvider` (a separate concern; stays). Lines `:12–17` are exactly the `bind + register-validator + ValidateOnStart` triple that `AddValidatedOptions` collapses into one call. (One nuance: the gateway uses `BindConfiguration(SectionName)` — which reads the section path off the type/const — whereas `AddValidatedOptions` takes an explicit `sectionPath` string; the adoption passes `GatewayOptions.SectionName` as that argument.) A bad `MxGateway` section surfaces as **`OptionsValidationException`** at host start, via `ValidateOnStart()` — the same path `AddValidatedOptions` produces. ## 3. Summary | Surface | What exists | Shared-lib mapping | |---|---|---| | Options validator | `GatewayOptionsValidator : IValidateOptions` (~360 LOC, 9 sub-validators) | → `OptionsValidatorBase` | | Failure accumulation | private `List failures` + `Count == 0 ? Success : Fail` tail | → owned by base + `ValidationBuilder` | | Rule helpers | `AddIfBlank` / `AddIfNotPositive` / `AddIfNegative` | → `Required` / `RequireThat` primitives | | App-specific helper | `AddIfInvalidPath` | → stays as a custom `RequireThat`/`Add` rule | | DI wiring | `AddGatewayConfiguration` (`AddOptions().BindConfiguration().ValidateOnStart()` + `AddSingleton`) | → `AddValidatedOptions` | | Pre-host preflight | none (single host, no pre-Akka stage) | n/a — `ConfigPreflight` not needed | --- ## Adoption plan → `ZB.MOM.WW.Configuration` **Migrate the validator to the shared base:** - Change `GatewayOptionsValidator : IValidateOptions` → `GatewayOptionsValidator : OptionsValidatorBase` (`GatewayOptionsValidator.cs:6`). - Replace the public `Validate(string? name, GatewayOptions options)` (`:17`) with the `protected override void Validate(ValidationBuilder v, GatewayOptions options)`. Delete the `List failures` and the `Count == 0 ? Success : Fail` tail (`:19`, `:31–33`) — the base supplies both. - Keep the nine sub-validators but re-thread them to take the `ValidationBuilder` instead of `List`. Map the helpers: `AddIfBlank` → `v.Required(...)`, `AddIfNotPositive(x, msg)` → `v.RequireThat(x > 0, msg)`, `AddIfNegative(x, msg)` → `v.RequireThat(x >= 0, msg)`. Keep `AddIfInvalidPath` as a private helper that records via `v.Add(...)` (filesystem-path validity is app-specific; not a shared primitive). **All gateway message strings are preserved verbatim** — domain rules do not change. **Migrate the DI wiring:** - In `AddGatewayConfiguration` (`GatewayConfigurationServiceCollectionExtensions.cs:12–17`), replace the `AddOptions().BindConfiguration().ValidateOnStart()` + `AddSingleton` pair with: ```csharp services.AddValidatedOptions( configuration, GatewayOptions.SectionName); ``` (The extension gains an `IConfiguration` parameter, or resolves it from the builder, since `AddValidatedOptions` binds from an explicit `IConfiguration` rather than the ambient `BindConfiguration`.) The `IGatewayConfigurationProvider` registration (`:18`) is unrelated and stays. **Keep bespoke (unchanged):** - Every `"MxGateway:
:"` message and every domain rule (worker `.exe` extension, heartbeat grace ≥ interval, protocol-version exact match, `\\`-prefixed alarm expression, etc.). - `GatewayOptions` and its section types — these are MxGateway's options classes; not shared. - The net48 x86 worker — does no `IConfiguration` validation; excluded entirely. **Status:** follow-on (tracked in [`../GAPS.md`](../GAPS.md)). Medium-weight, low-risk — behaviour-preserving plumbing swap; one validator, one DI extension.