Files
mxaccessgw/clients/dotnet
Joseph Doherty 71d2c39f01 e2e: port batch subcommand to all five client CLIs
scripts/run-client-e2e-tests.ps1 expects each language CLI to expose a
`batch` subcommand that reads command lines from stdin, runs each
through the normal subcommand dispatch, writes the JSON result, then
a sentinel line `__MXGW_BATCH_EOR__`. The implementation lived on a
divergent branch (commit 6126099) that was never merged into main —
this commit ports the same protocol to HEAD's renamed CLIs so the
existing matrix script runs end-to-end.

The protocol:
  - one line of stdin = one full CLI invocation
  - successful output → stdout, then __MXGW_BATCH_EOR__
  - failure → {"error":"...","type":"error"} JSON on stdout, then
    __MXGW_BATCH_EOR__ (errors do NOT exit the loop)
  - empty line or EOF terminates the loop

Per-CLI additions:

  .NET: RunBatchAsync + per-line StringWriter capture, JSON error
    envelope when forceJsonErrors is true. Two new tests in
    MxGatewayClientCliTests covering the success and error paths.

  Go:   runBatch with bufio.Scanner, runs each line through the
    existing runWithIO switch with a buffered stdout writer. One new
    test pinning the EOR sentinel.

  Rust: new `Batch` variant on the clap Command enum, run_batch
    re-parses each line via Cli::try_parse_from. Two new tests in the
    inline mod tests block.

  Python: new `batch` click command in commands.py that uses
    CliRunner to dispatch each line; synthesises {"error",..."type"}
    JSON from click error messages when the captured output isn't
    already JSON-shaped. Three new tests in test_cli.py.

  Java: BatchCommand inner @Command with BufferedReader stdin loop,
    fresh commandLine() per dispatch with captured stdout/stderr
    PrintWriters; non-zero exit codes and uncaught exceptions both
    surface as JSON-error blocks. Two new tests.

Also fixes scripts/run-client-e2e-tests.ps1 line 705: the Python
invocation was still passing the old module name `mxgateway_cli` to
`python -m`; the client SDK rename in 397d3c5 moved it to
`zb_mom_ww_mxgateway_cli`. Without the fix the Python leg fails
with "No module named mxgateway_cli" before reaching open-session.

Verification: full matrix at the redeployed gateway (localhost:5120,
running ZB.MOM.WW.MxGateway.Server.exe / ZB.MOM.WW.MxGateway.Worker.exe)
with -SkipBulk -SkipReadWriteBulk -SkipParity -SkipAuth (those phases
exercise bulk read/write CLI subcommands that also live on the
divergent branch — porting those is a follow-up). All five clients
report `closed=true, addedItems=120, eventCount=5` and overall
`success=true`. Per-language unit tests pass:
  - dotnet: 59/59
  - go:     all packages clean
  - rust:   cargo test --workspace clean
  - python: 42/42
  - java:   gradle build SUCCESSFUL

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:08:15 -04:00
..

.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

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:

$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:

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.

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.

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:

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.

CLI Usage

The test CLI supports deterministic JSON output for automation:

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 -- 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.

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.

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:

IReadOnlyList<GalaxyObject> pumps = await repository.DiscoverHierarchyAsync(
    new DiscoverHierarchyOptions
    {
        RootContainedPath = "Area1/Line3",
        TagNameGlob = "Pump_*",
        IncludeAttributes = false,
    });

The CLI exposes the same operations:

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

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.

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):

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:

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

Integration Checks

Run live checks only when a gateway and MXAccess-backed worker are available:

$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