diff --git a/ZB.MOM.WW.Configuration/src/ZB.MOM.WW.Configuration/ServiceCollectionExtensions.cs b/ZB.MOM.WW.Configuration/src/ZB.MOM.WW.Configuration/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..050b2fd
--- /dev/null
+++ b/ZB.MOM.WW.Configuration/src/ZB.MOM.WW.Configuration/ServiceCollectionExtensions.cs
@@ -0,0 +1,36 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+namespace ZB.MOM.WW.Configuration;
+
+/// DI extensions for binding-and-validating an options section in one call.
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Binds to the configuration section at
+ /// , registers as its
+ /// , and enables ValidateOnStart so a bad
+ /// configuration fails fast at host startup rather than on first use.
+ ///
+ /// The options type to bind and validate.
+ /// The validator registered for .
+ /// The service collection.
+ /// The configuration to bind from.
+ /// The configuration section path (e.g. "ScadaBridge:Cluster").
+ /// The for further chaining.
+ public static OptionsBuilder AddValidatedOptions(
+ this IServiceCollection services, IConfiguration configuration, string sectionPath)
+ where TOptions : class
+ where TValidator : class, IValidateOptions
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(configuration);
+ ArgumentException.ThrowIfNullOrWhiteSpace(sectionPath);
+
+ services.AddSingleton, TValidator>();
+ return services.AddOptions()
+ .Bind(configuration.GetSection(sectionPath))
+ .ValidateOnStart();
+ }
+}
diff --git a/ZB.MOM.WW.Configuration/tests/ZB.MOM.WW.Configuration.Tests/AddValidatedOptionsTests.cs b/ZB.MOM.WW.Configuration/tests/ZB.MOM.WW.Configuration.Tests/AddValidatedOptionsTests.cs
new file mode 100644
index 0000000..13d7d18
--- /dev/null
+++ b/ZB.MOM.WW.Configuration/tests/ZB.MOM.WW.Configuration.Tests/AddValidatedOptionsTests.cs
@@ -0,0 +1,47 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
+using ZB.MOM.WW.Configuration;
+
+namespace ZB.MOM.WW.Configuration.Tests;
+
+public sealed class AddValidatedOptionsTests
+{
+ private sealed class NodeOptions { public int Port { get; set; } public string? Name { get; set; } }
+
+ private sealed class NodeValidator : OptionsValidatorBase
+ {
+ protected override void Validate(ValidationBuilder v, NodeOptions o)
+ {
+ v.Port(o.Port, "Node:Port");
+ v.Required(o.Name, "Node:Name");
+ }
+ }
+
+ private static IHost BuildHost(Dictionary config)
+ {
+ var builder = Host.CreateApplicationBuilder();
+ builder.Configuration.AddInMemoryCollection(config);
+ builder.Services.AddValidatedOptions(builder.Configuration, "Node");
+ return builder.Build();
+ }
+
+ [Fact]
+ public async Task Bad_config_throws_at_startup()
+ {
+ using var host = BuildHost(new() { ["Node:Port"] = "0", ["Node:Name"] = "" });
+ await Assert.ThrowsAsync(() => host.StartAsync());
+ }
+
+ [Fact]
+ public async Task Good_config_starts_and_binds()
+ {
+ using var host = BuildHost(new() { ["Node:Port"] = "8080", ["Node:Name"] = "central" });
+ await host.StartAsync();
+ var opts = host.Services.GetRequiredService>().Value;
+ Assert.Equal(8080, opts.Port);
+ Assert.Equal("central", opts.Name);
+ await host.StopAsync();
+ }
+}