ZB.MOM.WW.HistorianGateway.Client (0.3.0)
Installation
dotnet nuget add source --name dohertj2 --username your_username --password your_token https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.jsondotnet add package --source dohertj2 --version 0.3.0 ZB.MOM.WW.HistorianGateway.ClientAbout 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-giteaNuGet feed:ZB.MOM.WW.HistorianGateway.ClientandZB.MOM.WW.HistorianGateway.Contracts, both0.3.0(0.1.0 first published 2026-06-26; 0.2.0 2026-07-09; 0.3.0 2026-07-14). Consumers add theZB.MOM.WW.HistorianGateway.*patterns to theirnuget.configpackageSourceMappingfor thedohertj2-giteasource.
<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=trueon the server, events are served fromRuntime.dbo.Eventsover SQL. A singleSource_Object/SourceNameEQUAL filter is supported on the SQL path; any other filter shape returnsUnimplemented. 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.