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