feat(sphistorianclient): add AddZbSpHistorianClient DI extension

This commit is contained in:
Joseph Doherty
2026-06-19 05:53:56 -04:00
parent 8033a7f12d
commit 81bf7322f0
4 changed files with 71 additions and 0 deletions
@@ -20,6 +20,7 @@
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
<!-- Test -->
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
@@ -0,0 +1,31 @@
using Microsoft.Extensions.DependencyInjection;
namespace ZB.MOM.WW.SPHistorianClient;
/// <summary>
/// ZB.MOM.WW DI registration for <see cref="HistorianClient"/>. Mirrors the family's
/// <c>AddZb*</c> convention. Because <see cref="HistorianClientOptions"/> is <c>required</c>/
/// <c>init</c>-only, callers pass a fully-built options instance (bind it from configuration in the
/// consuming app, e.g. <c>config.GetSection("Historian").Get&lt;HistorianClientOptions&gt;()</c>).
/// </summary>
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<HistorianClient>();
return services;
}
}
@@ -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<HistorianClientOptions>());
Assert.NotNull(sp.GetRequiredService<HistorianClient>());
}
[Fact]
public void AddZbSpHistorianClient_throws_when_host_missing()
{
var services = new ServiceCollection();
var options = new HistorianClientOptions { Host = "" };
Assert.Throws<ArgumentException>(() => services.AddZbSpHistorianClient(options));
}
[Fact]
public void AddZbSpHistorianClient_throws_on_null_options()
{
var services = new ServiceCollection();
Assert.Throws<ArgumentNullException>(() => services.AddZbSpHistorianClient(null!));
}
}
@@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.Data.SqlClient" />
<PackageReference Include="xunit" />