docs(sphistorianclient): implementation plan + task tracking
This commit is contained in:
@@ -0,0 +1,569 @@
|
||||
# ZB.MOM.WW.SPHistorianClient Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans (or subagent-driven-development) to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Repackage the proven, pure-managed .NET 10 `AVEVA.Historian.Client` SDK from the migration bundle as the family-branded shared library `ZB.MOM.WW.SPHistorianClient`, following the same conventions as the other `ZB.MOM.WW.*` libraries in this repo.
|
||||
|
||||
**Architecture:** This is a **port + rebrand**, not a rewrite. Copy the SDK `src/` and `tests/` into a new `ZB.MOM.WW.SPHistorianClient/` directory, rewrite the C# root namespace `AVEVA.Historian.Client` → `ZB.MOM.WW.SPHistorianClient` (leaving the proto-generated `ArchestrA.Grpc.Contract.*` wire contracts untouched), adopt ZB conventions (`Directory.Build.props` / `Directory.Packages.props` central package management, `.slnx`, `CLAUDE.md`/`README.md`), drop the non-shippable reverse-engineering tooling and proprietary decompilations, add one ZB-idiomatic DI extension, then build/test/pack.
|
||||
|
||||
**Tech Stack:** .NET 10, C# (net10.0), WCF/MDAS (`System.ServiceModel.*`, Windows-only transports), gRPC (`Grpc.Net.Client` + `Grpc.Tools`, cross-platform 2023 R2 transport), xUnit. Central package management.
|
||||
|
||||
**Design doc:** `docs/plans/2026-06-19-sphistorianclient-design.md`
|
||||
|
||||
**Branch:** `feat/sphistorianclient` (already created; design doc already committed at `bbb7942`).
|
||||
|
||||
---
|
||||
|
||||
## Source bundle location (read-only inputs)
|
||||
|
||||
The SDK source lives in an extracted bundle under `/tmp`:
|
||||
|
||||
- Extracted root: `/tmp/histsdk/extracted/histsdk-migration/histsdk/`
|
||||
- SDK source: `…/histsdk/src/AVEVA.Historian.Client/` — **74 `.cs` + 6 `.proto`**
|
||||
- SDK tests: `…/histsdk/tests/AVEVA.Historian.Client.Tests/` — **25 `.cs`**
|
||||
- Re-extract fallback (if `/tmp` was cleaned): `cd /tmp/histsdk && unzip -o -q histsdk-migration.zip -d extracted`
|
||||
|
||||
**Never copy:** `tools/` (RE harnesses, .NET Framework + native AVEVA refs), `analysis-2023r2/decompiled/` (proprietary, non-redistributable), `scripts/`, `docs/reverse-engineering/` (identity-bearing captures), `bin/`/`obj/`, the bundle's `.git/`, and the bundle's original `.csproj` files (we author fresh ZB ones).
|
||||
|
||||
**Gotchas baked into this plan (from prior repo experience):**
|
||||
- Do **not** set `TreatWarningsAsErrors` — the WCF/SSPI code carries `[SupportedOSPlatform("windows")]` and will emit CA platform warnings on macOS that must stay warnings.
|
||||
- Central package management means **no inline `Version=` on any `PackageReference`** (that is `NU1008`). All versions live in `Directory.Packages.props`.
|
||||
- `Microsoft.Data.SqlClient` may surface an `NU1903` advisory on restore. Without `TreatWarningsAsErrors` it is a warning. If a restore ever hard-fails on it, add `-p:NuGetAudit=false` to the build/test command.
|
||||
- macOS `sed -i` requires an explicit empty backup arg: `sed -i '' 's/…/…/g'`.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Scaffold the library skeleton
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~4 min
|
||||
**Parallelizable with:** none (every later task depends on this)
|
||||
|
||||
**Files:**
|
||||
- Create: `ZB.MOM.WW.SPHistorianClient/Directory.Build.props`
|
||||
- Create: `ZB.MOM.WW.SPHistorianClient/Directory.Packages.props`
|
||||
- Create: `ZB.MOM.WW.SPHistorianClient/.gitignore`
|
||||
- Create: `ZB.MOM.WW.SPHistorianClient/ZB.MOM.WW.SPHistorianClient.slnx`
|
||||
- Create: `ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/ZB.MOM.WW.SPHistorianClient.csproj`
|
||||
- Create: `ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/ZB.MOM.WW.SPHistorianClient.Tests.csproj`
|
||||
|
||||
**Step 1: `Directory.Build.props`** (mirrors `ZB.MOM.WW.Telemetry/Directory.Build.props`)
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.0</Version>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
```
|
||||
|
||||
**Step 2: `Directory.Packages.props`** (versions lifted verbatim from the bundle's two `.csproj` files)
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Historian SDK runtime deps (WCF/MDAS transports — Windows-only at runtime) -->
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.7" />
|
||||
<PackageVersion Include="System.ServiceModel.NetNamedPipe" Version="10.0.652802" />
|
||||
<PackageVersion Include="System.ServiceModel.NetTcp" Version="10.0.652802" />
|
||||
|
||||
<!-- 2023 R2 gRPC transport (cross-platform) -->
|
||||
<PackageVersion Include="Google.Protobuf" Version="3.24.4" />
|
||||
<PackageVersion Include="Grpc.Net.Client" Version="2.58.0" />
|
||||
<PackageVersion Include="Grpc.Net.Client.Web" Version="2.58.0" />
|
||||
<PackageVersion Include="Grpc.Tools" Version="2.59.0" />
|
||||
|
||||
<!-- ZB-idiomatic DI extension (only non-BCL lib dependency) -->
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
|
||||
|
||||
<!-- Test -->
|
||||
<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" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
```
|
||||
|
||||
**Step 3: `.gitignore`**
|
||||
|
||||
```gitignore
|
||||
bin/
|
||||
obj/
|
||||
# identity-bearing / non-redistributable — never commit
|
||||
*.ndjson
|
||||
current/
|
||||
aveva-install-*/
|
||||
```
|
||||
|
||||
**Step 4: `ZB.MOM.WW.SPHistorianClient.slnx`**
|
||||
|
||||
```xml
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/ZB.MOM.WW.SPHistorianClient/ZB.MOM.WW.SPHistorianClient.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/ZB.MOM.WW.SPHistorianClient.Tests/ZB.MOM.WW.SPHistorianClient.Tests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
```
|
||||
|
||||
**Step 5: `src/ZB.MOM.WW.SPHistorianClient/ZB.MOM.WW.SPHistorianClient.csproj`**
|
||||
|
||||
(Derived from the bundle's `AVEVA.Historian.Client.csproj`: inline versions removed for central
|
||||
management; ZB package metadata added; `InternalsVisibleTo` retargeted to the ZB test assembly and
|
||||
the `…ReverseEngineering` one dropped; proto glob uses forward slashes for cross-platform MSBuild.)
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>true</IsPackable>
|
||||
<PackageId>ZB.MOM.WW.SPHistorianClient</PackageId>
|
||||
<Authors>ZB.MOM.WW</Authors>
|
||||
<Description>Pure-managed .NET 10 client for AVEVA System Platform Historian (Wonderware) for the ZB.MOM.WW SCADA family. The wire protocol is reverse-engineered and re-implemented in C# — no native AVEVA runtime dependency. Surfaces history reads (raw / aggregate / at-time / event), tag browse + metadata, status, and tag create/delete over the WCF/MDAS transports (Windows) plus a cross-platform gRPC transport for 2023 R2.</Description>
|
||||
<PackageTags>aveva;wonderware;historian;system-platform;scada;timeseries;grpc;wcf;zb-mom-ww</PackageTags>
|
||||
<PackageProjectUrl>https://gitea.dohertylan.com/dohertj2/zb-mom-ww-sphistorianclient</PackageProjectUrl>
|
||||
<RepositoryUrl>https://gitea.dohertylan.com/dohertj2/zb-mom-ww-sphistorianclient</RepositoryUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Security.Cryptography.Xml" />
|
||||
<PackageReference Include="System.ServiceModel.NetNamedPipe" />
|
||||
<PackageReference Include="System.ServiceModel.NetTcp" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 2023 R2 gRPC transport (RemoteGrpc). Pure-managed: Grpc.Net.Client + Google.Protobuf.
|
||||
Grpc.Tools is build-only (PrivateAssets=all) and generates the client stubs from the
|
||||
recovered contract under Grpc/Protos at build. -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Protobuf" />
|
||||
<PackageReference Include="Grpc.Net.Client" />
|
||||
<PackageReference Include="Grpc.Net.Client.Web" />
|
||||
<PackageReference Include="Grpc.Tools">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Grpc/Protos/*.proto" GrpcServices="Client" ProtoRoot="Grpc/Protos" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
<_Parameter1>ZB.MOM.WW.SPHistorianClient.Tests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
```
|
||||
|
||||
**Step 6: `tests/ZB.MOM.WW.SPHistorianClient.Tests/ZB.MOM.WW.SPHistorianClient.Tests.csproj`**
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.SPHistorianClient\ZB.MOM.WW.SPHistorianClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
```
|
||||
|
||||
**Step 7: Verify the skeleton is well-formed (build will fail — no sources yet — that is expected)**
|
||||
|
||||
Run: `cd ZB.MOM.WW.SPHistorianClient && dotnet restore ZB.MOM.WW.SPHistorianClient.slnx`
|
||||
Expected: restore **succeeds** (proves the props/csproj XML and central package versions resolve). A
|
||||
follow-up `dotnet build` would fail only because no `.cs` exist yet — do not build here.
|
||||
|
||||
**Step 8: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/dohertj2/Desktop/scadaproj
|
||||
git add ZB.MOM.WW.SPHistorianClient/
|
||||
git commit -m "feat(sphistorianclient): scaffold shared library skeleton (props, csprojs, slnx)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Port source + tests with namespace rewrite
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~4 min
|
||||
**Parallelizable with:** none
|
||||
**Blocked by:** Task 1
|
||||
|
||||
**Files:**
|
||||
- Create (scripted copy): `ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/**/*.{cs,proto}` (74 `.cs` + 6 `.proto`)
|
||||
- Create (scripted copy): `ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/**/*.cs` (25 `.cs`)
|
||||
|
||||
This task is a deterministic copy + namespace rewrite — run the script, then verify counts.
|
||||
|
||||
**Step 1: Copy + rewrite (single script)**
|
||||
|
||||
```bash
|
||||
set -euo pipefail
|
||||
BUNDLE=/tmp/histsdk/extracted/histsdk-migration/histsdk
|
||||
DEST=/Users/dohertj2/Desktop/scadaproj/ZB.MOM.WW.SPHistorianClient
|
||||
|
||||
# Guard: re-extract if /tmp was cleaned
|
||||
if [ ! -d "$BUNDLE/src/AVEVA.Historian.Client" ]; then
|
||||
cd /tmp/histsdk && unzip -o -q histsdk-migration.zip -d extracted
|
||||
fi
|
||||
|
||||
# --- src: copy .cs + .proto, preserving subdirs (NOT the old .csproj) ---
|
||||
SRC="$DEST/src/ZB.MOM.WW.SPHistorianClient"
|
||||
cd "$BUNDLE/src/AVEVA.Historian.Client"
|
||||
find . \( -name '*.cs' -o -name '*.proto' \) | while read -r f; do
|
||||
mkdir -p "$SRC/$(dirname "$f")"
|
||||
cp "$f" "$SRC/$f"
|
||||
done
|
||||
|
||||
# --- tests: copy .cs only (NOT the old .csproj) ---
|
||||
TST="$DEST/tests/ZB.MOM.WW.SPHistorianClient.Tests"
|
||||
cd "$BUNDLE/tests/AVEVA.Historian.Client.Tests"
|
||||
find . -name '*.cs' | while read -r f; do
|
||||
mkdir -p "$TST/$(dirname "$f")"
|
||||
cp "$f" "$TST/$f"
|
||||
done
|
||||
|
||||
# --- namespace rewrite in .cs ONLY (proto wire contracts stay ArchestrA.Grpc.Contract.*) ---
|
||||
find "$SRC" "$TST" -name '*.cs' -print0 \
|
||||
| xargs -0 sed -i '' 's/AVEVA\.Historian\.Client/ZB.MOM.WW.SPHistorianClient/g'
|
||||
```
|
||||
|
||||
**Step 2: Verify counts and that the rename is total**
|
||||
|
||||
```bash
|
||||
DEST=/Users/dohertj2/Desktop/scadaproj/ZB.MOM.WW.SPHistorianClient
|
||||
echo "src cs: $(find "$DEST/src" -name '*.cs' | wc -l) (expect 74)"
|
||||
echo "src proto: $(find "$DEST/src" -name '*.proto' | wc -l) (expect 6)"
|
||||
echo "test cs: $(find "$DEST/tests" -name '*.cs' | wc -l) (expect 25)"
|
||||
echo "leftover AVEVA.Historian.Client in .cs: $(grep -rl 'AVEVA\.Historian\.Client' "$DEST" --include='*.cs' | wc -l) (expect 0)"
|
||||
echo "proto namespace preserved: $(grep -l 'ArchestrA.Grpc.Contract' "$DEST"/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/*.proto | wc -l) (expect 6)"
|
||||
```
|
||||
|
||||
Expected: `74`, `6`, `25`, `0`, `6`. If "leftover" is non-zero, inspect those files — the only legitimate
|
||||
remaining mentions would be inside comments/strings that happen to differ in casing/spacing; a clean
|
||||
port should show `0`.
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/dohertj2/Desktop/scadaproj
|
||||
git add ZB.MOM.WW.SPHistorianClient/src ZB.MOM.WW.SPHistorianClient/tests
|
||||
git commit -m "feat(sphistorianclient): port SDK source + tests, rebrand namespace to ZB.MOM.WW.SPHistorianClient"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Build + test green
|
||||
|
||||
**Classification:** high-risk
|
||||
**Estimated implement time:** ~5 min (plus restore/build wall-time)
|
||||
**Parallelizable with:** none
|
||||
**Blocked by:** Task 2
|
||||
|
||||
This is the integration gate. The port must compile and the offline test suite must pass on this macOS host.
|
||||
|
||||
**Files:**
|
||||
- Modify (only if the build surfaces a defect): any ported file under `ZB.MOM.WW.SPHistorianClient/src` or `…/tests`, or the two `.csproj`.
|
||||
|
||||
**Step 1: Build**
|
||||
|
||||
Run: `cd ZB.MOM.WW.SPHistorianClient && dotnet build ZB.MOM.WW.SPHistorianClient.slnx`
|
||||
Expected: **Build succeeded.** Platform-compatibility (CAxxxx `[SupportedOSPlatform("windows")]`) warnings
|
||||
are acceptable and must remain warnings. If restore hard-fails on `NU1903`, re-run with
|
||||
`-p:NuGetAudit=false`.
|
||||
|
||||
**Step 2: Test**
|
||||
|
||||
Run: `dotnet test ZB.MOM.WW.SPHistorianClient.slnx`
|
||||
Expected: all tests pass; the live integration tests (`HistorianClientIntegrationTests`,
|
||||
`HistorianGrpcIntegrationTests`, `RemoteTcpIntegrationTests`) **skip cleanly** because no `HISTORIAN_*`
|
||||
env vars are set. The bundle's `MIGRATION-README.md` documents ~188 tests passing on macOS with the
|
||||
live ones skipped — treat a comparable count with **zero failures** as success.
|
||||
|
||||
**Step 3: Triage rules (if not green)**
|
||||
- Compile error referencing `AVEVA.Historian.Client` → a file was missed by the rewrite; re-run the
|
||||
Task 2 sed on that file.
|
||||
- `NU1008` (version on PackageReference) → an inline `Version=` slipped into a `.csproj`; remove it
|
||||
(version belongs in `Directory.Packages.props`).
|
||||
- Missing generated gRPC type (e.g. `ArchestrA.Grpc.Contract.*` not found) → confirm the `<Protobuf>`
|
||||
glob in the src `.csproj` resolves the 6 `Grpc/Protos/*.proto` and that `Grpc.Tools` restored.
|
||||
- A genuine test failure (not a skip) → this is a real port defect; fix the ported code, do **not**
|
||||
delete/weaken the test.
|
||||
|
||||
**Step 4: Commit (only if Step 3 required edits)**
|
||||
|
||||
```bash
|
||||
git add -A ZB.MOM.WW.SPHistorianClient/
|
||||
git commit -m "fix(sphistorianclient): resolve port build/test fallout"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Add the `AddZbSpHistorianClient` DI extension (TDD)
|
||||
|
||||
**Classification:** standard
|
||||
**Estimated implement time:** ~4 min
|
||||
**Parallelizable with:** Task 5
|
||||
**Blocked by:** Task 3
|
||||
|
||||
`HistorianClientOptions` uses `required` + `init`-only properties, so the extension takes a fully-built
|
||||
options instance (not an `Action<T>` configurator). It depends only on
|
||||
`Microsoft.Extensions.DependencyInjection.Abstractions`.
|
||||
|
||||
**Files:**
|
||||
- Create: `ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/DependencyInjection/ZbSpHistorianClientServiceCollectionExtensions.cs`
|
||||
- Test: `ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/DependencyInjectionTests.cs`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ZB.MOM.WW.SPHistorianClient;
|
||||
|
||||
namespace ZB.MOM.WW.SPHistorianClient.Tests;
|
||||
|
||||
public class DependencyInjectionTests
|
||||
{
|
||||
[Fact]
|
||||
public void AddZbSpHistorianClient_resolves_client_and_options()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var options = new HistorianClientOptions { Host = "localhost" };
|
||||
|
||||
services.AddZbSpHistorianClient(options);
|
||||
|
||||
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!));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run — verify it fails to compile** (`AddZbSpHistorianClient` not defined)
|
||||
|
||||
Run: `dotnet test ZB.MOM.WW.SPHistorianClient.slnx --filter "FullyQualifiedName~DependencyInjectionTests"`
|
||||
Expected: FAIL (does not compile / method missing).
|
||||
|
||||
**Step 3: Implement**
|
||||
|
||||
```csharp
|
||||
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<HistorianClientOptions>()</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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Run — verify pass**
|
||||
|
||||
Run: `dotnet test ZB.MOM.WW.SPHistorianClient.slnx --filter "FullyQualifiedName~DependencyInjectionTests"`
|
||||
Expected: PASS (3/3).
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/DependencyInjection \
|
||||
ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/DependencyInjectionTests.cs
|
||||
git commit -m "feat(sphistorianclient): add AddZbSpHistorianClient DI extension"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Author `CLAUDE.md` + `README.md`
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~4 min
|
||||
**Parallelizable with:** Task 4
|
||||
**Blocked by:** Task 3
|
||||
|
||||
Sanitized docs only — **no hostnames, credentials, customer tag names, or capture data.** Model the
|
||||
structure on `ZB.MOM.WW.Telemetry/CLAUDE.md` (overview, package table, build/test/pack commands,
|
||||
status) but adapt to a single-package library.
|
||||
|
||||
**Files:**
|
||||
- Create: `ZB.MOM.WW.SPHistorianClient/CLAUDE.md`
|
||||
- Create: `ZB.MOM.WW.SPHistorianClient/README.md`
|
||||
|
||||
**`CLAUDE.md` must cover:**
|
||||
- One-paragraph overview: pure-managed .NET 10 AVEVA System Platform Historian client, no native AVEVA
|
||||
dependency, reverse-engineered wire protocol. Ported from the `histsdk` migration bundle.
|
||||
- The supported operation surface table (copy the README table from the bundle:
|
||||
`ProbeAsync`, `ReadRawAsync`, `ReadAggregateAsync` (16 modes), `ReadAtTimeAsync`, `ReadEventsAsync`,
|
||||
`BrowseTagNamesAsync`, `GetTagMetadataAsync`, `GetConnectionStatusAsync`,
|
||||
`GetStoreForwardStatusAsync`, `GetSystemParameterAsync`, `EnsureTagAsync`, `DeleteTagAsync`).
|
||||
- Transport matrix: `LocalPipe` / `RemoteTcpIntegrated` / `RemoteTcpCertificate` (WCF, Windows-only,
|
||||
live-verified) vs `RemoteGrpc` (2023 R2, cross-platform, **not yet live-verified**).
|
||||
- Out of scope: writing samples (`AddS2` architecturally blocked), discrete/string tag creation.
|
||||
- DI: the `AddZbSpHistorianClient(options)` extension + the bind-from-config note.
|
||||
- Build/test/pack commands (from this dir):
|
||||
`dotnet build ZB.MOM.WW.SPHistorianClient.slnx` / `dotnet test …` /
|
||||
`dotnet pack ZB.MOM.WW.SPHistorianClient.slnx -c Release -o ./artifacts`.
|
||||
- Live integration tests gated by `HISTORIAN_*` env vars (skip cleanly when unset). List the env vars.
|
||||
|
||||
**`README.md`:** a trimmed public-facing version — overview, quick-start snippet (the bundle's
|
||||
`HistorianClient` usage example, namespace updated to `ZB.MOM.WW.SPHistorianClient`), supported surface
|
||||
table, build/test commands.
|
||||
|
||||
**Commit:**
|
||||
|
||||
```bash
|
||||
git add ZB.MOM.WW.SPHistorianClient/CLAUDE.md ZB.MOM.WW.SPHistorianClient/README.md
|
||||
git commit -m "docs(sphistorianclient): add CLAUDE.md + README.md"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Pack verification
|
||||
|
||||
**Classification:** small
|
||||
**Estimated implement time:** ~3 min
|
||||
**Parallelizable with:** none
|
||||
**Blocked by:** Task 4, Task 5
|
||||
|
||||
**Files:**
|
||||
- Create (build output): `ZB.MOM.WW.SPHistorianClient/artifacts/ZB.MOM.WW.SPHistorianClient.0.1.0.nupkg`
|
||||
|
||||
**Step 1: Full green build + test once more, then pack**
|
||||
|
||||
```bash
|
||||
cd /Users/dohertj2/Desktop/scadaproj/ZB.MOM.WW.SPHistorianClient
|
||||
dotnet test ZB.MOM.WW.SPHistorianClient.slnx
|
||||
dotnet pack ZB.MOM.WW.SPHistorianClient.slnx -c Release -o ./artifacts
|
||||
```
|
||||
|
||||
Expected: tests pass (live ones skip); pack produces `artifacts/ZB.MOM.WW.SPHistorianClient.0.1.0.nupkg`.
|
||||
|
||||
**Step 2: Sanity-check the package contents**
|
||||
|
||||
```bash
|
||||
unzip -l artifacts/ZB.MOM.WW.SPHistorianClient.0.1.0.nupkg | grep -E 'ZB.MOM.WW.SPHistorianClient.dll|.nuspec'
|
||||
```
|
||||
Expected: the lib DLL and nuspec are present.
|
||||
|
||||
**Step 3: Commit the nupkg** (matches the family convention — `ZB.MOM.WW.Telemetry` commits its `artifacts/*.nupkg`)
|
||||
|
||||
```bash
|
||||
cd /Users/dohertj2/Desktop/scadaproj
|
||||
git add -f ZB.MOM.WW.SPHistorianClient/artifacts/ZB.MOM.WW.SPHistorianClient.0.1.0.nupkg
|
||||
git commit -m "build(sphistorianclient): pack 0.1.0 nupkg"
|
||||
```
|
||||
|
||||
> **Do NOT push or publish** to the Gitea feed. Per repo experience, "published/adopted" claims must
|
||||
> not be made without explicit user direction + feed verification.
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Index the new library in the umbrella `CLAUDE.md` (optional)
|
||||
|
||||
**Classification:** trivial
|
||||
**Estimated implement time:** ~2 min
|
||||
**Parallelizable with:** none
|
||||
**Blocked by:** Task 6
|
||||
|
||||
**Files:**
|
||||
- Modify: `CLAUDE.md` (repo root umbrella index)
|
||||
|
||||
Add a short reference so the umbrella index reflects the newly-hosted library (the intro paragraph
|
||||
that enumerates the hosted `ZB.MOM.WW.*` sources, and/or a one-line pointer near the component table
|
||||
noting `ZB.MOM.WW.SPHistorianClient` is a net-new shared library — **not** a component normalization).
|
||||
|
||||
> **Caveat:** repo-root `CLAUDE.md` already has **pre-existing uncommitted edits** (unrelated to this
|
||||
> work). Before editing, run `git diff CLAUDE.md` and make sure your commit message reflects that it
|
||||
> may bundle those edits — or stage only the hunks you add. If this risks entangling unrelated changes,
|
||||
> skip this task and leave it for the user.
|
||||
|
||||
**Commit:**
|
||||
|
||||
```bash
|
||||
git add CLAUDE.md
|
||||
git commit -m "docs: index ZB.MOM.WW.SPHistorianClient in umbrella CLAUDE.md"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Done criteria
|
||||
|
||||
- `ZB.MOM.WW.SPHistorianClient/` exists with `src/`, `tests/`, props, `.slnx`, `CLAUDE.md`, `README.md`.
|
||||
- `dotnet build` + `dotnet test` are green on macOS (live integration tests skip cleanly).
|
||||
- `AddZbSpHistorianClient` DI extension present + tested.
|
||||
- `artifacts/ZB.MOM.WW.SPHistorianClient.0.1.0.nupkg` produced.
|
||||
- All work committed on `feat/sphistorianclient`. Not pushed/published.
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"planPath": "docs/plans/2026-06-19-sphistorianclient.md",
|
||||
"tasks": [
|
||||
{"id": 1, "subject": "Task 1: Scaffold library skeleton", "status": "pending"},
|
||||
{"id": 2, "subject": "Task 2: Port source + tests with namespace rewrite", "status": "pending", "blockedBy": [1]},
|
||||
{"id": 3, "subject": "Task 3: Build + test green", "status": "pending", "blockedBy": [2]},
|
||||
{"id": 4, "subject": "Task 4: Add AddZbSpHistorianClient DI extension (TDD)", "status": "pending", "blockedBy": [3]},
|
||||
{"id": 5, "subject": "Task 5: Author CLAUDE.md + README.md", "status": "pending", "blockedBy": [3]},
|
||||
{"id": 6, "subject": "Task 6: Pack verification", "status": "pending", "blockedBy": [4, 5]},
|
||||
{"id": 7, "subject": "Task 7: Index new lib in umbrella CLAUDE.md (optional)", "status": "pending", "blockedBy": [6]}
|
||||
],
|
||||
"lastUpdated": "2026-06-19"
|
||||
}
|
||||
Reference in New Issue
Block a user