# Component: Client ## Purpose A .NET 10 class library providing a typed gRPC client for consuming the LmxProxy service. Used by ScadaLink's Data Connection Layer to connect to AVEVA System Platform via the LmxProxy Host. ## Location `src/ZB.MOM.WW.LmxProxy.Client/` — all files in this project. Key files: - `ILmxProxyClient.cs` — public interface. - `LmxProxyClient.cs` — main implementation (partial class across multiple files). - `LmxProxyClientBuilder.cs` — fluent builder for client construction. - `ServiceCollectionExtensions.cs` — DI integration and options classes. - `ILmxProxyClientFactory.cs` — factory interface and implementation. - `StreamingExtensions.cs` — batch and parallel streaming helpers. - `Domain/ScadaContracts.cs` — code-first gRPC contracts. - `Security/GrpcChannelFactory.cs` — TLS channel creation. ## Responsibilities - Connect to and communicate with the LmxProxy Host gRPC service. - Manage session lifecycle (connect, keep-alive, disconnect). - Execute read, write, and subscribe operations with retry and concurrency control. - Provide a fluent builder and DI integration for configuration. - Track client-side performance metrics. - Support TLS and mutual TLS connections. ## 1. Public Interface (ILmxProxyClient) | Method | Description | |--------|-------------| | `ConnectAsync(ct)` | Establish gRPC channel and session | | `DisconnectAsync()` | Graceful disconnect | | `IsConnectedAsync()` | Thread-safe connection state check | | `ReadAsync(address, ct)` | Read single tag, returns Vtq | | `ReadBatchAsync(addresses, ct)` | Read multiple tags, returns dictionary | | `WriteAsync(address, value, ct)` | Write single tag value | | `WriteBatchAsync(values, ct)` | Write multiple tag values | | `SubscribeAsync(addresses, onUpdate, onStreamError, ct)` | Subscribe to tag updates with value and error callbacks | | `GetMetrics()` | Return operation counts, errors, latency stats | | `DefaultTimeout` | Configurable timeout (default 30s, range 1s–10min) | Implements `IDisposable` and `IAsyncDisposable`. ## 2. Connection Management ### 2.1 Connect `ConnectAsync()`: 1. Creates a gRPC channel via `GrpcChannelFactory` (HTTP or HTTPS based on TLS config). 2. Creates a `protobuf-net.Grpc` client for `IScadaService`. 3. Calls the `Connect` RPC with a client ID (format: `ScadaBridge-{guid}`) and optional API key. 4. Stores the returned session ID. 5. Starts the keep-alive timer. ### 2.2 Keep-Alive - Timer-based ping every **30 seconds** (hardcoded). - Sends a lightweight `GetConnectionState` RPC. - On failure: stops the timer, marks disconnected, triggers subscription cleanup. ### 2.3 Disconnect `DisconnectAsync()`: 1. Stops keep-alive timer. 2. Calls `Disconnect` RPC. 3. Clears session ID. 4. Disposes gRPC channel. ### 2.4 Connection State `IsConnected` property: `!_disposed && _isConnected && !string.IsNullOrEmpty(_sessionId)`. ## 3. Builder Pattern (LmxProxyClientBuilder) | Method | Default | Constraint | |--------|---------|-----------| | `WithHost(string)` | Required | Non-null/non-empty | | `WithPort(int)` | 5050 | 1–65535 | | `WithApiKey(string?)` | null | Optional | | `WithTimeout(TimeSpan)` | 30 seconds | > 0 and ≤ 10 minutes | | `WithLogger(ILogger)` | NullLogger | Optional | | `WithSslCredentials(string?)` | Disabled | Optional cert path | | `WithTlsConfiguration(ClientTlsConfiguration)` | null | Full TLS config | | `WithRetryPolicy(int, TimeSpan)` | 3 attempts, 1s delay | maxAttempts > 0, delay > 0 | | `WithMetrics()` | Disabled | Enables metric collection | | `WithCorrelationIdHeader(string)` | null | Custom header name | ## 4. Retry Policy Polly-based exponential backoff: - Default: **3 attempts** with **1-second** initial delay. - Backoff sequence: `delay * 2^(retryAttempt - 1)` → 1s, 2s, 4s. - Transient errors retried: `Unavailable`, `DeadlineExceeded`, `ResourceExhausted`, `Aborted`. - Each retry is logged with correlation ID at Warning level. ## 5. Subscription ### 5.1 Subscribe API `SubscribeAsync(addresses, onUpdate, onStreamError, ct)` returns an `ISubscription`: - Calls the `Subscribe` RPC (server streaming) with the tag list and default sampling interval (**1000ms**). - Processes streamed `VtqMessage` items asynchronously, invoking the `onUpdate(tag, vtq)` callback for each. - On stream termination (server disconnect, gRPC error, or connection drop), invokes the `onStreamError` callback exactly once. - On stream error, the client immediately nullifies its session ID, causing `IsConnected` to return `false`. This triggers the DCL adapter's `Disconnected` event and reconnection cycle. - Errors are logged per-subscription. ### 5.2 ISubscription - `Dispose()` — synchronous disposal with **5-second** timeout. - Automatic callback on disposal for cleanup. ## 6. DI Integration ### 6.1 Service Collection Extensions | Method | Lifetime | Description | |--------|----------|-------------| | `AddLmxProxyClient(IConfiguration)` | Singleton | Bind `LmxProxy` config section | | `AddLmxProxyClient(IConfiguration, string)` | Singleton | Bind named config section | | `AddLmxProxyClient(Action)` | Singleton | Builder action | | `AddScopedLmxProxyClient(IConfiguration)` | Scoped | Per-scope lifetime | | `AddNamedLmxProxyClient(string, Action)` | Keyed singleton | Named/keyed registration | ### 6.2 Configuration Options (LmxProxyClientOptions) Bound from `appsettings.json`: | Setting | Default | Description | |---------|---------|-------------| | Host | `localhost` | Server hostname | | Port | 5050 | Server port | | ApiKey | null | API key | | Timeout | 30 seconds | Operation timeout | | UseSsl | false | Enable TLS | | CertificatePath | null | SSL certificate path | | EnableMetrics | false | Enable client metrics | | CorrelationIdHeader | null | Custom correlation header | | Retry:MaxAttempts | 3 | Retry attempts | | Retry:Delay | 1 second | Initial retry delay | ### 6.3 Factory Pattern `ILmxProxyClientFactory` creates configured clients: - `CreateClient()` — uses default `LmxProxy` config section. - `CreateClient(string)` — uses named config section. - `CreateClient(Action)` — uses builder action. Registered as singleton in DI. ## 7. Streaming Extensions Helper methods for large-scale batch operations: | Method | Default Batch Size | Description | |--------|--------------------|-------------| | `ReadStreamAsync` | 100 | Batched reads, 2 retries per batch, stops after 3 consecutive errors. Returns `IAsyncEnumerable>`. | | `WriteStreamAsync` | 100 | Batched writes from async enumerable input. Returns total count written. | | `ProcessInParallelAsync` | — | Parallel processing with max concurrency of **4** (configurable). Semaphore-based rate limiting. | | `SubscribeStreamAsync` | — | Wraps callback-based subscription into `IAsyncEnumerable` via `System.Threading.Channels`. | ## 8. Client Metrics When metrics are enabled (`WithMetrics()`): - Per-operation tracking: counts, error counts, latency. - Rolling buffer of **1000** latency samples per operation (prevents memory growth). - Snapshot via `GetMetrics()` returns: `{op}_count`, `{op}_errors`, `{op}_avg_latency_ms`, `{op}_p95_latency_ms`, `{op}_p99_latency_ms`. ## 9. Value and Quality Handling ### 9.1 Values (TypedValue) Read responses and subscription updates return values as `TypedValue` (protobuf oneof). The client extracts the value directly from the appropriate oneof field (e.g., `vtq.Value.DoubleValue`, `vtq.Value.BoolValue`). Write operations construct `TypedValue` with the correct oneof case for the value's native type. No string serialization or parsing is needed. ### 9.2 Quality (QualityCode) Quality is received as a `QualityCode` message. Category checks use bitmask: `IsGood = (statusCode & 0xC0000000) == 0x00000000`, `IsBad = (statusCode & 0xC0000000) == 0x80000000`. The `symbolic_name` field provides human-readable quality for logging and display. ### 9.3 Current Implementation (V1 Legacy) The current codebase still uses v1 string-based encoding. During v2 migration, the following will be removed: - `ConvertToVtq()` — parses string values via heuristic (double → bool → null → raw string). - `ConvertToString()` — serializes values via `.ToString()`. ## Dependencies - **protobuf-net.Grpc** — code-first gRPC client. - **Grpc.Net.Client** — HTTP/2 gRPC transport. - **Polly** — retry policies. - **Microsoft.Extensions.DependencyInjection** — DI integration. - **Microsoft.Extensions.Configuration** — options binding. - **Microsoft.Extensions.Logging** — logging abstraction. ## Interactions - **ScadaLink Data Connection Layer** consumes the client library via `ILmxProxyClient`. - **Protocol** — the client uses code-first contracts (`IScadaService`) that are wire-compatible with the Host's proto-generated service. - **Security** — `GrpcChannelFactory` creates TLS-configured channels matching the Host's TLS configuration.