# .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 `. 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`. ### Bulk Commands The session exposes bulk variants for every command family that has one upstream — they all carry a list of entries in one gRPC round-trip, the worker runs the per-item MXAccess calls sequentially on its STA, and the reply returns one result per requested entry. Per-entry failures populate `WasSuccessful = false` with the underlying HRESULT and never throw; only protocol-level failures throw via `EnsureProtocolSuccess`. ```csharp // Subscribe + Unsubscribe to a batch of tags in one round-trip IReadOnlyList subResults = await session.SubscribeBulkAsync( serverHandle, new[] { "Area001.Pump001.Speed", "Area001.Pump001.RunHours" }); int[] itemHandles = subResults.Where(r => r.WasSuccessful).Select(r => r.ItemHandle).ToArray(); await session.UnsubscribeBulkAsync(serverHandle, itemHandles); // Bulk Write — sequential MXAccess Write per entry. IReadOnlyList writeResults = await session.WriteBulkAsync( serverHandle, new[] { new WriteBulkEntry { ItemHandle = h1, UserId = 0, Value = 1.0.ToMxValue() }, new WriteBulkEntry { ItemHandle = h2, UserId = 0, Value = 2.0.ToMxValue() }, }); foreach (BulkWriteResult r in writeResults.Where(r => !r.WasSuccessful)) { Console.Error.WriteLine($"item {r.ItemHandle}: {r.ErrorMessage}"); } // Bulk Read — returns the cached OnDataChange value when the tag is already // advised (was_cached = true) or takes a one-shot snapshot otherwise. IReadOnlyList readResults = await session.ReadBulkAsync( serverHandle, new[] { "Area001.Pump001.Speed", "Area001.Pump002.Speed" }, timeout: TimeSpan.FromMilliseconds(750)); ``` `Write2BulkAsync`, `WriteSecuredBulkAsync`, and `WriteSecured2BulkAsync` follow the same shape; the secured variants additionally carry `CurrentUserId` and `VerifierUserId` per entry and require `invoke:secure` scope. `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 --client-name mxgw-dotnet-cli --json dotnet run --project clients/dotnet/MxGateway.Client.Cli -- add-item --session-id --server-handle 1 --item Area001.Pump001.Speed --json dotnet run --project clients/dotnet/MxGateway.Client.Cli -- advise --session-id --server-handle 1 --item-handle 1 --json dotnet run --project clients/dotnet/MxGateway.Client.Cli -- write --session-id --server-handle 1 --item-handle 1 --type int32 --value 123 --json dotnet run --project clients/dotnet/MxGateway.Client.Cli -- write2 --session-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 --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 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 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 = '' $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)