Files
scadaproj/components/configuration/current-state/mxaccessgw/CURRENT-STATE.md
T

7.8 KiB
Raw Blame History

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:

  • :6public sealed class GatewayOptionsValidator : IValidateOptions<GatewayOptions> — implements the interface directly (no shared base).
  • :1734Validate(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 ValidateAuthenticationEnum.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 ValidateEventsQueueCapacity positivity (:196) + Enum.IsDefined on policy (:198).
    • :204 ValidateDashboardGroupToRole 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 ValidateTlsValidityYears 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:

  • :1021AddGatewayConfiguration(this IServiceCollection services):
    • :1215services.AddOptions<GatewayOptions>().BindConfiguration(GatewayOptions.SectionName).ValidateOnStart();
    • :17services.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: AddIfBlankv.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:
    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). Medium-weight, low-risk — behaviour-preserving plumbing swap; one validator, one DI extension.