From ef68016c7a9c7dc5cd267d67990debb2cf778f7d Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 22 Jun 2026 01:26:33 -0400 Subject: [PATCH] feat(grpc): tool the WCF-only config ops over the gRPC transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the config operations that previously only worked over WCF onto RemoteGrpc, reusing the proven 2020 byte serializers verbatim inside the protobuf bytes fields (keyed by the Open2 session handle). Live-verified against a real 2023 R2 server where noted. Read ops (live-verified): - GetRuntimeParameterAsync -> StatusService.GetRuntimeParameter (GETRP serializer) - GetTagExtendedPropertiesAsync -> RetrievalService.GetTagExtendedPropertiesFromName (GetTepByNm serializer + sequence paging; page-0 FillBufferFromVector is the benign no-data terminator, matched to the WCF break-and-return-empty semantics) Server-walled (bounded with captured evidence): - ExecuteSqlCommandAsync -> RetrievalService.ExecuteSqlCommand. The request rides the RPC but the server-side CSrvDbConnection.ExecuteSqlCommand faults (IndexOutOfRange / native err 38) on a DB-connection precondition the pure managed gRPC session doesn't establish (same class as OpenStorageConnection). Surfaced as ProtocolEvidenceMissingException. Write ops (tooled + routed, sandbox-gated — not run destructively live): - EnsureTagAsync / DeleteTagAsync / RenameTagsAsync / AddTagExtendedPropertiesAsync via HistoryService.EnsureTags / DeleteTags / StartJob / AddTagExtendedProperties on a write-enabled (0x401) session, reusing the WCF golden serializers. The WCF priming discovery-dance is omitted (the M3 gRPC write probe worked without it); add it first if a live sandbox run is rejected. Routed in Historian2020ProtocolDialect / HistorianClient on the RemoteGrpc branch. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC --- .../Grpc/HistorianGrpcSqlClient.cs | 114 ++++++++++ .../Grpc/HistorianGrpcStatusClient.cs | 44 ++++ .../Grpc/HistorianGrpcTagClient.cs | 72 +++++++ .../Grpc/HistorianGrpcTagWriteOrchestrator.cs | 197 ++++++++++++++++++ src/AVEVA.Historian.Client/HistorianClient.cs | 16 +- .../Protocol/Historian2020ProtocolDialect.cs | 12 +- 6 files changed, 448 insertions(+), 7 deletions(-) create mode 100644 src/AVEVA.Historian.Client/Grpc/HistorianGrpcSqlClient.cs create mode 100644 src/AVEVA.Historian.Client/Grpc/HistorianGrpcTagWriteOrchestrator.cs diff --git a/src/AVEVA.Historian.Client/Grpc/HistorianGrpcSqlClient.cs b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcSqlClient.cs new file mode 100644 index 0000000..b5bb36d --- /dev/null +++ b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcSqlClient.cs @@ -0,0 +1,114 @@ +using Google.Protobuf; +using AVEVA.Historian.Client.Models; +using AVEVA.Historian.Client.Protocol; +using AVEVA.Historian.Client.Wcf; +using GrpcRetrieval = ArchestrA.Grpc.Contract.Retrieval; + +namespace AVEVA.Historian.Client.Grpc; + +/// +/// Executes SQL commands over the 2023 R2 gRPC transport (HCAL R1.1), mirroring +/// 's two-op ExeC/GetR flow. The 2020 WCF path uses a +/// dedicated GetRecordSetByteStream op; the gRPC front door has no such RPC, so the NRBF +/// recordset stream would be fetched through the generic RetrievalService.GetNextQueryResultBuffer +/// keyed by the query handle ExecuteSqlCommand returns. ExecuteSqlCommand takes the +/// uppercase string session handle; the result-buffer fetch takes the transient uint client +/// handle (both come from the one Open2 session). +/// +/// SERVER-WALLED (captured 2026-06-22). The 2023 R2 front-door +/// RetrievalService.ExecuteSqlCommand faults server-side before returning a query handle: +/// the response carries native error 38 wrapping a managed +/// System.IndexOutOfRangeException ... at aahClientAccessPoint.CSrvDbConnection.ExecuteSqlCommand. +/// This is a server-side CSrvDbConnection (SQL DB-connection) precondition that the pure +/// managed gRPC session does not establish — the same class of wall as +/// StorageService.OpenStorageConnection (whose real precondition is the front-door +/// HistoryService.RegisterTags family). Priming Retr.GetV does not clear it. The request +/// framing here is the captured/expected shape; the op stays bounded behind +/// until the DB-connection registration is reproduced. +/// +/// +internal static class HistorianGrpcSqlClient +{ + // GetNextQueryResultBuffer is byte-stream-paged; a small record set returns in one page. Runaway guard. + private const int MaxPages = 4096; + + public static Task ExecuteSqlCommandAsync( + HistorianClientOptions options, + string command, + HistorianSqlExecuteOption option, + CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrWhiteSpace(command); + return Task.Run(() => ExecuteSqlCommand(options, command, option, cancellationToken), cancellationToken); + } + + private static HistorianSqlResult ExecuteSqlCommand( + HistorianClientOptions options, + string command, + HistorianSqlExecuteOption option, + CancellationToken cancellationToken) + { + using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options); + HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken); + var retrievalClient = new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel); + DateTime Deadline() => DateTime.UtcNow.Add(options.RequestTimeout); + + // Prime the Retrieval service version handshake (Retr.GetV) before the string-handle SQL op, as + // the native WCF SQL path does — the server-side ExecuteSqlCommand otherwise faults. + retrievalClient.GetRetrievalInterfaceVersion( + new GrpcRetrieval.GetRetrievalInterfaceVersionRequest(), connection.Metadata, Deadline(), cancellationToken); + + GrpcRetrieval.ExecuteSqlCommandResponse exec = retrievalClient.ExecuteSqlCommand( + new GrpcRetrieval.ExecuteSqlCommandRequest + { + StrHandle = session.StringHandle, + StrCommand = command, + UiOption = (uint)option, + UiQueryHandle = 0 + }, + connection.Metadata, + Deadline(), + cancellationToken); + + if (!(exec.Status?.BSuccess ?? false)) + { + // Captured 2026-06-22: the server-side CSrvDbConnection.ExecuteSqlCommand throws + // IndexOutOfRange (native error 38) — a DB-connection precondition the pure managed gRPC + // session doesn't establish. Surface the SDK's evidence-missing signal rather than a raw + // server fault. See the class remarks. + throw new ProtocolEvidenceMissingException( + "ExecuteSqlCommand over gRPC: server-side CSrvDbConnection.ExecuteSqlCommand faults " + + "(IndexOutOfRange / native error 38) — an unmet DB-connection precondition (gRPC transport). Use WCF."); + } + + int returnValue = exec.IRetValue; + uint queryHandle = exec.UiQueryHandle; + + using MemoryStream accumulated = new(); + for (int page = 0; page < MaxPages; page++) + { + cancellationToken.ThrowIfCancellationRequested(); + GrpcRetrieval.GetNextQueryResultBufferResponse buffer = retrievalClient.GetNextQueryResultBuffer( + new GrpcRetrieval.GetNextQueryResultBufferRequest { UiHandle = session.ClientHandle, UiQueryHandle = queryHandle }, + connection.Metadata, + Deadline(), + cancellationToken); + + byte[] resultBuffer = buffer.BtQueryResult?.ToByteArray() ?? []; + + // GetR is false-even-on-success: the final page returns false with the data still in the + // buffer, so always consume the buffer first, then stop on a false status or an empty page. + if (resultBuffer.Length > 0) + { + accumulated.Write(resultBuffer, 0, resultBuffer.Length); + } + + if (!(buffer.Status?.BSuccess ?? false) || resultBuffer.Length == 0) + { + break; + } + } + + return HistorianSqlResultProtocol.Parse(accumulated.ToArray(), returnValue); + } +} diff --git a/src/AVEVA.Historian.Client/Grpc/HistorianGrpcStatusClient.cs b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcStatusClient.cs index f96141a..3e67538 100644 --- a/src/AVEVA.Historian.Client/Grpc/HistorianGrpcStatusClient.cs +++ b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcStatusClient.cs @@ -1,5 +1,7 @@ +using Google.Protobuf; using Grpc.Core; using AVEVA.Historian.Client.Models; +using AVEVA.Historian.Client.Wcf; using GrpcStatus = ArchestrA.Grpc.Contract.Status; namespace AVEVA.Historian.Client.Grpc; @@ -142,4 +144,46 @@ internal static class HistorianGrpcStatusClient string? value = response.StrSystemTimeZoneName; return string.IsNullOrEmpty(value) ? null : value; } + + /// + /// Reads a Historian runtime parameter over gRPC (StatusService.GetRuntimeParameter). + /// The request/response byte buffers are the proven 2020 GETRP wire format + /// () carried unchanged inside the protobuf + /// btRequest/btResponse fields; the op keys on the uppercase string session handle. + /// + public static Task GetRuntimeParameterAsync( + HistorianClientOptions options, + string parameterName, + CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrWhiteSpace(parameterName); + return Task.Run(() => GetRuntimeParameter(options, parameterName, cancellationToken), cancellationToken); + } + + private static string? GetRuntimeParameter(HistorianClientOptions options, string parameterName, CancellationToken cancellationToken) + { + using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options); + HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken); + + byte[] request = HistorianRuntimeParameterProtocol.SerializeRequest(parameterName); + + var statusClient = new GrpcStatus.StatusService.StatusServiceClient(connection.Channel); + GrpcStatus.GetRuntimeParameterResponse response = statusClient.GetRuntimeParameter( + new GrpcStatus.GetRuntimeParameterRequest + { + StrHandle = session.StringHandle, + BtRequest = ByteString.CopyFrom(request) + }, + connection.Metadata, + DateTime.UtcNow.Add(options.RequestTimeout), + cancellationToken); + + if (!(response.Status?.BSuccess ?? false)) + { + return null; + } + + byte[] responseBuffer = response.BtResponse?.ToByteArray() ?? []; + return HistorianRuntimeParameterProtocol.ParseSingleStringResult(responseBuffer); + } } diff --git a/src/AVEVA.Historian.Client/Grpc/HistorianGrpcTagClient.cs b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcTagClient.cs index 7600d80..e8476b0 100644 --- a/src/AVEVA.Historian.Client/Grpc/HistorianGrpcTagClient.cs +++ b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcTagClient.cs @@ -92,6 +92,78 @@ internal static class HistorianGrpcTagClient return response.BtTagInfos?.ToByteArray() ?? []; } + // GetTagExtendedPropertiesFromName is sequence-paged; a single tag returns everything on page 0 + // and an empty/false buffer next. The cap is a runaway guard (mirrors the WCF path). + private const int MaxExtendedPropertyPages = 64; + + /// + /// Reads a tag's extended (user-defined) properties over gRPC + /// (RetrievalService.GetTagExtendedPropertiesFromName, a string-handle op). The request + /// btTagNames and response btTeps buffers are the proven 2020 GetTepByNm wire + /// format () carried unchanged; paging follows + /// the same sequence loop as the WCF path. + /// + public static Task> GetTagExtendedPropertiesAsync( + HistorianClientOptions options, + string tag, + CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrWhiteSpace(tag); + return Task.Run(() => GetTagExtendedProperties(options, tag, cancellationToken), cancellationToken); + } + + private static IReadOnlyList GetTagExtendedProperties( + HistorianClientOptions options, string tag, CancellationToken cancellationToken) + { + using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options); + HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken); + var retrievalClient = new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel); + + byte[] tagNames = HistorianTagExtendedPropertyProtocol.SerializeRequest(tag); + List properties = []; + uint sequence = 0; + + for (int page = 0; page < MaxExtendedPropertyPages; page++) + { + cancellationToken.ThrowIfCancellationRequested(); + GrpcRetrieval.GetTagExtendedPropertiesFromNameResponse response = retrievalClient.GetTagExtendedPropertiesFromName( + new GrpcRetrieval.GetTagExtendedPropertiesFromNameRequest + { + StrHandle = session.StringHandle, + BtTagNames = ByteString.CopyFrom(tagNames), + UiSequence = sequence + }, + connection.Metadata, + DateTime.UtcNow.Add(options.RequestTimeout), + cancellationToken); + + if (!(response.Status?.BSuccess ?? false)) + { + // A non-success terminates paging. The server signals "no more rows" with a + // CClientUtil::FillBufferFromVector marker (live-confirmed) — including on page 0 when + // the tag has no user-defined properties, which is a legitimate empty result, not an + // error. This mirrors the WCF path, which also breaks (returns empty) rather than throws. + break; + } + + IReadOnlyList rows = + HistorianTagExtendedPropertyProtocol.ParseResponse(response.BtTeps?.ToByteArray() ?? []); + if (rows.Count == 0) + { + break; + } + + foreach (HistorianTagExtendedPropertyRow row in rows) + { + properties.Add(new HistorianTagExtendedProperty(row.PropertyName, row.Value)); + } + + sequence = response.UiSequence; + } + + return properties; + } + // QueryTag (browse paging) request framing, recovered from the .rdata packet-descriptor table // in aahClientManaged.dll (entries {0x6751,1}=StartTagQuery, {0x6752,1}=QueryTag) and confirmed // live: btRequest = u16 marker(0x6752) + u16 version(1) + u16 queryType + u32 startIndex + u32 count. diff --git a/src/AVEVA.Historian.Client/Grpc/HistorianGrpcTagWriteOrchestrator.cs b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcTagWriteOrchestrator.cs new file mode 100644 index 0000000..480b991 --- /dev/null +++ b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcTagWriteOrchestrator.cs @@ -0,0 +1,197 @@ +using Google.Protobuf; +using AVEVA.Historian.Client.Models; +using AVEVA.Historian.Client.Wcf; +using GrpcHistory = ArchestrA.Grpc.Contract.History; + +namespace AVEVA.Historian.Client.Grpc; + +/// +/// Tag-configuration write ops over the 2023 R2 gRPC transport, mirroring +/// . Each op opens a write-enabled Open2 session +/// (0x401) and reuses the proven 2020 byte serializers verbatim inside the protobuf +/// bytes fields: +/// +/// HistoryService.EnsureTags (string handle, +/// btTagInfos = ) +/// HistoryService.DeleteTags (uint handle, +/// btTagnames = ) +/// HistoryService.StartJob (string handle, +/// btInput = ) +/// HistoryService.AddTagExtendedProperties +/// (string handle, btTeps = ) +/// +/// +/// Tooled but not yet live-verified. The request framing reuses the WCF serializers proven on +/// the 2020 transport, and the read-side config ops confirm WCF config buffers ride the gRPC RPC +/// unchanged — but these mutate server state (create/delete/rename tags, write properties), so they +/// are gated behind a sandbox-tag in the integration tests and have not been run destructively against +/// a shared live server. The WCF path additionally runs a priming "discovery dance" (UpdC3 + system +/// parameters + cross-service GetV) before the write; the gRPC front door established the equivalent +/// session state in the M3 non-streamed-write probe without it, so it is omitted here pending live +/// confirmation. If a live run is rejected, that priming is the first thing to add. +/// +/// +internal sealed class HistorianGrpcTagWriteOrchestrator +{ + private const uint WriteEnabledConnectionMode = HistorianWcfAuthChainHelper.NativeIntegratedWriteEnabledConnectionMode; + + private readonly HistorianClientOptions _options; + + public HistorianGrpcTagWriteOrchestrator(HistorianClientOptions options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public Task EnsureTagAsync(HistorianTagDefinition definition, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(definition); + ArgumentException.ThrowIfNullOrWhiteSpace(definition.TagName, nameof(definition)); + // Surface unsupported (non-analog) types early, exactly as the WCF path does. + _ = HistorianTagWriteProtocol.GetAnalogDataTypeCode(definition.DataType); + return Task.Run(() => EnsureTag(definition, cancellationToken), cancellationToken); + } + + private bool EnsureTag(HistorianTagDefinition definition, CancellationToken cancellationToken) + { + using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(_options); + HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, _options, cancellationToken, WriteEnabledConnectionMode); + + byte[] payload = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata( + tagName: definition.TagName, + description: definition.Description, + engineeringUnit: definition.EngineeringUnit, + dateCreatedUtc: DateTime.UtcNow, + dataType: definition.DataType, + minEU: definition.MinEU, + maxEU: definition.MaxEU, + minRaw: definition.MinRaw, + maxRaw: definition.MaxRaw, + storageRateMs: definition.StorageRateMs, + applyScaling: definition.ApplyScaling, + storageType: definition.StorageType, + integralDivisor: definition.IntegralDivisor); + + var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel); + GrpcHistory.EnsureTagsResponse response = historyClient.EnsureTags( + new GrpcHistory.EnsureTagsRequest + { + StrHandle = session.StringHandle, + BtTagInfos = ByteString.CopyFrom(payload), + ElementCount = 1 + }, + connection.Metadata, + DateTime.UtcNow.Add(_options.RequestTimeout), + cancellationToken); + + return response.Status?.BSuccess ?? false; + } + + public Task DeleteTagAsync(string tagName, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrWhiteSpace(tagName); + return Task.Run(() => DeleteTag(tagName, cancellationToken), cancellationToken); + } + + private bool DeleteTag(string tagName, CancellationToken cancellationToken) + { + using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(_options); + HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, _options, cancellationToken, WriteEnabledConnectionMode); + + // DeleteTags takes the transient uint client handle (not the string handle), per the WCF wire capture. + byte[] tagNames = HistorianTagWriteProtocol.SerializeDeleteTagNames([tagName]); + var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel); + GrpcHistory.DeleteTagsResponse response = historyClient.DeleteTags( + new GrpcHistory.DeleteTagsRequest + { + UiHandle = session.ClientHandle, + BtTagnames = ByteString.CopyFrom(tagNames) + }, + connection.Metadata, + DateTime.UtcNow.Add(_options.RequestTimeout), + cancellationToken); + + return response.Status?.BSuccess ?? false; + } + + public Task AddTagExtendedPropertiesAsync( + string tagName, IReadOnlyList properties, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrWhiteSpace(tagName); + ArgumentNullException.ThrowIfNull(properties); + if (properties.Count == 0) + { + throw new ArgumentException("At least one extended property is required.", nameof(properties)); + } + return Task.Run(() => AddTagExtendedProperties(tagName, properties, cancellationToken), cancellationToken); + } + + private bool AddTagExtendedProperties( + string tagName, IReadOnlyList properties, CancellationToken cancellationToken) + { + using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(_options); + HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, _options, cancellationToken, WriteEnabledConnectionMode); + + byte[] inBuff = HistorianTagExtendedPropertyProtocol.SerializeAddRequest(tagName, properties); + var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel); + GrpcHistory.AddTagExtendedPropertiesResponse response = historyClient.AddTagExtendedProperties( + new GrpcHistory.AddTagExtendedPropertiesRequest + { + StrHandle = session.StringHandle, + BtTeps = ByteString.CopyFrom(inBuff) + }, + connection.Metadata, + DateTime.UtcNow.Add(_options.RequestTimeout), + cancellationToken); + + return response.Status?.BSuccess ?? false; + } + + public Task RenameTagsAsync( + IReadOnlyList<(string OldName, string NewName)> pairs, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(pairs); + if (pairs.Count == 0) + { + throw new ArgumentException("At least one (old,new) name pair is required.", nameof(pairs)); + } + foreach ((string oldName, string newName) in pairs) + { + ArgumentException.ThrowIfNullOrWhiteSpace(oldName, nameof(pairs)); + ArgumentException.ThrowIfNullOrWhiteSpace(newName, nameof(pairs)); + } + return Task.Run(() => RenameTags(pairs, cancellationToken), cancellationToken); + } + + private HistorianTagRenameResult RenameTags(IReadOnlyList<(string OldName, string NewName)> pairs, CancellationToken cancellationToken) + { + using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(_options); + HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, _options, cancellationToken, WriteEnabledConnectionMode); + + byte[] jobBuffer = HistorianTagRenameProtocol.SerializeRenameJob(pairs); + var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel); + GrpcHistory.StartJobResponse response = historyClient.StartJob( + new GrpcHistory.StartJobRequest + { + StrHandle = session.StringHandle, + BtInput = ByteString.CopyFrom(jobBuffer) + }, + connection.Metadata, + DateTime.UtcNow.Add(_options.RequestTimeout), + cancellationToken); + + bool ok = response.Status?.BSuccess ?? false; + Guid parsedJobId = Guid.Empty; + if (!string.IsNullOrWhiteSpace(response.StrJobid)) + { + Guid.TryParse(response.StrJobid.Trim().Trim('$', '{', '}'), out parsedJobId); + } + + return new HistorianTagRenameResult + { + Accepted = ok, + JobId = parsedJobId, + PairCount = pairs.Count, + Error = ok ? null : "Server rejected the rename job (StartJob returned false). Check that the 'AllowRenameTags' system parameter is enabled.", + }; + } +} diff --git a/src/AVEVA.Historian.Client/HistorianClient.cs b/src/AVEVA.Historian.Client/HistorianClient.cs index d044514..fd7f8d7 100644 --- a/src/AVEVA.Historian.Client/HistorianClient.cs +++ b/src/AVEVA.Historian.Client/HistorianClient.cs @@ -237,7 +237,9 @@ public sealed class HistorianClient : IAsyncDisposable { ArgumentException.ThrowIfNullOrWhiteSpace(tag); ArgumentNullException.ThrowIfNull(properties); - return new HistorianWcfTagWriteOrchestrator(_options).AddTagExtendedPropertiesAsync(tag, properties, cancellationToken); + return _options.Transport == HistorianTransport.RemoteGrpc + ? new Grpc.HistorianGrpcTagWriteOrchestrator(_options).AddTagExtendedPropertiesAsync(tag, properties, cancellationToken) + : new HistorianWcfTagWriteOrchestrator(_options).AddTagExtendedPropertiesAsync(tag, properties, cancellationToken); } /// Convenience overload of for a single @@ -285,7 +287,9 @@ public sealed class HistorianClient : IAsyncDisposable public Task EnsureTagAsync(HistorianTagDefinition definition, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(definition); - return new HistorianWcfTagWriteOrchestrator(_options).EnsureTagAsync(definition, cancellationToken); + return _options.Transport == HistorianTransport.RemoteGrpc + ? new Grpc.HistorianGrpcTagWriteOrchestrator(_options).EnsureTagAsync(definition, cancellationToken) + : new HistorianWcfTagWriteOrchestrator(_options).EnsureTagAsync(definition, cancellationToken); } /// @@ -299,7 +303,9 @@ public sealed class HistorianClient : IAsyncDisposable public Task DeleteTagAsync(string tagName, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(tagName); - return new HistorianWcfTagWriteOrchestrator(_options).DeleteTagAsync(tagName, cancellationToken); + return _options.Transport == HistorianTransport.RemoteGrpc + ? new Grpc.HistorianGrpcTagWriteOrchestrator(_options).DeleteTagAsync(tagName, cancellationToken) + : new HistorianWcfTagWriteOrchestrator(_options).DeleteTagAsync(tagName, cancellationToken); } /// @@ -325,7 +331,9 @@ public sealed class HistorianClient : IAsyncDisposable public Task RenameTagsAsync(IReadOnlyList<(string OldName, string NewName)> pairs, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(pairs); - return new HistorianWcfTagWriteOrchestrator(_options).RenameTagsAsync(pairs, cancellationToken); + return _options.Transport == HistorianTransport.RemoteGrpc + ? new Grpc.HistorianGrpcTagWriteOrchestrator(_options).RenameTagsAsync(pairs, cancellationToken) + : new HistorianWcfTagWriteOrchestrator(_options).RenameTagsAsync(pairs, cancellationToken); } public ValueTask DisposeAsync() diff --git a/src/AVEVA.Historian.Client/Protocol/Historian2020ProtocolDialect.cs b/src/AVEVA.Historian.Client/Protocol/Historian2020ProtocolDialect.cs index 82b4290..b13d90b 100644 --- a/src/AVEVA.Historian.Client/Protocol/Historian2020ProtocolDialect.cs +++ b/src/AVEVA.Historian.Client/Protocol/Historian2020ProtocolDialect.cs @@ -95,21 +95,27 @@ internal sealed class Historian2020ProtocolDialect { cancellationToken.ThrowIfCancellationRequested(); ArgumentException.ThrowIfNullOrWhiteSpace(name); - return Wcf.HistorianWcfStatusClient.GetRuntimeParameterAsync(_options, name, cancellationToken); + return UseGrpc + ? HistorianGrpcStatusClient.GetRuntimeParameterAsync(_options, name, cancellationToken) + : Wcf.HistorianWcfStatusClient.GetRuntimeParameterAsync(_options, name, cancellationToken); } public Task> GetTagExtendedPropertiesAsync(string tag, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ArgumentException.ThrowIfNullOrWhiteSpace(tag); - return Wcf.HistorianWcfTagExtendedPropertyClient.GetTagExtendedPropertiesAsync(_options, tag, cancellationToken); + return UseGrpc + ? Grpc.HistorianGrpcTagClient.GetTagExtendedPropertiesAsync(_options, tag, cancellationToken) + : Wcf.HistorianWcfTagExtendedPropertyClient.GetTagExtendedPropertiesAsync(_options, tag, cancellationToken); } public Task ExecuteSqlCommandAsync(string command, HistorianSqlExecuteOption option, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ArgumentException.ThrowIfNullOrWhiteSpace(command); - return Wcf.HistorianWcfSqlClient.ExecuteSqlCommandAsync(_options, command, option, cancellationToken); + return UseGrpc + ? Grpc.HistorianGrpcSqlClient.ExecuteSqlCommandAsync(_options, command, option, cancellationToken) + : Wcf.HistorianWcfSqlClient.ExecuteSqlCommandAsync(_options, command, option, cancellationToken); } private static async IAsyncEnumerable Missing(