feat(options): eager startup validation for Communication + DataConnectionLayer options (arch-review 08 §1.5)

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 06:48:06 -04:00
parent 187fbd7cc8
commit af2cfde484
8 changed files with 203 additions and 2 deletions
@@ -0,0 +1,39 @@
using ZB.MOM.WW.Configuration;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer;
/// <summary>
/// Validates <see cref="DataConnectionOptions"/> at startup (ValidateOnStart) so a
/// malformed "DataConnectionLayer" appsettings section fails fast at boot with a
/// key-naming message instead of surfacing later at runtime. The intervals drive
/// the reconnect/tag-resolution timers and the seed-read retry loop; a
/// zero/negative duration (or a non-positive <c>SeedReadMaxAttempts</c>) would
/// otherwise produce an opaque failure far from the offending config key.
/// </summary>
public sealed class DataConnectionOptionsValidator : OptionsValidatorBase<DataConnectionOptions>
{
/// <inheritdoc />
protected override void Validate(ValidationBuilder builder, DataConnectionOptions options)
{
builder.RequireThat(options.ReconnectInterval > TimeSpan.Zero,
$"DataConnectionLayer:ReconnectInterval must be a positive duration (was {options.ReconnectInterval}).");
builder.RequireThat(options.TagResolutionRetryInterval > TimeSpan.Zero,
$"DataConnectionLayer:TagResolutionRetryInterval must be a positive duration (was {options.TagResolutionRetryInterval}).");
builder.RequireThat(options.WriteTimeout > TimeSpan.Zero,
$"DataConnectionLayer:WriteTimeout must be a positive duration (was {options.WriteTimeout}).");
builder.RequireThat(options.SeedReadTimeout > TimeSpan.Zero,
$"DataConnectionLayer:SeedReadTimeout must be a positive duration (was {options.SeedReadTimeout}).");
builder.RequireThat(options.StableConnectionThreshold > TimeSpan.Zero,
$"DataConnectionLayer:StableConnectionThreshold must be a positive duration (was {options.StableConnectionThreshold}).");
builder.RequireThat(options.SeedReadRetryDelay > TimeSpan.Zero,
$"DataConnectionLayer:SeedReadRetryDelay must be a positive duration (was {options.SeedReadRetryDelay}).");
builder.RequireThat(options.SeedReadMaxAttempts > 0,
$"DataConnectionLayer:SeedReadMaxAttempts must be positive (was {options.SeedReadMaxAttempts}).");
}
}
@@ -1,4 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer;
@@ -13,8 +15,14 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddDataConnectionLayer(this IServiceCollection services)
{
services.AddOptions<DataConnectionOptions>()
.BindConfiguration("DataConnectionLayer");
.BindConfiguration("DataConnectionLayer")
.ValidateOnStart();
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IValidateOptions<DataConnectionOptions>, DataConnectionOptionsValidator>());
// OpcUaGlobalOptions / MxGatewayGlobalOptions carry only string identity/path
// fields (no numeric or duration knobs), so there is nothing to range-validate
// — no dedicated validator is registered for them.
services.AddOptions<OpcUaGlobalOptions>()
.BindConfiguration("OpcUa");
@@ -17,6 +17,7 @@
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" />
<PackageReference Include="ZB.MOM.WW.MxGateway.Client" />
<PackageReference Include="ZB.MOM.WW.Configuration" />
</ItemGroup>
<ItemGroup>