7.1 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/
MxGateway.Client.sln
MxGateway.Client/
MxGateway.Client.csproj
GatewayClient.cs
MxGatewaySession.cs
MxGatewayClientOptions.cs
Authentication/
Conversion/
Errors/
Generated/
MxGateway.Client.Cli/
MxGateway.Client.Cli.csproj
Program.cs
Commands/
MxGateway.Client.Tests/
MxGateway.Client.Tests.csproj
MxGateway.Client.IntegrationTests/
MxGateway.Client.IntegrationTests.csproj
Target framework:
<TargetFramework>net10.0</TargetFramework>
The scaffold uses a project reference to
src/MxGateway.Contracts/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.ClientGoogle.ProtobufGrpc.Toolsfor generationMicrosoft.Extensions.Logging.AbstractionsSystem.CommandLineor 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: 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,
RegisterAsyncbuilds the right command payload,AddItem2Asyncincludes context,WriteAsyncconverts scalar and array values,- command reply status helpers preserve MXAccess HRESULT,
StreamEventsAsyncyields 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.