# .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
  ZB.MOM.WW.MxGateway.Client.IntegrationTests/
    ZB.MOM.WW.MxGateway.Client.IntegrationTests.csproj
```

Target framework:

```xml
<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:

```csharp
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

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

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

```text
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.

## 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)
