diff --git a/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayConfigurationServiceCollectionExtensions.cs b/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayConfigurationServiceCollectionExtensions.cs
index d572311..814e9ac 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayConfigurationServiceCollectionExtensions.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayConfigurationServiceCollectionExtensions.cs
@@ -1,4 +1,5 @@
-using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Configuration;
+using ZB.MOM.WW.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
@@ -6,15 +7,14 @@ public static class GatewayConfigurationServiceCollectionExtensions
{
/// Registers gateway configuration services in the dependency injection container.
/// The service collection.
+ /// The configuration to bind gateway options from.
/// The service collection for chaining.
- public static IServiceCollection AddGatewayConfiguration(this IServiceCollection services)
+ public static IServiceCollection AddGatewayConfiguration(
+ this IServiceCollection services, IConfiguration configuration)
{
- services
- .AddOptions()
- .BindConfiguration(GatewayOptions.SectionName)
- .ValidateOnStart();
+ services.AddValidatedOptions(
+ configuration, GatewayOptions.SectionName);
- services.AddSingleton, GatewayOptionsValidator>();
services.AddSingleton();
return services;
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs b/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs
index 19994bc..fd104ca 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs
@@ -1,9 +1,9 @@
-using Microsoft.Extensions.Options;
+using ZB.MOM.WW.Configuration;
using ZB.MOM.WW.MxGateway.Contracts;
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
-public sealed class GatewayOptionsValidator : IValidateOptions
+public sealed class GatewayOptionsValidator : OptionsValidatorBase
{
private const int MinimumMaxMessageBytes = 1024;
private const int MaximumMaxMessageBytes = 256 * 1024 * 1024;
@@ -11,33 +11,26 @@ public sealed class GatewayOptionsValidator : IValidateOptions
///
/// Validates gateway configuration options.
///
- /// Options name.
+ /// The accumulator to record failures on.
/// Gateway options to validate.
- /// Validation result.
- public ValidateOptionsResult Validate(string? name, GatewayOptions options)
+ protected override void Validate(ValidationBuilder builder, GatewayOptions options)
{
- List failures = [];
-
- ValidateAuthentication(options.Authentication, failures);
- ValidateLdap(options.Ldap, failures);
- ValidateWorker(options.Worker, failures);
- ValidateSessions(options.Sessions, failures);
- ValidateEvents(options.Events, failures);
- ValidateDashboard(options.Dashboard, failures);
- ValidateProtocol(options.Protocol, failures);
- ValidateAlarms(options.Alarms, failures);
- ValidateTls(options.Tls, failures);
-
- return failures.Count == 0
- ? ValidateOptionsResult.Success
- : ValidateOptionsResult.Fail(failures);
+ ValidateAuthentication(options.Authentication, builder);
+ ValidateLdap(options.Ldap, builder);
+ ValidateWorker(options.Worker, builder);
+ ValidateSessions(options.Sessions, builder);
+ ValidateEvents(options.Events, builder);
+ ValidateDashboard(options.Dashboard, builder);
+ ValidateProtocol(options.Protocol, builder);
+ ValidateAlarms(options.Alarms, builder);
+ ValidateTls(options.Tls, builder);
}
- private static void ValidateAuthentication(AuthenticationOptions options, List failures)
+ private static void ValidateAuthentication(AuthenticationOptions options, ValidationBuilder builder)
{
if (!Enum.IsDefined(options.Mode))
{
- failures.Add("MxGateway:Authentication:Mode must be a supported authentication mode.");
+ builder.Add("MxGateway:Authentication:Mode must be a supported authentication mode.");
return;
}
@@ -46,67 +39,67 @@ public sealed class GatewayOptionsValidator : IValidateOptions
AddIfBlank(
options.SqlitePath,
"MxGateway:Authentication:SqlitePath is required when API-key authentication is enabled.",
- failures);
+ builder);
AddIfInvalidPath(
options.SqlitePath,
"MxGateway:Authentication:SqlitePath must be a valid filesystem path.",
- failures);
+ builder);
AddIfBlank(
options.PepperSecretName,
"MxGateway:Authentication:PepperSecretName is required when API-key authentication is enabled.",
- failures);
+ builder);
}
}
- private static void ValidateLdap(LdapOptions options, List failures)
+ private static void ValidateLdap(LdapOptions options, ValidationBuilder builder)
{
if (!options.Enabled)
{
return;
}
- AddIfBlank(options.Server, "MxGateway:Ldap:Server is required when LDAP login is enabled.", failures);
- AddIfBlank(options.SearchBase, "MxGateway:Ldap:SearchBase is required when LDAP login is enabled.", failures);
+ AddIfBlank(options.Server, "MxGateway:Ldap:Server is required when LDAP login is enabled.", builder);
+ AddIfBlank(options.SearchBase, "MxGateway:Ldap:SearchBase is required when LDAP login is enabled.", builder);
AddIfBlank(
options.ServiceAccountDn,
"MxGateway:Ldap:ServiceAccountDn is required when LDAP login is enabled.",
- failures);
+ builder);
AddIfBlank(
options.ServiceAccountPassword,
"MxGateway:Ldap:ServiceAccountPassword is required when LDAP login is enabled.",
- failures);
+ builder);
AddIfBlank(
options.UserNameAttribute,
"MxGateway:Ldap:UserNameAttribute is required when LDAP login is enabled.",
- failures);
+ builder);
AddIfBlank(
options.DisplayNameAttribute,
"MxGateway:Ldap:DisplayNameAttribute is required when LDAP login is enabled.",
- failures);
+ builder);
AddIfBlank(
options.GroupAttribute,
"MxGateway:Ldap:GroupAttribute is required when LDAP login is enabled.",
- failures);
- AddIfNotPositive(options.Port, "MxGateway:Ldap:Port must be greater than zero.", failures);
+ builder);
+ AddIfNotPositive(options.Port, "MxGateway:Ldap:Port must be greater than zero.", builder);
if (!options.UseTls && !options.AllowInsecureLdap)
{
- failures.Add("MxGateway:Ldap:AllowInsecureLdap must be true when UseTls is false.");
+ builder.Add("MxGateway:Ldap:AllowInsecureLdap must be true when UseTls is false.");
}
}
- private static void ValidateWorker(WorkerOptions options, List failures)
+ private static void ValidateWorker(WorkerOptions options, ValidationBuilder builder)
{
- AddIfBlank(options.ExecutablePath, "MxGateway:Worker:ExecutablePath is required.", failures);
+ AddIfBlank(options.ExecutablePath, "MxGateway:Worker:ExecutablePath is required.", builder);
AddIfInvalidPath(
options.ExecutablePath,
"MxGateway:Worker:ExecutablePath must be a valid filesystem path.",
- failures);
+ builder);
if (!string.IsNullOrWhiteSpace(options.ExecutablePath)
&& !string.Equals(Path.GetExtension(options.ExecutablePath), ".exe", StringComparison.OrdinalIgnoreCase))
{
- failures.Add("MxGateway:Worker:ExecutablePath must point to a .exe file.");
+ builder.Add("MxGateway:Worker:ExecutablePath must point to a .exe file.");
}
if (!string.IsNullOrWhiteSpace(options.WorkingDirectory))
@@ -114,94 +107,94 @@ public sealed class GatewayOptionsValidator : IValidateOptions
AddIfInvalidPath(
options.WorkingDirectory,
"MxGateway:Worker:WorkingDirectory must be a valid filesystem path.",
- failures);
+ builder);
}
if (!Enum.IsDefined(options.RequiredArchitecture))
{
- failures.Add("MxGateway:Worker:RequiredArchitecture must be a supported worker architecture.");
+ builder.Add("MxGateway:Worker:RequiredArchitecture must be a supported worker architecture.");
}
AddIfNotPositive(
options.StartupTimeoutSeconds,
"MxGateway:Worker:StartupTimeoutSeconds must be greater than zero.",
- failures);
+ builder);
AddIfNotPositive(
options.StartupProbeRetryAttempts,
"MxGateway:Worker:StartupProbeRetryAttempts must be greater than zero.",
- failures);
+ builder);
AddIfNotPositive(
options.StartupProbeRetryDelayMilliseconds,
"MxGateway:Worker:StartupProbeRetryDelayMilliseconds must be greater than zero.",
- failures);
+ builder);
AddIfNotPositive(
options.PipeConnectAttemptTimeoutMilliseconds,
"MxGateway:Worker:PipeConnectAttemptTimeoutMilliseconds must be greater than zero.",
- failures);
+ builder);
AddIfNotPositive(
options.ShutdownTimeoutSeconds,
"MxGateway:Worker:ShutdownTimeoutSeconds must be greater than zero.",
- failures);
+ builder);
AddIfNotPositive(
options.HeartbeatIntervalSeconds,
"MxGateway:Worker:HeartbeatIntervalSeconds must be greater than zero.",
- failures);
+ builder);
AddIfNotPositive(
options.HeartbeatGraceSeconds,
"MxGateway:Worker:HeartbeatGraceSeconds must be greater than zero.",
- failures);
+ builder);
if (options.HeartbeatGraceSeconds < options.HeartbeatIntervalSeconds)
{
- failures.Add(
+ builder.Add(
"MxGateway:Worker:HeartbeatGraceSeconds must be greater than or equal to HeartbeatIntervalSeconds.");
}
if (options.MaxMessageBytes is < MinimumMaxMessageBytes or > MaximumMaxMessageBytes)
{
- failures.Add(
+ builder.Add(
$"MxGateway:Worker:MaxMessageBytes must be between {MinimumMaxMessageBytes} and {MaximumMaxMessageBytes}.");
}
}
- private static void ValidateSessions(SessionOptions options, List failures)
+ private static void ValidateSessions(SessionOptions options, ValidationBuilder builder)
{
AddIfNotPositive(
options.DefaultCommandTimeoutSeconds,
"MxGateway:Sessions:DefaultCommandTimeoutSeconds must be greater than zero.",
- failures);
- AddIfNotPositive(options.MaxSessions, "MxGateway:Sessions:MaxSessions must be greater than zero.", failures);
+ builder);
+ AddIfNotPositive(options.MaxSessions, "MxGateway:Sessions:MaxSessions must be greater than zero.", builder);
AddIfNotPositive(
options.MaxPendingCommandsPerSession,
"MxGateway:Sessions:MaxPendingCommandsPerSession must be greater than zero.",
- failures);
+ builder);
AddIfNotPositive(
options.DefaultLeaseSeconds,
"MxGateway:Sessions:DefaultLeaseSeconds must be greater than zero.",
- failures);
+ builder);
AddIfNotPositive(
options.LeaseSweepIntervalSeconds,
"MxGateway:Sessions:LeaseSweepIntervalSeconds must be greater than zero.",
- failures);
+ builder);
if (options.AllowMultipleEventSubscribers)
{
- failures.Add(
+ builder.Add(
"MxGateway:Sessions:AllowMultipleEventSubscribers is not supported until event fan-out is implemented.");
}
}
- private static void ValidateEvents(EventOptions options, List failures)
+ private static void ValidateEvents(EventOptions options, ValidationBuilder builder)
{
- AddIfNotPositive(options.QueueCapacity, "MxGateway:Events:QueueCapacity must be greater than zero.", failures);
+ AddIfNotPositive(options.QueueCapacity, "MxGateway:Events:QueueCapacity must be greater than zero.", builder);
if (!Enum.IsDefined(options.BackpressurePolicy))
{
- failures.Add("MxGateway:Events:BackpressurePolicy must be a supported backpressure policy.");
+ builder.Add("MxGateway:Events:BackpressurePolicy must be a supported backpressure policy.");
}
}
- private static void ValidateDashboard(DashboardOptions options, List failures)
+ private static void ValidateDashboard(DashboardOptions options, ValidationBuilder builder)
{
// GroupToRole shape is validated even when the dashboard is disabled so
// misconfiguration surfaces at startup; emptiness is allowed, with the
@@ -212,13 +205,13 @@ public sealed class GatewayOptionsValidator : IValidateOptions
{
if (string.IsNullOrWhiteSpace(entry.Key))
{
- failures.Add("MxGateway:Dashboard:GroupToRole keys (LDAP group names) must be non-blank.");
+ builder.Add("MxGateway:Dashboard:GroupToRole keys (LDAP group names) must be non-blank.");
}
if (!string.Equals(entry.Value, Dashboard.DashboardRoles.Admin, StringComparison.Ordinal)
&& !string.Equals(entry.Value, Dashboard.DashboardRoles.Viewer, StringComparison.Ordinal))
{
- failures.Add(
+ builder.Add(
$"MxGateway:Dashboard:GroupToRole['{entry.Key}'] must be '{Dashboard.DashboardRoles.Admin}' or '{Dashboard.DashboardRoles.Viewer}'.");
}
}
@@ -226,18 +219,18 @@ public sealed class GatewayOptionsValidator : IValidateOptions
AddIfNotPositive(
options.SnapshotIntervalMilliseconds,
"MxGateway:Dashboard:SnapshotIntervalMilliseconds must be greater than zero.",
- failures);
+ builder);
AddIfNegative(
options.RecentFaultLimit,
"MxGateway:Dashboard:RecentFaultLimit must be greater than or equal to zero.",
- failures);
+ builder);
AddIfNegative(
options.RecentSessionLimit,
"MxGateway:Dashboard:RecentSessionLimit must be greater than or equal to zero.",
- failures);
+ builder);
}
- private static void ValidateAlarms(AlarmsOptions options, List failures)
+ private static void ValidateAlarms(AlarmsOptions options, ValidationBuilder builder)
{
if (!options.Enabled)
{
@@ -251,14 +244,14 @@ public sealed class GatewayOptionsValidator : IValidateOptions
if (string.IsNullOrWhiteSpace(options.SubscriptionExpression)
&& string.IsNullOrWhiteSpace(options.DefaultArea))
{
- failures.Add(
+ builder.Add(
"MxGateway:Alarms requires either a non-blank SubscriptionExpression or a non-blank DefaultArea when Enabled is true.");
}
if (!string.IsNullOrWhiteSpace(options.SubscriptionExpression)
&& !options.SubscriptionExpression.StartsWith(@"\\", StringComparison.Ordinal))
{
- failures.Add(
+ builder.Add(
@"MxGateway:Alarms:SubscriptionExpression must start with '\\' (canonical \\\Galaxy! shape).");
}
}
@@ -266,11 +259,11 @@ public sealed class GatewayOptionsValidator : IValidateOptions
private const int MinimumCertValidityYears = 1;
private const int MaximumCertValidityYears = 100;
- private static void ValidateTls(TlsOptions options, List failures)
+ private static void ValidateTls(TlsOptions options, ValidationBuilder builder)
{
if (options.ValidityYears is < MinimumCertValidityYears or > MaximumCertValidityYears)
{
- failures.Add(
+ builder.Add(
$"MxGateway:Tls:ValidityYears must be between {MinimumCertValidityYears} and {MaximumCertValidityYears}.");
}
@@ -278,61 +271,52 @@ public sealed class GatewayOptionsValidator : IValidateOptions
AddIfBlank(
options.SelfSignedCertPath,
"MxGateway:Tls:SelfSignedCertPath must not be blank.",
- failures);
+ builder);
AddIfInvalidPath(
options.SelfSignedCertPath,
"MxGateway:Tls:SelfSignedCertPath must be a valid filesystem path.",
- failures);
+ builder);
foreach (string dns in options.AdditionalDnsNames)
{
if (string.IsNullOrWhiteSpace(dns))
{
- failures.Add("MxGateway:Tls:AdditionalDnsNames entries must be non-blank.");
+ builder.Add("MxGateway:Tls:AdditionalDnsNames entries must be non-blank.");
}
}
}
- private static void ValidateProtocol(ProtocolOptions options, List failures)
+ private static void ValidateProtocol(ProtocolOptions options, ValidationBuilder builder)
{
if (options.WorkerProtocolVersion != GatewayContractInfo.WorkerProtocolVersion)
{
- failures.Add(
+ builder.Add(
$"MxGateway:Protocol:WorkerProtocolVersion must be {GatewayContractInfo.WorkerProtocolVersion}.");
}
if (options.MaxGrpcMessageBytes is < MinimumMaxMessageBytes or > MaximumMaxMessageBytes)
{
- failures.Add(
+ builder.Add(
$"MxGateway:Protocol:MaxGrpcMessageBytes must be between {MinimumMaxMessageBytes} and {MaximumMaxMessageBytes}.");
}
}
- private static void AddIfBlank(string? value, string message, List failures)
+ private static void AddIfBlank(string? value, string message, ValidationBuilder builder)
{
- if (string.IsNullOrWhiteSpace(value))
- {
- failures.Add(message);
- }
+ builder.RequireThat(!string.IsNullOrWhiteSpace(value), message);
}
- private static void AddIfNotPositive(int value, string message, List failures)
+ private static void AddIfNotPositive(int value, string message, ValidationBuilder builder)
{
- if (value <= 0)
- {
- failures.Add(message);
- }
+ builder.RequireThat(value > 0, message);
}
- private static void AddIfNegative(int value, string message, List failures)
+ private static void AddIfNegative(int value, string message, ValidationBuilder builder)
{
- if (value < 0)
- {
- failures.Add(message);
- }
+ builder.RequireThat(value >= 0, message);
}
- private static void AddIfInvalidPath(string? value, string message, List failures)
+ private static void AddIfInvalidPath(string? value, string message, ValidationBuilder builder)
{
if (string.IsNullOrWhiteSpace(value))
{
@@ -345,15 +329,15 @@ public sealed class GatewayOptionsValidator : IValidateOptions
}
catch (ArgumentException)
{
- failures.Add(message);
+ builder.Add(message);
}
catch (NotSupportedException)
{
- failures.Add(message);
+ builder.Add(message);
}
catch (PathTooLongException)
{
- failures.Add(message);
+ builder.Add(message);
}
}
}
diff --git a/src/ZB.MOM.WW.MxGateway.Server/GatewayApplication.cs b/src/ZB.MOM.WW.MxGateway.Server/GatewayApplication.cs
index 3483d37..60abdc2 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/GatewayApplication.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/GatewayApplication.cs
@@ -65,7 +65,7 @@ public static class GatewayApplication
builder.AddZbSerilog(o => o.ServiceName = "mxgateway");
- builder.Services.AddGatewayConfiguration();
+ builder.Services.AddGatewayConfiguration(builder.Configuration);
builder.Services.AddSqliteAuthStore();
builder.Services.AddGatewayGrpcAuthorization();
builder.Services.AddHealthChecks()
diff --git a/src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj b/src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj
index 855acc4..b36e4a2 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj
+++ b/src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj
@@ -6,6 +6,7 @@
+
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsTests.cs
index fcb4af9..2a5aac4 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsTests.cs
@@ -132,7 +132,7 @@ public sealed class GatewayOptionsTests
ServiceCollection services = new();
services.AddSingleton(configuration);
- services.AddGatewayConfiguration();
+ services.AddGatewayConfiguration(configuration);
return services.BuildServiceProvider(validateScopes: true);
}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyAdminCliRunnerTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyAdminCliRunnerTests.cs
index 4cecdc0..173d4b4 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyAdminCliRunnerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyAdminCliRunnerTests.cs
@@ -245,7 +245,7 @@ public sealed class ApiKeyAdminCliRunnerTests : IDisposable
ServiceCollection services = new();
services.AddSingleton(configuration);
- services.AddGatewayConfiguration();
+ services.AddGatewayConfiguration(configuration);
services.AddSqliteAuthStore();
return services.BuildServiceProvider(validateScopes: true);
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/SqliteAuthStoreTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/SqliteAuthStoreTests.cs
index fd8076d..985ef00 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/SqliteAuthStoreTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/SqliteAuthStoreTests.cs
@@ -188,7 +188,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
ServiceCollection services = new();
services.AddSingleton(configuration);
- services.AddGatewayConfiguration();
+ services.AddGatewayConfiguration(configuration);
services.AddSqliteAuthStore();
return services.BuildServiceProvider(validateScopes: true);