Files
mxaccessgw/clients/dotnet/DotnetClientDesign.md
T
Joseph Doherty 397d3c5c4f rename: apply ZB.MOM.WW prefix to all client SDKs + fix pre-existing alarm-RPC breaks
Rename across every client surface using each language's idiomatic convention:

  * .NET   clients/dotnet/MxGateway.Client[.Cli|.Tests]/
             -> clients/dotnet/ZB.MOM.WW.MxGateway.Client[.Cli|.Tests]/
             namespaces -> ZB.MOM.WW.MxGateway.Client[.Cli|.Tests]
             contracts ProjectReference repointed to ZB.MOM.WW.MxGateway.Contracts
             sln migrated to slnx (dotnet sln migrate)
  * Python src/mxgateway -> src/zb_mom_ww_mxgateway
             src/mxgateway_cli -> src/zb_mom_ww_mxgateway_cli
             distribution: mxaccess-gateway-client -> zb-mom-ww-mxaccess-gateway-client
  * Rust   crate: mxgateway-client -> zb-mom-ww-mxgateway-client
             build.rs proto path repointed
  * Java   subprojects: mxgateway-{client,cli} -> zb-mom-ww-mxgateway-{client,cli}
             packages com.dohertylan.mxgateway -> com.zb.mom.ww.mxgateway
             group   com.dohertylan.mxgateway -> com.zb.mom.ww.mxgateway
             rootProject mxaccessgw-java -> zb-mom-ww-mxaccessgw-java
  * Go     generate-proto.ps1 proto path repointed; module path and
             package mxgateway kept (Go convention).
  * proto-inputs.json: generatedOutputs.python updated to new package path.
  * scripts/run-client-e2e-tests.ps1: Java CLI install path + gradle task
             updated to zb-mom-ww-mxgateway-cli.

CLI binary names (mxgw, mxgw-py, mxgw-go, mxgateway-cli) and wire-level
identifiers (MXGATEWAY_* env vars, the mxgw_<id>_<secret> API key
prefix, protobuf package names like mxaccess_gateway.v1, all MXAccess
references) intentionally NOT renamed.

