dohertj2 dohertj2
  • Joined on 2026-02-20

ZB.MOM.WW.HistorianGateway.Client (0.3.0)

Published 2026-07-14 08:16:23 -04:00 by dohertj2

Installation

dotnet nuget add source --name dohertj2 --username your_username --password your_token https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json
dotnet add package --source dohertj2 --version 0.3.0 ZB.MOM.WW.HistorianGateway.Client

About this package

.NET 10 gRPC client for the ZB.MOM.WW.HistorianGateway service: typed wrappers over the historian read/write/tags/status RPCs, Polly retry, and bearer-key auth.

ZB.MOM.WW.HistorianGateway.Client

.NET 10 gRPC client for the ZB.MOM.WW.HistorianGateway sidecar. Typed wrappers over the historian_gateway.v1 read / write / tags / status services with Polly retry, bearer-key auth, and a typed exception hierarchy.

The client wraps the four historian gRPC services (HistorianRead, HistorianWrite, HistorianTags, HistorianStatus). It does not wrap the GalaxyRepository service — use the generated client stubs from the ZB.MOM.WW.GalaxyRepository package for Galaxy browse.

Install

Published to the dohertj2-gitea NuGet feed: ZB.MOM.WW.HistorianGateway.Client and ZB.MOM.WW.HistorianGateway.Contracts, both 0.3.0 (0.1.0 first published 2026-06-26; 0.2.0 2026-07-09; 0.3.0 2026-07-14). Consumers add the ZB.MOM.WW.HistorianGateway.* patterns to their nuget.config packageSourceMapping for the dohertj2-gitea source.

<PackageReference Include="ZB.MOM.WW.HistorianGateway.Client" Version="0.3.0" />

This transitively pulls in ZB.MOM.WW.HistorianGateway.Contracts (the generated protobuf types). You do not need a direct Contracts reference unless you are building a server stub.

Quick start

await using var client = HistorianGatewayClient.Create(new HistorianGatewayClientOptions
{
    Endpoint = new Uri("http://historian-gateway:5221"),   // https:// requires UseTls = true
    ApiKey   = "histgw_<id>_<secret>",
});

// Probe connectivity
bool reachable = await client.ProbeAsync();

// Read raw samples — server-streaming, async-enumerable
await foreach (HistorianSample sample in client.ReadRawAsync(
    tag:       "SysTimeSec",
    startUtc:  DateTime.UtcNow.AddHours(-1),
    endUtc:    DateTime.UtcNow,
    maxValues: 100))
{
    Console.WriteLine($"{sample.Timestamp.ToDateTime():O}  {sample.NumericValue}");
}

Create options

HistorianGatewayClient.Create(HistorianGatewayClientOptions options) builds the GrpcChannel and all four service stubs. Dispose via await using or using to release the channel.

