diff --git a/ZB.MOM.WW.SPHistorianClient/Directory.Packages.props b/ZB.MOM.WW.SPHistorianClient/Directory.Packages.props index ab763ff..ead2913 100644 --- a/ZB.MOM.WW.SPHistorianClient/Directory.Packages.props +++ b/ZB.MOM.WW.SPHistorianClient/Directory.Packages.props @@ -20,6 +20,7 @@ + diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/DependencyInjection/ZbSpHistorianClientServiceCollectionExtensions.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/DependencyInjection/ZbSpHistorianClientServiceCollectionExtensions.cs new file mode 100644 index 0000000..a8e231d --- /dev/null +++ b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/DependencyInjection/ZbSpHistorianClientServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace ZB.MOM.WW.SPHistorianClient; + +/// +/// ZB.MOM.WW DI registration for . Mirrors the family's +/// AddZb* convention. Because is required/ +/// init-only, callers pass a fully-built options instance (bind it from configuration in the +/// consuming app, e.g. config.GetSection("Historian").Get<HistorianClientOptions>()). +/// +public static class ZbSpHistorianClientServiceCollectionExtensions +{ + public static IServiceCollection AddZbSpHistorianClient( + this IServiceCollection services, + HistorianClientOptions options) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(options); + if (string.IsNullOrWhiteSpace(options.Host)) + { + throw new ArgumentException( + "HistorianClientOptions.Host must be set.", nameof(options)); + } + + services.AddSingleton(options); + // HistorianClient opens a fresh channel per operation and has a no-op DisposeAsync, + // so transient is safe and avoids assuming the shared dialect is concurrency-safe. + services.AddTransient(); + return services; + } +} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/DependencyInjectionTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/DependencyInjectionTests.cs new file mode 100644 index 0000000..1b36707 --- /dev/null +++ b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/DependencyInjectionTests.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.DependencyInjection; +using ZB.MOM.WW.SPHistorianClient; + +namespace ZB.MOM.WW.SPHistorianClient.Tests; + +public class DependencyInjectionTests +{ + [Fact] + public async Task AddZbSpHistorianClient_resolves_client_and_options() + { + var services = new ServiceCollection(); + var options = new HistorianClientOptions { Host = "localhost" }; + + services.AddZbSpHistorianClient(options); + + // HistorianClient is IAsyncDisposable-only, so the container must be disposed + // asynchronously (a synchronous `using` throws InvalidOperationException). + await using var sp = services.BuildServiceProvider(); + Assert.Same(options, sp.GetRequiredService()); + Assert.NotNull(sp.GetRequiredService()); + } + + [Fact] + public void AddZbSpHistorianClient_throws_when_host_missing() + { + var services = new ServiceCollection(); + var options = new HistorianClientOptions { Host = "" }; + + Assert.Throws(() => services.AddZbSpHistorianClient(options)); + } + + [Fact] + public void AddZbSpHistorianClient_throws_on_null_options() + { + var services = new ServiceCollection(); + Assert.Throws(() => services.AddZbSpHistorianClient(null!)); + } +} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/ZB.MOM.WW.SPHistorianClient.Tests.csproj b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/ZB.MOM.WW.SPHistorianClient.Tests.csproj index e593f46..011e716 100644 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/ZB.MOM.WW.SPHistorianClient.Tests.csproj +++ b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/ZB.MOM.WW.SPHistorianClient.Tests.csproj @@ -6,6 +6,7 @@ +