Fix pre-existing alarms-over-gateway breaks unblocked by the rename:

  * mxaccess_gateway.proto: add missing public message QueryActiveAlarmsRequest
    {session_id, client_correlation_id, alarm_filter_prefix} and missing
    rpc QueryActiveAlarms(QueryActiveAlarmsRequest) returns
    (stream ActiveAlarmSnapshot). All four typed clients referenced
    these but they were absent from the proto.
  * MxAccessGatewayService.QueryActiveAlarms: implement the new RPC on
    the server, streaming from IGatewayAlarmService.CurrentAlarms with
    optional alarm_filter_prefix filter.
  * clients/dotnet/.../DiscoverHierarchyOptions.cs: add the hand-written
    .NET POCO that wraps DiscoverHierarchyRequest (referenced by
    GalaxyRepositoryClient.DiscoverHierarchyAsync but never authored).
  * Drop retired session_id field references from
    AcknowledgeAlarmRequest/AcknowledgeAlarmReply test fixtures across
    .NET, Rust, Go, and Python clients.
  * Rust integration test: add the missing stream_alarms impl on the
    fake MxAccessGateway server (the trait gained the method, fake
    didn't).
  * Rust CLI test: bump expected gatewayProtocolVersion 2 -> 3.

Regenerated artifacts updated in this commit:
  * src/ZB.MOM.WW.MxGateway.Contracts/Generated/{MxaccessGateway,MxaccessGatewayGrpc}.cs
  * clients/python/src/zb_mom_ww_mxgateway/generated/*_pb2{,_grpc}.py
  * clients/go/internal/generated/*.pb.go
(C# regenerated by Grpc.Tools on contracts build; Python and Go via
their generate-proto.ps1 scripts; Rust regenerates from .proto via
tonic-build at compile time so no checked-in artefact.)

Verification: 472 server tests, 275 worker tests (9 dev-rig skipped),
18 integration tests (live MxAccess + LDAP + Galaxy), 57 .NET client
tests, 32 Rust workspace tests, 39 Python tests, all Go packages, and
gradle build for Java all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 19:09:34 -04:00

7.5 KiB

.NET 10 C# Client Detailed Design

Purpose

Provide an idiomatic .NET 10 C# client library for MXAccess Gateway, plus a test CLI and unit tests. This client is for modern .NET callers and must not load MXAccess COM.

Follow the C# Style Guide for handwritten code and the Protobuf Style Guide for generated contract inputs.

Projects

Recommended layout:

clients/dotnet/
  ZB.MOM.WW.MxGateway.Client.slnx
  ZB.MOM.WW.MxGateway.Client/
    ZB.MOM.WW.MxGateway.Client.csproj
    GatewayClient.cs
    MxGatewaySession.cs
    MxGatewayClientOptions.cs
    Authentication/
    Conversion/
    Errors/
    Generated/
  ZB.MOM.WW.MxGateway.Client.Cli/
    ZB.MOM.WW.MxGateway.Client.Cli.csproj
    Program.cs
    Commands/
  ZB.MOM.WW.MxGateway.Client.Tests/
    ZB.MOM.WW.MxGateway.Client.Tests.csproj
  ZB.MOM.WW.MxGateway.Client.IntegrationTests/
    ZB.MOM.WW.MxGateway.Client.IntegrationTests.csproj

Target framework:

<TargetFramework>net10.0</TargetFramework>

The scaffold uses a project reference to src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj for generated protobuf and gRPC types. clients/dotnet/generated remains reserved for client-local generator output if the .NET client later needs to decouple from the contracts project.

Expected packages:

  • Grpc.Net.Client
  • Google.Protobuf
  • Grpc.Tools for generation
  • Microsoft.Extensions.Logging.Abstractions
  • System.CommandLine or similar for CLI
  • test framework: xUnit or NUnit

Library API

Suggested public types:

public sealed class MxGatewayClient : IAsyncDisposable
{
    public static MxGatewayClient Create(MxGatewayClientOptions options);
    public Task<MxGatewaySession> OpenSessionAsync(
        OpenSessionOptions? options = null,
        CancellationToken cancellationToken = default);
    public Task<MxCommandReply> InvokeAsync(
        MxCommandRequest request,
        CancellationToken cancellationToken = default);
}

public sealed class MxGatewaySession : IAsyncDisposable
{
    public string SessionId { get; }

    public Task<int> RegisterAsync(string clientName, CancellationToken ct = default);
    public Task UnregisterAsync(int serverHandle, CancellationToken ct = default);
    public Task<int> AddItemAsync(int serverHandle, string item, CancellationToken ct = default);
    public Task<int> AddItem2Async(int serverHandle, string item, string context, CancellationToken ct = default);
    public Task AdviseAsync(int serverHandle, int itemHandle, CancellationToken ct = default);
    public Task UnAdviseAsync(int serverHandle, int itemHandle, CancellationToken ct = default);
    public Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(int serverHandle, IReadOnlyList<string> tagAddresses, CancellationToken ct = default);
    public Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(int serverHandle, IReadOnlyList<int> itemHandles, CancellationToken ct = default);
    public Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(int serverHandle, IReadOnlyList<int> itemHandles, CancellationToken ct = default);
    public Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(int serverHandle, IReadOnlyList<int> itemHandles, CancellationToken ct = default);
    public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(int serverHandle, IReadOnlyList<string> tagAddresses, CancellationToken ct = default);
    public Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(int serverHandle, IReadOnlyList<int> itemHandles, CancellationToken ct = default);
    public Task WriteAsync(int serverHandle, int itemHandle, MxValue value, int userId, CancellationToken ct = default);
    public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken ct = default);
    public Task CloseAsync(CancellationToken ct = default);
}

Generated protobuf types should remain available under a generated namespace. Handwritten wrappers should not hide raw replies.

Options

public sealed class MxGatewayClientOptions
{
    public required Uri Endpoint { get; init; }
    public required string ApiKey { get; init; }
    public bool UseTls { get; init; }
    public string? CaCertificatePath { get; init; }
    public string? ServerNameOverride { get; init; }
    public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(10);
    public TimeSpan DefaultCallTimeout { get; init; } = TimeSpan.FromSeconds(30);
    public MxGatewayClientRetryOptions Retry { get; init; } = new();
    public ILoggerFactory? LoggerFactory { get; init; }
}

The .NET client applies a bounded Polly retry policy only to idempotent calls: CloseSession and diagnostic Invoke commands such as Ping, GetSessionState, and GetWorkerInfo. It does not retry OpenSession, event streams, writes, secured writes, authentication, registration, item management, or subscription changes because those calls can partially succeed in MXAccess.

API key may be loaded from MXGATEWAY_API_KEY by the CLI, not implicitly by the library constructor unless a helper explicitly says it does that.

Auth Interceptor

Use a gRPC call credentials/interceptor layer to attach:

authorization: Bearer <api key>

The interceptor must redact the key in logs and exceptions.

Streaming

Expose StreamEventsAsync as IAsyncEnumerable<MxEvent>. On cancellation, cancel the gRPC stream and surface OperationCanceledException only when the caller initiated cancellation.

Do not reorder events.

Error Handling

Recommended exceptions:

MxGatewayException
MxGatewayAuthenticationException
MxGatewayAuthorizationException
MxGatewaySessionException
MxGatewayWorkerException
MxGatewayCommandException
MxAccessException

For command replies that include MXAccess HRESULT/status, prefer returning the reply and exposing helper methods:

reply.EnsureProtocolSuccess();
reply.EnsureMxAccessSuccess();

Test CLI

Project: ZB.MOM.WW.MxGateway.Client.Cli.

Command examples:

mxgw-dotnet version
mxgw-dotnet smoke --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --item TestChildObject.TestInt
mxgw-dotnet stream-events --session-id <id> --json
mxgw-dotnet write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123

The CLI should use System.CommandLine or a similarly testable parser. JSON output should be deterministic and redact API keys.

Unit Tests

Use an in-process fake gRPC service with Grpc.AspNetCore.Server test host or mock the generated client behind an internal interface.

Required tests:

  • auth metadata is attached,
  • API key is redacted,
  • options build plaintext and TLS channels correctly,
  • RegisterAsync builds the right command payload,
  • AddItem2Async includes context,
  • WriteAsync converts scalar and array values,
  • command reply status helpers preserve MXAccess HRESULT,
  • StreamEventsAsync yields ordered events,
  • stream cancellation disposes the call,
  • CLI parsing and JSON output.

Integration Tests

Use xUnit traits or categories. Skip unless:

MXGATEWAY_INTEGRATION=1
MXGATEWAY_ENDPOINT=<endpoint>
MXGATEWAY_API_KEY=<key>
MXGATEWAY_TEST_ITEM=<item>

Integration smoke should open, register, add, advise, stream for bounded time, and close.