Files
mxaccessgw/docs/clients-dotnet-csharp-design.md
T
2026-04-26 22:29:27 -04:00

6.7 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 ILoggerFactory? LoggerFactory { get; init; }
}

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.