9b2abef4e1
Converges all five clients on one version after four had drifted onto the already-published 0.1.2/0.1.1 while their APIs kept changing underneath it: - Rust Cargo.toml [package] + [workspace.package] -> 0.2.0 (CLIENT_VERSION already derives from CARGO_PKG_VERSION, no separate edit). - Python pyproject.toml + version.py -> 0.2.0; new test asserts __version__ matches pyproject.toml (closes the CLI-26 residual drift mode). - Go mxgateway/version.go ClientVersion -> 0.2.0. - .NET ZB.MOM.WW.MxGateway.Client.csproj <Version> -> 0.2.0. - Java -> 0.2.1, not 0.2.0: the live Gitea Maven feed already had 0.2.0 published (2026-06-26), before the CLI-37/38/40/41 conformance fixes changed the client's observable behavior, so reusing 0.2.0 would label two different APIs identically. Recorded as an exception in docs/ClientPackaging.md's new Versioning section. Publish-pipeline guards: - scripts/tag-go-module.ps1 implements the CLI-21 guard: after semver validation it refuses to tag unless clients/go/mxgateway/version.go's ClientVersion already matches the requested tag version. - scripts/pack-clients.ps1 gains a Gitea package-registry collision guard wired into every per-language -Publish step; it aborts if the target name+version already exists rather than force-overwriting. Verified live against the real Gitea registry (credentials already present in this environment) — correctly refuses on every known-published artifact and passes on every unpublished target. Docs updated in the same commit: docs/ClientPackaging.md (new Versioning section), and the five client READMEs' stale 0.1.1/0.1.2 example versions. No .proto changes. No publish performed.
500 lines
22 KiB
Markdown
500 lines
22 KiB
Markdown
# .NET Client Projects
|
|
|
|
The .NET client workspace contains the MXAccess Gateway client library, test
|
|
CLI, and unit tests.
|
|
|
|
## Projects
|
|
|
|
| Project | Purpose |
|
|
|---------|---------|
|
|
| `ZB.MOM.WW.MxGateway.Client` | .NET 10 library entry point, raw gRPC calls, and session helpers. |
|
|
| `ZB.MOM.WW.MxGateway.Client.Cli` | Test CLI for smoke and diagnostic commands. |
|
|
| `ZB.MOM.WW.MxGateway.Client.Tests` | Unit tests for client options, generated contract wiring, auth metadata, session helpers, cancellation, and event streaming. |
|
|
|
|
The projects reference `src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj` so
|
|
the client compiles against the same generated protobuf and gRPC types as the
|
|
gateway. `clients/dotnet/generated` remains reserved for generator output if a
|
|
future client build switches to client-local `Grpc.Tools` generation.
|
|
|
|
## Build And Test
|
|
|
|
```powershell
|
|
dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx
|
|
dotnet test clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx --no-build
|
|
```
|
|
|
|
## Packaging
|
|
|
|
Create local library and CLI artifacts from the repository root:
|
|
|
|
```powershell
|
|
$dotnetPackageOutput = Join-Path (Get-Location) 'artifacts/clients/dotnet'
|
|
dotnet pack clients/dotnet/ZB.MOM.WW.MxGateway.Client/ZB.MOM.WW.MxGateway.Client.csproj -c Release -p:PackageOutputPath="$dotnetPackageOutput"
|
|
dotnet publish clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/ZB.MOM.WW.MxGateway.Client.Cli.csproj -c Release -o artifacts/clients/dotnet/mxgw-dotnet
|
|
```
|
|
|
|
The library package references the shared contracts project at build time. The
|
|
published CLI runs from `artifacts/clients/dotnet/mxgw-dotnet`.
|
|
|
|
## Regenerating Protobuf Bindings
|
|
|
|
The .NET client uses the generated C# types from
|
|
`src/ZB.MOM.WW.MxGateway.Contracts/Generated`. Regenerate those files through the
|
|
contracts project:
|
|
|
|
```powershell
|
|
dotnet build src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj
|
|
```
|
|
|
|
## Client Usage
|
|
|
|
`MxGatewayClient` opens a gRPC channel to the gateway and attaches the API key
|
|
to every unary and streaming call as `authorization: Bearer <api-key>`.
|
|
Cancellation tokens passed to the public methods flow to the generated gRPC
|
|
call. Client-side cancellation stops waiting for the gateway response; it does
|
|
not abort an MXAccess COM call that is already executing inside a worker.
|
|
|
|
```csharp
|
|
await using MxGatewayClient client = MxGatewayClient.Create(
|
|
new MxGatewayClientOptions
|
|
{
|
|
Endpoint = new Uri("http://localhost:5000"),
|
|
ApiKey = apiKey,
|
|
});
|
|
|
|
MxGatewaySession session = await client.OpenSessionAsync();
|
|
try
|
|
{
|
|
int serverHandle = await session.RegisterAsync("sample-client");
|
|
int itemHandle = await session.AddItemAsync(
|
|
serverHandle,
|
|
"Area001.Pump001.Speed");
|
|
|
|
await session.AdviseAsync(serverHandle, itemHandle);
|
|
}
|
|
finally
|
|
{
|
|
await session.CloseAsync();
|
|
}
|
|
```
|
|
|
|
Use `OpenSessionRawAsync`, `CloseSessionRawAsync`, `InvokeAsync`, and
|
|
`StreamEventsAsync` when tests or parity tools need direct generated protobuf
|
|
messages. `MxGatewaySession.OpenSessionReply` keeps the raw session-open reply
|
|
available, and command helpers have `*RawAsync` variants when callers need the
|
|
complete `MxCommandReply`.
|
|
|
|
### Event Streaming And Replay Gaps
|
|
|
|
`StreamEventsAsync(afterWorkerSequence)` yields raw generated `MxEvent`
|
|
messages. Passing a non-zero `afterWorkerSequence` resumes a session's event
|
|
stream after a known worker sequence — this is the reconnect cursor. If that
|
|
cursor is *stale* — older than the oldest event the gateway still retains in the
|
|
session replay ring — the events in between were evicted and cannot be replayed.
|
|
The gateway signals this by emitting a single **replay-gap sentinel** at the head
|
|
of the resumed stream: an `MxEvent` with its `ReplayGap` field set, `Family`
|
|
unspecified, and no body. It means "you missed events — discard local state and
|
|
re-snapshot."
|
|
|
|
Rather than force callers to inspect the raw sentinel, the client exposes a
|
|
typed surface, `StreamEventItemsAsync`, which yields `MxEventStreamItem` values:
|
|
|
|
```csharp
|
|
await foreach (MxEventStreamItem item in session.StreamEventItemsAsync(
|
|
afterWorkerSequence: lastSeenSequence))
|
|
{
|
|
if (item.IsReplayGap)
|
|
{
|
|
// We missed events: throw away local state and re-snapshot.
|
|
ReplayGap gap = item.ReplayGap!;
|
|
// Resume without incurring another gap:
|
|
lastSeenSequence = gap.OldestAvailableSequence - 1;
|
|
await ReSnapshotAsync();
|
|
continue;
|
|
}
|
|
|
|
HandleEvent(item.Event); // normal MXAccess event, IsReplayGap == false
|
|
lastSeenSequence = item.Event.WorkerSequence;
|
|
}
|
|
```
|
|
|
|
The typed surface never synthesizes or drops events — it only makes the
|
|
gateway's own sentinel observable. Normal events pass through with
|
|
`IsReplayGap == false` and `ReplayGap == null`. The gap is only ever produced by
|
|
`StreamEvents`; the diagnostic drain path never emits it. If you already consume
|
|
the raw `StreamEventsAsync` (or the client-level stream), the
|
|
`AsStreamItemsAsync()` extension projects any `IAsyncEnumerable<MxEvent>` into
|
|
the same `MxEventStreamItem` surface.
|
|
|
|
For alarms, the client exposes `QueryActiveAlarmsAsync` (one-shot snapshot of
|
|
the active alarms the gateway's central monitor currently holds),
|
|
`StreamAlarmsAsync` (server-streaming feed of alarm-state-change messages
|
|
keyed by the same monitor), and `AcknowledgeAlarmAsync` (ack by alarm
|
|
reference, optional comment, ack target). All three accept a cancellation
|
|
token and pass through the `MxGateway:Alarms` configuration on the
|
|
server — when alarms are disabled, the gateway returns an empty list / empty
|
|
stream rather than failing.
|
|
|
|
`MxGatewaySession.CloseAsync` is explicit and idempotent. Repeated calls return
|
|
the first `CloseSessionReply` instead of sending another close request.
|
|
|
|
## Values, Status, And Errors
|
|
|
|
The client provides extension helpers for generated protobuf values. Use
|
|
`ToMxValue()` on .NET scalar values and typed arrays to create `MxValue`
|
|
instances for `Write` and `Write2`. Use `ToClrValue()` and
|
|
`GetProjectionKind()` when test or diagnostic code needs to inspect generated
|
|
`MxValue` replies while preserving `rawDiagnostic`, raw data type fields, and
|
|
raw byte payloads.
|
|
|
|
`MxStatusProxy.IsSuccess()` and `ToDiagnosticSummary()` expose MXAccess status
|
|
arrays without collapsing them into a single gateway success flag. Command
|
|
reply helpers follow the same split:
|
|
|
|
```csharp
|
|
reply.EnsureProtocolSuccess();
|
|
reply.EnsureMxAccessSuccess();
|
|
```
|
|
|
|
`EnsureProtocolSuccess()` raises gateway, session, worker, or command
|
|
exceptions for gateway-level failures. It leaves
|
|
`PROTOCOL_STATUS_CODE_MXACCESS_FAILURE` to `EnsureMxAccessSuccess()` so callers
|
|
can keep the full `MxCommandReply`, HRESULT, and status array when MXAccess
|
|
itself rejects a command. `MxAccessException.Reply` contains the raw generated
|
|
reply.
|
|
|
|
`EnsureMxAccessSuccess()` follows COM semantics: only a **negative** HRESULT is
|
|
a failure, so positive success codes such as `S_FALSE` (1) pass. A status entry
|
|
fails only when `Category` is not `MxStatusCategory.Ok` — `MxStatusProxy.Success`
|
|
mirrors the raw COM member for diagnostics and never decides the verdict, which
|
|
is why `IsSuccess()` branches on the category alone.
|
|
|
|
## Write Semantics And Common Pitfalls
|
|
|
|
These are MXAccess parity behaviors that surprise new callers. The gateway
|
|
forwards them unchanged — it does not paper over them.
|
|
|
|
### Attributing a write to a user without `AuthenticateUser`
|
|
|
|
MXAccess only stamps a plain `Write`/`Write2` with a Galaxy user id when the
|
|
item carries an active *supervisory* advise. If you are **not** using the
|
|
verified/secured path (`AuthenticateUser` → `WriteSecured`/`WriteSecured2`) but
|
|
still need the write attributed to a user id, you must first advise the item
|
|
supervisory and then pass that user id on the write. Without the supervisory
|
|
advise the `userId` on a plain write is ignored.
|
|
|
|
The library exposes `Advise`/`UnAdvise` as named helpers but not supervisory
|
|
advise, so send it through the generic command channel:
|
|
|
|
```csharp
|
|
await session.InvokeAsync(new MxCommandRequest
|
|
{
|
|
SessionId = session.SessionId,
|
|
Command = new MxCommand
|
|
{
|
|
Kind = MxCommandKind.AdviseSupervisory,
|
|
AdviseSupervisory = new AdviseSupervisoryCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
},
|
|
},
|
|
});
|
|
|
|
await session.WriteAsync(serverHandle, itemHandle, value.ToMxValue(), userId);
|
|
```
|
|
|
|
The CLI exposes the same command as `advise-supervisory`, and `write` /
|
|
`write2` take `--user-id`.
|
|
|
|
### Array writes replace the whole array
|
|
|
|
A write to an array attribute **replaces the entire array**; it is not an
|
|
element-wise patch. To change a subset of elements, send the full array with
|
|
the unchanged elements included. For example, to change 2 elements of a
|
|
20-element array, build the `MxValue` from all 20 values (the 18 unchanged plus
|
|
the 2 new ones). Sending only the 2 changed values overwrites the attribute
|
|
with a 2-element array.
|
|
|
|
When only a few indices need changing and the rest should be reset to the
|
|
element type's default, use `WriteArrayElementsAsync` instead of building the
|
|
full array manually:
|
|
|
|
```csharp
|
|
await session.WriteArrayElementsAsync(
|
|
serverHandle, itemHandle,
|
|
elementDataType: MxDataType.Integer,
|
|
totalLength: 20,
|
|
elements: new Dictionary<uint, MxValue>
|
|
{
|
|
[2] = 42.ToMxValue(),
|
|
[7] = 99.ToMxValue(),
|
|
});
|
|
```
|
|
|
|
The gateway expands the sparse descriptor into a full `totalLength`-element
|
|
array before forwarding to the worker. Indices not listed in `elements` are
|
|
written as the element type's default — this is a **reset**, not a preserve;
|
|
current values at those positions are discarded. `totalLength` is required and
|
|
must match the declared length of the array attribute. Bare-name array items
|
|
(`Area001.Pump001.Speed`) are auto-normalized to the `[]` form across the whole
|
|
add family — `AddItem`, `AddItem2`, `AddItemBulk`, and `AddBufferedItem` — so the
|
|
array attribute accepts the write.
|
|
|
|
## CLI Usage
|
|
|
|
The test CLI supports deterministic JSON output for automation:
|
|
|
|
```powershell
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- version --json
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- open-session --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- register --session-id <id> --client-name mxgw-dotnet-cli --json
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- add-item --session-id <id> --server-handle 1 --item Area001.Pump001.Speed --json
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- advise --session-id <id> --server-handle 1 --item-handle 1 --json
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --json
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- write2 --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --timestamp 2026-01-01T00:00:00Z --json
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- stream-events --session-id <id> --max-events 1 --json
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- stream-alarms --filter-prefix Area001 --max-events 1 --json
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- acknowledge-alarm --reference "\\Galaxy\Area001.Pump001.PumpFault" --comment "ack from cli" --operator operator1 --json
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- smoke --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --item Area001.Pump001.Speed --json
|
|
```
|
|
|
|
`smoke` opens a session, registers a client, adds one item, advises it,
|
|
optionally writes a value when `--type` and `--value` are supplied, reads a
|
|
bounded event stream, and closes the session in a `finally` block. CLI error
|
|
output redacts API keys supplied through `--api-key`.
|
|
|
|
### `authenticate-user` credentials
|
|
|
|
```powershell
|
|
$env:MXGATEWAY_VERIFY_PASSWORD = "<verify-user password>"
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- authenticate-user --session-id <id> --server-handle 1 --verify-user operator --json
|
|
```
|
|
|
|
The credential comes from `--password` or, preferably, the environment variable
|
|
named by `--password-env` (default `MXGATEWAY_VERIFY_PASSWORD`) so it stays out
|
|
of shell history and the process table. It is never echoed to stdout or stderr,
|
|
and error output routes it through the same redaction seam as the API key. A
|
|
missing or empty resolved credential is a usage error naming the option and the
|
|
variable: the CLI fails before the invoke rather than authenticating with an
|
|
empty password.
|
|
|
|
`MXGATEWAY_VERIFY_PASSWORD` is the canonical variable across all five client CLIs
|
|
— see [Cross-Language Smoke Matrix](../../docs/CrossLanguageSmokeMatrix.md).
|
|
|
|
**Deprecated names.** This CLI previously used `--verify-user-password`,
|
|
`--verify-user-password-env`, and `MXGATEWAY_VERIFY_USER_PASSWORD`. All three
|
|
still resolve, for one release only, so existing scripts keep working; migrate to
|
|
the canonical names above. The full resolution order is `--password`,
|
|
`--verify-user-password`, the variable named by `--password-env` (or the
|
|
deprecated `--verify-user-password-env`, default `MXGATEWAY_VERIFY_PASSWORD`),
|
|
then `MXGATEWAY_VERIFY_USER_PASSWORD`.
|
|
|
|
## Galaxy Repository Browse
|
|
|
|
`GalaxyRepositoryClient` is a separate read-only wrapper around the
|
|
`GalaxyRepository` gRPC service exposed by the same gateway. It shares the API
|
|
key auth interceptor with `MxGatewayClient` and requires the `metadata:read`
|
|
scope server-side. Use it to probe the ZB SQL connection, watch
|
|
`time_of_last_deploy` for redeployments, and enumerate the deployed Galaxy
|
|
object hierarchy plus each object's dynamic attributes.
|
|
|
|
```csharp
|
|
await using GalaxyRepositoryClient repository = GalaxyRepositoryClient.Create(
|
|
new MxGatewayClientOptions
|
|
{
|
|
Endpoint = new Uri("http://localhost:5000"),
|
|
ApiKey = apiKey,
|
|
});
|
|
|
|
bool ok = await repository.TestConnectionAsync();
|
|
DateTime? lastDeploy = await repository.GetLastDeployTimeAsync();
|
|
|
|
IReadOnlyList<GalaxyObject> objects = await repository.DiscoverHierarchyAsync();
|
|
foreach (GalaxyObject galaxyObject in objects)
|
|
{
|
|
Console.WriteLine($"{galaxyObject.TagName} ({galaxyObject.ContainedName})");
|
|
foreach (GalaxyAttribute attribute in galaxyObject.Attributes)
|
|
{
|
|
Console.WriteLine($" {attribute.AttributeName} -> {attribute.FullTagReference}");
|
|
}
|
|
}
|
|
```
|
|
|
|
Use `DiscoverHierarchyOptions` to request a server-side slice without pulling
|
|
the full Galaxy:
|
|
|
|
```csharp
|
|
IReadOnlyList<GalaxyObject> pumps = await repository.DiscoverHierarchyAsync(
|
|
new DiscoverHierarchyOptions
|
|
{
|
|
RootContainedPath = "Area1/Line3",
|
|
TagNameGlob = "Pump_*",
|
|
IncludeAttributes = false,
|
|
});
|
|
```
|
|
|
|
The CLI exposes the same operations:
|
|
|
|
```powershell
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-test-connection --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-last-deploy --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-discover --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY
|
|
```
|
|
|
|
### Browsing lazily
|
|
|
|
For UI trees or OPC UA bridges, use `BrowseChildrenAsync` to walk one level at a
|
|
time instead of paging the full hierarchy. Pass an empty request for root objects;
|
|
subsequent calls supply `ParentGobjectId`, `ParentTagName`, or
|
|
`ParentContainedPath`. Each child's `ChildHasChildren[i]` tells you whether to
|
|
draw an expand triangle. Filter fields match `DiscoverHierarchy`. See
|
|
[Galaxy Repository](../../docs/GalaxyRepository.md#browsechildren) for full
|
|
request and filter semantics.
|
|
|
|
```csharp
|
|
BrowseChildrenReply roots = await repository.BrowseChildrenAsync(
|
|
new BrowseChildrenRequest());
|
|
|
|
for (int i = 0; i < roots.Children.Count; i++)
|
|
{
|
|
GalaxyObject child = roots.Children[i];
|
|
bool hasChildren = roots.ChildHasChildren[i];
|
|
Console.WriteLine($"{child.TagName} expand={hasChildren}");
|
|
}
|
|
```
|
|
|
|
#### High-level walker
|
|
|
|
For UI trees, the client provides a `LazyBrowseNode` walker that handles
|
|
sibling pagination and the `child_has_children` hint for you:
|
|
|
|
```csharp
|
|
await using GalaxyRepositoryClient repository = GalaxyRepositoryClient.Create(
|
|
new MxGatewayClientOptions { Endpoint = new Uri("http://localhost:5000"), ApiKey = apiKey });
|
|
IReadOnlyList<LazyBrowseNode> roots = await repository.BrowseAsync();
|
|
foreach (LazyBrowseNode root in roots)
|
|
{
|
|
if (root.HasChildrenHint)
|
|
{
|
|
await root.ExpandAsync();
|
|
}
|
|
foreach (LazyBrowseNode child in root.Children)
|
|
{
|
|
Console.WriteLine($"{child.Object.TagName} ({(child.HasChildrenHint ? "has children" : "leaf")})");
|
|
}
|
|
}
|
|
```
|
|
|
|
`ExpandAsync` is idempotent — calling it twice fires only one RPC,
|
|
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
|
|
`BrowseAsync` again from the root.
|
|
|
|
The CLI counterpart is `galaxy-browse`. Without `--parent` it walks the root
|
|
objects and eagerly expands `--depth` further levels into an indented tree; with
|
|
`--parent <gobject-id>` it fetches exactly one level of children for that object
|
|
(`--depth` is ignored there). Filter flags map onto `BrowseChildrenOptions`:
|
|
`--category-ids` and `--template-contains` are comma-separated lists,
|
|
`--tag-name-glob` / `--alarm-bearing-only` / `--historized-only` are scalar, and
|
|
`--include-attributes` overrides the server default for attribute population.
|
|
|
|
```powershell
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-browse --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --depth 1
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-browse --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --parent 42 --json
|
|
```
|
|
|
|
### Watching deploy events
|
|
|
|
`WatchDeployEventsAsync` opens the `WatchDeployEvents` server-streaming RPC. The
|
|
server emits a bootstrap event with the current state on subscribe, then one
|
|
event per new `time_of_last_deploy`. Pass a `lastSeenDeployTime` to suppress the
|
|
bootstrap when the caller already holds the current deploy time. Use the
|
|
monotonic `Sequence` field to detect dropped events: gaps mean the
|
|
per-subscriber server-side buffer overflowed and the caller should reconcile.
|
|
|
|
Streaming RPCs are not wrapped by the unary safe-read retry pipeline. The
|
|
caller is responsible for reopening the stream on transient failures.
|
|
|
|
```csharp
|
|
await using GalaxyRepositoryClient repository = GalaxyRepositoryClient.Create(options);
|
|
|
|
DateTimeOffset? lastSeen = null;
|
|
await foreach (DeployEvent evt in repository.WatchDeployEventsAsync(
|
|
lastSeen,
|
|
cancellationToken))
|
|
{
|
|
Console.WriteLine(
|
|
$"seq={evt.Sequence} objects={evt.ObjectCount} attributes={evt.AttributeCount}");
|
|
if (evt.TimeOfLastDeployPresent && evt.TimeOfLastDeploy is not null)
|
|
{
|
|
lastSeen = evt.TimeOfLastDeploy.ToDateTimeOffset();
|
|
}
|
|
}
|
|
```
|
|
|
|
The CLI counterpart streams events until Ctrl+C (or `--max-events`):
|
|
|
|
```powershell
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --last-seen-deploy-time 2026-04-28T14:30:00Z --json
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --max-events 5 --json
|
|
```
|
|
|
|
Use TLS options for a secured gateway:
|
|
|
|
```powershell
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- smoke --endpoint https://ZB.MOM.WW.MxGateway.example.local:5001 --tls --ca-file C:\certs\mxgateway-ca.pem --server-name ZB.MOM.WW.MxGateway.example.local --api-key-env MXGATEWAY_API_KEY --item Area001.Pump001.Speed --json
|
|
```
|
|
|
|
### TLS trust
|
|
|
|
The gateway can auto-generate its own self-signed certificate (it has no PKI), so
|
|
the client is **lenient by default**: a TLS connection (`UseTls` / `--tls`) with
|
|
no pinned CA accepts whatever certificate the gateway presents. To verify
|
|
instead, pin a CA with `CaCertificatePath` / `--ca-file` (this path also enforces
|
|
the certificate hostname/SAN match), or set `RequireCertificateValidation` to
|
|
force OS/system-trust verification without pinning. Use `ServerNameOverride` /
|
|
`--server-name` when the dialed host differs from the certificate SAN. See
|
|
[Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate).
|
|
|
|
## Integration Checks
|
|
|
|
Run live checks only when a gateway and MXAccess-backed worker are available:
|
|
|
|
```powershell
|
|
$env:MXGATEWAY_INTEGRATION = '1'
|
|
$env:MXGATEWAY_ENDPOINT = 'http://localhost:5000'
|
|
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
|
|
$env:MXGATEWAY_TEST_ITEM = 'Area001.Pump001.Speed'
|
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- smoke --endpoint $env:MXGATEWAY_ENDPOINT --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
|
|
```
|
|
|
|
## Installing as a NuGet Package
|
|
|
|
The client publishes to the internal Gitea NuGet feed at
|
|
`https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json`.
|
|
|
|
Add the feed once:
|
|
|
|
````bash
|
|
dotnet nuget add source https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json \
|
|
--name dohertj2-gitea \
|
|
--username <gitea-username> \
|
|
--password <gitea-token-or-password> \
|
|
--store-password-in-clear-text
|
|
````
|
|
|
|
Then add the package to your project:
|
|
|
|
````bash
|
|
dotnet add package ZB.MOM.WW.MxGateway.Client --version 0.2.0
|
|
````
|
|
|
|
The `ZB.MOM.WW.MxGateway.Contracts` package is pulled in transitively.
|
|
|
|
## Related Documentation
|
|
|
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
|
- [.NET Client Detailed Design](./DotnetClientDesign.md)
|