123 lines
7.8 KiB
Markdown
123 lines
7.8 KiB
Markdown
# 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.
|