7.8 KiB
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): createsList<string> failures(:19), dispatches to nine sub-validators (:21–29), and returns thefailures.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)andfailures.Add(...)s)::36ValidateAuthentication—Enum.IsDefinedonMode; conditional requiredSqlitePath/PepperSecretNamewhenMode == ApiKey.:61ValidateLdap— short-circuits when!Enabled(:63); seven required-string checks (:68–89),Portpositivity (:90), and a cross-fieldUseTls/AllowInsecureLdaprule (:92).:98ValidateWorker— requiredExecutablePath(:100), valid-path +.exe-extension checks (:101–110),Enum.IsDefinedon architecture (:120), eight positive-int checks (:125–152), and a cross-fieldHeartbeatGraceSeconds >= HeartbeatIntervalSecondsrule (:154), plus aMaxMessageBytesrange (:160).:167ValidateSessions— five positive-int checks (:169–185) + an "unsupported feature" guard (:187).:194ValidateEvents—QueueCapacitypositivity (:196) +Enum.IsDefinedon policy (:198).:204ValidateDashboard—GroupToRolemap shape (:211–224) + interval/limit bounds (:226–237).:240ValidateAlarms— short-circuits when!Enabled(:242); a "need expression or area" rule (:251) + a canonical-prefix rule (:258).:269ValidateTls—ValidityYearsrange (:271), required non-blank cert path + valid path (:278–285), non-blank DNS-name entries (:287).:296ValidateProtocol— exactWorkerProtocolVersionmatch (:298) +MaxGrpcMessageBytesrange (:304).
- Private helpers that duplicate the shared primitives (
:311–358)::311AddIfBlank→ maps toValidationBuilder.Required.:319AddIfNotPositive→ maps toRequireThat(value > 0, ...).:327AddIfNegative→ maps toRequireThat(value >= 0, ...).:335AddIfInvalidPath→ app-specific; stays as aRequireThat/Addcustom 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 registersIGatewayConfigurationProvider(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 theprotected override void Validate(ValidationBuilder v, GatewayOptions options). Delete theList<string> failuresand theCount == 0 ? Success : Failtail (:19,:31–33) — the base supplies both. - Keep the nine sub-validators but re-thread them to take the
ValidationBuilderinstead ofList<string>. Map the helpers:AddIfBlank→v.Required(...),AddIfNotPositive(x, msg)→v.RequireThat(x > 0, msg),AddIfNegative(x, msg)→v.RequireThat(x >= 0, msg). KeepAddIfInvalidPathas a private helper that records viav.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 theAddOptions().BindConfiguration().ValidateOnStart()+AddSingleton<IValidateOptions...>pair with:(The extension gains anservices.AddValidatedOptions<GatewayOptions, GatewayOptionsValidator>( configuration, GatewayOptions.SectionName);IConfigurationparameter, or resolves it from the builder, sinceAddValidatedOptionsbinds from an explicitIConfigurationrather than the ambientBindConfiguration.) TheIGatewayConfigurationProviderregistration (:18) is unrelated and stays.
Keep bespoke (unchanged):
- Every
"MxGateway:<Section>:<Field>"message and every domain rule (worker.exeextension, heartbeat grace ≥ interval, protocol-version exact match,\\-prefixed alarm expression, etc.). GatewayOptionsand its section types — these are MxGateway's options classes; not shared.- The net48 x86 worker — does no
IConfigurationvalidation; excluded entirely.
Status: follow-on (tracked in ../GAPS.md). Medium-weight, low-risk —
behaviour-preserving plumbing swap; one validator, one DI extension.