Add XML documentation across gateway, worker, and .NET client

This commit is contained in:
Joseph Doherty
2026-04-30 11:49:58 -04:00
parent 4731ab535c
commit eed1e88a37
269 changed files with 4555 additions and 13 deletions
@@ -2,11 +2,14 @@ using System.Globalization;
namespace MxGateway.Client.Cli;
/// <summary>Parses command-line arguments into flags and named values.</summary>
internal sealed class CliArguments
{
private readonly Dictionary<string, string> _values = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _flags = new(StringComparer.OrdinalIgnoreCase);
/// <summary>Initializes a new instance by parsing the given command-line arguments.</summary>
/// <param name="args">Unparsed command-line arguments; flags prefixed with '--' and values follow their flag.</param>
public CliArguments(IEnumerable<string> args)
{
string? pendingName = null;
@@ -39,11 +42,15 @@ internal sealed class CliArguments
}
}
/// <summary>Returns whether the named flag was present in the arguments.</summary>
/// <param name="name">The flag name (without '--' prefix).</param>
public bool HasFlag(string name)
{
return _flags.Contains(name);
}
/// <summary>Returns the value for a named argument, or <c>null</c> if absent.</summary>
/// <param name="name">The argument name (without '--' prefix).</param>
public string? GetOptional(string name)
{
return _values.TryGetValue(name, out string? value)
@@ -51,6 +58,8 @@ internal sealed class CliArguments
: null;
}
/// <summary>Returns the value for a required named argument, or throws if absent.</summary>
/// <param name="name">The argument name (without '--' prefix).</param>
public string GetRequired(string name)
{
string? value = GetOptional(name);
@@ -62,6 +71,9 @@ internal sealed class CliArguments
return value;
}
/// <summary>Parses and returns an int32 argument, or the default value if absent.</summary>
/// <param name="name">The argument name (without '--' prefix).</param>
/// <param name="defaultValue">The default value if the argument is absent; if <c>null</c>, the argument is required.</param>
public int GetInt32(string name, int? defaultValue = null)
{
string? value = GetOptional(name);
@@ -78,6 +90,9 @@ internal sealed class CliArguments
return int.Parse(value, CultureInfo.InvariantCulture);
}
/// <summary>Parses and returns a uint32 argument, or the default value if absent.</summary>
/// <param name="name">The argument name (without '--' prefix).</param>
/// <param name="defaultValue">The default value if the argument is absent.</param>
public uint GetUInt32(string name, uint defaultValue)
{
string? value = GetOptional(name);
@@ -86,6 +101,9 @@ internal sealed class CliArguments
: uint.Parse(value, CultureInfo.InvariantCulture);
}
/// <summary>Parses and returns a uint64 argument, or the default value if absent.</summary>
/// <param name="name">The argument name (without '--' prefix).</param>
/// <param name="defaultValue">The default value if the argument is absent.</param>
public ulong GetUInt64(string name, ulong defaultValue)
{
string? value = GetOptional(name);
@@ -94,6 +112,9 @@ internal sealed class CliArguments
: ulong.Parse(value, CultureInfo.InvariantCulture);
}
/// <summary>Parses and returns a TimeSpan argument, or the default value if absent. Supports "ms", "s", and standard TimeSpan format.</summary>
/// <param name="name">The argument name (without '--' prefix).</param>
/// <param name="defaultValue">The default value if the argument is absent.</param>
public TimeSpan GetDuration(string name, TimeSpan defaultValue)
{
string? value = GetOptional(name);
@@ -5,34 +5,82 @@ namespace MxGateway.Client.Cli;
public interface IMxGatewayCliClient : IAsyncDisposable
{
/// <summary>
/// Opens a new gateway session.
/// </summary>
/// <param name="request">Session open request.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The session open reply.</returns>
Task<OpenSessionReply> OpenSessionAsync(
OpenSessionRequest request,
CancellationToken cancellationToken);
/// <summary>
/// Closes an open gateway session.
/// </summary>
/// <param name="request">Session close request.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The session close reply.</returns>
Task<CloseSessionReply> CloseSessionAsync(
CloseSessionRequest request,
CancellationToken cancellationToken);
/// <summary>
/// Invokes an MXAccess command on the session.
/// </summary>
/// <param name="request">The command request.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The command reply.</returns>
Task<MxCommandReply> InvokeAsync(
MxCommandRequest request,
CancellationToken cancellationToken);
/// <summary>
/// Streams events from the gateway session.
/// </summary>
/// <param name="request">The stream events request.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>An async enumerable of events.</returns>
IAsyncEnumerable<MxEvent> StreamEventsAsync(
StreamEventsRequest request,
CancellationToken cancellationToken);
/// <summary>
/// Tests connection to the Galaxy Repository.
/// </summary>
/// <param name="request">The connection test request.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The connection test reply.</returns>
Task<TestConnectionReply> GalaxyTestConnectionAsync(
TestConnectionRequest request,
CancellationToken cancellationToken);
/// <summary>
/// Gets the last deployment time from the Galaxy Repository.
/// </summary>
/// <param name="request">The last deploy time request.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The last deploy time reply.</returns>
Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
GetLastDeployTimeRequest request,
CancellationToken cancellationToken);
/// <summary>
/// Discovers the Galaxy Repository hierarchy.
/// </summary>
/// <param name="request">The discover hierarchy request.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The discover hierarchy reply.</returns>
Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
DiscoverHierarchyRequest request,
CancellationToken cancellationToken);
/// <summary>
/// Watches for deployment events from the Galaxy Repository.
/// </summary>
/// <param name="request">The watch deploy events request.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>An async enumerable of deployment events.</returns>
IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
WatchDeployEventsRequest request,
CancellationToken cancellationToken);
@@ -9,6 +9,10 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
private readonly MxGatewayClient _client;
private readonly Lazy<GalaxyRepositoryClient> _galaxyClient;
/// <summary>
/// Initializes a new instance of the <see cref="MxGatewayCliClientAdapter"/> that bridges the CLI to the gateway client.
/// </summary>
/// <param name="client">The gateway client to adapt.</param>
public MxGatewayCliClientAdapter(MxGatewayClient client)
{
_client = client;
@@ -16,6 +20,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
() => GalaxyRepositoryClient.Create(_client.Options));
}
/// <inheritdoc />
public Task<OpenSessionReply> OpenSessionAsync(
OpenSessionRequest request,
CancellationToken cancellationToken)
@@ -23,6 +28,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _client.OpenSessionRawAsync(request, cancellationToken);
}
/// <inheritdoc />
public Task<CloseSessionReply> CloseSessionAsync(
CloseSessionRequest request,
CancellationToken cancellationToken)
@@ -30,6 +36,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _client.CloseSessionRawAsync(request, cancellationToken);
}
/// <inheritdoc />
public Task<MxCommandReply> InvokeAsync(
MxCommandRequest request,
CancellationToken cancellationToken)
@@ -37,6 +44,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _client.InvokeAsync(request, cancellationToken);
}
/// <inheritdoc />
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
StreamEventsRequest request,
CancellationToken cancellationToken)
@@ -44,6 +52,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _client.StreamEventsAsync(request, cancellationToken);
}
/// <inheritdoc />
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
TestConnectionRequest request,
CancellationToken cancellationToken)
@@ -51,6 +60,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _galaxyClient.Value.TestConnectionRawAsync(request, cancellationToken);
}
/// <inheritdoc />
public Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
GetLastDeployTimeRequest request,
CancellationToken cancellationToken)
@@ -58,6 +68,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _galaxyClient.Value.GetLastDeployTimeRawAsync(request, cancellationToken);
}
/// <inheritdoc />
public Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
DiscoverHierarchyRequest request,
CancellationToken cancellationToken)
@@ -65,6 +76,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _galaxyClient.Value.DiscoverHierarchyRawAsync(request, cancellationToken);
}
/// <inheritdoc />
public IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
WatchDeployEventsRequest request,
CancellationToken cancellationToken)
@@ -72,6 +84,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _galaxyClient.Value.WatchDeployEventsRawAsync(request, cancellationToken);
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
if (_galaxyClient.IsValueCreated)
@@ -1,7 +1,11 @@
namespace MxGateway.Client.Cli;
/// <summary>Utility to redact API keys from error messages for safe output.</summary>
internal static class MxGatewayCliSecretRedactor
{
/// <summary>Replaces occurrences of the API key in the value with a redacted placeholder.</summary>
/// <param name="value">The message text to redact.</param>
/// <param name="apiKey">The API key to remove; no redaction if null or empty.</param>
public static string Redact(string value, string? apiKey)
{
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(apiKey))
@@ -7,6 +7,7 @@ using MxGateway.Contracts.Proto.Galaxy;
namespace MxGateway.Client.Cli;
/// <summary>Command-line interface for the MXAccess Gateway client, supporting session and command operations.</summary>
public static class MxGatewayClientCli
{
private const uint MaxAggregateEvents = 10_000;
@@ -15,6 +16,10 @@ public static class MxGatewayClientCli
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
/// <summary>Runs the CLI synchronously with the given arguments, writing output and errors.</summary>
/// <param name="args">Command-line arguments (command name followed by options).</param>
/// <param name="standardOutput">TextWriter for command output.</param>
/// <param name="standardError">TextWriter for error messages.</param>
public static int Run(
string[] args,
TextWriter standardOutput,
@@ -25,6 +30,11 @@ public static class MxGatewayClientCli
.GetResult();
}
/// <summary>Runs the CLI asynchronously with the given arguments, writing output and errors.</summary>
/// <param name="args">Command-line arguments (command name followed by options).</param>
/// <param name="standardOutput">TextWriter for command output.</param>
/// <param name="standardError">TextWriter for error messages.</param>
/// <param name="clientFactory">Optional factory to create the gateway client; defaults to MxGatewayClient.Create.</param>
public static Task<int> RunAsync(
string[] args,
TextWriter standardOutput,
@@ -3,30 +3,71 @@ using MxGateway.Contracts.Proto.Galaxy;
namespace MxGateway.Client.Tests;
/// <summary>
/// Fake Galaxy Repository client transport for testing.
/// </summary>
internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions options) : IGalaxyRepositoryClientTransport
{
/// <summary>
/// Gets the gateway client options.
/// </summary>
public MxGatewayClientOptions Options { get; } = options;
/// <summary>
/// Gets the raw gRPC client; always null for the fake.
/// </summary>
public GalaxyRepository.GalaxyRepositoryClient? RawClient => null;
/// <summary>
/// Gets the list of TestConnection RPC calls made by the client.
/// </summary>
public List<(TestConnectionRequest Request, CallOptions CallOptions)> TestConnectionCalls { get; } = [];
/// <summary>
/// Gets the list of GetLastDeployTime RPC calls made by the client.
/// </summary>
public List<(GetLastDeployTimeRequest Request, CallOptions CallOptions)> GetLastDeployTimeCalls { get; } = [];
/// <summary>
/// Gets the list of DiscoverHierarchy RPC calls made by the client.
/// </summary>
public List<(DiscoverHierarchyRequest Request, CallOptions CallOptions)> DiscoverHierarchyCalls { get; } = [];
/// <summary>
/// Gets or sets the reply to return from TestConnection; defaults to successful response.
/// </summary>
public TestConnectionReply TestConnectionReply { get; set; } = new() { Ok = true };
/// <summary>
/// Gets or sets the reply to return from GetLastDeployTime; defaults to no deploy time present.
/// </summary>
public GetLastDeployTimeReply GetLastDeployTimeReply { get; set; } = new() { Present = false };
/// <summary>
/// Gets or sets the reply to return from DiscoverHierarchy; defaults to empty response.
/// </summary>
public DiscoverHierarchyReply DiscoverHierarchyReply { get; set; } = new();
/// <summary>
/// Gets the queue of exceptions to throw from TestConnection; dequeued in FIFO order.
/// </summary>
public Queue<Exception> TestConnectionExceptions { get; } = new();
/// <summary>
/// Gets the queue of exceptions to throw from GetLastDeployTime; dequeued in FIFO order.
/// </summary>
public Queue<Exception> GetLastDeployTimeExceptions { get; } = new();
/// <summary>
/// Gets the queue of exceptions to throw from DiscoverHierarchy; dequeued in FIFO order.
/// </summary>
public Queue<Exception> DiscoverHierarchyExceptions { get; } = new();
/// <summary>
/// Records the request and either throws a queued exception or returns the configured reply.
/// </summary>
/// <param name="request">The TestConnectionRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public Task<TestConnectionReply> TestConnectionAsync(
TestConnectionRequest request,
CallOptions callOptions)
@@ -40,6 +81,11 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
return Task.FromResult(TestConnectionReply);
}
/// <summary>
/// Records the request and either throws a queued exception or returns the configured reply.
/// </summary>
/// <param name="request">The GetLastDeployTimeRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
GetLastDeployTimeRequest request,
CallOptions callOptions)
@@ -53,6 +99,11 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
return Task.FromResult(GetLastDeployTimeReply);
}
/// <summary>
/// Records the request and either throws a queued exception or returns the configured reply.
/// </summary>
/// <param name="request">The DiscoverHierarchyRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
DiscoverHierarchyRequest request,
CallOptions callOptions)
@@ -66,10 +117,19 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
return Task.FromResult(DiscoverHierarchyReply);
}
/// <summary>
/// Gets the list of WatchDeployEvents RPC calls made by the client.
/// </summary>
public List<(WatchDeployEventsRequest Request, CallOptions CallOptions)> WatchDeployEventsCalls { get; } = [];
/// <summary>
/// Gets or sets the list of events to stream from WatchDeployEvents.
/// </summary>
public List<DeployEvent> WatchDeployEvents { get; } = [];
/// <summary>
/// Gets or sets the exception to throw from WatchDeployEvents, if any.
/// </summary>
public Exception? WatchDeployEventsException { get; set; }
/// <summary>
@@ -78,6 +138,11 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
/// </summary>
public Func<CancellationToken, Task>? WatchDeployEventsBeforeYield { get; set; }
/// <summary>
/// Records the request and streams events, checking for queued exceptions and calling WatchDeployEventsBeforeYield before each event.
/// </summary>
/// <param name="request">The WatchDeployEventsRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
WatchDeployEventsRequest request,
CallOptions callOptions)
@@ -3,23 +3,47 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client.Tests;
/// <summary>
/// Fake implementation of IMxGatewayClientTransport for testing.
/// </summary>
internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMxGatewayClientTransport
{
private readonly Queue<MxCommandReply> _invokeReplies = new();
private readonly List<MxEvent> _events = [];
/// <summary>
/// Gets the gateway client options.
/// </summary>
public MxGatewayClientOptions Options { get; } = options;
/// <summary>
/// Gets null, since this is a test fake without a real gRPC client.
/// </summary>
public MxAccessGateway.MxAccessGatewayClient? RawClient => null;
/// <summary>
/// Gets the list of captured OpenSessionAsync calls.
/// </summary>
public List<(OpenSessionRequest Request, CallOptions CallOptions)> OpenSessionCalls { get; } = [];
/// <summary>
/// Gets the list of captured CloseSessionAsync calls.
/// </summary>
public List<(CloseSessionRequest Request, CallOptions CallOptions)> CloseSessionCalls { get; } = [];
/// <summary>
/// Gets the list of captured InvokeAsync calls.
/// </summary>
public List<(MxCommandRequest Request, CallOptions CallOptions)> InvokeCalls { get; } = [];
/// <summary>
/// Gets the list of captured StreamEventsAsync calls.
/// </summary>
public List<(StreamEventsRequest Request, CallOptions CallOptions)> StreamEventsCalls { get; } = [];
/// <summary>
/// Gets or sets the reply to return from OpenSessionAsync.
/// </summary>
public OpenSessionReply OpenSessionReply { get; set; } = new()
{
SessionId = "session-fixture",
@@ -29,6 +53,9 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
};
/// <summary>
/// Gets or sets the reply to return from CloseSessionAsync.
/// </summary>
public CloseSessionReply CloseSessionReply { get; set; } = new()
{
SessionId = "session-fixture",
@@ -36,12 +63,26 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
};
/// <summary>
/// Gets the queue of exceptions to throw from OpenSessionAsync.
/// </summary>
public Queue<Exception> OpenSessionExceptions { get; } = new();
/// <summary>
/// Gets the queue of exceptions to throw from CloseSessionAsync.
/// </summary>
public Queue<Exception> CloseSessionExceptions { get; } = new();
/// <summary>
/// Gets the queue of exceptions to throw from InvokeAsync.
/// </summary>
public Queue<Exception> InvokeExceptions { get; } = new();
/// <summary>
/// Verifies that the OpenSessionAsync call is recorded and returns the configured reply.
/// </summary>
/// <param name="request">The OpenSessionRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public Task<OpenSessionReply> OpenSessionAsync(
OpenSessionRequest request,
CallOptions callOptions)
@@ -55,6 +96,11 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
return Task.FromResult(OpenSessionReply);
}
/// <summary>
/// Verifies that the CloseSessionAsync call is recorded and returns the configured reply.
/// </summary>
/// <param name="request">The CloseSessionRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public Task<CloseSessionReply> CloseSessionAsync(
CloseSessionRequest request,
CallOptions callOptions)
@@ -68,6 +114,11 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
return Task.FromResult(CloseSessionReply);
}
/// <summary>
/// Verifies that the InvokeAsync call is recorded and returns the next enqueued reply.
/// </summary>
/// <param name="request">The MxCommandRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public Task<MxCommandReply> InvokeAsync(
MxCommandRequest request,
CallOptions callOptions)
@@ -81,6 +132,11 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
return Task.FromResult(_invokeReplies.Dequeue());
}
/// <summary>
/// Verifies that the StreamEventsAsync call is recorded and yields all enqueued events.
/// </summary>
/// <param name="request">The StreamEventsRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
StreamEventsRequest request,
CallOptions callOptions)
@@ -95,11 +151,19 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
}
}
/// <summary>
/// Enqueues a reply to be returned from the next InvokeAsync call.
/// </summary>
/// <param name="reply">The reply to enqueue.</param>
public void AddInvokeReply(MxCommandReply reply)
{
_invokeReplies.Enqueue(reply);
}
/// <summary>
/// Enqueues an event to be yielded from StreamEventsAsync.
/// </summary>
/// <param name="gatewayEvent">The event to enqueue.</param>
public void AddEvent(MxEvent gatewayEvent)
{
_events.Add(gatewayEvent);
@@ -6,6 +6,9 @@ namespace MxGateway.Client.Tests;
public sealed class GalaxyRepositoryClientTests
{
/// <summary>
/// Verifies that TestConnectionAsync attaches the API key in request metadata and returns the Ok flag.
/// </summary>
[Fact]
public async Task TestConnectionAsync_AttachesApiKeyMetadataAndReturnsOkFlag()
{
@@ -21,6 +24,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization"));
}
/// <summary>
/// Verifies that TestConnectionAsync returns false when the server reports NotOk.
/// </summary>
[Fact]
public async Task TestConnectionAsync_ReturnsFalseWhenServerReportsNotOk()
{
@@ -33,6 +39,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.False(ok);
}
/// <summary>
/// Verifies that GetLastDeployTimeAsync returns null when the server reports not present.
/// </summary>
[Fact]
public async Task GetLastDeployTimeAsync_ReturnsNullWhenNotPresent()
{
@@ -46,6 +55,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Single(transport.GetLastDeployTimeCalls);
}
/// <summary>
/// Verifies that GetLastDeployTimeAsync returns the timestamp when the server reports it present.
/// </summary>
[Fact]
public async Task GetLastDeployTimeAsync_ReturnsTimestampWhenPresent()
{
@@ -64,6 +76,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Equal(expected, deployTime!.Value);
}
/// <summary>
/// Verifies that DiscoverHierarchyAsync returns the objects from the server reply.
/// </summary>
[Fact]
public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply()
{
@@ -104,6 +119,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Equal("DelmiaReceiver_001.DownloadPath", attribute.FullTagReference);
}
/// <summary>
/// Verifies that DiscoverHierarchyAsync propagates cancellation tokens to the transport.
/// </summary>
[Fact]
public async Task DiscoverHierarchyAsync_PropagatesCancellationToTransport()
{
@@ -121,6 +139,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.False(call.CallOptions.CancellationToken.IsCancellationRequested);
}
/// <summary>
/// Verifies that TestConnectionAsync retries on transient gRPC failures.
/// </summary>
[Fact]
public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure()
{
@@ -135,6 +156,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Equal(2, transport.TestConnectionCalls.Count);
}
/// <summary>
/// Verifies that DiscoverHierarchyAsync retries on transient gRPC failures.
/// </summary>
[Fact]
public async Task DiscoverHierarchyAsync_RetriesOnTransientGrpcFailure()
{
@@ -148,6 +172,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Equal(2, transport.DiscoverHierarchyCalls.Count);
}
/// <summary>
/// Verifies that WatchDeployEventsAsync delivers the bootstrap event.
/// </summary>
[Fact]
public async Task WatchDeployEventsAsync_DeliversBootstrapEvent()
{
@@ -181,6 +208,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Null(call.Request.LastSeenDeployTime);
}
/// <summary>
/// Verifies that WatchDeployEventsAsync delivers multiple events in order.
/// </summary>
[Fact]
public async Task WatchDeployEventsAsync_DeliversMultipleEventsInOrder()
{
@@ -216,6 +246,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Equal(t0, call.Request.LastSeenDeployTime!.ToDateTime());
}
/// <summary>
/// Verifies that WatchDeployEventsAsync stops iteration cleanly when cancelled.
/// </summary>
[Fact]
public async Task WatchDeployEventsAsync_CancellationStopsIterationCleanly()
{
@@ -257,6 +290,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Equal(1ul, received[0].Sequence);
}
/// <summary>
/// Verifies that WatchDeployEventsAsync throws ObjectDisposedException after the client is disposed.
/// </summary>
[Fact]
public async Task WatchDeployEventsAsync_ThrowsAfterDisposal()
{
@@ -269,6 +305,9 @@ public sealed class GalaxyRepositoryClientTests
client.WatchDeployEventsAsync());
}
/// <summary>
/// Verifies that TestConnectionAsync throws ObjectDisposedException after the client is disposed.
/// </summary>
[Fact]
public async Task TestConnectionAsync_ThrowsAfterDisposal()
{
@@ -6,6 +6,7 @@ namespace MxGateway.Client.Tests;
public sealed class MxCommandReplyExtensionsTests
{
/// <summary>Verifies that successful replies pass both protocol and MxAccess success checks.</summary>
[Fact]
public void EnsureSuccess_WithRegisterFixture_ReturnsReply()
{
@@ -15,6 +16,7 @@ public sealed class MxCommandReplyExtensionsTests
Assert.Same(reply, reply.EnsureMxAccessSuccess());
}
/// <summary>Verifies that MxAccess failures throw with preserved HResult and status details.</summary>
[Fact]
public void EnsureMxAccessSuccess_WithFailureFixture_PreservesHResultAndStatuses()
{
@@ -30,6 +32,7 @@ public sealed class MxCommandReplyExtensionsTests
Assert.Contains("0x80040200", exception.Message);
}
/// <summary>Verifies that session-not-found protocol failures throw the correct gateway exception.</summary>
[Fact]
public void EnsureProtocolSuccess_WithSessionFailure_ThrowsSessionException()
{
@@ -5,8 +5,10 @@ using MxGateway.Contracts.Proto.Galaxy;
namespace MxGateway.Client.Tests;
/// <summary>Tests for the CLI command interface.</summary>
public sealed class MxGatewayClientCliTests
{
/// <summary>Verifies that the version command prints compiled protocol versions.</summary>
[Fact]
public void Run_Version_PrintsCompiledProtocolVersions()
{
@@ -21,6 +23,7 @@ public sealed class MxGatewayClientCliTests
Assert.Equal(string.Empty, error.ToString());
}
/// <summary>Verifies that the version command with --json flag prints JSON protocol versions.</summary>
[Fact]
public async Task RunAsync_VersionJson_PrintsJsonProtocolVersions()
{
@@ -34,6 +37,7 @@ public sealed class MxGatewayClientCliTests
Assert.Equal(string.Empty, error.ToString());
}
/// <summary>Verifies that the write command builds a write request and prints JSON reply.</summary>
[Fact]
public async Task RunAsync_Write_BuildsWriteCommandAndPrintsJsonReply()
{
@@ -78,6 +82,7 @@ public sealed class MxGatewayClientCliTests
Assert.Equal(string.Empty, error.ToString());
}
/// <summary>Verifies that error output redacts sensitive API key values.</summary>
[Fact]
public async Task RunAsync_ErrorOutput_RedactsApiKey()
{
@@ -101,6 +106,7 @@ public sealed class MxGatewayClientCliTests
Assert.Contains("[redacted]", error.ToString());
}
/// <summary>Verifies that stream-events with max-events limit stops output in non-JSON format.</summary>
[Fact]
public async Task RunAsync_StreamEvents_WithMaxEventsStopsNonJsonOutput()
{
@@ -142,6 +148,7 @@ public sealed class MxGatewayClientCliTests
}
/// <summary>Verifies that smoke command closes opened session when a command fails.</summary>
[Fact]
public async Task RunAsync_Smoke_WhenCommandFails_ClosesOpenedSession()
{
@@ -172,6 +179,7 @@ public sealed class MxGatewayClientCliTests
Assert.Equal("session-fixture", closeRequest.SessionId);
}
/// <summary>Verifies that galaxy-test-connection command prints JSON reply.</summary>
[Fact]
public async Task RunAsync_GalaxyTestConnection_PrintsJsonReply()
{
@@ -201,6 +209,7 @@ public sealed class MxGatewayClientCliTests
Assert.Equal(string.Empty, error.ToString());
}
/// <summary>Verifies that galaxy-discover command prints hierarchy summary.</summary>
[Fact]
public async Task RunAsync_GalaxyDiscover_PrintsHierarchySummary()
{
@@ -250,6 +259,7 @@ public sealed class MxGatewayClientCliTests
Assert.Equal(string.Empty, error.ToString());
}
/// <summary>Verifies that galaxy-watch command prints text output for deploy events.</summary>
[Fact]
public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents()
{
@@ -303,6 +313,7 @@ public sealed class MxGatewayClientCliTests
Assert.Equal(string.Empty, error.ToString());
}
/// <summary>Verifies that galaxy-watch with --json emits one JSON object per event.</summary>
[Fact]
public async Task RunAsync_GalaxyWatch_JsonEmitsOneObjectPerEvent()
{
@@ -337,23 +348,31 @@ public sealed class MxGatewayClientCliTests
Assert.Contains("\"objectCount\": 99", text);
}
/// <summary>Fake CLI client for testing.</summary>
private sealed class FakeCliClient : IMxGatewayCliClient
{
/// <summary>Queue of invoke replies to return.</summary>
public Queue<MxCommandReply> InvokeReplies { get; } = new();
/// <summary>List of received invoke requests.</summary>
public List<MxCommandRequest> InvokeRequests { get; } = [];
/// <summary>List of received close session requests.</summary>
public List<CloseSessionRequest> CloseSessionRequests { get; } = [];
/// <summary>List of events to yield when streaming.</summary>
public List<MxEvent> Events { get; } = [];
/// <summary>Exception to throw on invoke, if any.</summary>
public Exception? InvokeFailure { get; init; }
/// <inheritdoc />
public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
}
/// <inheritdoc />
public Task<OpenSessionReply> OpenSessionAsync(
OpenSessionRequest request,
CancellationToken cancellationToken)
@@ -367,6 +386,7 @@ public sealed class MxGatewayClientCliTests
});
}
/// <inheritdoc />
public Task<CloseSessionReply> CloseSessionAsync(
CloseSessionRequest request,
CancellationToken cancellationToken)
@@ -380,6 +400,7 @@ public sealed class MxGatewayClientCliTests
});
}
/// <inheritdoc />
public Task<MxCommandReply> InvokeAsync(
MxCommandRequest request,
CancellationToken cancellationToken)
@@ -393,6 +414,7 @@ public sealed class MxGatewayClientCliTests
return Task.FromResult(InvokeReplies.Dequeue());
}
/// <inheritdoc />
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
StreamEventsRequest request,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
@@ -405,18 +427,25 @@ public sealed class MxGatewayClientCliTests
}
}
/// <summary>Galaxy test connection reply to return.</summary>
public TestConnectionReply GalaxyTestConnectionReply { get; set; } = new() { Ok = true };
/// <summary>Galaxy get last deploy time reply to return.</summary>
public GetLastDeployTimeReply GalaxyGetLastDeployTimeReply { get; set; } = new() { Present = false };
/// <summary>Galaxy discover hierarchy reply to return.</summary>
public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new();
/// <summary>List of received galaxy test connection requests.</summary>
public List<TestConnectionRequest> GalaxyTestConnectionRequests { get; } = [];
/// <summary>List of received galaxy get last deploy time requests.</summary>
public List<GetLastDeployTimeRequest> GalaxyGetLastDeployTimeRequests { get; } = [];
/// <summary>List of received galaxy discover hierarchy requests.</summary>
public List<DiscoverHierarchyRequest> GalaxyDiscoverHierarchyRequests { get; } = [];
/// <inheritdoc />
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
TestConnectionRequest request,
CancellationToken cancellationToken)
@@ -425,6 +454,7 @@ public sealed class MxGatewayClientCliTests
return Task.FromResult(GalaxyTestConnectionReply);
}
/// <inheritdoc />
public Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
GetLastDeployTimeRequest request,
CancellationToken cancellationToken)
@@ -433,6 +463,7 @@ public sealed class MxGatewayClientCliTests
return Task.FromResult(GalaxyGetLastDeployTimeReply);
}
/// <inheritdoc />
public Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
DiscoverHierarchyRequest request,
CancellationToken cancellationToken)
@@ -441,10 +472,13 @@ public sealed class MxGatewayClientCliTests
return Task.FromResult(GalaxyDiscoverHierarchyReply);
}
/// <summary>List of received galaxy watch deploy events requests.</summary>
public List<WatchDeployEventsRequest> GalaxyWatchDeployEventsRequests { get; } = [];
/// <summary>List of deploy events to yield when watching.</summary>
public List<DeployEvent> GalaxyDeployEvents { get; } = [];
/// <inheritdoc />
public async IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
WatchDeployEventsRequest request,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
@@ -4,6 +4,7 @@ namespace MxGateway.Client.Tests;
public sealed class MxGatewayClientContractInfoTests
{
/// <summary>Verifies that the client's gateway protocol version matches the shared contract definition.</summary>
[Fact]
public void GatewayProtocolVersion_MatchesSharedContract()
{
@@ -12,6 +13,7 @@ public sealed class MxGatewayClientContractInfoTests
MxGatewayClientContractInfo.GatewayProtocolVersion);
}
/// <summary>Verifies that the client's worker protocol version matches the shared contract definition.</summary>
[Fact]
public void WorkerProtocolVersion_MatchesSharedContract()
{
@@ -2,6 +2,7 @@ namespace MxGateway.Client.Tests;
public sealed class MxGatewayClientOptionsTests
{
/// <summary>Verifies that options with valid endpoint and API key pass validation.</summary>
[Fact]
public void Validate_WithAbsoluteEndpointAndApiKey_Succeeds()
{
@@ -14,6 +15,7 @@ public sealed class MxGatewayClientOptionsTests
options.Validate();
}
/// <summary>Verifies that empty API key causes validation to fail.</summary>
[Fact]
public void Validate_WithEmptyApiKey_Throws()
{
@@ -26,6 +28,7 @@ public sealed class MxGatewayClientOptionsTests
Assert.Throws<ArgumentException>(options.Validate);
}
/// <summary>Verifies that invalid retry options cause validation to fail.</summary>
[Fact]
public void Validate_WithInvalidRetryOptions_Throws()
{
@@ -3,8 +3,10 @@ using Grpc.Core;
namespace MxGateway.Client.Tests;
/// <summary>Tests for MxGatewaySession and client command behavior.</summary>
public sealed class MxGatewayClientSessionTests
{
/// <summary>Verifies that open session attaches API key metadata and cancellation token.</summary>
[Fact]
public async Task OpenSessionRawAsync_AttachesApiKeyMetadataAndCancellation()
{
@@ -19,6 +21,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal(cancellation.Token, call.CallOptions.CancellationToken);
}
/// <summary>Verifies that open session returns a session with the raw open reply.</summary>
[Fact]
public async Task OpenSessionAsync_ReturnsSessionWithRawOpenReply()
{
@@ -33,6 +36,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal(1234, session.OpenSessionReply.WorkerProcessId);
}
/// <summary>Verifies that register builds a register command and returns server handle.</summary>
[Fact]
public async Task RegisterAsync_BuildsRegisterCommandAndReturnsServerHandle()
{
@@ -57,6 +61,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal("fixture-client", call.Request.Command.Register.ClientName);
}
/// <summary>Verifies that add item 2 builds a command with the specified context.</summary>
[Fact]
public async Task AddItem2Async_BuildsAddItem2CommandWithContext()
{
@@ -81,6 +86,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal("runtime", request.Command.AddItem2.ItemContext);
}
/// <summary>Verifies that write raw builds a write command with the raw value.</summary>
[Fact]
public async Task WriteRawAsync_BuildsWriteCommandWithRawValue()
{
@@ -111,6 +117,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal(56, request.Command.Write.UserId);
}
/// <summary>Verifies that write 2 raw builds a write 2 command with value and timestamp.</summary>
[Fact]
public async Task Write2RawAsync_BuildsWrite2CommandWithValueAndTimestamp()
{
@@ -138,6 +145,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal(56, request.Command.Write2.UserId);
}
/// <summary>Verifies that subscribe bulk builds one command and returns per-item results.</summary>
[Fact]
public async Task SubscribeBulkAsync_BuildsOneBulkCommandAndReturnsPerItemResults()
{
@@ -176,6 +184,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal(["Area001.Pump001.Speed"], request.Command.SubscribeBulk.TagAddresses);
}
/// <summary>Verifies that stream events yields events in the order received from the gateway.</summary>
[Fact]
public async Task StreamEventsAsync_YieldsEventsInGatewayOrder()
{
@@ -206,6 +215,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal("session-fixture", request.SessionId);
}
/// <summary>Verifies that close is explicit and idempotent.</summary>
[Fact]
public async Task CloseAsync_IsExplicitAndIdempotent()
{
@@ -221,6 +231,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal("session-fixture", call.Request.SessionId);
}
/// <summary>Verifies that invoke retries safe diagnostic commands on transient RPC failure.</summary>
[Fact]
public async Task InvokeAsync_RetriesSafeDiagnosticCommandOnTransientGrpcFailure()
{
@@ -244,6 +255,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal(2, transport.InvokeCalls.Count);
}
/// <summary>Verifies that open session does not retry on transient RPC failure.</summary>
[Fact]
public async Task OpenSessionAsync_DoesNotRetryTransientGrpcFailure()
{
@@ -256,6 +268,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Single(transport.OpenSessionCalls);
}
/// <summary>Verifies that invoke does not retry write commands on transient RPC failure.</summary>
[Fact]
public async Task InvokeAsync_DoesNotRetryWriteCommand()
{
@@ -270,6 +283,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Single(transport.InvokeCalls);
}
/// <summary>Verifies that invoke helpers pass cancellation token to the transport.</summary>
[Fact]
public async Task InvokeHelpers_PassCancellationTokenToTransport()
{
@@ -2,6 +2,7 @@ namespace MxGateway.Client.Tests;
public sealed class MxGatewayGeneratedContractTests
{
/// <summary>Verifies that the generated gRPC client can be instantiated from the client factory.</summary>
[Fact]
public async Task GeneratedGrpcClient_CanBeConstructedFromClientFactory()
{
@@ -7,6 +7,7 @@ namespace MxGateway.Client.Tests;
public sealed class MxStatusProxyExtensionsTests
{
/// <summary>Verifies that fixture statuses correctly project success and preserve raw integer fields.</summary>
[Fact]
public void FixtureStatuses_ProjectSuccessAndPreserveRawFields()
{
@@ -7,6 +7,7 @@ namespace MxGateway.Client.Tests;
public sealed class MxValueExtensionsTests
{
/// <summary>Verifies that scalar values are converted to correctly-typed MxValue protobuf messages.</summary>
[Fact]
public void ToMxValue_WithScalarValues_CreatesTypedProtobufValues()
{
@@ -18,6 +19,7 @@ public sealed class MxValueExtensionsTests
Assert.Equal(MxValue.KindOneofCase.StringValue, "alpha".ToMxValue().KindCase);
}
/// <summary>Verifies that array values are converted to array-kind MxValue messages with correct element types and dimensions.</summary>
[Fact]
public void ToMxValue_WithArrays_CreatesTypedArrayProtobufValues()
{
@@ -29,6 +31,7 @@ public sealed class MxValueExtensionsTests
Assert.Equal([2U], value.ArrayValue.Dimensions);
}
/// <summary>Verifies that fixture test cases project to expected MxValue kinds and preserve raw type metadata.</summary>
[Fact]
public void FixtureValues_ProjectExpectedKindsAndPreserveRawMetadata()
{
@@ -23,6 +23,11 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
private bool _disposed;
/// <summary>
/// Initializes a Galaxy Repository client with custom transport and options.
/// </summary>
/// <param name="options">Client options.</param>
/// <param name="transport">The underlying gRPC transport.</param>
internal GalaxyRepositoryClient(
MxGatewayClientOptions options,
IGalaxyRepositoryClientTransport transport)
@@ -50,12 +55,23 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
Options.LoggerFactory?.CreateLogger<GalaxyRepositoryClient>());
}
/// <summary>
/// Client options used to configure timeouts, authentication, and retry policy.
/// </summary>
public MxGatewayClientOptions Options { get; }
/// <summary>
/// The underlying generated gRPC client for advanced operations.
/// </summary>
public GalaxyRepository.GalaxyRepositoryClient RawClient =>
_transport.RawClient
?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");
/// <summary>
/// Creates a Galaxy Repository client with the given options, establishing a new gRPC channel.
/// </summary>
/// <param name="options">Client options.</param>
/// <returns>A new client instance.</returns>
public static GalaxyRepositoryClient Create(MxGatewayClientOptions options)
{
ArgumentNullException.ThrowIfNull(options);
@@ -81,6 +97,8 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
/// Probes the Galaxy Repository database connection. Returns true when the
/// gateway can reach the configured ZB SQL Server.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if connection is successful, false otherwise.</returns>
public async Task<bool> TestConnectionAsync(CancellationToken cancellationToken = default)
{
TestConnectionReply reply = await TestConnectionRawAsync(
@@ -91,6 +109,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
return reply.Ok;
}
/// <summary>
/// Probes the Galaxy Repository database connection without result wrapping.
/// </summary>
/// <param name="request">The test connection request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<TestConnectionReply> TestConnectionRawAsync(
TestConnectionRequest request,
CancellationToken cancellationToken = default)
@@ -107,6 +131,8 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
/// Returns the timestamp of the most recent Galaxy deployment, or
/// <see langword="null"/> when no deployment has been recorded.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The deployment timestamp, or null if not recorded.</returns>
public async Task<DateTime?> GetLastDeployTimeAsync(CancellationToken cancellationToken = default)
{
GetLastDeployTimeReply reply = await GetLastDeployTimeRawAsync(
@@ -122,6 +148,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
return reply.TimeOfLastDeploy.ToDateTime();
}
/// <summary>
/// Returns the most recent Galaxy deployment timestamp without result wrapping.
/// </summary>
/// <param name="request">The last deploy-time request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<GetLastDeployTimeReply> GetLastDeployTimeRawAsync(
GetLastDeployTimeRequest request,
CancellationToken cancellationToken = default)
@@ -139,6 +171,8 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
/// includes its dynamic attributes so callers can determine which tag references
/// they may subscribe to via the MxAccessGateway service.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The collection of Galaxy objects in the hierarchy.</returns>
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(CancellationToken cancellationToken = default)
{
DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync(
@@ -149,6 +183,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
return reply.Objects;
}
/// <summary>
/// Enumerates the Galaxy object hierarchy without result wrapping.
/// </summary>
/// <param name="request">The discover-hierarchy request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<DiscoverHierarchyReply> DiscoverHierarchyRawAsync(
DiscoverHierarchyRequest request,
CancellationToken cancellationToken = default)
@@ -173,6 +213,9 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
/// at-least-once delivery beyond the per-subscriber buffer (gaps in
/// <see cref="DeployEvent.Sequence"/> indicate dropped events).
/// </remarks>
/// <param name="lastSeenDeployTime">Optional timestamp to suppress the bootstrap event.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An async enumerable of deploy events.</returns>
public IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
DateTimeOffset? lastSeenDeployTime = null,
CancellationToken cancellationToken = default)
@@ -188,6 +231,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
return WatchDeployEventsRawAsync(request, cancellationToken);
}
/// <summary>
/// Subscribes to Galaxy deploy events without result wrapping.
/// </summary>
/// <param name="request">The watch-deploy-events request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An async enumerable of raw deploy events.</returns>
public IAsyncEnumerable<DeployEvent> WatchDeployEventsRawAsync(
WatchDeployEventsRequest request,
CancellationToken cancellationToken = default)
@@ -211,6 +260,9 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
}
}
/// <summary>
/// Closes the gRPC channel and releases resources.
/// </summary>
public ValueTask DisposeAsync()
{
if (_disposed)
@@ -223,16 +275,32 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
return ValueTask.CompletedTask;
}
/// <summary>
/// Creates gRPC call options with the client's default timeout and API-key authorization.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The call options.</returns>
internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
{
return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout);
}
/// <summary>
/// Creates gRPC call options for streaming RPCs with the stream timeout and API-key authorization.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The stream call options.</returns>
internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken)
{
return CreateCallOptions(cancellationToken, Options.StreamTimeout);
}
/// <summary>
/// Creates gRPC call options with the specified timeout and API-key authorization.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="timeout">Optional timeout duration.</param>
/// <returns>The call options.</returns>
internal CallOptions CreateCallOptions(
CancellationToken cancellationToken,
TimeSpan? timeout)
@@ -3,16 +3,27 @@ using MxGateway.Contracts.Proto.Galaxy;
namespace MxGateway.Client;
/// <summary>
/// gRPC implementation of IGalaxyRepositoryClientTransport.
/// </summary>
internal sealed class GrpcGalaxyRepositoryClientTransport(
MxGatewayClientOptions options,
GalaxyRepository.GalaxyRepositoryClient rawClient) : IGalaxyRepositoryClientTransport
{
/// <summary>
/// Gets the gateway client options.
/// </summary>
public MxGatewayClientOptions Options { get; } = options;
/// <summary>
/// Gets the underlying gRPC client.
/// </summary>
public GalaxyRepository.GalaxyRepositoryClient RawClient { get; } = rawClient;
/// <inheritdoc />
GalaxyRepository.GalaxyRepositoryClient? IGalaxyRepositoryClientTransport.RawClient => RawClient;
/// <inheritdoc />
public async Task<TestConnectionReply> TestConnectionAsync(
TestConnectionRequest request,
CallOptions callOptions)
@@ -29,6 +40,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
}
}
/// <inheritdoc />
public async Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
GetLastDeployTimeRequest request,
CallOptions callOptions)
@@ -45,6 +57,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
}
}
/// <inheritdoc />
public async Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
DiscoverHierarchyRequest request,
CallOptions callOptions)
@@ -61,6 +74,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
}
}
/// <inheritdoc />
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
WatchDeployEventsRequest request,
CallOptions callOptions,
@@ -94,6 +108,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
}
}
/// <inheritdoc />
IAsyncEnumerable<DeployEvent> IGalaxyRepositoryClientTransport.WatchDeployEventsAsync(
WatchDeployEventsRequest request,
CallOptions callOptions)
@@ -3,16 +3,27 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
/// <summary>
/// gRPC implementation of IMxGatewayClientTransport.
/// </summary>
internal sealed class GrpcMxGatewayClientTransport(
MxGatewayClientOptions options,
MxAccessGateway.MxAccessGatewayClient rawClient) : IMxGatewayClientTransport
{
/// <summary>
/// Gets the gateway client options.
/// </summary>
public MxGatewayClientOptions Options { get; } = options;
/// <summary>
/// Gets the underlying gRPC client.
/// </summary>
public MxAccessGateway.MxAccessGatewayClient RawClient { get; } = rawClient;
/// <inheritdoc />
MxAccessGateway.MxAccessGatewayClient? IMxGatewayClientTransport.RawClient => RawClient;
/// <inheritdoc />
public async Task<OpenSessionReply> OpenSessionAsync(
OpenSessionRequest request,
CallOptions callOptions)
@@ -29,6 +40,7 @@ internal sealed class GrpcMxGatewayClientTransport(
}
}
/// <inheritdoc />
public async Task<CloseSessionReply> CloseSessionAsync(
CloseSessionRequest request,
CallOptions callOptions)
@@ -45,6 +57,7 @@ internal sealed class GrpcMxGatewayClientTransport(
}
}
/// <inheritdoc />
public async Task<MxCommandReply> InvokeAsync(
MxCommandRequest request,
CallOptions callOptions)
@@ -61,6 +74,7 @@ internal sealed class GrpcMxGatewayClientTransport(
}
}
/// <inheritdoc />
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
StreamEventsRequest request,
CallOptions callOptions,
@@ -94,6 +108,7 @@ internal sealed class GrpcMxGatewayClientTransport(
}
}
/// <inheritdoc />
IAsyncEnumerable<MxEvent> IMxGatewayClientTransport.StreamEventsAsync(
StreamEventsRequest request,
CallOptions callOptions)
@@ -3,24 +3,39 @@ using MxGateway.Contracts.Proto.Galaxy;
namespace MxGateway.Client;
/// <summary>Transport layer for Galaxy Repository gRPC operations.</summary>
internal interface IGalaxyRepositoryClientTransport
{
/// <summary>Gets the client options used to configure this transport.</summary>
MxGatewayClientOptions Options { get; }
/// <summary>Gets the underlying gRPC client, or <c>null</c> if not yet initialized.</summary>
GalaxyRepository.GalaxyRepositoryClient? RawClient { get; }
/// <summary>Tests the connection to the Galaxy Repository server.</summary>
/// <param name="request">The test connection request.</param>
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
Task<TestConnectionReply> TestConnectionAsync(
TestConnectionRequest request,
CallOptions callOptions);
/// <summary>Gets the last deploy time from the Galaxy Repository server.</summary>
/// <param name="request">The get last deploy time request.</param>
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
GetLastDeployTimeRequest request,
CallOptions callOptions);
/// <summary>Discovers the object hierarchy in the Galaxy Repository.</summary>
/// <param name="request">The discover hierarchy request.</param>
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
DiscoverHierarchyRequest request,
CallOptions callOptions);
/// <summary>Watches for deployment events from the Galaxy Repository server.</summary>
/// <param name="request">The watch deploy events request.</param>
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
WatchDeployEventsRequest request,
CallOptions callOptions);
@@ -5,22 +5,52 @@ namespace MxGateway.Client;
internal interface IMxGatewayClientTransport
{
/// <summary>
/// Gets the client configuration options.
/// </summary>
MxGatewayClientOptions Options { get; }
/// <summary>
/// Gets the underlying gRPC client, if available.
/// </summary>
MxAccessGateway.MxAccessGatewayClient? RawClient { get; }
/// <summary>
/// Opens a new gateway session.
/// </summary>
/// <param name="request">Session open request.</param>
/// <param name="callOptions">gRPC call options.</param>
/// <returns>The session open reply.</returns>
Task<OpenSessionReply> OpenSessionAsync(
OpenSessionRequest request,
CallOptions callOptions);
/// <summary>
/// Closes an open gateway session.
/// </summary>
/// <param name="request">Session close request.</param>
/// <param name="callOptions">gRPC call options.</param>
/// <returns>The session close reply.</returns>
Task<CloseSessionReply> CloseSessionAsync(
CloseSessionRequest request,
CallOptions callOptions);
/// <summary>
/// Invokes an MXAccess command on the session.
/// </summary>
/// <param name="request">The command request.</param>
/// <param name="callOptions">gRPC call options.</param>
/// <returns>The command reply.</returns>
Task<MxCommandReply> InvokeAsync(
MxCommandRequest request,
CallOptions callOptions);
/// <summary>
/// Streams events from the session.
/// </summary>
/// <param name="request">The stream events request.</param>
/// <param name="callOptions">gRPC call options.</param>
/// <returns>An async enumerable of events.</returns>
IAsyncEnumerable<MxEvent> StreamEventsAsync(
StreamEventsRequest request,
CallOptions callOptions);
@@ -2,8 +2,13 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
/// <summary>Exception thrown when an MXAccess command fails with a non-zero HResult or failing status.</summary>
public sealed class MxAccessException : MxGatewayCommandException
{
/// <summary>Initializes a new instance with the given message, reply, and optional inner exception.</summary>
/// <param name="message">The error message describing the MXAccess failure.</param>
/// <param name="reply">The MxCommandReply containing the failure details (statuses, HResult, etc.).</param>
/// <param name="innerException">The underlying exception, if any.</param>
public MxAccessException(
string message,
MxCommandReply reply,
@@ -20,5 +25,6 @@ public sealed class MxAccessException : MxGatewayCommandException
Reply = reply;
}
/// <summary>Gets the underlying MxCommandReply containing full failure details.</summary>
public MxCommandReply Reply { get; }
}
@@ -2,8 +2,11 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
/// <summary>Extension methods for checking MxCommandReply success conditions.</summary>
public static class MxCommandReplyExtensions
{
/// <summary>Validates that the reply has a successful protocol status (Ok or MxAccessFailure), throwing a gateway exception if not.</summary>
/// <param name="reply">The command reply to check.</param>
public static MxCommandReply EnsureProtocolSuccess(this MxCommandReply reply)
{
ArgumentNullException.ThrowIfNull(reply);
@@ -19,6 +22,8 @@ public static class MxCommandReplyExtensions
throw CreateProtocolException(reply, code);
}
/// <summary>Validates that the reply indicates MXAccess success (no HResult or status failures), throwing MxAccessException if not.</summary>
/// <param name="reply">The command reply to check.</param>
public static MxCommandReply EnsureMxAccessSuccess(this MxCommandReply reply)
{
ArgumentNullException.ThrowIfNull(reply);
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
/// <summary>Exception thrown when an API key is invalid, expired, or malformed.</summary>
public sealed class MxGatewayAuthenticationException : MxGatewayException
{
/// <summary>Initializes a new instance with the given details.</summary>
/// <param name="message">The error message describing the authentication failure.</param>
/// <param name="sessionId">The session ID, if available.</param>
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
/// <param name="protocolStatus">The protocol status details, if available.</param>
/// <param name="hResult">The HResult code, if available.</param>
/// <param name="statuses">The MXAccess statuses, if available.</param>
/// <param name="innerException">The underlying exception, if any.</param>
public MxGatewayAuthenticationException(
string message,
string? sessionId = null,
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
/// <summary>Exception thrown when the API key lacks required scopes for an operation.</summary>
public sealed class MxGatewayAuthorizationException : MxGatewayException
{
/// <summary>Initializes a new instance with the given details.</summary>
/// <param name="message">The error message describing the authorization failure.</param>
/// <param name="sessionId">The session ID, if available.</param>
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
/// <param name="protocolStatus">The protocol status details, if available.</param>
/// <param name="hResult">The HResult code, if available.</param>
/// <param name="statuses">The MXAccess statuses, if available.</param>
/// <param name="innerException">The underlying exception, if any.</param>
public MxGatewayAuthorizationException(
string message,
string? sessionId = null,
@@ -19,6 +19,11 @@ public sealed class MxGatewayClient : IAsyncDisposable
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="MxGatewayClient"/> with given options and transport.
/// </summary>
/// <param name="options">Client configuration options.</param>
/// <param name="transport">Transport implementation for gateway communication.</param>
internal MxGatewayClient(
MxGatewayClientOptions options,
IMxGatewayClientTransport transport)
@@ -46,12 +51,23 @@ public sealed class MxGatewayClient : IAsyncDisposable
Options.LoggerFactory?.CreateLogger<MxGatewayClient>());
}
/// <summary>
/// Gets the client configuration options.
/// </summary>
public MxGatewayClientOptions Options { get; }
/// <summary>
/// Gets the underlying generated gRPC client.
/// </summary>
public MxAccessGateway.MxAccessGatewayClient RawClient =>
_transport.RawClient
?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");
/// <summary>
/// Creates a new gateway client with the given options.
/// </summary>
/// <param name="options">Client configuration options.</param>
/// <returns>A new gateway client instance.</returns>
public static MxGatewayClient Create(MxGatewayClientOptions options)
{
ArgumentNullException.ThrowIfNull(options);
@@ -73,6 +89,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
new MxAccessGateway.MxAccessGatewayClient(channel)));
}
/// <summary>
/// Opens a new gateway session.
/// </summary>
/// <param name="request">Session open request; defaults to empty request if null.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A wrapped gateway session.</returns>
public async Task<MxGatewaySession> OpenSessionAsync(
OpenSessionRequest? request = null,
CancellationToken cancellationToken = default)
@@ -85,6 +107,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
return new MxGatewaySession(this, reply);
}
/// <summary>
/// Opens a new gateway session and returns the raw protobuf reply.
/// </summary>
/// <param name="request">Session open request.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The raw gateway session open reply.</returns>
public Task<OpenSessionReply> OpenSessionRawAsync(
OpenSessionRequest request,
CancellationToken cancellationToken = default)
@@ -95,6 +123,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
return _transport.OpenSessionAsync(request, CreateCallOptions(cancellationToken));
}
/// <summary>
/// Closes an open gateway session.
/// </summary>
/// <param name="request">Session close request.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The session close reply.</returns>
public Task<CloseSessionReply> CloseSessionRawAsync(
CloseSessionRequest request,
CancellationToken cancellationToken = default)
@@ -107,6 +141,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
cancellationToken);
}
/// <summary>
/// Invokes an MXAccess command on the open session.
/// </summary>
/// <param name="request">The command request.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The command reply.</returns>
public Task<MxCommandReply> InvokeAsync(
MxCommandRequest request,
CancellationToken cancellationToken = default)
@@ -124,6 +164,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
return _transport.InvokeAsync(request, CreateCallOptions(cancellationToken));
}
/// <summary>
/// Streams events from the gateway session.
/// </summary>
/// <param name="request">The stream events request.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>An async enumerable of events.</returns>
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
StreamEventsRequest request,
CancellationToken cancellationToken = default)
@@ -134,6 +180,9 @@ public sealed class MxGatewayClient : IAsyncDisposable
return _transport.StreamEventsAsync(request, CreateStreamCallOptions(cancellationToken));
}
/// <summary>
/// Disposes the client and releases all resources.
/// </summary>
public ValueTask DisposeAsync()
{
if (_disposed)
@@ -146,16 +195,32 @@ public sealed class MxGatewayClient : IAsyncDisposable
return ValueTask.CompletedTask;
}
/// <summary>
/// Creates gRPC call options with default timeout and authorization.
/// </summary>
/// <param name="cancellationToken">Cancellation token for the call.</param>
/// <returns>Configured call options.</returns>
internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
{
return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout);
}
/// <summary>
/// Creates gRPC call options for streaming with stream timeout and authorization.
/// </summary>
/// <param name="cancellationToken">Cancellation token for the call.</param>
/// <returns>Configured call options.</returns>
internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken)
{
return CreateCallOptions(cancellationToken, Options.StreamTimeout);
}
/// <summary>
/// Creates gRPC call options with specified timeout and authorization.
/// </summary>
/// <param name="cancellationToken">Cancellation token for the call.</param>
/// <param name="timeout">Optional timeout duration; null means no timeout.</param>
/// <returns>Configured call options.</returns>
internal CallOptions CreateCallOptions(
CancellationToken cancellationToken,
TimeSpan? timeout)
@@ -7,26 +7,62 @@ namespace MxGateway.Client;
/// </summary>
public sealed class MxGatewayClientOptions
{
/// <summary>
/// Gets the gateway endpoint URI (required).
/// </summary>
public required Uri Endpoint { get; init; }
/// <summary>
/// Gets the API key for gateway authentication (required).
/// </summary>
public required string ApiKey { get; init; }
/// <summary>
/// Gets a value indicating whether to use TLS for the gateway connection.
/// </summary>
public bool UseTls { get; init; }
/// <summary>
/// Gets the path to a CA certificate file for custom certificate validation.
/// </summary>
public string? CaCertificatePath { get; init; }
/// <summary>
/// Gets the server name override for SNI during TLS handshake.
/// </summary>
public string? ServerNameOverride { get; init; }
/// <summary>
/// Gets the timeout for establishing connection to the gateway.
/// </summary>
public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(10);
/// <summary>
/// Gets the default timeout for unary gRPC calls.
/// </summary>
public TimeSpan DefaultCallTimeout { get; init; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Gets the optional timeout for streaming gRPC calls.
/// </summary>
public TimeSpan? StreamTimeout { get; init; }
/// <summary>
/// Gets the retry configuration for safe unary calls.
/// </summary>
public MxGatewayClientRetryOptions Retry { get; init; } = new();
/// <summary>
/// Gets the logger factory for diagnostic logging.
/// </summary>
public ILoggerFactory? LoggerFactory { get; init; }
/// <summary>
/// Validates the client options for consistency and correctness.
/// </summary>
/// <exception cref="ArgumentNullException">Endpoint is null.</exception>
/// <exception cref="ArgumentException">Options are invalid or inconsistent.</exception>
/// <exception cref="ArgumentOutOfRangeException">Timeout values are not greater than zero.</exception>
public void Validate()
{
ArgumentNullException.ThrowIfNull(Endpoint);
@@ -1,15 +1,21 @@
namespace MxGateway.Client;
/// <summary>Configuration for automatic retry behavior on transient gRPC call failures.</summary>
public sealed class MxGatewayClientRetryOptions
{
/// <summary>Gets the maximum number of attempts (initial + retries); default is 2.</summary>
public int MaxAttempts { get; init; } = 2;
/// <summary>Gets the initial delay between retry attempts; default is 200 milliseconds.</summary>
public TimeSpan Delay { get; init; } = TimeSpan.FromMilliseconds(200);
/// <summary>Gets the maximum delay between retry attempts; default is 2 seconds.</summary>
public TimeSpan MaxDelay { get; init; } = TimeSpan.FromSeconds(2);
/// <summary>Gets a value indicating whether to add randomness to retry delays; default is true.</summary>
public bool UseJitter { get; init; } = true;
/// <summary>Validates the retry options and throws if any constraint is violated.</summary>
public void Validate()
{
if (MaxAttempts <= 0)
@@ -6,8 +6,12 @@ using Polly.Retry;
namespace MxGateway.Client;
/// <summary>Factory and helpers for exponential-backoff retry policies on transient gRPC failures.</summary>
internal static class MxGatewayClientRetryPolicy
{
/// <summary>Creates a Polly ResiliencePipeline that retries transient gRPC failures with exponential backoff.</summary>
/// <param name="options">Retry configuration (max attempts, delay bounds, jitter).</param>
/// <param name="logger">Optional logger for retry diagnostics.</param>
public static ResiliencePipeline Create(
MxGatewayClientRetryOptions options,
ILogger? logger)
@@ -36,6 +40,8 @@ internal static class MxGatewayClientRetryPolicy
.Build();
}
/// <summary>Returns whether a command kind is eligible for automatic retry on transient failures.</summary>
/// <param name="kind">The command kind to check.</param>
public static bool IsRetryableCommand(MxCommandKind kind)
{
return kind is MxCommandKind.Ping
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
/// <summary>Exception thrown when a gateway command fails due to an unclassified protocol error.</summary>
public class MxGatewayCommandException : MxGatewayException
{
/// <summary>Initializes a new instance with the given details.</summary>
/// <param name="message">The error message describing the command failure.</param>
/// <param name="sessionId">The session ID, if available.</param>
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
/// <param name="protocolStatus">The protocol status details, if available.</param>
/// <param name="hResult">The HResult code, if available.</param>
/// <param name="statuses">The MXAccess statuses, if available.</param>
/// <param name="innerException">The underlying exception, if any.</param>
public MxGatewayCommandException(
string message,
string? sessionId = null,
@@ -2,20 +2,42 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
/// <summary>
/// Exception thrown when a gateway RPC call fails or returns an error status.
/// </summary>
public class MxGatewayException : Exception
{
/// <summary>
/// Initializes a new instance of the MxGatewayException class with the specified message.
/// </summary>
/// <param name="message">Diagnostic message describing the failure.</param>
public MxGatewayException(string message)
: base(message)
{
Statuses = [];
}
/// <summary>
/// Initializes a new instance of the MxGatewayException class with the specified message and inner exception.
/// </summary>
/// <param name="message">Diagnostic message describing the failure.</param>
/// <param name="innerException">Underlying exception that caused this failure.</param>
public MxGatewayException(string message, Exception? innerException)
: base(message, innerException)
{
Statuses = [];
}
/// <summary>
/// Initializes a new instance of the MxGatewayException class with full diagnostic information.
/// </summary>
/// <param name="message">Diagnostic message describing the failure.</param>
/// <param name="sessionId">Session ID associated with the exception, if available.</param>
/// <param name="correlationId">Correlation ID associated with the exception, if available.</param>
/// <param name="protocolStatus">Protocol-level status returned by the gateway, if available.</param>
/// <param name="hResult">HRESULT code returned by the worker or MXAccess, if available.</param>
/// <param name="statuses">List of MXAccess status codes returned by the operation.</param>
/// <param name="innerException">Underlying exception that caused this failure.</param>
public MxGatewayException(
string message,
string? sessionId,
@@ -33,13 +55,28 @@ public class MxGatewayException : Exception
Statuses = statuses;
}
/// <summary>
/// Gets the session ID associated with the exception, if available.
/// </summary>
public string? SessionId { get; }
/// <summary>
/// Gets the correlation ID associated with the exception, if available.
/// </summary>
public string? CorrelationId { get; }
/// <summary>
/// Gets the protocol-level status returned by the gateway, if available.
/// </summary>
public ProtocolStatus? ProtocolStatus { get; }
/// <summary>
/// Gets the HRESULT code returned by the worker or MXAccess, if available.
/// </summary>
public int? HResultCode { get; }
/// <summary>
/// Gets the list of MXAccess status codes returned by the operation.
/// </summary>
public IReadOnlyList<MxStatusProxy> Statuses { get; }
}
@@ -11,6 +11,11 @@ public sealed class MxGatewaySession : IAsyncDisposable
private readonly SemaphoreSlim _closeLock = new(1, 1);
private CloseSessionReply? _closeReply;
/// <summary>
/// Initializes a new session backed by the given MXAccess gateway client.
/// </summary>
/// <param name="client">The gateway client used for commands and events.</param>
/// <param name="openSessionReply">The server's session creation response.</param>
internal MxGatewaySession(
MxGatewayClient client,
OpenSessionReply openSessionReply)
@@ -19,10 +24,21 @@ public sealed class MxGatewaySession : IAsyncDisposable
OpenSessionReply = openSessionReply ?? throw new ArgumentNullException(nameof(openSessionReply));
}
/// <summary>
/// The session ID assigned by the gateway.
/// </summary>
public string SessionId => OpenSessionReply.SessionId;
/// <summary>
/// The server's session creation response containing metadata.
/// </summary>
public OpenSessionReply OpenSessionReply { get; }
/// <summary>
/// Closes the session on the gateway. Idempotent.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The server's close-session reply.</returns>
public async Task<CloseSessionReply> CloseAsync(CancellationToken cancellationToken = default)
{
if (_closeReply is not null)
@@ -50,6 +66,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
}
}
/// <summary>
/// Registers a client with the MXAccess session, returning a ServerHandle.
/// </summary>
/// <param name="clientName">Name to register.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The server handle assigned to the registered client.</returns>
public async Task<int> RegisterAsync(
string clientName,
CancellationToken cancellationToken = default)
@@ -60,6 +82,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.Register?.ServerHandle ?? reply.ReturnValue.Int32Value;
}
/// <summary>
/// Registers a client with the MXAccess session without error checking.
/// </summary>
/// <param name="clientName">Name to register.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> RegisterRawAsync(
string clientName,
CancellationToken cancellationToken = default)
@@ -75,6 +103,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
/// <summary>
/// Adds an item to the MXAccess session, returning an ItemHandle.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemDefinition">The item tag address.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The item handle assigned to the new item.</returns>
public async Task<int> AddItemAsync(
int serverHandle,
string itemDefinition,
@@ -89,6 +124,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.AddItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
}
/// <summary>
/// Adds an item to the MXAccess session without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemDefinition">The item tag address.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> AddItemRawAsync(
int serverHandle,
string itemDefinition,
@@ -109,6 +151,14 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
/// <summary>
/// Adds an item with context to the MXAccess session, returning an ItemHandle.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemDefinition">The item tag address.</param>
/// <param name="itemContext">Additional context for the item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The item handle assigned to the new item.</returns>
public async Task<int> AddItem2Async(
int serverHandle,
string itemDefinition,
@@ -125,6 +175,14 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.AddItem2?.ItemHandle ?? reply.ReturnValue.Int32Value;
}
/// <summary>
/// Adds an item with context to the MXAccess session without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemDefinition">The item tag address.</param>
/// <param name="itemContext">Additional context for the item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> AddItem2RawAsync(
int serverHandle,
string itemDefinition,
@@ -147,6 +205,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
/// <summary>
/// Subscribes to events for an item (advises in MXAccess terminology).
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task AdviseAsync(
int serverHandle,
int itemHandle,
@@ -157,6 +221,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Subscribes to events for an item without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> AdviseRawAsync(
int serverHandle,
int itemHandle,
@@ -175,6 +246,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
/// <summary>
/// Unsubscribes from events for an item (unadvises in MXAccess terminology).
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task UnAdviseAsync(
int serverHandle,
int itemHandle,
@@ -185,6 +262,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Unsubscribes from events for an item without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> UnAdviseRawAsync(
int serverHandle,
int itemHandle,
@@ -203,6 +287,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
/// <summary>
/// Removes an item from the MXAccess session.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task RemoveItemAsync(
int serverHandle,
int itemHandle,
@@ -213,6 +303,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Removes an item from the MXAccess session without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> RemoveItemRawAsync(
int serverHandle,
int itemHandle,
@@ -231,6 +328,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
/// <summary>
/// Adds multiple items to the MXAccess session in a single command.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="tagAddresses">The item tag addresses to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Per-item subscription results.</returns>
public async Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(
int serverHandle,
IReadOnlyList<string> tagAddresses,
@@ -253,6 +357,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.AddItemBulk?.Results.ToArray() ?? [];
}
/// <summary>
/// Advises multiple items in a single command.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandles">The ItemHandles to advise.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Per-item subscription results.</returns>
public async Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(
int serverHandle,
IReadOnlyList<int> itemHandles,
@@ -275,6 +386,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.AdviseItemBulk?.Results.ToArray() ?? [];
}
/// <summary>
/// Removes multiple items in a single command.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandles">The ItemHandles to remove.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Per-item subscription results.</returns>
public async Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(
int serverHandle,
IReadOnlyList<int> itemHandles,
@@ -297,6 +415,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.RemoveItemBulk?.Results.ToArray() ?? [];
}
/// <summary>
/// Unadvises multiple items in a single command.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandles">The ItemHandles to unadvise.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Per-item subscription results.</returns>
public async Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(
int serverHandle,
IReadOnlyList<int> itemHandles,
@@ -319,6 +444,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.UnAdviseItemBulk?.Results.ToArray() ?? [];
}
/// <summary>
/// Adds and advises multiple items in a single command.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="tagAddresses">The item tag addresses to add and advise.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Per-item subscription results.</returns>
public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
int serverHandle,
IReadOnlyList<string> tagAddresses,
@@ -341,6 +473,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.SubscribeBulk?.Results.ToArray() ?? [];
}
/// <summary>
/// Unadvises and removes multiple items in a single command.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandles">The ItemHandles to unsubscribe.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Per-item subscription results.</returns>
public async Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(
int serverHandle,
IReadOnlyList<int> itemHandles,
@@ -363,6 +502,14 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.UnsubscribeBulk?.Results.ToArray() ?? [];
}
/// <summary>
/// Writes a value to an item on the MXAccess server.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="value">The value to write.</param>
/// <param name="userId">User ID context for the write.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task WriteAsync(
int serverHandle,
int itemHandle,
@@ -375,6 +522,15 @@ public sealed class MxGatewaySession : IAsyncDisposable
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Writes a value to an item on the MXAccess server without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="value">The value to write.</param>
/// <param name="userId">User ID context for the write.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> WriteRawAsync(
int serverHandle,
int itemHandle,
@@ -399,6 +555,15 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
/// <summary>
/// Writes a value and timestamp to an item on the MXAccess server.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="value">The value to write.</param>
/// <param name="timestampValue">The timestamp to write with the value.</param>
/// <param name="userId">User ID context for the write.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task Write2Async(
int serverHandle,
int itemHandle,
@@ -418,6 +583,16 @@ public sealed class MxGatewaySession : IAsyncDisposable
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Writes a value and timestamp to an item on the MXAccess server without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="value">The value to write.</param>
/// <param name="timestampValue">The timestamp to write with the value.</param>
/// <param name="userId">User ID context for the write.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> Write2RawAsync(
int serverHandle,
int itemHandle,
@@ -445,6 +620,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
/// <summary>
/// Invokes an MXAccess command on this session.
/// </summary>
/// <param name="request">The command request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> InvokeAsync(
MxCommandRequest request,
CancellationToken cancellationToken = default)
@@ -453,6 +634,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
return _client.InvokeAsync(request, cancellationToken);
}
/// <summary>
/// Streams events from the worker for this session, optionally starting after a given sequence number.
/// </summary>
/// <param name="afterWorkerSequence">The sequence number to stream from. Defaults to 0.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An async enumerable of events.</returns>
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
ulong afterWorkerSequence = 0,
CancellationToken cancellationToken = default)
@@ -466,6 +653,9 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
/// <summary>
/// Closes the session and releases resources.
/// </summary>
public async ValueTask DisposeAsync()
{
await CloseAsync().ConfigureAwait(false);
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
/// <summary>Exception thrown when a session is not found, not ready, or invalid.</summary>
public sealed class MxGatewaySessionException : MxGatewayException
{
/// <summary>Initializes a new instance with the given details.</summary>
/// <param name="message">The error message describing the session failure.</param>
/// <param name="sessionId">The session ID, if available.</param>
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
/// <param name="protocolStatus">The protocol status details, if available.</param>
/// <param name="hResult">The HResult code, if available.</param>
/// <param name="statuses">The MXAccess statuses, if available.</param>
/// <param name="innerException">The underlying exception, if any.</param>
public MxGatewaySessionException(
string message,
string? sessionId = null,
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
/// <summary>Exception thrown when the worker process is unavailable or fails to process a command.</summary>
public sealed class MxGatewayWorkerException : MxGatewayException
{
/// <summary>Initializes a new instance with the given details.</summary>
/// <param name="message">The error message describing the worker failure.</param>
/// <param name="sessionId">The session ID, if available.</param>
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
/// <param name="protocolStatus">The protocol status details, if available.</param>
/// <param name="hResult">The HResult code, if available.</param>
/// <param name="statuses">The MXAccess statuses, if available.</param>
/// <param name="innerException">The underlying exception, if any.</param>
public MxGatewayWorkerException(
string message,
string? sessionId = null,
@@ -2,8 +2,11 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
/// <summary>Extension methods for MxStatusProxy values.</summary>
public static class MxStatusProxyExtensions
{
/// <summary>Returns whether the status indicates success (success flag set and category is Ok).</summary>
/// <param name="status">The status to check.</param>
public static bool IsSuccess(this MxStatusProxy status)
{
ArgumentNullException.ThrowIfNull(status);
@@ -12,6 +15,8 @@ public static class MxStatusProxyExtensions
&& status.Category is MxStatusCategory.Ok;
}
/// <summary>Returns a formatted summary of the status for diagnostic output.</summary>
/// <param name="status">The status to summarize.</param>
public static string ToDiagnosticSummary(this MxStatusProxy status)
{
ArgumentNullException.ThrowIfNull(status);
@@ -10,6 +10,10 @@ namespace MxGateway.Client;
/// </summary>
public static class MxValueExtensions
{
/// <summary>
/// Converts a boolean value to an MxValue with MxDataType.Boolean.
/// </summary>
/// <param name="value">Scalar boolean value to wrap.</param>
public static MxValue ToMxValue(this bool value)
{
return new MxValue
@@ -20,6 +24,10 @@ public static class MxValueExtensions
};
}
/// <summary>
/// Converts a 32-bit integer value to an MxValue with MxDataType.Integer.
/// </summary>
/// <param name="value">32-bit integer value to wrap.</param>
public static MxValue ToMxValue(this int value)
{
return new MxValue
@@ -30,6 +38,10 @@ public static class MxValueExtensions
};
}
/// <summary>
/// Converts a 64-bit integer value to an MxValue with MxDataType.Integer.
/// </summary>
/// <param name="value">64-bit integer value to wrap.</param>
public static MxValue ToMxValue(this long value)
{
return new MxValue
@@ -40,6 +52,10 @@ public static class MxValueExtensions
};
}
/// <summary>
/// Converts a single-precision floating-point value to an MxValue with MxDataType.Float.
/// </summary>
/// <param name="value">Single-precision floating-point value to wrap.</param>
public static MxValue ToMxValue(this float value)
{
return new MxValue
@@ -50,6 +66,10 @@ public static class MxValueExtensions
};
}
/// <summary>
/// Converts a double-precision floating-point value to an MxValue with MxDataType.Double.
/// </summary>
/// <param name="value">Double-precision floating-point value to wrap.</param>
public static MxValue ToMxValue(this double value)
{
return new MxValue
@@ -60,6 +80,10 @@ public static class MxValueExtensions
};
}
/// <summary>
/// Converts a string value to an MxValue with MxDataType.String.
/// </summary>
/// <param name="value">String value to wrap.</param>
public static MxValue ToMxValue(this string value)
{
ArgumentNullException.ThrowIfNull(value);
@@ -72,6 +96,10 @@ public static class MxValueExtensions
};
}
/// <summary>
/// Converts a DateTimeOffset value to an MxValue with MxDataType.Time.
/// </summary>
/// <param name="value">DateTimeOffset value to wrap.</param>
public static MxValue ToMxValue(this DateTimeOffset value)
{
return new MxValue
@@ -82,6 +110,10 @@ public static class MxValueExtensions
};
}
/// <summary>
/// Converts a DateTime value to an MxValue with MxDataType.Time.
/// </summary>
/// <param name="value">DateTime value to wrap.</param>
public static MxValue ToMxValue(this DateTime value)
{
return new DateTimeOffset(
@@ -91,6 +123,10 @@ public static class MxValueExtensions
.ToMxValue();
}
/// <summary>
/// Converts a boolean array to an MxValue with MxDataType.Boolean.
/// </summary>
/// <param name="values">Array of boolean values to wrap.</param>
public static MxValue ToMxValue(this IReadOnlyList<bool> values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -105,6 +141,10 @@ public static class MxValueExtensions
});
}
/// <summary>
/// Converts a 32-bit integer array to an MxValue with MxDataType.Integer.
/// </summary>
/// <param name="values">Array of 32-bit integer values to wrap.</param>
public static MxValue ToMxValue(this IReadOnlyList<int> values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -119,6 +159,10 @@ public static class MxValueExtensions
});
}
/// <summary>
/// Converts a 64-bit integer array to an MxValue with MxDataType.Integer.
/// </summary>
/// <param name="values">Array of 64-bit integer values to wrap.</param>
public static MxValue ToMxValue(this IReadOnlyList<long> values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -133,6 +177,10 @@ public static class MxValueExtensions
});
}
/// <summary>
/// Converts a single-precision floating-point array to an MxValue with MxDataType.Float.
/// </summary>
/// <param name="values">Array of single-precision floating-point values to wrap.</param>
public static MxValue ToMxValue(this IReadOnlyList<float> values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -147,6 +195,10 @@ public static class MxValueExtensions
});
}
/// <summary>
/// Converts a double-precision floating-point array to an MxValue with MxDataType.Double.
/// </summary>
/// <param name="values">Array of double-precision floating-point values to wrap.</param>
public static MxValue ToMxValue(this IReadOnlyList<double> values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -161,6 +213,10 @@ public static class MxValueExtensions
});
}
/// <summary>
/// Converts a string array to an MxValue with MxDataType.String.
/// </summary>
/// <param name="values">Array of string values to wrap.</param>
public static MxValue ToMxValue(this IReadOnlyList<string> values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -175,6 +231,10 @@ public static class MxValueExtensions
});
}
/// <summary>
/// Converts a DateTimeOffset array to an MxValue with MxDataType.Time.
/// </summary>
/// <param name="values">Array of DateTimeOffset values to wrap.</param>
public static MxValue ToMxValue(this IReadOnlyList<DateTimeOffset> values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -189,6 +249,10 @@ public static class MxValueExtensions
});
}
/// <summary>
/// Gets the projection kind (field name) of the given MxValue's current oneof value.
/// </summary>
/// <param name="value">The MxValue whose oneof projection kind is returned.</param>
public static string GetProjectionKind(this MxValue value)
{
ArgumentNullException.ThrowIfNull(value);
@@ -208,6 +272,10 @@ public static class MxValueExtensions
};
}
/// <summary>
/// Converts an MxValue to a CLR object; returns the boxed value or null for null MxValues.
/// </summary>
/// <param name="value">The MxValue to convert.</param>
public static object? ToClrValue(this MxValue value)
{
ArgumentNullException.ThrowIfNull(value);
@@ -227,6 +295,10 @@ public static class MxValueExtensions
};
}
/// <summary>
/// Converts an MxArray to a CLR array; returns null if the array does not have a known element type.
/// </summary>
/// <param name="array">The MxArray to convert.</param>
public static object? ToClrArrayValue(this MxArray array)
{
ArgumentNullException.ThrowIfNull(array);
@@ -249,6 +321,13 @@ public static class MxValueExtensions
};
}
/// <summary>
/// Creates an MxValue with MxDataType.Unknown from raw byte data, variant type, and diagnostic info.
/// </summary>
/// <param name="value">Raw byte data representing the value.</param>
/// <param name="variantType">Variant type string (e.g., "VT_BSTR").</param>
/// <param name="rawDiagnostic">Diagnostic string describing the raw value.</param>
/// <param name="rawDataType">Optional MXAccess data type override.</param>
public static MxValue ToRawMxValue(
byte[] value,
string variantType,