diff --git a/src/AVEVA.Historian.Client/HistorianClient.cs b/src/AVEVA.Historian.Client/HistorianClient.cs
index 73c9b26..14373c8 100644
--- a/src/AVEVA.Historian.Client/HistorianClient.cs
+++ b/src/AVEVA.Historian.Client/HistorianClient.cs
@@ -347,6 +347,41 @@ public sealed class HistorianClient : IAsyncDisposable
: new HistorianWcfTagWriteOrchestrator(_options).RenameTagsAsync(pairs, cancellationToken);
}
+ ///
+ /// Opens a reusable authenticated over the 2023 R2 gRPC transport.
+ /// The caller owns the session and must dispose it. Reusing the session across ops amortizes the auth
+ /// handshake; the server idle-expires it in ~20-25s, so keep it warm (HistorianSession.PingAsync) or
+ /// re-open. RemoteGrpc only.
+ ///
+ public async Task OpenSessionAsync(HistorianSessionKind kind, CancellationToken cancellationToken = default)
+ {
+ if (_options.Transport != HistorianTransport.RemoteGrpc)
+ {
+ throw new ProtocolEvidenceMissingException(
+ "HistorianSession is only supported over the 2023 R2 RemoteGrpc transport.");
+ }
+
+ return await Task.Run(() =>
+ {
+ uint mode = kind == HistorianSessionKind.WriteEnabled
+ ? HistorianWcfAuthChainHelper.NativeIntegratedWriteEnabledConnectionMode
+ : HistorianWcfAuthChainHelper.NativeIntegratedReadOnlyConnectionMode;
+
+ Grpc.HistorianGrpcConnection connection = Grpc.HistorianGrpcChannelFactory.Create(_options);
+ try
+ {
+ Grpc.HistorianGrpcHandshake.Session session =
+ Grpc.HistorianGrpcHandshake.OpenSession(connection, _options, cancellationToken, connectionMode: mode);
+ return new HistorianSession(connection, session, _options, kind);
+ }
+ catch
+ {
+ connection.Dispose(); // don't leak the channel if the handshake fails
+ throw;
+ }
+ }, cancellationToken).ConfigureAwait(false);
+ }
+
public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;