# 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<string>` 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<GatewayOptions>`

`src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs`:
- `:6` — `public sealed class GatewayOptionsValidator : IValidateOptions<GatewayOptions>` —
  implements the interface directly (no shared base).
- `:17–34` — `Validate(string? name, GatewayOptions options)`: creates `List<string> 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<string> 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:<Section>:<Field> ..."`) — 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<GatewayOptions>().BindConfiguration(GatewayOptions.SectionName).ValidateOnStart();`
  - `:17` — `services.AddSingleton<IValidateOptions<GatewayOptions>, GatewayOptionsValidator>();`
  - `:18` — also registers `IGatewayConfigurationProvider` (a separate concern; stays).

Lines `:12–17` are exactly the `bind + register-validator + ValidateOnStart` triple that
`AddValidatedOptions<GatewayOptions, GatewayOptionsValidator>` 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<GatewayOptions>` (~360 LOC, 9 sub-validators) | → `OptionsValidatorBase<GatewayOptions>` |
| Failure accumulation | private `List<string> 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<IValidateOptions...>`) | → `AddValidatedOptions<GatewayOptions, GatewayOptionsValidator>` |
| 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<GatewayOptions>` →
  `GatewayOptionsValidator : OptionsValidatorBase<GatewayOptions>`
  (`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<string> 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<string>`. 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<IValidateOptions...>`
  pair with:
  ```csharp
  services.AddValidatedOptions<GatewayOptions, GatewayOptionsValidator>(
      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:<Section>:<Field>"` 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.