Property Required Default Notes
Endpoint Yes Absolute URI of the gateway gRPC port (http:// for h2c plaintext, https:// for TLS)
ApiKey Yes histgw_<id>_<secret> bearer key sent as Authorization: Bearer <ApiKey>
UseTls No false Must be true when Endpoint uses https://; validated
CaCertificatePath No null Path to a PEM CA certificate; pins the trust chain against this root
RequireCertificateValidation No false When false (default) and CaCertificatePath is unset, the gateway certificate is accepted without verification — appropriate for internal self-signed certs. Set true to enforce OS trust-store validation. Planned breaking change: this default flips to true in 1.0.0 (pre-announced in the CHANGELOG/COMPATIBILITY.md) — set it explicitly now
ServerNameOverride No null SNI target-host override for TLS
ConnectTimeout No 10 s TCP connect timeout
DefaultCallTimeout No 30 s Deadline applied to every unary RPC
StreamTimeout No 10 min Deadline for server-streaming RPCs (reads + streaming browse). Since 0.3.0 the default is 10 minutes (was null), matching the gateway's MaxStreamLeaseHold; set StreamTimeout = null explicitly to restore an unbounded stream
MaxGrpcMessageBytes No 16 MiB gRPC message size cap (send and receive)
Retry No see below Polly retry configuration
LoggerFactory No null ILoggerFactory for channel and retry diagnostics

Retry

Polly exponential-backoff retry applies only to idempotent RPCs (reads + status). Write and mutation RPCs are never retried — they go directly to the transport. Transient gRPC codes that trigger a retry: Unavailable, DeadlineExceeded, ResourceExhausted.

Idempotent (retried): ReadRawAsync, ReadAggregateAsync, ReadAtTimeAsync, ReadBlocksAsync, ReadEventsAsync, BrowseTagNamesAsync, GetTagMetadataAsync, ProbeAsync, GetConnectionStatusAsync, GetStoreForwardStatusAsync, GetSystemParameterAsync.

Not retried: AddHistoricalValuesAsync, SendEventAsync, WriteLiveValuesAsync, EnsureTagsAsync, DeleteTagsAsync, RenameTagsAsync, AddTagExtendedPropertiesAsync.

Default HistorianGatewayClientRetryOptions:

Property Default Notes
MaxAttempts 2 1 initial attempt + 1 retry
Delay 200 ms Initial backoff; doubles per attempt
MaxDelay 2 s Backoff cap
UseJitter true Adds randomness to avoid thundering-herd

Auth

The API key is sent as an HTTP/2 metadata header on every gRPC call:

authorization: Bearer histgw_<id>_<secret>

Issue keys with the gateway's apikey subcommand:

dotnet run --project src/ZB.MOM.WW.HistorianGateway.Server/ZB.MOM.WW.HistorianGateway.Server.csproj -- \
  apikey create --display-name "my-client" --scopes historian:read

Required scopes: historian:read (reads, status, tag browse/metadata), historian:write (historical/event/live-value writes), historian:tags:write (tag mutations).

Read API

ReadRawAsync

Streams raw historian samples for a tag over a UTC time range.

IAsyncEnumerable<HistorianSample> ReadRawAsync(
    string tag,
    DateTime startUtc,
    DateTime endUtc,
    int maxValues,
    CancellationToken ct = default)
await foreach (var s in client.ReadRawAsync("SysTimeSec",
    DateTime.UtcNow.AddHours(-1), DateTime.UtcNow, maxValues: 500))
{
    Console.WriteLine($"{s.Timestamp.ToDateTime():O}  {s.NumericValue}");
}

ReadAggregateAsync

Streams aggregate samples (15 retrieval modes) at a fixed interval.

IAsyncEnumerable<HistorianAggregateSample> ReadAggregateAsync(
    string tag,
    DateTime startUtc,
    DateTime endUtc,
    RetrievalMode mode,
    TimeSpan interval,
    CancellationToken ct = default)
await foreach (var s in client.ReadAggregateAsync(
    "SysTimeSec",
    DateTime.UtcNow.AddDays(-1), DateTime.UtcNow,
    RetrievalMode.Average,
    TimeSpan.FromMinutes(15)))
{
    Console.WriteLine($"{s.Timestamp.ToDateTime():O}  avg={s.Value}");
}

ReadAtTimeAsync

Returns samples at specific UTC timestamps (unary; retried).

Task<IReadOnlyList<HistorianSample>> ReadAtTimeAsync(
    string tag,
    IReadOnlyList<DateTime> timestampsUtc,
    CancellationToken ct = default)

ReadEventsAsync

Streams historian events over a UTC time range with an optional server-side filter.

IAsyncEnumerable<HistorianEvent> ReadEventsAsync(
    DateTime startUtc,
    DateTime endUtc,
    HistorianEventFilter? filter = null,
    CancellationToken ct = default)

When RuntimeDb:EventReadsEnabled=true on the server, events are served from Runtime.dbo.Events over SQL. A single Source_Object/SourceName EQUAL filter is supported on the SQL path; any other filter shape returns Unimplemented. The native gRPC event-read path is server-gated on 2023 R2 (C2, closed won't-fix).

Write API

AddHistoricalValuesAsync

Writes backfill (historical) values for a tag. Not retried.

Task<WriteAck> AddHistoricalValuesAsync(
    string tag,
    IReadOnlyList<HistorianHistoricalValue> values,
    CancellationToken ct = default)
var ack = await client.AddHistoricalValuesAsync(
    "MyAnalogTag",
    new[]
    {
        new HistorianHistoricalValue
        {
            Timestamp  = Timestamp.FromDateTime(DateTime.UtcNow.AddMinutes(-5)),
            Value      = 42.5,
            OpcQuality = 192,   // 192 = OPC good quality
        },
    });
Console.WriteLine($"Accepted={ack.Accepted}");

WriteLiveValuesAsync

Writes live values via the SQL path (aaAnalogTagInsert / INSERT INTO History). Requires RuntimeDb:Enabled=true on the server. Not retried.

Task<WriteAck> WriteLiveValuesAsync(
    string tag,
    IReadOnlyList<HistorianLiveValue> values,
    CancellationToken ct = default)
var ack = await client.WriteLiveValuesAsync(
    "MyAnalogTag",
    new[]
    {
        new HistorianLiveValue { NumericValue = 42.5 },   // Timestamp absent = server SYSDATETIME()
    });

SendEventAsync

Sends a historian event. Not retried.

Task<WriteAck> SendEventAsync(HistorianEvent historianEvent, CancellationToken ct = default)

Status API

All status calls are idempotent and retried.

Task<bool> ProbeAsync(CancellationToken ct = default)
Task<ConnectionStatus> GetConnectionStatusAsync(CancellationToken ct = default)
Task<StoreForwardStatus> GetStoreForwardStatusAsync(CancellationToken ct = default)
Task<string?> GetSystemParameterAsync(string name, CancellationToken ct = default)
bool up = await client.ProbeAsync();
ConnectionStatus status = await client.GetConnectionStatusAsync();

Tags API

Browse and metadata calls are idempotent and retried. Mutation calls are not retried.

// Stream tag names matching filter (default "*")
IAsyncEnumerable<string> BrowseTagNamesAsync(string filter = "*", CancellationToken ct = default)

// Metadata for a single tag; null when tag absent (idempotent, retried)
Task<HistorianTagMetadata?> GetTagMetadataAsync(string tag, CancellationToken ct = default)

// Upsert tags (not retried)
Task<TagOperationResults> EnsureTagsAsync(IReadOnlyList<HistorianTagDefinition> definitions, CancellationToken ct = default)

// Delete tags (not retried)
Task<TagOperationResults> DeleteTagsAsync(IReadOnlyList<string> names, CancellationToken ct = default)

// Rename tags (not retried)
Task<RenameTagsReply> RenameTagsAsync(IReadOnlyList<TagRenamePair> pairs, CancellationToken ct = default)

// Add extended properties (not retried)
Task<WriteAck> AddTagExtendedPropertiesAsync(string tag, IReadOnlyList<HistorianTagExtendedProperty> properties, CancellationToken ct = default)

TLS example

await using var client = HistorianGatewayClient.Create(new HistorianGatewayClientOptions
{
    Endpoint          = new Uri("https://historian-gateway:5222"),
    ApiKey            = apiKey,
    UseTls            = true,
    CaCertificatePath = "/etc/ssl/certs/gateway-ca.pem",   // pin custom CA
});

When CaCertificatePath is set, the client validates every connection against a custom chain rooted at the pinned certificate(s). Since 0.2.0 this honours intermediate-CA chains (handshake-presented intermediates are used for chain building) and accepts a multi-cert PEM bundle; a single-cert DER .cer still loads. Note the anchor semantics: every certificate in a pinned bundle is trusted as a root — an intermediate included "to help validation" is itself a trust anchor. When CaCertificatePath is absent and RequireCertificateValidation=false (default), self-signed gateway certs are accepted without validation — appropriate for internal deployments only, and this default is committed to flip to true in 1.0.0.

Exception hierarchy

Type gRPC trigger
HistorianGatewayException Base; any unexpected gRPC error
HistorianGatewayAuthenticationException Unauthenticated — invalid or missing API key
HistorianGatewayAuthorizationException PermissionDenied — key lacks the required scope
HistorianGatewayUnavailableException Unavailable — gateway or historian unreachable

All are in the ZB.MOM.WW.HistorianGateway.Client namespace.

try
{
    await foreach (var s in client.ReadRawAsync("SysTimeSec", ...)) { ... }
}
catch (HistorianGatewayAuthenticationException)
{
    // API key rejected — check key value and ApiKeys:Mode on the gateway
}
catch (HistorianGatewayAuthorizationException)
{
    // Key exists but lacks historian:read scope
}
catch (HistorianGatewayUnavailableException)
{
    // Gateway or historian unreachable (after retry exhausted)
}
catch (HistorianGatewayException ex)
{
    // Other gRPC-level failure
}

Live smoke test (env-gated)

The test project includes a live smoke test that skips cleanly when the environment variables are absent — safe to run on any machine without a live gateway.

export HISTGW_GRPC_ENDPOINT=http://wonder-sql-vd03:5221   # e.g. http://... or https://...
export HISTGW_API_KEY=histgw_<id>_<secret>
export HISTORIAN_TEST_TAG=SysTimeSec
dotnet test clients/dotnet/ZB.MOM.WW.HistorianGateway.Client.Tests

Contracts package

Protobuf-generated types (HistorianSample, ReadRawRequest, RetrievalMode, etc.) come from the companion ZB.MOM.WW.HistorianGateway.Contracts package (0.3.0). The client package declares a transitive dependency; you do not need to reference Contracts directly.

Contracts was switched from the Grpc.AspNetCore SDK to Google.Protobuf + Grpc.Core.Api + Grpc.Tools (GrpcServices=Both) so it ships both server base-classes and client stubs without pulling in ASP.NET Core, making it suitable for packaging and cross-repo consumption. The gateway server carries Grpc.AspNetCore directly — the host binding is unaffected.

Dependencies

ID Version Target Framework
ZB.MOM.WW.HistorianGateway.Contracts 0.3.0 net10.0
Grpc.Net.Client 2.76.0 net10.0
Microsoft.Extensions.Logging.Abstractions 10.0.7 net10.0
Polly.Core 8.6.6 net10.0
Details
NuGet
2026-07-14 08:16:23 -04:00
318
Joseph Doherty
82 KiB
Assets (4)
Versions (3) View all
0.3.0 2026-07-14
0.2.0 2026-07-09
0.1.0 2026-06-26