From 81aff0374862e39195259aeaa4d870b4c19e804f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 10:28:44 -0400 Subject: [PATCH] feat: HistorianSession browse + metadata on the reused session (+ env-gated round-trip) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browse mirrors ReadRawAsync (collect-then-yield); metadata mirrors ReadAtTimeAsync (unary). Delegates to the HistorianGrpcTagClient …OnSession seams so a leased session browses + reads metadata without re-handshaking (pending.md A1 broadening). Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii --- .../HistorianSession.cs | 24 ++++++++++++++++++- .../HistorianSessionRoundTripTests.cs | 16 ++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/AVEVA.Historian.Client/HistorianSession.cs b/src/AVEVA.Historian.Client/HistorianSession.cs index 120bbe6..228fc75 100644 --- a/src/AVEVA.Historian.Client/HistorianSession.cs +++ b/src/AVEVA.Historian.Client/HistorianSession.cs @@ -7,7 +7,7 @@ namespace AVEVA.Historian.Client; /// A live, reusable authenticated Historian session: holds one gRPC connection + one /// OpenConnection handle and runs ops on them WITHOUT re-handshaking. Reuse across ops amortizes the /// auth handshake. Idle-expires server-side in ~20-25s — callers keep it warm (PingAsync) or re-open. -/// Reads/historical-write/tag-write/status only; events are NOT exposed (separate channel+auth). +/// Reads, browse/metadata, historical-write, tag-write and status; events are NOT exposed (separate channel+auth). public sealed class HistorianSession : IAsyncDisposable { private readonly HistorianGrpcConnection _connection; @@ -89,6 +89,28 @@ public sealed class HistorianSession : IAsyncDisposable () => orch.RunAtTimeOnSession(_connection, _session.ClientHandle, tag, timestampsUtc, ct), ct); } + // --- browse / metadata (call the …OnSession seams, which take the full Session for the string handle) --- + + /// Browses tag names matching on the held session. + public async IAsyncEnumerable BrowseTagNamesAsync( + string filter = "*", + [EnumeratorCancellation] CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + List names = await Task.Run( + () => HistorianGrpcTagClient.BrowseTagNamesOnSession(_connection, _session, filter, _options, ct), ct) + .ConfigureAwait(false); + foreach (string name in names) + { + ct.ThrowIfCancellationRequested(); + yield return name; + } + } + + /// Reads metadata for on the held session (null if unknown). + public Task GetTagMetadataAsync(string tag, CancellationToken ct = default) + => Task.Run(() => HistorianGrpcTagClient.GetTagMetadataOnSession(_connection, _session, tag, _options, ct), ct); + // --- writes (the …OnSession seams take the full Session, since the historical write keys on the // string handle + tag GUID and the tag-config ops mix string/uint handles) --- diff --git a/tests/AVEVA.Historian.Client.Tests/HistorianSessionRoundTripTests.cs b/tests/AVEVA.Historian.Client.Tests/HistorianSessionRoundTripTests.cs index 1cfce4d..37c4c90 100644 --- a/tests/AVEVA.Historian.Client.Tests/HistorianSessionRoundTripTests.cs +++ b/tests/AVEVA.Historian.Client.Tests/HistorianSessionRoundTripTests.cs @@ -53,7 +53,21 @@ public sealed class HistorianSessionRoundTripTests await session.PingAsync(CancellationToken.None); // must not throw _output.WriteLine("4) ping -> ok"); - _output.WriteLine("session round-trip OK (write+read+status+ping on one session)"); + // 5) metadata + browse on the SAME session (no re-handshake) + HistorianTagMetadata? meta = await session.GetTagMetadataAsync(sandboxTag, CancellationToken.None); + Assert.NotNull(meta); + _output.WriteLine("5) metadata -> ok"); + + List browsed = []; + await foreach (string n in session.BrowseTagNamesAsync("*", CancellationToken.None)) + { + browsed.Add(n); + if (browsed.Count >= 5) break; + } + Assert.NotEmpty(browsed); + _output.WriteLine($"6) browse rows={browsed.Count}"); + + _output.WriteLine("session round-trip OK (write+read+status+ping+metadata+browse on one session)"); } // verbatim copy of BuildOptions from HandshakeReuseSpikeTests