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,69 @@
using ZB.MOM.WW.Configuration;
namespace ZB.MOM.WW.ScadaBridge.Communication;
/// <summary>
/// Validates <see cref="CommunicationOptions"/> at startup (ValidateOnStart) so a
/// malformed "Communication" appsettings section fails fast at boot with a
/// key-naming message instead of surfacing later at first Ask/gRPC use. Every
/// timeout feeds a per-pattern Ask deadline or a gRPC keepalive/stream-lifetime
/// setting, and a zero/negative value there produces an opaque runtime failure
/// far from the offending config key.
/// </summary>
public sealed class CommunicationOptionsValidator : OptionsValidatorBase<CommunicationOptions>
{
/// <inheritdoc />
protected override void Validate(ValidationBuilder builder, CommunicationOptions options)
{
builder.RequireThat(options.DeploymentTimeout > TimeSpan.Zero,
$"Communication:DeploymentTimeout must be a positive duration (was {options.DeploymentTimeout}).");
builder.RequireThat(options.LifecycleTimeout > TimeSpan.Zero,
$"Communication:LifecycleTimeout must be a positive duration (was {options.LifecycleTimeout}).");
builder.RequireThat(options.ArtifactDeploymentTimeout > TimeSpan.Zero,
$"Communication:ArtifactDeploymentTimeout must be a positive duration (was {options.ArtifactDeploymentTimeout}).");
builder.RequireThat(options.QueryTimeout > TimeSpan.Zero,
$"Communication:QueryTimeout must be a positive duration (was {options.QueryTimeout}).");
builder.RequireThat(options.IntegrationTimeout > TimeSpan.Zero,
$"Communication:IntegrationTimeout must be a positive duration (was {options.IntegrationTimeout}).");
builder.RequireThat(options.DebugViewTimeout > TimeSpan.Zero,
$"Communication:DebugViewTimeout must be a positive duration (was {options.DebugViewTimeout}).");
builder.RequireThat(options.HealthReportTimeout > TimeSpan.Zero,
$"Communication:HealthReportTimeout must be a positive duration (was {options.HealthReportTimeout}).");
builder.RequireThat(options.NotificationForwardTimeout > TimeSpan.Zero,
$"Communication:NotificationForwardTimeout must be a positive duration (was {options.NotificationForwardTimeout}).");
builder.RequireThat(options.GrpcKeepAlivePingDelay > TimeSpan.Zero,
$"Communication:GrpcKeepAlivePingDelay must be a positive duration (was {options.GrpcKeepAlivePingDelay}).");
builder.RequireThat(options.GrpcKeepAlivePingTimeout > TimeSpan.Zero,
$"Communication:GrpcKeepAlivePingTimeout must be a positive duration (was {options.GrpcKeepAlivePingTimeout}).");
builder.RequireThat(options.GrpcMaxStreamLifetime > TimeSpan.Zero,
$"Communication:GrpcMaxStreamLifetime must be a positive duration (was {options.GrpcMaxStreamLifetime}).");
builder.RequireThat(options.TransportHeartbeatInterval > TimeSpan.Zero,
$"Communication:TransportHeartbeatInterval must be a positive duration (was {options.TransportHeartbeatInterval}).");
builder.RequireThat(options.ApplicationHeartbeatInterval > TimeSpan.Zero,
$"Communication:ApplicationHeartbeatInterval must be a positive duration (was {options.ApplicationHeartbeatInterval}).");
builder.RequireThat(options.TransportFailureThreshold > TimeSpan.Zero,
$"Communication:TransportFailureThreshold must be a positive duration (was {options.TransportFailureThreshold}).");
builder.RequireThat(options.PendingDeploymentTtl > TimeSpan.Zero,
$"Communication:PendingDeploymentTtl must be a positive duration (was {options.PendingDeploymentTtl}).");
builder.RequireThat(options.PendingDeploymentPurgeInterval > TimeSpan.Zero,
$"Communication:PendingDeploymentPurgeInterval must be a positive duration (was {options.PendingDeploymentPurgeInterval}).");
builder.RequireThat(options.GrpcMaxConcurrentStreams > 0,
$"Communication:GrpcMaxConcurrentStreams must be positive (was {options.GrpcMaxConcurrentStreams}).");
}
}
@@ -1,4 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
namespace ZB.MOM.WW.ScadaBridge.Communication;
@@ -11,7 +13,10 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddCommunication(this IServiceCollection services)
{
services.AddOptions<CommunicationOptions>()
.BindConfiguration("Communication");
.BindConfiguration("Communication")
.ValidateOnStart();
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IValidateOptions<CommunicationOptions>, CommunicationOptionsValidator>());
services.AddSingleton<CommunicationService>();
services.AddSingleton<SiteStreamGrpcClientFactory>();
@@ -24,6 +24,7 @@
<PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.Net.Client" />
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
<PackageReference Include="ZB.MOM.WW.Configuration" />
</ItemGroup>
<ItemGroup>