# .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](../../docs/style-guides/CSharpStyleGuide.md) for handwritten code and the [Protobuf Style Guide](../../docs/style-guides/ProtobufStyleGuide.md) for generated contract inputs. ## Projects Recommended layout: ```text 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 ``` Target framework: ```xml net10.0 ``` 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` - `Microsoft.Extensions.Logging.Abstractions` - `System.CommandLine` or similar for CLI - test framework: xUnit or NUnit ## Library API Suggested public types: ```csharp public sealed class MxGatewayClient : IAsyncDisposable { public static MxGatewayClient Create(MxGatewayClientOptions options); public Task OpenSessionAsync( OpenSessionOptions? options = null, CancellationToken cancellationToken = default); public Task InvokeAsync( MxCommandRequest request, CancellationToken cancellationToken = default); } public sealed class MxGatewaySession : IAsyncDisposable { public string SessionId { get; } public Task RegisterAsync(string clientName, CancellationToken ct = default); public Task UnregisterAsync(int serverHandle, CancellationToken ct = default); public Task AddItemAsync(int serverHandle, string item, CancellationToken ct = default); public Task 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> AddItemBulkAsync(int serverHandle, IReadOnlyList tagAddresses, CancellationToken ct = default); public Task> AdviseItemBulkAsync(int serverHandle, IReadOnlyList itemHandles, CancellationToken ct = default); public Task> RemoveItemBulkAsync(int serverHandle, IReadOnlyList itemHandles, CancellationToken ct = default); public Task> UnAdviseItemBulkAsync(int serverHandle, IReadOnlyList itemHandles, CancellationToken ct = default); public Task> SubscribeBulkAsync(int serverHandle, IReadOnlyList tagAddresses, CancellationToken ct = default); public Task> UnsubscribeBulkAsync(int serverHandle, IReadOnlyList itemHandles, CancellationToken ct = default); public Task WriteAsync(int serverHandle, int itemHandle, MxValue value, int userId, CancellationToken ct = default); public IAsyncEnumerable 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 ```csharp 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 bool RequireCertificateValidation { 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. ### TLS trust posture The gateway can serve a self-signed certificate it generates itself (it has no PKI). To make that usable, TLS is **lenient by default**: when `UseTls` is set and `CaCertificatePath` is empty, `CreateHttpHandler` installs a `RemoteCertificateValidationCallback` that returns `true`, so the gateway's self-signed certificate is accepted without verification. To verify the gateway instead: - set `CaCertificatePath` to pin a CA — validated via a `CustomRootTrust` `X509Chain` against that root, and the callback additionally rejects a hostname/SAN mismatch (`RemoteCertificateNameMismatch`); or - set `RequireCertificateValidation` to `true` to keep the default OS/system-trust verification on a connection with no pinned CA. Pinning a CA always wins over the lenient default. ## Auth Interceptor Use a gRPC call credentials/interceptor layer to attach: ```text authorization: Bearer ``` The interceptor must redact the key in logs and exceptions. ## Streaming Expose `StreamEventsAsync` as `IAsyncEnumerable`. On cancellation, cancel the gRPC stream and surface `OperationCanceledException` only when the caller initiated cancellation. Do not reorder events. ## Error Handling Recommended exceptions: ```csharp MxGatewayException MxGatewayAuthenticationException MxGatewayAuthorizationException MxGatewaySessionException MxGatewayWorkerException MxGatewayCommandException MxAccessException ``` For command replies that include MXAccess HRESULT/status, prefer returning the reply and exposing helper methods: ```csharp reply.EnsureProtocolSuccess(); reply.EnsureMxAccessSuccess(); ``` ## Test CLI Project: `ZB.MOM.WW.MxGateway.Client.Cli`. Command examples: ```powershell 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 --json mxgw-dotnet write --session-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: ```text MXGATEWAY_INTEGRATION=1 MXGATEWAY_ENDPOINT= MXGATEWAY_API_KEY= MXGATEWAY_TEST_ITEM= ``` Integration smoke should open, register, add, advise, stream for bounded time, and close. ## Related Documentation - [Client Libraries Detailed Design](../../docs/ClientLibrariesDesign.md) - [Client Proto Generation](../../docs/ClientProtoGeneration.md) - [Client Packaging](../../docs/ClientPackaging.md) - [C# Style Guide](../../docs/style-guides/CSharpStyleGuide.md)