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;