Files
mxaccessgw/docs/clients-dotnet-csharp-design.md
T
2026-04-27 15:37:56 -04:00

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.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: 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.