docs(config): components/configuration normalization (spec, shared-contract, current-state x3, GAPS, README)

This commit is contained in:
Joseph Doherty
2026-06-01 09:48:49 -04:00
parent b754873a44
commit 46c4bfae31
7 changed files with 1033 additions and 0 deletions
@@ -0,0 +1,122 @@
# 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).
- `:1734``Validate(string? name, GatewayOptions options)`: creates `List<string> failures`
(`:19`), dispatches to **nine** sub-validators (`:2129`), and returns the
`failures.Count == 0 ? Success : Fail(failures)` tail (`:3133`). 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
(`:6889`), `Port` positivity (`:90`), and a cross-field `UseTls`/`AllowInsecureLdap` rule (`:92`).
- `:98` `ValidateWorker` — required `ExecutablePath` (`:100`), valid-path + `.exe`-extension
checks (`:101110`), `Enum.IsDefined` on architecture (`:120`), eight positive-int checks
(`:125152`), and a cross-field `HeartbeatGraceSeconds >= HeartbeatIntervalSeconds` rule
(`:154`), plus a `MaxMessageBytes` range (`:160`).
- `:167` `ValidateSessions` — five positive-int checks (`:169185`) + an "unsupported feature"
guard (`:187`).
- `:194` `ValidateEvents``QueueCapacity` positivity (`:196`) + `Enum.IsDefined` on policy (`:198`).
- `:204` `ValidateDashboard``GroupToRole` map shape (`:211224`) + interval/limit bounds (`:226237`).
- `: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
(`:278285`), non-blank DNS-name entries (`:287`).
- `:296` `ValidateProtocol` — exact `WorkerProtocolVersion` match (`:298`) + `MaxGrpcMessageBytes`
range (`:304`).
- **Private helpers that duplicate the shared primitives** (`:311358`):
- `: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`:
- `:1021``AddGatewayConfiguration(this IServiceCollection services)`:
- `:1215``services.AddOptions<GatewayOptions>().BindConfiguration(GatewayOptions.SectionName).ValidateOnStart();`
- `:17``services.AddSingleton<IValidateOptions<GatewayOptions>, GatewayOptionsValidator>();`
- `:18` — also registers `IGatewayConfigurationProvider` (a separate concern; stays).
Lines `:1217` 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`, `:3133`) — 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:1217`),
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.