feat(grpc): …OnSession seams on HistorianGrpcTagClient (browse + metadata)

DRY split mirroring HistorianGrpcReadOrchestrator.RunRawQueryOnSession: browse +
GetTagInfos(metadata) gain externally-supplied connection+session seams; per-call
wrappers delegate. Behaviour-preserving (pending.md A1 broadening).

Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
Joseph Doherty
2026-06-25 10:20:21 -04:00
parent be60d0b8d9
commit 2f689cbe71
2 changed files with 71 additions and 0 deletions
@@ -39,6 +39,27 @@ internal static class HistorianGrpcTagClient
private static HistorianTagMetadata? GetTagMetadata(HistorianClientOptions options, string tag, CancellationToken cancellationToken)
{
byte[] tagInfos = GetTagInfosRaw(options, [tag], cancellationToken);
return ParseTagMetadata(tagInfos);
}
// Spike/Phase-1 seam (pending.md A1): resolve tag metadata against an EXTERNALLY-supplied,
// already-authenticated connection + session — i.e. NO Create()/handshake here. The per-call
// GetTagMetadata and this seam share the parse tail (ParseTagMetadata) so neither duplicates the
// decode logic (DRY).
internal static HistorianTagMetadata? GetTagMetadataOnSession(
HistorianGrpcConnection connection,
HistorianGrpcHandshake.Session session,
string tag,
HistorianClientOptions options,
CancellationToken cancellationToken)
{
byte[] tagInfos = GetTagInfosRawOnSession(connection, session, [tag], options, cancellationToken);
return ParseTagMetadata(tagInfos);
}
// Shared parse tail for both the per-call GetTagMetadata and the reuse-path GetTagMetadataOnSession.
private static HistorianTagMetadata? ParseTagMetadata(byte[] tagInfos)
{
if (tagInfos.Length < 4)
{
return null;
@@ -69,7 +90,19 @@ internal static class HistorianGrpcTagClient
{
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken);
return GetTagInfosRawOnSession(connection, session, tags, options, cancellationToken);
}
// Spike/Phase-1 seam (pending.md A1): issue GetTagInfosFromName against an EXTERNALLY-supplied,
// already-authenticated connection + session — i.e. NO Create()/handshake here. GetTagInfosRaw
// delegates to this so the per-call path and the reuse path share one query implementation (DRY).
internal static byte[] GetTagInfosRawOnSession(
HistorianGrpcConnection connection,
HistorianGrpcHandshake.Session session,
IReadOnlyList<string> tags,
HistorianClientOptions options,
CancellationToken cancellationToken)
{
var retrievalClient = new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel);
byte[] requestBuffer = BuildTagNamesBuffer(tags);
GrpcRetrieval.GetTagInfosFromNameResponse response = retrievalClient.GetTagInfosFromName(
@@ -112,6 +145,8 @@ internal static class HistorianGrpcTagClient
return Task.Run(() => GetTagExtendedProperties(options, tag, cancellationToken), cancellationToken);
}
// No …OnSession seam: extended-properties browse stays per-call (not amortized through the session
// pool — out of A1-broadening scope). Add a seam here only if the pool ever needs to route it.
/// <summary>
/// Issues a single page-0 <c>GetTagExtendedPropertiesFromName</c> call and returns the raw native
/// <c>btTeps</c> response buffer (empty when the server reports no rows / non-success). Internal so
@@ -222,6 +257,20 @@ internal static class HistorianGrpcTagClient
{
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken);
return BrowseTagNamesOnSession(connection, session, filter, options, cancellationToken);
}
// Spike/Phase-1 seam (pending.md A1): drive StartTagQuery → paged QueryTag → EndTagQuery against an
// EXTERNALLY-supplied, already-authenticated connection + session — i.e. NO Create()/handshake here.
// BrowseTagNames delegates to this so the per-call path and the reuse path share one browse
// implementation (DRY).
internal static List<string> BrowseTagNamesOnSession(
HistorianGrpcConnection connection,
HistorianGrpcHandshake.Session session,
string filter,
HistorianClientOptions options,
CancellationToken cancellationToken)
{
var retrievalClient = new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel);
DateTime Deadline() => DateTime.UtcNow.Add(options.RequestTimeout);
@@ -0,0 +1,22 @@
using System.Reflection;
using AVEVA.Historian.Client.Grpc;
using Xunit;
namespace AVEVA.Historian.Client.Tests;
public class TagClientOnSessionSeamTests
{
[Theory]
[InlineData("BrowseTagNamesOnSession")]
[InlineData("GetTagInfosRawOnSession")]
[InlineData("GetTagMetadataOnSession")]
public void TagClient_ExposesOnSessionSeam(string name)
{
MethodInfo? m = typeof(HistorianGrpcTagClient).GetMethod(
name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
Assert.NotNull(m);
ParameterInfo[] ps = m!.GetParameters();
Assert.Equal("HistorianGrpcConnection", ps[0].ParameterType.Name);
Assert.Equal("Session", ps[1].ParameterType.Name);
}
}