namespace AVEVA.Historian.Client; /// /// Identifies a versioned native Historian service interface whose reported interface /// version is validated at connect time by . /// internal enum HistorianServiceInterface { History, Retrieval, Status, Transaction } /// /// Fail-closed check (roadmap item R0.6) that a Historian server reports the native /// interface version this SDK's byte serializers were built against. /// /// The opaque native buffers carried inside the WCF/MDAS message body — and, on 2023 R2, /// inside the gRPC bytes fields — are framed per native interface version. Parsing /// them against an unexpected version risks silent misinterpretation, so per the /// "version-pin, fail closed" principle this throws /// rather than best-effort parsing. /// /// Supported versions are evidence-based, discovered from a live AVEVA Historian 2020 /// server (product 20.0.000) via the reverse-engineering wcf-probe command: /// /// History (Hist) interface version = 11 /// Retrieval (Retr) interface version = 4 /// Transaction (Trx) interface version = 2 /// /// The Status (Stat) service's GetInterfaceVersion returns 0 (not a real /// version), so the Status interface is validated for reachability only, never value. /// /// A 2023 R2 gRPC server reports History interface version 12 even though it carries the /// same proven 2020 native buffers. That value is captured and accepted (see /// ), so a v12 server passes with the default /// =; /// the opt-out is only a safety valve for some future, not-yet-captured interface integer. /// internal static class HistorianServerVersionGate { public const uint HistoryInterfaceVersion = 11; public const uint RetrievalInterfaceVersion = 4; public const uint TransactionInterfaceVersion = 2; /// /// The 2023 R2 gRPC HistoryService reports interface version 12. It is buffer-compatible with /// the 2020 version 11 — the OpenConnection3 v6 / token / DataQueryRequest / row buffers are /// byte-identical — confirmed by a live end-to-end gRPC read against a real 2023 R2 server /// (2026-06-21). So both 11 and 12 are accepted for History. (Retrieval reported 4, matching /// the 2020 value, so it needs no widening.) /// public const uint HistoryInterfaceVersionGrpc2023R2 = 12; /// /// True when the service interface reports a meaningful version that should be matched. /// Status is reachability-only (its GetInterfaceVersion returns 0). /// public static bool IsValueGated(HistorianServiceInterface service) => service switch { HistorianServiceInterface.History => true, HistorianServiceInterface.Retrieval => true, HistorianServiceInterface.Transaction => true, HistorianServiceInterface.Status => false, _ => false }; /// The canonical interface version this SDK's serializers target for a value-gated service. public static uint ExpectedVersion(HistorianServiceInterface service) => service switch { HistorianServiceInterface.History => HistoryInterfaceVersion, HistorianServiceInterface.Retrieval => RetrievalInterfaceVersion, HistorianServiceInterface.Transaction => TransactionInterfaceVersion, _ => throw new ArgumentOutOfRangeException(nameof(service), service, "Service interface is not value-gated.") }; /// /// All interface versions accepted for a value-gated service. Usually a single value, but /// History accepts both the 2020 value (11) and the buffer-compatible 2023 R2 gRPC value (12). /// public static uint[] AcceptedVersions(HistorianServiceInterface service) => service switch { HistorianServiceInterface.History => [HistoryInterfaceVersion, HistoryInterfaceVersionGrpc2023R2], HistorianServiceInterface.Retrieval => [RetrievalInterfaceVersion], HistorianServiceInterface.Transaction => [TransactionInterfaceVersion], _ => throw new ArgumentOutOfRangeException(nameof(service), service, "Service interface is not value-gated.") }; /// /// Throws when version verification is enabled /// and the server's reported interface version differs from the version this SDK targets. /// No-op when is /// , when the service is not value-gated (Status), or on a match. /// public static void Validate(HistorianServiceInterface service, uint reportedVersion, HistorianClientOptions options) { ArgumentNullException.ThrowIfNull(options); if (!options.VerifyServerInterfaceVersion || !IsValueGated(service)) { return; } uint[] accepted = AcceptedVersions(service); if (Array.IndexOf(accepted, reportedVersion) >= 0) { return; } string acceptedList = string.Join(", ", accepted); throw new ProtocolEvidenceMissingException( $"{service} interface version {reportedVersion} (this SDK's serializers target version {acceptedList}); " + $"set {nameof(HistorianClientOptions)}.{nameof(HistorianClientOptions.VerifyServerInterfaceVersion)}=false to bypass at your own risk"); } }