89043cb2b6
Client.Dotnet-004: documented DefaultCallTimeout as both the per-attempt deadline and the shared retry budget, and removed DeadlineExceeded from the transient-retry set (a client-imposed deadline cannot be helped by retrying). Client.Dotnet-005: RegisterAsync/AddItemAsync/AddItem2Async silently returned 0 when a successful reply lacked the typed payload. They now throw a descriptive MxGatewayException. Client.Dotnet-006: added XML docs to the previously undocumented public members MaxGrpcMessageBytes, GatewayProtocolVersion, WorkerProtocolVersion. Client.Dotnet-007: corrected the AcknowledgeAlarmAsync XML comment — the RPC requires the admin scope, not a non-existent invoke:alarm-ack sub-scope. Client.Dotnet-008: the CLI redactor missed env-var-sourced keys because the caller passed only the --api-key option. Redaction now uses the same resolver, stripping env-var keys too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
260 lines
11 KiB
Markdown
260 lines
11 KiB
Markdown
# .NET Client Projects
|
|
|
|
The .NET client workspace contains the MXAccess Gateway client library, test
|
|
CLI, and unit tests.
|
|
|
|
## Projects
|
|
|
|
| Project | Purpose |
|
|
|---------|---------|
|
|
| `MxGateway.Client` | .NET 10 library entry point, raw gRPC calls, and session helpers. |
|
|
| `MxGateway.Client.Cli` | Test CLI for smoke and diagnostic commands. |
|
|
| `MxGateway.Client.Tests` | Unit tests for client options, generated contract wiring, auth metadata, session helpers, cancellation, and event streaming. |
|
|
|
|
The projects reference `src/MxGateway.Contracts/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/MxGateway.Client.sln
|
|
dotnet test clients/dotnet/MxGateway.Client.sln --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/MxGateway.Client/MxGateway.Client.csproj -c Release -p:PackageOutputPath="$dotnetPackageOutput"
|
|
dotnet publish clients/dotnet/MxGateway.Client.Cli/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/MxGateway.Contracts/Generated`. Regenerate those files through the
|
|
contracts project:
|
|
|
|
```powershell
|
|
dotnet build src/MxGateway.Contracts/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`.
|
|
|
|
`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.
|
|
|
|
When a gRPC call itself fails, the transport maps the underlying
|
|
`RpcException` to a native exception: `Unauthenticated` becomes
|
|
`MxGatewayAuthenticationException`, `PermissionDenied` becomes
|
|
`MxGatewayAuthorizationException`, a cancelled call becomes
|
|
`OperationCanceledException`, and every other status becomes a base
|
|
`MxGatewayException`. `MxGatewayException.StatusCode` carries the originating
|
|
gRPC `Grpc.Core.StatusCode` (non-null whenever the failure came from a gRPC
|
|
status), so callers can distinguish a transient outage (`Unavailable`) from a
|
|
permanent error (`InvalidArgument`, `NotFound`) without downcasting
|
|
`InnerException`.
|
|
|
|
## CLI Usage
|
|
|
|
The test CLI supports deterministic JSON output for automation:
|
|
|
|
```powershell
|
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- version --json
|
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- open-session --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- register --session-id <id> --client-name mxgw-dotnet-cli --json
|
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- add-item --session-id <id> --server-handle 1 --item Area001.Pump001.Speed --json
|
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- advise --session-id <id> --server-handle 1 --item-handle 1 --json
|
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --json
|
|
dotnet run --project clients/dotnet/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/MxGateway.Client.Cli -- stream-events --session-id <id> --max-events 1 --json
|
|
dotnet run --project clients/dotnet/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 the effective API key, whether it was supplied through
|
|
`--api-key` or resolved from the `--api-key-env` environment variable.
|
|
|
|
## 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/MxGateway.Client.Cli -- galaxy-test-connection --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
|
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- galaxy-last-deploy --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
|
dotnet run --project clients/dotnet/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.
|
|
|
|
```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/MxGateway.Client.Cli -- galaxy-watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY
|
|
dotnet run --project clients/dotnet/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/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/MxGateway.Client.Cli -- smoke --endpoint https://mxgateway.example.local:5001 --tls --ca-file C:\certs\mxgateway-ca.pem --server-name 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:
|
|
|
|
```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/MxGateway.Client.Cli -- smoke --endpoint $env:MXGATEWAY_ENDPOINT --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
|
|
```
|
|
|
|
## Related Documentation
|
|
|
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
|
- [.NET Client Detailed Design](./DotnetClientDesign.md)
|