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(); + } +}