diff --git a/clients/dotnet/MxGateway.Client.Cli/CliArguments.cs b/clients/dotnet/MxGateway.Client.Cli/CliArguments.cs
index 31d6fa0..2da2d2a 100644
--- a/clients/dotnet/MxGateway.Client.Cli/CliArguments.cs
+++ b/clients/dotnet/MxGateway.Client.Cli/CliArguments.cs
@@ -2,11 +2,14 @@ using System.Globalization;
namespace MxGateway.Client.Cli;
+/// Parses command-line arguments into flags and named values.
internal sealed class CliArguments
{
private readonly Dictionary _values = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet _flags = new(StringComparer.OrdinalIgnoreCase);
+ /// Initializes a new instance by parsing the given command-line arguments.
+ /// Unparsed command-line arguments; flags prefixed with '--' and values follow their flag.
public CliArguments(IEnumerable args)
{
string? pendingName = null;
@@ -39,11 +42,15 @@ internal sealed class CliArguments
}
}
+ /// Returns whether the named flag was present in the arguments.
+ /// The flag name (without '--' prefix).
public bool HasFlag(string name)
{
return _flags.Contains(name);
}
+ /// Returns the value for a named argument, or null if absent.
+ /// The argument name (without '--' prefix).
public string? GetOptional(string name)
{
return _values.TryGetValue(name, out string? value)
@@ -51,6 +58,8 @@ internal sealed class CliArguments
: null;
}
+ /// Returns the value for a required named argument, or throws if absent.
+ /// The argument name (without '--' prefix).
public string GetRequired(string name)
{
string? value = GetOptional(name);
@@ -62,6 +71,9 @@ internal sealed class CliArguments
return value;
}
+ /// Parses and returns an int32 argument, or the default value if absent.
+ /// The argument name (without '--' prefix).
+ /// The default value if the argument is absent; if null, the argument is required.
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);
}
+ /// Parses and returns a uint32 argument, or the default value if absent.
+ /// The argument name (without '--' prefix).
+ /// The default value if the argument is absent.
public uint GetUInt32(string name, uint defaultValue)
{
string? value = GetOptional(name);
@@ -86,6 +101,9 @@ internal sealed class CliArguments
: uint.Parse(value, CultureInfo.InvariantCulture);
}
+ /// Parses and returns a uint64 argument, or the default value if absent.
+ /// The argument name (without '--' prefix).
+ /// The default value if the argument is absent.
public ulong GetUInt64(string name, ulong defaultValue)
{
string? value = GetOptional(name);
@@ -94,6 +112,9 @@ internal sealed class CliArguments
: ulong.Parse(value, CultureInfo.InvariantCulture);
}
+ /// Parses and returns a TimeSpan argument, or the default value if absent. Supports "ms", "s", and standard TimeSpan format.
+ /// The argument name (without '--' prefix).
+ /// The default value if the argument is absent.
public TimeSpan GetDuration(string name, TimeSpan defaultValue)
{
string? value = GetOptional(name);
diff --git a/clients/dotnet/MxGateway.Client.Cli/IMxGatewayCliClient.cs b/clients/dotnet/MxGateway.Client.Cli/IMxGatewayCliClient.cs
index 60c3340..ba04136 100644
--- a/clients/dotnet/MxGateway.Client.Cli/IMxGatewayCliClient.cs
+++ b/clients/dotnet/MxGateway.Client.Cli/IMxGatewayCliClient.cs
@@ -5,34 +5,82 @@ namespace MxGateway.Client.Cli;
public interface IMxGatewayCliClient : IAsyncDisposable
{
+ ///
+ /// Opens a new gateway session.
+ ///
+ /// Session open request.
+ /// Cancellation token for the operation.
+ /// The session open reply.
Task OpenSessionAsync(
OpenSessionRequest request,
CancellationToken cancellationToken);
+ ///
+ /// Closes an open gateway session.
+ ///
+ /// Session close request.
+ /// Cancellation token for the operation.
+ /// The session close reply.
Task CloseSessionAsync(
CloseSessionRequest request,
CancellationToken cancellationToken);
+ ///
+ /// Invokes an MXAccess command on the session.
+ ///
+ /// The command request.
+ /// Cancellation token for the operation.
+ /// The command reply.
Task InvokeAsync(
MxCommandRequest request,
CancellationToken cancellationToken);
+ ///
+ /// Streams events from the gateway session.
+ ///
+ /// The stream events request.
+ /// Cancellation token for the operation.
+ /// An async enumerable of events.
IAsyncEnumerable StreamEventsAsync(
StreamEventsRequest request,
CancellationToken cancellationToken);
+ ///
+ /// Tests connection to the Galaxy Repository.
+ ///
+ /// The connection test request.
+ /// Cancellation token for the operation.
+ /// The connection test reply.
Task GalaxyTestConnectionAsync(
TestConnectionRequest request,
CancellationToken cancellationToken);
+ ///
+ /// Gets the last deployment time from the Galaxy Repository.
+ ///
+ /// The last deploy time request.
+ /// Cancellation token for the operation.
+ /// The last deploy time reply.
Task GalaxyGetLastDeployTimeAsync(
GetLastDeployTimeRequest request,
CancellationToken cancellationToken);
+ ///
+ /// Discovers the Galaxy Repository hierarchy.
+ ///
+ /// The discover hierarchy request.
+ /// Cancellation token for the operation.
+ /// The discover hierarchy reply.
Task GalaxyDiscoverHierarchyAsync(
DiscoverHierarchyRequest request,
CancellationToken cancellationToken);
+ ///
+ /// Watches for deployment events from the Galaxy Repository.
+ ///
+ /// The watch deploy events request.
+ /// Cancellation token for the operation.
+ /// An async enumerable of deployment events.
IAsyncEnumerable GalaxyWatchDeployEventsAsync(
WatchDeployEventsRequest request,
CancellationToken cancellationToken);
diff --git a/clients/dotnet/MxGateway.Client.Cli/MxGatewayCliClientAdapter.cs b/clients/dotnet/MxGateway.Client.Cli/MxGatewayCliClientAdapter.cs
index 9fe099b..47f0d06 100644
--- a/clients/dotnet/MxGateway.Client.Cli/MxGatewayCliClientAdapter.cs
+++ b/clients/dotnet/MxGateway.Client.Cli/MxGatewayCliClientAdapter.cs
@@ -9,6 +9,10 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
private readonly MxGatewayClient _client;
private readonly Lazy _galaxyClient;
+ ///
+ /// Initializes a new instance of the that bridges the CLI to the gateway client.
+ ///
+ /// The gateway client to adapt.
public MxGatewayCliClientAdapter(MxGatewayClient client)
{
_client = client;
@@ -16,6 +20,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
() => GalaxyRepositoryClient.Create(_client.Options));
}
+ ///
public Task OpenSessionAsync(
OpenSessionRequest request,
CancellationToken cancellationToken)
@@ -23,6 +28,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _client.OpenSessionRawAsync(request, cancellationToken);
}
+ ///
public Task CloseSessionAsync(
CloseSessionRequest request,
CancellationToken cancellationToken)
@@ -30,6 +36,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _client.CloseSessionRawAsync(request, cancellationToken);
}
+ ///
public Task InvokeAsync(
MxCommandRequest request,
CancellationToken cancellationToken)
@@ -37,6 +44,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _client.InvokeAsync(request, cancellationToken);
}
+ ///
public IAsyncEnumerable StreamEventsAsync(
StreamEventsRequest request,
CancellationToken cancellationToken)
@@ -44,6 +52,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _client.StreamEventsAsync(request, cancellationToken);
}
+ ///
public Task GalaxyTestConnectionAsync(
TestConnectionRequest request,
CancellationToken cancellationToken)
@@ -51,6 +60,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _galaxyClient.Value.TestConnectionRawAsync(request, cancellationToken);
}
+ ///
public Task GalaxyGetLastDeployTimeAsync(
GetLastDeployTimeRequest request,
CancellationToken cancellationToken)
@@ -58,6 +68,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _galaxyClient.Value.GetLastDeployTimeRawAsync(request, cancellationToken);
}
+ ///
public Task GalaxyDiscoverHierarchyAsync(
DiscoverHierarchyRequest request,
CancellationToken cancellationToken)
@@ -65,6 +76,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _galaxyClient.Value.DiscoverHierarchyRawAsync(request, cancellationToken);
}
+ ///
public IAsyncEnumerable GalaxyWatchDeployEventsAsync(
WatchDeployEventsRequest request,
CancellationToken cancellationToken)
@@ -72,6 +84,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _galaxyClient.Value.WatchDeployEventsRawAsync(request, cancellationToken);
}
+ ///
public async ValueTask DisposeAsync()
{
if (_galaxyClient.IsValueCreated)
diff --git a/clients/dotnet/MxGateway.Client.Cli/MxGatewayCliSecretRedactor.cs b/clients/dotnet/MxGateway.Client.Cli/MxGatewayCliSecretRedactor.cs
index 42a6a96..1feada4 100644
--- a/clients/dotnet/MxGateway.Client.Cli/MxGatewayCliSecretRedactor.cs
+++ b/clients/dotnet/MxGateway.Client.Cli/MxGatewayCliSecretRedactor.cs
@@ -1,7 +1,11 @@
namespace MxGateway.Client.Cli;
+/// Utility to redact API keys from error messages for safe output.
internal static class MxGatewayCliSecretRedactor
{
+ /// Replaces occurrences of the API key in the value with a redacted placeholder.
+ /// The message text to redact.
+ /// The API key to remove; no redaction if null or empty.
public static string Redact(string value, string? apiKey)
{
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(apiKey))
diff --git a/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs b/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs
index c24f24e..54723e1 100644
--- a/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs
+++ b/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs
@@ -7,6 +7,7 @@ using MxGateway.Contracts.Proto.Galaxy;
namespace MxGateway.Client.Cli;
+/// Command-line interface for the MXAccess Gateway client, supporting session and command operations.
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);
+ /// Runs the CLI synchronously with the given arguments, writing output and errors.
+ /// Command-line arguments (command name followed by options).
+ /// TextWriter for command output.
+ /// TextWriter for error messages.
public static int Run(
string[] args,
TextWriter standardOutput,
@@ -25,6 +30,11 @@ public static class MxGatewayClientCli
.GetResult();
}
+ /// Runs the CLI asynchronously with the given arguments, writing output and errors.
+ /// Command-line arguments (command name followed by options).
+ /// TextWriter for command output.
+ /// TextWriter for error messages.
+ /// Optional factory to create the gateway client; defaults to MxGatewayClient.Create.
public static Task RunAsync(
string[] args,
TextWriter standardOutput,
diff --git a/clients/dotnet/MxGateway.Client.Tests/FakeGalaxyRepositoryTransport.cs b/clients/dotnet/MxGateway.Client.Tests/FakeGalaxyRepositoryTransport.cs
index 8053aba..4e9c912 100644
--- a/clients/dotnet/MxGateway.Client.Tests/FakeGalaxyRepositoryTransport.cs
+++ b/clients/dotnet/MxGateway.Client.Tests/FakeGalaxyRepositoryTransport.cs
@@ -3,30 +3,71 @@ using MxGateway.Contracts.Proto.Galaxy;
namespace MxGateway.Client.Tests;
+///
+/// Fake Galaxy Repository client transport for testing.
+///
internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions options) : IGalaxyRepositoryClientTransport
{
+ ///
+ /// Gets the gateway client options.
+ ///
public MxGatewayClientOptions Options { get; } = options;
+ ///
+ /// Gets the raw gRPC client; always null for the fake.
+ ///
public GalaxyRepository.GalaxyRepositoryClient? RawClient => null;
+ ///
+ /// Gets the list of TestConnection RPC calls made by the client.
+ ///
public List<(TestConnectionRequest Request, CallOptions CallOptions)> TestConnectionCalls { get; } = [];
+ ///
+ /// Gets the list of GetLastDeployTime RPC calls made by the client.
+ ///
public List<(GetLastDeployTimeRequest Request, CallOptions CallOptions)> GetLastDeployTimeCalls { get; } = [];
+ ///
+ /// Gets the list of DiscoverHierarchy RPC calls made by the client.
+ ///
public List<(DiscoverHierarchyRequest Request, CallOptions CallOptions)> DiscoverHierarchyCalls { get; } = [];
+ ///
+ /// Gets or sets the reply to return from TestConnection; defaults to successful response.
+ ///
public TestConnectionReply TestConnectionReply { get; set; } = new() { Ok = true };
+ ///
+ /// Gets or sets the reply to return from GetLastDeployTime; defaults to no deploy time present.
+ ///
public GetLastDeployTimeReply GetLastDeployTimeReply { get; set; } = new() { Present = false };
+ ///
+ /// Gets or sets the reply to return from DiscoverHierarchy; defaults to empty response.
+ ///
public DiscoverHierarchyReply DiscoverHierarchyReply { get; set; } = new();
+ ///
+ /// Gets the queue of exceptions to throw from TestConnection; dequeued in FIFO order.
+ ///
public Queue TestConnectionExceptions { get; } = new();
+ ///
+ /// Gets the queue of exceptions to throw from GetLastDeployTime; dequeued in FIFO order.
+ ///
public Queue GetLastDeployTimeExceptions { get; } = new();
+ ///
+ /// Gets the queue of exceptions to throw from DiscoverHierarchy; dequeued in FIFO order.
+ ///
public Queue DiscoverHierarchyExceptions { get; } = new();
+ ///
+ /// Records the request and either throws a queued exception or returns the configured reply.
+ ///
+ /// The TestConnectionRequest to process.
+ /// Call options specifying RPC behavior.
public Task TestConnectionAsync(
TestConnectionRequest request,
CallOptions callOptions)
@@ -40,6 +81,11 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
return Task.FromResult(TestConnectionReply);
}
+ ///
+ /// Records the request and either throws a queued exception or returns the configured reply.
+ ///
+ /// The GetLastDeployTimeRequest to process.
+ /// Call options specifying RPC behavior.
public Task GetLastDeployTimeAsync(
GetLastDeployTimeRequest request,
CallOptions callOptions)
@@ -53,6 +99,11 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
return Task.FromResult(GetLastDeployTimeReply);
}
+ ///
+ /// Records the request and either throws a queued exception or returns the configured reply.
+ ///
+ /// The DiscoverHierarchyRequest to process.
+ /// Call options specifying RPC behavior.
public Task DiscoverHierarchyAsync(
DiscoverHierarchyRequest request,
CallOptions callOptions)
@@ -66,10 +117,19 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
return Task.FromResult(DiscoverHierarchyReply);
}
+ ///
+ /// Gets the list of WatchDeployEvents RPC calls made by the client.
+ ///
public List<(WatchDeployEventsRequest Request, CallOptions CallOptions)> WatchDeployEventsCalls { get; } = [];
+ ///
+ /// Gets or sets the list of events to stream from WatchDeployEvents.
+ ///
public List WatchDeployEvents { get; } = [];
+ ///
+ /// Gets or sets the exception to throw from WatchDeployEvents, if any.
+ ///
public Exception? WatchDeployEventsException { get; set; }
///
@@ -78,6 +138,11 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
///
public Func? WatchDeployEventsBeforeYield { get; set; }
+ ///
+ /// Records the request and streams events, checking for queued exceptions and calling WatchDeployEventsBeforeYield before each event.
+ ///
+ /// The WatchDeployEventsRequest to process.
+ /// Call options specifying RPC behavior.
public async IAsyncEnumerable WatchDeployEventsAsync(
WatchDeployEventsRequest request,
CallOptions callOptions)
diff --git a/clients/dotnet/MxGateway.Client.Tests/FakeGatewayTransport.cs b/clients/dotnet/MxGateway.Client.Tests/FakeGatewayTransport.cs
index f5163bd..9f27e5c 100644
--- a/clients/dotnet/MxGateway.Client.Tests/FakeGatewayTransport.cs
+++ b/clients/dotnet/MxGateway.Client.Tests/FakeGatewayTransport.cs
@@ -3,23 +3,47 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client.Tests;
+///
+/// Fake implementation of IMxGatewayClientTransport for testing.
+///
internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMxGatewayClientTransport
{
private readonly Queue _invokeReplies = new();
private readonly List _events = [];
+ ///
+ /// Gets the gateway client options.
+ ///
public MxGatewayClientOptions Options { get; } = options;
+ ///
+ /// Gets null, since this is a test fake without a real gRPC client.
+ ///
public MxAccessGateway.MxAccessGatewayClient? RawClient => null;
+ ///
+ /// Gets the list of captured OpenSessionAsync calls.
+ ///
public List<(OpenSessionRequest Request, CallOptions CallOptions)> OpenSessionCalls { get; } = [];
+ ///
+ /// Gets the list of captured CloseSessionAsync calls.
+ ///
public List<(CloseSessionRequest Request, CallOptions CallOptions)> CloseSessionCalls { get; } = [];
+ ///
+ /// Gets the list of captured InvokeAsync calls.
+ ///
public List<(MxCommandRequest Request, CallOptions CallOptions)> InvokeCalls { get; } = [];
+ ///
+ /// Gets the list of captured StreamEventsAsync calls.
+ ///
public List<(StreamEventsRequest Request, CallOptions CallOptions)> StreamEventsCalls { get; } = [];
+ ///
+ /// Gets or sets the reply to return from OpenSessionAsync.
+ ///
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 },
};
+ ///
+ /// Gets or sets the reply to return from CloseSessionAsync.
+ ///
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 },
};
+ ///
+ /// Gets the queue of exceptions to throw from OpenSessionAsync.
+ ///
public Queue OpenSessionExceptions { get; } = new();
+ ///
+ /// Gets the queue of exceptions to throw from CloseSessionAsync.
+ ///
public Queue CloseSessionExceptions { get; } = new();
+ ///
+ /// Gets the queue of exceptions to throw from InvokeAsync.
+ ///
public Queue InvokeExceptions { get; } = new();
+ ///
+ /// Verifies that the OpenSessionAsync call is recorded and returns the configured reply.
+ ///
+ /// The OpenSessionRequest to process.
+ /// Call options specifying RPC behavior.
public Task OpenSessionAsync(
OpenSessionRequest request,
CallOptions callOptions)
@@ -55,6 +96,11 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
return Task.FromResult(OpenSessionReply);
}
+ ///
+ /// Verifies that the CloseSessionAsync call is recorded and returns the configured reply.
+ ///
+ /// The CloseSessionRequest to process.
+ /// Call options specifying RPC behavior.
public Task CloseSessionAsync(
CloseSessionRequest request,
CallOptions callOptions)
@@ -68,6 +114,11 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
return Task.FromResult(CloseSessionReply);
}
+ ///
+ /// Verifies that the InvokeAsync call is recorded and returns the next enqueued reply.
+ ///
+ /// The MxCommandRequest to process.
+ /// Call options specifying RPC behavior.
public Task InvokeAsync(
MxCommandRequest request,
CallOptions callOptions)
@@ -81,6 +132,11 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
return Task.FromResult(_invokeReplies.Dequeue());
}
+ ///
+ /// Verifies that the StreamEventsAsync call is recorded and yields all enqueued events.
+ ///
+ /// The StreamEventsRequest to process.
+ /// Call options specifying RPC behavior.
public async IAsyncEnumerable StreamEventsAsync(
StreamEventsRequest request,
CallOptions callOptions)
@@ -95,11 +151,19 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
}
}
+ ///
+ /// Enqueues a reply to be returned from the next InvokeAsync call.
+ ///
+ /// The reply to enqueue.
public void AddInvokeReply(MxCommandReply reply)
{
_invokeReplies.Enqueue(reply);
}
+ ///
+ /// Enqueues an event to be yielded from StreamEventsAsync.
+ ///
+ /// The event to enqueue.
public void AddEvent(MxEvent gatewayEvent)
{
_events.Add(gatewayEvent);
diff --git a/clients/dotnet/MxGateway.Client.Tests/GalaxyRepositoryClientTests.cs b/clients/dotnet/MxGateway.Client.Tests/GalaxyRepositoryClientTests.cs
index 4ffd875..4c0ecfb 100644
--- a/clients/dotnet/MxGateway.Client.Tests/GalaxyRepositoryClientTests.cs
+++ b/clients/dotnet/MxGateway.Client.Tests/GalaxyRepositoryClientTests.cs
@@ -6,6 +6,9 @@ namespace MxGateway.Client.Tests;
public sealed class GalaxyRepositoryClientTests
{
+ ///
+ /// Verifies that TestConnectionAsync attaches the API key in request metadata and returns the Ok flag.
+ ///
[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"));
}
+ ///
+ /// Verifies that TestConnectionAsync returns false when the server reports NotOk.
+ ///
[Fact]
public async Task TestConnectionAsync_ReturnsFalseWhenServerReportsNotOk()
{
@@ -33,6 +39,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.False(ok);
}
+ ///
+ /// Verifies that GetLastDeployTimeAsync returns null when the server reports not present.
+ ///
[Fact]
public async Task GetLastDeployTimeAsync_ReturnsNullWhenNotPresent()
{
@@ -46,6 +55,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Single(transport.GetLastDeployTimeCalls);
}
+ ///
+ /// Verifies that GetLastDeployTimeAsync returns the timestamp when the server reports it present.
+ ///
[Fact]
public async Task GetLastDeployTimeAsync_ReturnsTimestampWhenPresent()
{
@@ -64,6 +76,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Equal(expected, deployTime!.Value);
}
+ ///
+ /// Verifies that DiscoverHierarchyAsync returns the objects from the server reply.
+ ///
[Fact]
public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply()
{
@@ -104,6 +119,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Equal("DelmiaReceiver_001.DownloadPath", attribute.FullTagReference);
}
+ ///
+ /// Verifies that DiscoverHierarchyAsync propagates cancellation tokens to the transport.
+ ///
[Fact]
public async Task DiscoverHierarchyAsync_PropagatesCancellationToTransport()
{
@@ -121,6 +139,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.False(call.CallOptions.CancellationToken.IsCancellationRequested);
}
+ ///
+ /// Verifies that TestConnectionAsync retries on transient gRPC failures.
+ ///
[Fact]
public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure()
{
@@ -135,6 +156,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Equal(2, transport.TestConnectionCalls.Count);
}
+ ///
+ /// Verifies that DiscoverHierarchyAsync retries on transient gRPC failures.
+ ///
[Fact]
public async Task DiscoverHierarchyAsync_RetriesOnTransientGrpcFailure()
{
@@ -148,6 +172,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Equal(2, transport.DiscoverHierarchyCalls.Count);
}
+ ///
+ /// Verifies that WatchDeployEventsAsync delivers the bootstrap event.
+ ///
[Fact]
public async Task WatchDeployEventsAsync_DeliversBootstrapEvent()
{
@@ -181,6 +208,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Null(call.Request.LastSeenDeployTime);
}
+ ///
+ /// Verifies that WatchDeployEventsAsync delivers multiple events in order.
+ ///
[Fact]
public async Task WatchDeployEventsAsync_DeliversMultipleEventsInOrder()
{
@@ -216,6 +246,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Equal(t0, call.Request.LastSeenDeployTime!.ToDateTime());
}
+ ///
+ /// Verifies that WatchDeployEventsAsync stops iteration cleanly when cancelled.
+ ///
[Fact]
public async Task WatchDeployEventsAsync_CancellationStopsIterationCleanly()
{
@@ -257,6 +290,9 @@ public sealed class GalaxyRepositoryClientTests
Assert.Equal(1ul, received[0].Sequence);
}
+ ///
+ /// Verifies that WatchDeployEventsAsync throws ObjectDisposedException after the client is disposed.
+ ///
[Fact]
public async Task WatchDeployEventsAsync_ThrowsAfterDisposal()
{
@@ -269,6 +305,9 @@ public sealed class GalaxyRepositoryClientTests
client.WatchDeployEventsAsync());
}
+ ///
+ /// Verifies that TestConnectionAsync throws ObjectDisposedException after the client is disposed.
+ ///
[Fact]
public async Task TestConnectionAsync_ThrowsAfterDisposal()
{
diff --git a/clients/dotnet/MxGateway.Client.Tests/MxCommandReplyExtensionsTests.cs b/clients/dotnet/MxGateway.Client.Tests/MxCommandReplyExtensionsTests.cs
index 686c1e4..d10761b 100644
--- a/clients/dotnet/MxGateway.Client.Tests/MxCommandReplyExtensionsTests.cs
+++ b/clients/dotnet/MxGateway.Client.Tests/MxCommandReplyExtensionsTests.cs
@@ -6,6 +6,7 @@ namespace MxGateway.Client.Tests;
public sealed class MxCommandReplyExtensionsTests
{
+ /// Verifies that successful replies pass both protocol and MxAccess success checks.
[Fact]
public void EnsureSuccess_WithRegisterFixture_ReturnsReply()
{
@@ -15,6 +16,7 @@ public sealed class MxCommandReplyExtensionsTests
Assert.Same(reply, reply.EnsureMxAccessSuccess());
}
+ /// Verifies that MxAccess failures throw with preserved HResult and status details.
[Fact]
public void EnsureMxAccessSuccess_WithFailureFixture_PreservesHResultAndStatuses()
{
@@ -30,6 +32,7 @@ public sealed class MxCommandReplyExtensionsTests
Assert.Contains("0x80040200", exception.Message);
}
+ /// Verifies that session-not-found protocol failures throw the correct gateway exception.
[Fact]
public void EnsureProtocolSuccess_WithSessionFailure_ThrowsSessionException()
{
diff --git a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs
index 5eda13d..e293ee4 100644
--- a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs
+++ b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs
@@ -5,8 +5,10 @@ using MxGateway.Contracts.Proto.Galaxy;
namespace MxGateway.Client.Tests;
+/// Tests for the CLI command interface.
public sealed class MxGatewayClientCliTests
{
+ /// Verifies that the version command prints compiled protocol versions.
[Fact]
public void Run_Version_PrintsCompiledProtocolVersions()
{
@@ -21,6 +23,7 @@ public sealed class MxGatewayClientCliTests
Assert.Equal(string.Empty, error.ToString());
}
+ /// Verifies that the version command with --json flag prints JSON protocol versions.
[Fact]
public async Task RunAsync_VersionJson_PrintsJsonProtocolVersions()
{
@@ -34,6 +37,7 @@ public sealed class MxGatewayClientCliTests
Assert.Equal(string.Empty, error.ToString());
}
+ /// Verifies that the write command builds a write request and prints JSON reply.
[Fact]
public async Task RunAsync_Write_BuildsWriteCommandAndPrintsJsonReply()
{
@@ -78,6 +82,7 @@ public sealed class MxGatewayClientCliTests
Assert.Equal(string.Empty, error.ToString());
}
+ /// Verifies that error output redacts sensitive API key values.
[Fact]
public async Task RunAsync_ErrorOutput_RedactsApiKey()
{
@@ -101,6 +106,7 @@ public sealed class MxGatewayClientCliTests
Assert.Contains("[redacted]", error.ToString());
}
+ /// Verifies that stream-events with max-events limit stops output in non-JSON format.
[Fact]
public async Task RunAsync_StreamEvents_WithMaxEventsStopsNonJsonOutput()
{
@@ -142,6 +148,7 @@ public sealed class MxGatewayClientCliTests
}
+ /// Verifies that smoke command closes opened session when a command fails.
[Fact]
public async Task RunAsync_Smoke_WhenCommandFails_ClosesOpenedSession()
{
@@ -172,6 +179,7 @@ public sealed class MxGatewayClientCliTests
Assert.Equal("session-fixture", closeRequest.SessionId);
}
+ /// Verifies that galaxy-test-connection command prints JSON reply.
[Fact]
public async Task RunAsync_GalaxyTestConnection_PrintsJsonReply()
{
@@ -201,6 +209,7 @@ public sealed class MxGatewayClientCliTests
Assert.Equal(string.Empty, error.ToString());
}
+ /// Verifies that galaxy-discover command prints hierarchy summary.
[Fact]
public async Task RunAsync_GalaxyDiscover_PrintsHierarchySummary()
{
@@ -250,6 +259,7 @@ public sealed class MxGatewayClientCliTests
Assert.Equal(string.Empty, error.ToString());
}
+ /// Verifies that galaxy-watch command prints text output for deploy events.
[Fact]
public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents()
{
@@ -303,6 +313,7 @@ public sealed class MxGatewayClientCliTests
Assert.Equal(string.Empty, error.ToString());
}
+ /// Verifies that galaxy-watch with --json emits one JSON object per event.
[Fact]
public async Task RunAsync_GalaxyWatch_JsonEmitsOneObjectPerEvent()
{
@@ -337,23 +348,31 @@ public sealed class MxGatewayClientCliTests
Assert.Contains("\"objectCount\": 99", text);
}
+ /// Fake CLI client for testing.
private sealed class FakeCliClient : IMxGatewayCliClient
{
+ /// Queue of invoke replies to return.
public Queue InvokeReplies { get; } = new();
+ /// List of received invoke requests.
public List InvokeRequests { get; } = [];
+ /// List of received close session requests.
public List CloseSessionRequests { get; } = [];
+ /// List of events to yield when streaming.
public List Events { get; } = [];
+ /// Exception to throw on invoke, if any.
public Exception? InvokeFailure { get; init; }
+ ///
public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
}
+ ///
public Task OpenSessionAsync(
OpenSessionRequest request,
CancellationToken cancellationToken)
@@ -367,6 +386,7 @@ public sealed class MxGatewayClientCliTests
});
}
+ ///
public Task CloseSessionAsync(
CloseSessionRequest request,
CancellationToken cancellationToken)
@@ -380,6 +400,7 @@ public sealed class MxGatewayClientCliTests
});
}
+ ///
public Task InvokeAsync(
MxCommandRequest request,
CancellationToken cancellationToken)
@@ -393,6 +414,7 @@ public sealed class MxGatewayClientCliTests
return Task.FromResult(InvokeReplies.Dequeue());
}
+ ///
public async IAsyncEnumerable StreamEventsAsync(
StreamEventsRequest request,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
@@ -405,18 +427,25 @@ public sealed class MxGatewayClientCliTests
}
}
+ /// Galaxy test connection reply to return.
public TestConnectionReply GalaxyTestConnectionReply { get; set; } = new() { Ok = true };
+ /// Galaxy get last deploy time reply to return.
public GetLastDeployTimeReply GalaxyGetLastDeployTimeReply { get; set; } = new() { Present = false };
+ /// Galaxy discover hierarchy reply to return.
public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new();
+ /// List of received galaxy test connection requests.
public List GalaxyTestConnectionRequests { get; } = [];
+ /// List of received galaxy get last deploy time requests.
public List GalaxyGetLastDeployTimeRequests { get; } = [];
+ /// List of received galaxy discover hierarchy requests.
public List GalaxyDiscoverHierarchyRequests { get; } = [];
+ ///
public Task GalaxyTestConnectionAsync(
TestConnectionRequest request,
CancellationToken cancellationToken)
@@ -425,6 +454,7 @@ public sealed class MxGatewayClientCliTests
return Task.FromResult(GalaxyTestConnectionReply);
}
+ ///
public Task GalaxyGetLastDeployTimeAsync(
GetLastDeployTimeRequest request,
CancellationToken cancellationToken)
@@ -433,6 +463,7 @@ public sealed class MxGatewayClientCliTests
return Task.FromResult(GalaxyGetLastDeployTimeReply);
}
+ ///
public Task GalaxyDiscoverHierarchyAsync(
DiscoverHierarchyRequest request,
CancellationToken cancellationToken)
@@ -441,10 +472,13 @@ public sealed class MxGatewayClientCliTests
return Task.FromResult(GalaxyDiscoverHierarchyReply);
}
+ /// List of received galaxy watch deploy events requests.
public List GalaxyWatchDeployEventsRequests { get; } = [];
+ /// List of deploy events to yield when watching.
public List GalaxyDeployEvents { get; } = [];
+ ///
public async IAsyncEnumerable GalaxyWatchDeployEventsAsync(
WatchDeployEventsRequest request,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
diff --git a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientContractInfoTests.cs b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientContractInfoTests.cs
index 1458347..ce4d4d2 100644
--- a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientContractInfoTests.cs
+++ b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientContractInfoTests.cs
@@ -4,6 +4,7 @@ namespace MxGateway.Client.Tests;
public sealed class MxGatewayClientContractInfoTests
{
+ /// Verifies that the client's gateway protocol version matches the shared contract definition.
[Fact]
public void GatewayProtocolVersion_MatchesSharedContract()
{
@@ -12,6 +13,7 @@ public sealed class MxGatewayClientContractInfoTests
MxGatewayClientContractInfo.GatewayProtocolVersion);
}
+ /// Verifies that the client's worker protocol version matches the shared contract definition.
[Fact]
public void WorkerProtocolVersion_MatchesSharedContract()
{
diff --git a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientOptionsTests.cs b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientOptionsTests.cs
index c019a52..cb9268c 100644
--- a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientOptionsTests.cs
+++ b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientOptionsTests.cs
@@ -2,6 +2,7 @@ namespace MxGateway.Client.Tests;
public sealed class MxGatewayClientOptionsTests
{
+ /// Verifies that options with valid endpoint and API key pass validation.
[Fact]
public void Validate_WithAbsoluteEndpointAndApiKey_Succeeds()
{
@@ -14,6 +15,7 @@ public sealed class MxGatewayClientOptionsTests
options.Validate();
}
+ /// Verifies that empty API key causes validation to fail.
[Fact]
public void Validate_WithEmptyApiKey_Throws()
{
@@ -26,6 +28,7 @@ public sealed class MxGatewayClientOptionsTests
Assert.Throws(options.Validate);
}
+ /// Verifies that invalid retry options cause validation to fail.
[Fact]
public void Validate_WithInvalidRetryOptions_Throws()
{
diff --git a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientSessionTests.cs b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientSessionTests.cs
index b8d0f58..7764bc6 100644
--- a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientSessionTests.cs
+++ b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientSessionTests.cs
@@ -3,8 +3,10 @@ using Grpc.Core;
namespace MxGateway.Client.Tests;
+/// Tests for MxGatewaySession and client command behavior.
public sealed class MxGatewayClientSessionTests
{
+ /// Verifies that open session attaches API key metadata and cancellation token.
[Fact]
public async Task OpenSessionRawAsync_AttachesApiKeyMetadataAndCancellation()
{
@@ -19,6 +21,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal(cancellation.Token, call.CallOptions.CancellationToken);
}
+ /// Verifies that open session returns a session with the raw open reply.
[Fact]
public async Task OpenSessionAsync_ReturnsSessionWithRawOpenReply()
{
@@ -33,6 +36,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal(1234, session.OpenSessionReply.WorkerProcessId);
}
+ /// Verifies that register builds a register command and returns server handle.
[Fact]
public async Task RegisterAsync_BuildsRegisterCommandAndReturnsServerHandle()
{
@@ -57,6 +61,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal("fixture-client", call.Request.Command.Register.ClientName);
}
+ /// Verifies that add item 2 builds a command with the specified context.
[Fact]
public async Task AddItem2Async_BuildsAddItem2CommandWithContext()
{
@@ -81,6 +86,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal("runtime", request.Command.AddItem2.ItemContext);
}
+ /// Verifies that write raw builds a write command with the raw value.
[Fact]
public async Task WriteRawAsync_BuildsWriteCommandWithRawValue()
{
@@ -111,6 +117,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal(56, request.Command.Write.UserId);
}
+ /// Verifies that write 2 raw builds a write 2 command with value and timestamp.
[Fact]
public async Task Write2RawAsync_BuildsWrite2CommandWithValueAndTimestamp()
{
@@ -138,6 +145,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal(56, request.Command.Write2.UserId);
}
+ /// Verifies that subscribe bulk builds one command and returns per-item results.
[Fact]
public async Task SubscribeBulkAsync_BuildsOneBulkCommandAndReturnsPerItemResults()
{
@@ -176,6 +184,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal(["Area001.Pump001.Speed"], request.Command.SubscribeBulk.TagAddresses);
}
+ /// Verifies that stream events yields events in the order received from the gateway.
[Fact]
public async Task StreamEventsAsync_YieldsEventsInGatewayOrder()
{
@@ -206,6 +215,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal("session-fixture", request.SessionId);
}
+ /// Verifies that close is explicit and idempotent.
[Fact]
public async Task CloseAsync_IsExplicitAndIdempotent()
{
@@ -221,6 +231,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal("session-fixture", call.Request.SessionId);
}
+ /// Verifies that invoke retries safe diagnostic commands on transient RPC failure.
[Fact]
public async Task InvokeAsync_RetriesSafeDiagnosticCommandOnTransientGrpcFailure()
{
@@ -244,6 +255,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Equal(2, transport.InvokeCalls.Count);
}
+ /// Verifies that open session does not retry on transient RPC failure.
[Fact]
public async Task OpenSessionAsync_DoesNotRetryTransientGrpcFailure()
{
@@ -256,6 +268,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Single(transport.OpenSessionCalls);
}
+ /// Verifies that invoke does not retry write commands on transient RPC failure.
[Fact]
public async Task InvokeAsync_DoesNotRetryWriteCommand()
{
@@ -270,6 +283,7 @@ public sealed class MxGatewayClientSessionTests
Assert.Single(transport.InvokeCalls);
}
+ /// Verifies that invoke helpers pass cancellation token to the transport.
[Fact]
public async Task InvokeHelpers_PassCancellationTokenToTransport()
{
diff --git a/clients/dotnet/MxGateway.Client.Tests/MxGatewayGeneratedContractTests.cs b/clients/dotnet/MxGateway.Client.Tests/MxGatewayGeneratedContractTests.cs
index 846d9e3..be0d0b0 100644
--- a/clients/dotnet/MxGateway.Client.Tests/MxGatewayGeneratedContractTests.cs
+++ b/clients/dotnet/MxGateway.Client.Tests/MxGatewayGeneratedContractTests.cs
@@ -2,6 +2,7 @@ namespace MxGateway.Client.Tests;
public sealed class MxGatewayGeneratedContractTests
{
+ /// Verifies that the generated gRPC client can be instantiated from the client factory.
[Fact]
public async Task GeneratedGrpcClient_CanBeConstructedFromClientFactory()
{
diff --git a/clients/dotnet/MxGateway.Client.Tests/MxStatusProxyExtensionsTests.cs b/clients/dotnet/MxGateway.Client.Tests/MxStatusProxyExtensionsTests.cs
index 76ecca8..61fd2fb 100644
--- a/clients/dotnet/MxGateway.Client.Tests/MxStatusProxyExtensionsTests.cs
+++ b/clients/dotnet/MxGateway.Client.Tests/MxStatusProxyExtensionsTests.cs
@@ -7,6 +7,7 @@ namespace MxGateway.Client.Tests;
public sealed class MxStatusProxyExtensionsTests
{
+ /// Verifies that fixture statuses correctly project success and preserve raw integer fields.
[Fact]
public void FixtureStatuses_ProjectSuccessAndPreserveRawFields()
{
diff --git a/clients/dotnet/MxGateway.Client.Tests/MxValueExtensionsTests.cs b/clients/dotnet/MxGateway.Client.Tests/MxValueExtensionsTests.cs
index 2fbde41..9b318e5 100644
--- a/clients/dotnet/MxGateway.Client.Tests/MxValueExtensionsTests.cs
+++ b/clients/dotnet/MxGateway.Client.Tests/MxValueExtensionsTests.cs
@@ -7,6 +7,7 @@ namespace MxGateway.Client.Tests;
public sealed class MxValueExtensionsTests
{
+ /// Verifies that scalar values are converted to correctly-typed MxValue protobuf messages.
[Fact]
public void ToMxValue_WithScalarValues_CreatesTypedProtobufValues()
{
@@ -18,6 +19,7 @@ public sealed class MxValueExtensionsTests
Assert.Equal(MxValue.KindOneofCase.StringValue, "alpha".ToMxValue().KindCase);
}
+ /// Verifies that array values are converted to array-kind MxValue messages with correct element types and dimensions.
[Fact]
public void ToMxValue_WithArrays_CreatesTypedArrayProtobufValues()
{
@@ -29,6 +31,7 @@ public sealed class MxValueExtensionsTests
Assert.Equal([2U], value.ArrayValue.Dimensions);
}
+ /// Verifies that fixture test cases project to expected MxValue kinds and preserve raw type metadata.
[Fact]
public void FixtureValues_ProjectExpectedKindsAndPreserveRawMetadata()
{
diff --git a/clients/dotnet/MxGateway.Client/GalaxyRepositoryClient.cs b/clients/dotnet/MxGateway.Client/GalaxyRepositoryClient.cs
index f9568cc..4894af8 100644
--- a/clients/dotnet/MxGateway.Client/GalaxyRepositoryClient.cs
+++ b/clients/dotnet/MxGateway.Client/GalaxyRepositoryClient.cs
@@ -23,6 +23,11 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
private bool _disposed;
+ ///
+ /// Initializes a Galaxy Repository client with custom transport and options.
+ ///
+ /// Client options.
+ /// The underlying gRPC transport.
internal GalaxyRepositoryClient(
MxGatewayClientOptions options,
IGalaxyRepositoryClientTransport transport)
@@ -50,12 +55,23 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
Options.LoggerFactory?.CreateLogger());
}
+ ///
+ /// Client options used to configure timeouts, authentication, and retry policy.
+ ///
public MxGatewayClientOptions Options { get; }
+ ///
+ /// The underlying generated gRPC client for advanced operations.
+ ///
public GalaxyRepository.GalaxyRepositoryClient RawClient =>
_transport.RawClient
?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");
+ ///
+ /// Creates a Galaxy Repository client with the given options, establishing a new gRPC channel.
+ ///
+ /// Client options.
+ /// A new client instance.
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.
///
+ /// Cancellation token.
+ /// True if connection is successful, false otherwise.
public async Task TestConnectionAsync(CancellationToken cancellationToken = default)
{
TestConnectionReply reply = await TestConnectionRawAsync(
@@ -91,6 +109,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
return reply.Ok;
}
+ ///
+ /// Probes the Galaxy Repository database connection without result wrapping.
+ ///
+ /// The test connection request.
+ /// Cancellation token.
+ /// The raw server reply.
public Task 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
/// when no deployment has been recorded.
///
+ /// Cancellation token.
+ /// The deployment timestamp, or null if not recorded.
public async Task GetLastDeployTimeAsync(CancellationToken cancellationToken = default)
{
GetLastDeployTimeReply reply = await GetLastDeployTimeRawAsync(
@@ -122,6 +148,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
return reply.TimeOfLastDeploy.ToDateTime();
}
+ ///
+ /// Returns the most recent Galaxy deployment timestamp without result wrapping.
+ ///
+ /// The last deploy-time request.
+ /// Cancellation token.
+ /// The raw server reply.
public Task 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.
///
+ /// Cancellation token.
+ /// The collection of Galaxy objects in the hierarchy.
public async Task> DiscoverHierarchyAsync(CancellationToken cancellationToken = default)
{
DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync(
@@ -149,6 +183,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
return reply.Objects;
}
+ ///
+ /// Enumerates the Galaxy object hierarchy without result wrapping.
+ ///
+ /// The discover-hierarchy request.
+ /// Cancellation token.
+ /// The raw server reply.
public Task 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
/// indicate dropped events).
///
+ /// Optional timestamp to suppress the bootstrap event.
+ /// Cancellation token.
+ /// An async enumerable of deploy events.
public IAsyncEnumerable WatchDeployEventsAsync(
DateTimeOffset? lastSeenDeployTime = null,
CancellationToken cancellationToken = default)
@@ -188,6 +231,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
return WatchDeployEventsRawAsync(request, cancellationToken);
}
+ ///
+ /// Subscribes to Galaxy deploy events without result wrapping.
+ ///
+ /// The watch-deploy-events request.
+ /// Cancellation token.
+ /// An async enumerable of raw deploy events.
public IAsyncEnumerable WatchDeployEventsRawAsync(
WatchDeployEventsRequest request,
CancellationToken cancellationToken = default)
@@ -211,6 +260,9 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
}
}
+ ///
+ /// Closes the gRPC channel and releases resources.
+ ///
public ValueTask DisposeAsync()
{
if (_disposed)
@@ -223,16 +275,32 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
return ValueTask.CompletedTask;
}
+ ///
+ /// Creates gRPC call options with the client's default timeout and API-key authorization.
+ ///
+ /// Cancellation token.
+ /// The call options.
internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
{
return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout);
}
+ ///
+ /// Creates gRPC call options for streaming RPCs with the stream timeout and API-key authorization.
+ ///
+ /// Cancellation token.
+ /// The stream call options.
internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken)
{
return CreateCallOptions(cancellationToken, Options.StreamTimeout);
}
+ ///
+ /// Creates gRPC call options with the specified timeout and API-key authorization.
+ ///
+ /// Cancellation token.
+ /// Optional timeout duration.
+ /// The call options.
internal CallOptions CreateCallOptions(
CancellationToken cancellationToken,
TimeSpan? timeout)
diff --git a/clients/dotnet/MxGateway.Client/GrpcGalaxyRepositoryClientTransport.cs b/clients/dotnet/MxGateway.Client/GrpcGalaxyRepositoryClientTransport.cs
index 67931b5..e6fa05d 100644
--- a/clients/dotnet/MxGateway.Client/GrpcGalaxyRepositoryClientTransport.cs
+++ b/clients/dotnet/MxGateway.Client/GrpcGalaxyRepositoryClientTransport.cs
@@ -3,16 +3,27 @@ using MxGateway.Contracts.Proto.Galaxy;
namespace MxGateway.Client;
+///
+/// gRPC implementation of IGalaxyRepositoryClientTransport.
+///
internal sealed class GrpcGalaxyRepositoryClientTransport(
MxGatewayClientOptions options,
GalaxyRepository.GalaxyRepositoryClient rawClient) : IGalaxyRepositoryClientTransport
{
+ ///
+ /// Gets the gateway client options.
+ ///
public MxGatewayClientOptions Options { get; } = options;
+ ///
+ /// Gets the underlying gRPC client.
+ ///
public GalaxyRepository.GalaxyRepositoryClient RawClient { get; } = rawClient;
+ ///
GalaxyRepository.GalaxyRepositoryClient? IGalaxyRepositoryClientTransport.RawClient => RawClient;
+ ///
public async Task TestConnectionAsync(
TestConnectionRequest request,
CallOptions callOptions)
@@ -29,6 +40,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
}
}
+ ///
public async Task GetLastDeployTimeAsync(
GetLastDeployTimeRequest request,
CallOptions callOptions)
@@ -45,6 +57,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
}
}
+ ///
public async Task DiscoverHierarchyAsync(
DiscoverHierarchyRequest request,
CallOptions callOptions)
@@ -61,6 +74,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
}
}
+ ///
public async IAsyncEnumerable WatchDeployEventsAsync(
WatchDeployEventsRequest request,
CallOptions callOptions,
@@ -94,6 +108,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
}
}
+ ///
IAsyncEnumerable IGalaxyRepositoryClientTransport.WatchDeployEventsAsync(
WatchDeployEventsRequest request,
CallOptions callOptions)
diff --git a/clients/dotnet/MxGateway.Client/GrpcMxGatewayClientTransport.cs b/clients/dotnet/MxGateway.Client/GrpcMxGatewayClientTransport.cs
index c196a6f..f2ea77c 100644
--- a/clients/dotnet/MxGateway.Client/GrpcMxGatewayClientTransport.cs
+++ b/clients/dotnet/MxGateway.Client/GrpcMxGatewayClientTransport.cs
@@ -3,16 +3,27 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
+///
+/// gRPC implementation of IMxGatewayClientTransport.
+///
internal sealed class GrpcMxGatewayClientTransport(
MxGatewayClientOptions options,
MxAccessGateway.MxAccessGatewayClient rawClient) : IMxGatewayClientTransport
{
+ ///
+ /// Gets the gateway client options.
+ ///
public MxGatewayClientOptions Options { get; } = options;
+ ///
+ /// Gets the underlying gRPC client.
+ ///
public MxAccessGateway.MxAccessGatewayClient RawClient { get; } = rawClient;
+ ///
MxAccessGateway.MxAccessGatewayClient? IMxGatewayClientTransport.RawClient => RawClient;
+ ///
public async Task OpenSessionAsync(
OpenSessionRequest request,
CallOptions callOptions)
@@ -29,6 +40,7 @@ internal sealed class GrpcMxGatewayClientTransport(
}
}
+ ///
public async Task CloseSessionAsync(
CloseSessionRequest request,
CallOptions callOptions)
@@ -45,6 +57,7 @@ internal sealed class GrpcMxGatewayClientTransport(
}
}
+ ///
public async Task InvokeAsync(
MxCommandRequest request,
CallOptions callOptions)
@@ -61,6 +74,7 @@ internal sealed class GrpcMxGatewayClientTransport(
}
}
+ ///
public async IAsyncEnumerable StreamEventsAsync(
StreamEventsRequest request,
CallOptions callOptions,
@@ -94,6 +108,7 @@ internal sealed class GrpcMxGatewayClientTransport(
}
}
+ ///
IAsyncEnumerable IMxGatewayClientTransport.StreamEventsAsync(
StreamEventsRequest request,
CallOptions callOptions)
diff --git a/clients/dotnet/MxGateway.Client/IGalaxyRepositoryClientTransport.cs b/clients/dotnet/MxGateway.Client/IGalaxyRepositoryClientTransport.cs
index de55692..c91ccb5 100644
--- a/clients/dotnet/MxGateway.Client/IGalaxyRepositoryClientTransport.cs
+++ b/clients/dotnet/MxGateway.Client/IGalaxyRepositoryClientTransport.cs
@@ -3,24 +3,39 @@ using MxGateway.Contracts.Proto.Galaxy;
namespace MxGateway.Client;
+/// Transport layer for Galaxy Repository gRPC operations.
internal interface IGalaxyRepositoryClientTransport
{
+ /// Gets the client options used to configure this transport.
MxGatewayClientOptions Options { get; }
+ /// Gets the underlying gRPC client, or null if not yet initialized.
GalaxyRepository.GalaxyRepositoryClient? RawClient { get; }
+ /// Tests the connection to the Galaxy Repository server.
+ /// The test connection request.
+ /// gRPC call options (timeout, cancellation, etc.).
Task TestConnectionAsync(
TestConnectionRequest request,
CallOptions callOptions);
+ /// Gets the last deploy time from the Galaxy Repository server.
+ /// The get last deploy time request.
+ /// gRPC call options (timeout, cancellation, etc.).
Task GetLastDeployTimeAsync(
GetLastDeployTimeRequest request,
CallOptions callOptions);
+ /// Discovers the object hierarchy in the Galaxy Repository.
+ /// The discover hierarchy request.
+ /// gRPC call options (timeout, cancellation, etc.).
Task DiscoverHierarchyAsync(
DiscoverHierarchyRequest request,
CallOptions callOptions);
+ /// Watches for deployment events from the Galaxy Repository server.
+ /// The watch deploy events request.
+ /// gRPC call options (timeout, cancellation, etc.).
IAsyncEnumerable WatchDeployEventsAsync(
WatchDeployEventsRequest request,
CallOptions callOptions);
diff --git a/clients/dotnet/MxGateway.Client/IMxGatewayClientTransport.cs b/clients/dotnet/MxGateway.Client/IMxGatewayClientTransport.cs
index 77586c6..53a6951 100644
--- a/clients/dotnet/MxGateway.Client/IMxGatewayClientTransport.cs
+++ b/clients/dotnet/MxGateway.Client/IMxGatewayClientTransport.cs
@@ -5,22 +5,52 @@ namespace MxGateway.Client;
internal interface IMxGatewayClientTransport
{
+ ///
+ /// Gets the client configuration options.
+ ///
MxGatewayClientOptions Options { get; }
+ ///
+ /// Gets the underlying gRPC client, if available.
+ ///
MxAccessGateway.MxAccessGatewayClient? RawClient { get; }
+ ///
+ /// Opens a new gateway session.
+ ///
+ /// Session open request.
+ /// gRPC call options.
+ /// The session open reply.
Task OpenSessionAsync(
OpenSessionRequest request,
CallOptions callOptions);
+ ///
+ /// Closes an open gateway session.
+ ///
+ /// Session close request.
+ /// gRPC call options.
+ /// The session close reply.
Task CloseSessionAsync(
CloseSessionRequest request,
CallOptions callOptions);
+ ///
+ /// Invokes an MXAccess command on the session.
+ ///
+ /// The command request.
+ /// gRPC call options.
+ /// The command reply.
Task InvokeAsync(
MxCommandRequest request,
CallOptions callOptions);
+ ///
+ /// Streams events from the session.
+ ///
+ /// The stream events request.
+ /// gRPC call options.
+ /// An async enumerable of events.
IAsyncEnumerable StreamEventsAsync(
StreamEventsRequest request,
CallOptions callOptions);
diff --git a/clients/dotnet/MxGateway.Client/MxAccessException.cs b/clients/dotnet/MxGateway.Client/MxAccessException.cs
index a2a14ca..38ccc7e 100644
--- a/clients/dotnet/MxGateway.Client/MxAccessException.cs
+++ b/clients/dotnet/MxGateway.Client/MxAccessException.cs
@@ -2,8 +2,13 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
+/// Exception thrown when an MXAccess command fails with a non-zero HResult or failing status.
public sealed class MxAccessException : MxGatewayCommandException
{
+ /// Initializes a new instance with the given message, reply, and optional inner exception.
+ /// The error message describing the MXAccess failure.
+ /// The MxCommandReply containing the failure details (statuses, HResult, etc.).
+ /// The underlying exception, if any.
public MxAccessException(
string message,
MxCommandReply reply,
@@ -20,5 +25,6 @@ public sealed class MxAccessException : MxGatewayCommandException
Reply = reply;
}
+ /// Gets the underlying MxCommandReply containing full failure details.
public MxCommandReply Reply { get; }
}
diff --git a/clients/dotnet/MxGateway.Client/MxCommandReplyExtensions.cs b/clients/dotnet/MxGateway.Client/MxCommandReplyExtensions.cs
index ce9f2b8..cd7b37a 100644
--- a/clients/dotnet/MxGateway.Client/MxCommandReplyExtensions.cs
+++ b/clients/dotnet/MxGateway.Client/MxCommandReplyExtensions.cs
@@ -2,8 +2,11 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
+/// Extension methods for checking MxCommandReply success conditions.
public static class MxCommandReplyExtensions
{
+ /// Validates that the reply has a successful protocol status (Ok or MxAccessFailure), throwing a gateway exception if not.
+ /// The command reply to check.
public static MxCommandReply EnsureProtocolSuccess(this MxCommandReply reply)
{
ArgumentNullException.ThrowIfNull(reply);
@@ -19,6 +22,8 @@ public static class MxCommandReplyExtensions
throw CreateProtocolException(reply, code);
}
+ /// Validates that the reply indicates MXAccess success (no HResult or status failures), throwing MxAccessException if not.
+ /// The command reply to check.
public static MxCommandReply EnsureMxAccessSuccess(this MxCommandReply reply)
{
ArgumentNullException.ThrowIfNull(reply);
diff --git a/clients/dotnet/MxGateway.Client/MxGatewayAuthenticationException.cs b/clients/dotnet/MxGateway.Client/MxGatewayAuthenticationException.cs
index e1164fb..e70b8df 100644
--- a/clients/dotnet/MxGateway.Client/MxGatewayAuthenticationException.cs
+++ b/clients/dotnet/MxGateway.Client/MxGatewayAuthenticationException.cs
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
+/// Exception thrown when an API key is invalid, expired, or malformed.
public sealed class MxGatewayAuthenticationException : MxGatewayException
{
+ /// Initializes a new instance with the given details.
+ /// The error message describing the authentication failure.
+ /// The session ID, if available.
+ /// The correlation ID for tracing, if available.
+ /// The protocol status details, if available.
+ /// The HResult code, if available.
+ /// The MXAccess statuses, if available.
+ /// The underlying exception, if any.
public MxGatewayAuthenticationException(
string message,
string? sessionId = null,
diff --git a/clients/dotnet/MxGateway.Client/MxGatewayAuthorizationException.cs b/clients/dotnet/MxGateway.Client/MxGatewayAuthorizationException.cs
index 1c1c67c..2df383b 100644
--- a/clients/dotnet/MxGateway.Client/MxGatewayAuthorizationException.cs
+++ b/clients/dotnet/MxGateway.Client/MxGatewayAuthorizationException.cs
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
+/// Exception thrown when the API key lacks required scopes for an operation.
public sealed class MxGatewayAuthorizationException : MxGatewayException
{
+ /// Initializes a new instance with the given details.
+ /// The error message describing the authorization failure.
+ /// The session ID, if available.
+ /// The correlation ID for tracing, if available.
+ /// The protocol status details, if available.
+ /// The HResult code, if available.
+ /// The MXAccess statuses, if available.
+ /// The underlying exception, if any.
public MxGatewayAuthorizationException(
string message,
string? sessionId = null,
diff --git a/clients/dotnet/MxGateway.Client/MxGatewayClient.cs b/clients/dotnet/MxGateway.Client/MxGatewayClient.cs
index 1ca109b..4d29cf4 100644
--- a/clients/dotnet/MxGateway.Client/MxGatewayClient.cs
+++ b/clients/dotnet/MxGateway.Client/MxGatewayClient.cs
@@ -19,6 +19,11 @@ public sealed class MxGatewayClient : IAsyncDisposable
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
private bool _disposed;
+ ///
+ /// Initializes a new instance of the with given options and transport.
+ ///
+ /// Client configuration options.
+ /// Transport implementation for gateway communication.
internal MxGatewayClient(
MxGatewayClientOptions options,
IMxGatewayClientTransport transport)
@@ -46,12 +51,23 @@ public sealed class MxGatewayClient : IAsyncDisposable
Options.LoggerFactory?.CreateLogger());
}
+ ///
+ /// Gets the client configuration options.
+ ///
public MxGatewayClientOptions Options { get; }
+ ///
+ /// Gets the underlying generated gRPC client.
+ ///
public MxAccessGateway.MxAccessGatewayClient RawClient =>
_transport.RawClient
?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");
+ ///
+ /// Creates a new gateway client with the given options.
+ ///
+ /// Client configuration options.
+ /// A new gateway client instance.
public static MxGatewayClient Create(MxGatewayClientOptions options)
{
ArgumentNullException.ThrowIfNull(options);
@@ -73,6 +89,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
new MxAccessGateway.MxAccessGatewayClient(channel)));
}
+ ///
+ /// Opens a new gateway session.
+ ///
+ /// Session open request; defaults to empty request if null.
+ /// Cancellation token for the operation.
+ /// A wrapped gateway session.
public async Task OpenSessionAsync(
OpenSessionRequest? request = null,
CancellationToken cancellationToken = default)
@@ -85,6 +107,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
return new MxGatewaySession(this, reply);
}
+ ///
+ /// Opens a new gateway session and returns the raw protobuf reply.
+ ///
+ /// Session open request.
+ /// Cancellation token for the operation.
+ /// The raw gateway session open reply.
public Task OpenSessionRawAsync(
OpenSessionRequest request,
CancellationToken cancellationToken = default)
@@ -95,6 +123,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
return _transport.OpenSessionAsync(request, CreateCallOptions(cancellationToken));
}
+ ///
+ /// Closes an open gateway session.
+ ///
+ /// Session close request.
+ /// Cancellation token for the operation.
+ /// The session close reply.
public Task CloseSessionRawAsync(
CloseSessionRequest request,
CancellationToken cancellationToken = default)
@@ -107,6 +141,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
cancellationToken);
}
+ ///
+ /// Invokes an MXAccess command on the open session.
+ ///
+ /// The command request.
+ /// Cancellation token for the operation.
+ /// The command reply.
public Task InvokeAsync(
MxCommandRequest request,
CancellationToken cancellationToken = default)
@@ -124,6 +164,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
return _transport.InvokeAsync(request, CreateCallOptions(cancellationToken));
}
+ ///
+ /// Streams events from the gateway session.
+ ///
+ /// The stream events request.
+ /// Cancellation token for the operation.
+ /// An async enumerable of events.
public IAsyncEnumerable StreamEventsAsync(
StreamEventsRequest request,
CancellationToken cancellationToken = default)
@@ -134,6 +180,9 @@ public sealed class MxGatewayClient : IAsyncDisposable
return _transport.StreamEventsAsync(request, CreateStreamCallOptions(cancellationToken));
}
+ ///
+ /// Disposes the client and releases all resources.
+ ///
public ValueTask DisposeAsync()
{
if (_disposed)
@@ -146,16 +195,32 @@ public sealed class MxGatewayClient : IAsyncDisposable
return ValueTask.CompletedTask;
}
+ ///
+ /// Creates gRPC call options with default timeout and authorization.
+ ///
+ /// Cancellation token for the call.
+ /// Configured call options.
internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
{
return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout);
}
+ ///
+ /// Creates gRPC call options for streaming with stream timeout and authorization.
+ ///
+ /// Cancellation token for the call.
+ /// Configured call options.
internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken)
{
return CreateCallOptions(cancellationToken, Options.StreamTimeout);
}
+ ///
+ /// Creates gRPC call options with specified timeout and authorization.
+ ///
+ /// Cancellation token for the call.
+ /// Optional timeout duration; null means no timeout.
+ /// Configured call options.
internal CallOptions CreateCallOptions(
CancellationToken cancellationToken,
TimeSpan? timeout)
diff --git a/clients/dotnet/MxGateway.Client/MxGatewayClientOptions.cs b/clients/dotnet/MxGateway.Client/MxGatewayClientOptions.cs
index 760fa8d..5cd4551 100644
--- a/clients/dotnet/MxGateway.Client/MxGatewayClientOptions.cs
+++ b/clients/dotnet/MxGateway.Client/MxGatewayClientOptions.cs
@@ -7,26 +7,62 @@ namespace MxGateway.Client;
///
public sealed class MxGatewayClientOptions
{
+ ///
+ /// Gets the gateway endpoint URI (required).
+ ///
public required Uri Endpoint { get; init; }
+ ///
+ /// Gets the API key for gateway authentication (required).
+ ///
public required string ApiKey { get; init; }
+ ///
+ /// Gets a value indicating whether to use TLS for the gateway connection.
+ ///
public bool UseTls { get; init; }
+ ///
+ /// Gets the path to a CA certificate file for custom certificate validation.
+ ///
public string? CaCertificatePath { get; init; }
+ ///
+ /// Gets the server name override for SNI during TLS handshake.
+ ///
public string? ServerNameOverride { get; init; }
+ ///
+ /// Gets the timeout for establishing connection to the gateway.
+ ///
public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(10);
+ ///
+ /// Gets the default timeout for unary gRPC calls.
+ ///
public TimeSpan DefaultCallTimeout { get; init; } = TimeSpan.FromSeconds(30);
+ ///
+ /// Gets the optional timeout for streaming gRPC calls.
+ ///
public TimeSpan? StreamTimeout { get; init; }
+ ///
+ /// Gets the retry configuration for safe unary calls.
+ ///
public MxGatewayClientRetryOptions Retry { get; init; } = new();
+ ///
+ /// Gets the logger factory for diagnostic logging.
+ ///
public ILoggerFactory? LoggerFactory { get; init; }
+ ///
+ /// Validates the client options for consistency and correctness.
+ ///
+ /// Endpoint is null.
+ /// Options are invalid or inconsistent.
+ /// Timeout values are not greater than zero.
public void Validate()
{
ArgumentNullException.ThrowIfNull(Endpoint);
diff --git a/clients/dotnet/MxGateway.Client/MxGatewayClientRetryOptions.cs b/clients/dotnet/MxGateway.Client/MxGatewayClientRetryOptions.cs
index 9de5943..6ad212a 100644
--- a/clients/dotnet/MxGateway.Client/MxGatewayClientRetryOptions.cs
+++ b/clients/dotnet/MxGateway.Client/MxGatewayClientRetryOptions.cs
@@ -1,15 +1,21 @@
namespace MxGateway.Client;
+/// Configuration for automatic retry behavior on transient gRPC call failures.
public sealed class MxGatewayClientRetryOptions
{
+ /// Gets the maximum number of attempts (initial + retries); default is 2.
public int MaxAttempts { get; init; } = 2;
+ /// Gets the initial delay between retry attempts; default is 200 milliseconds.
public TimeSpan Delay { get; init; } = TimeSpan.FromMilliseconds(200);
+ /// Gets the maximum delay between retry attempts; default is 2 seconds.
public TimeSpan MaxDelay { get; init; } = TimeSpan.FromSeconds(2);
+ /// Gets a value indicating whether to add randomness to retry delays; default is true.
public bool UseJitter { get; init; } = true;
+ /// Validates the retry options and throws if any constraint is violated.
public void Validate()
{
if (MaxAttempts <= 0)
diff --git a/clients/dotnet/MxGateway.Client/MxGatewayClientRetryPolicy.cs b/clients/dotnet/MxGateway.Client/MxGatewayClientRetryPolicy.cs
index c4cd072..af6e63d 100644
--- a/clients/dotnet/MxGateway.Client/MxGatewayClientRetryPolicy.cs
+++ b/clients/dotnet/MxGateway.Client/MxGatewayClientRetryPolicy.cs
@@ -6,8 +6,12 @@ using Polly.Retry;
namespace MxGateway.Client;
+/// Factory and helpers for exponential-backoff retry policies on transient gRPC failures.
internal static class MxGatewayClientRetryPolicy
{
+ /// Creates a Polly ResiliencePipeline that retries transient gRPC failures with exponential backoff.
+ /// Retry configuration (max attempts, delay bounds, jitter).
+ /// Optional logger for retry diagnostics.
public static ResiliencePipeline Create(
MxGatewayClientRetryOptions options,
ILogger? logger)
@@ -36,6 +40,8 @@ internal static class MxGatewayClientRetryPolicy
.Build();
}
+ /// Returns whether a command kind is eligible for automatic retry on transient failures.
+ /// The command kind to check.
public static bool IsRetryableCommand(MxCommandKind kind)
{
return kind is MxCommandKind.Ping
diff --git a/clients/dotnet/MxGateway.Client/MxGatewayCommandException.cs b/clients/dotnet/MxGateway.Client/MxGatewayCommandException.cs
index 83baf90..334a619 100644
--- a/clients/dotnet/MxGateway.Client/MxGatewayCommandException.cs
+++ b/clients/dotnet/MxGateway.Client/MxGatewayCommandException.cs
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
+/// Exception thrown when a gateway command fails due to an unclassified protocol error.
public class MxGatewayCommandException : MxGatewayException
{
+ /// Initializes a new instance with the given details.
+ /// The error message describing the command failure.
+ /// The session ID, if available.
+ /// The correlation ID for tracing, if available.
+ /// The protocol status details, if available.
+ /// The HResult code, if available.
+ /// The MXAccess statuses, if available.
+ /// The underlying exception, if any.
public MxGatewayCommandException(
string message,
string? sessionId = null,
diff --git a/clients/dotnet/MxGateway.Client/MxGatewayException.cs b/clients/dotnet/MxGateway.Client/MxGatewayException.cs
index eb5b59e..7607317 100644
--- a/clients/dotnet/MxGateway.Client/MxGatewayException.cs
+++ b/clients/dotnet/MxGateway.Client/MxGatewayException.cs
@@ -2,20 +2,42 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
+///
+/// Exception thrown when a gateway RPC call fails or returns an error status.
+///
public class MxGatewayException : Exception
{
+ ///
+ /// Initializes a new instance of the MxGatewayException class with the specified message.
+ ///
+ /// Diagnostic message describing the failure.
public MxGatewayException(string message)
: base(message)
{
Statuses = [];
}
+ ///
+ /// Initializes a new instance of the MxGatewayException class with the specified message and inner exception.
+ ///
+ /// Diagnostic message describing the failure.
+ /// Underlying exception that caused this failure.
public MxGatewayException(string message, Exception? innerException)
: base(message, innerException)
{
Statuses = [];
}
+ ///
+ /// Initializes a new instance of the MxGatewayException class with full diagnostic information.
+ ///
+ /// Diagnostic message describing the failure.
+ /// Session ID associated with the exception, if available.
+ /// Correlation ID associated with the exception, if available.
+ /// Protocol-level status returned by the gateway, if available.
+ /// HRESULT code returned by the worker or MXAccess, if available.
+ /// List of MXAccess status codes returned by the operation.
+ /// Underlying exception that caused this failure.
public MxGatewayException(
string message,
string? sessionId,
@@ -33,13 +55,28 @@ public class MxGatewayException : Exception
Statuses = statuses;
}
+ ///
+ /// Gets the session ID associated with the exception, if available.
+ ///
public string? SessionId { get; }
+ ///
+ /// Gets the correlation ID associated with the exception, if available.
+ ///
public string? CorrelationId { get; }
+ ///
+ /// Gets the protocol-level status returned by the gateway, if available.
+ ///
public ProtocolStatus? ProtocolStatus { get; }
+ ///
+ /// Gets the HRESULT code returned by the worker or MXAccess, if available.
+ ///
public int? HResultCode { get; }
+ ///
+ /// Gets the list of MXAccess status codes returned by the operation.
+ ///
public IReadOnlyList Statuses { get; }
}
diff --git a/clients/dotnet/MxGateway.Client/MxGatewaySession.cs b/clients/dotnet/MxGateway.Client/MxGatewaySession.cs
index 4730c50..62a9a1a 100644
--- a/clients/dotnet/MxGateway.Client/MxGatewaySession.cs
+++ b/clients/dotnet/MxGateway.Client/MxGatewaySession.cs
@@ -11,6 +11,11 @@ public sealed class MxGatewaySession : IAsyncDisposable
private readonly SemaphoreSlim _closeLock = new(1, 1);
private CloseSessionReply? _closeReply;
+ ///
+ /// Initializes a new session backed by the given MXAccess gateway client.
+ ///
+ /// The gateway client used for commands and events.
+ /// The server's session creation response.
internal MxGatewaySession(
MxGatewayClient client,
OpenSessionReply openSessionReply)
@@ -19,10 +24,21 @@ public sealed class MxGatewaySession : IAsyncDisposable
OpenSessionReply = openSessionReply ?? throw new ArgumentNullException(nameof(openSessionReply));
}
+ ///
+ /// The session ID assigned by the gateway.
+ ///
public string SessionId => OpenSessionReply.SessionId;
+ ///
+ /// The server's session creation response containing metadata.
+ ///
public OpenSessionReply OpenSessionReply { get; }
+ ///
+ /// Closes the session on the gateway. Idempotent.
+ ///
+ /// Cancellation token.
+ /// The server's close-session reply.
public async Task CloseAsync(CancellationToken cancellationToken = default)
{
if (_closeReply is not null)
@@ -50,6 +66,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
}
}
+ ///
+ /// Registers a client with the MXAccess session, returning a ServerHandle.
+ ///
+ /// Name to register.
+ /// Cancellation token.
+ /// The server handle assigned to the registered client.
public async Task RegisterAsync(
string clientName,
CancellationToken cancellationToken = default)
@@ -60,6 +82,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.Register?.ServerHandle ?? reply.ReturnValue.Int32Value;
}
+ ///
+ /// Registers a client with the MXAccess session without error checking.
+ ///
+ /// Name to register.
+ /// Cancellation token.
+ /// The raw server reply.
public Task RegisterRawAsync(
string clientName,
CancellationToken cancellationToken = default)
@@ -75,6 +103,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
+ ///
+ /// Adds an item to the MXAccess session, returning an ItemHandle.
+ ///
+ /// The ServerHandle from register.
+ /// The item tag address.
+ /// Cancellation token.
+ /// The item handle assigned to the new item.
public async Task AddItemAsync(
int serverHandle,
string itemDefinition,
@@ -89,6 +124,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.AddItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
}
+ ///
+ /// Adds an item to the MXAccess session without error checking.
+ ///
+ /// The ServerHandle from register.
+ /// The item tag address.
+ /// Cancellation token.
+ /// The raw server reply.
public Task AddItemRawAsync(
int serverHandle,
string itemDefinition,
@@ -109,6 +151,14 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
+ ///
+ /// Adds an item with context to the MXAccess session, returning an ItemHandle.
+ ///
+ /// The ServerHandle from register.
+ /// The item tag address.
+ /// Additional context for the item.
+ /// Cancellation token.
+ /// The item handle assigned to the new item.
public async Task AddItem2Async(
int serverHandle,
string itemDefinition,
@@ -125,6 +175,14 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.AddItem2?.ItemHandle ?? reply.ReturnValue.Int32Value;
}
+ ///
+ /// Adds an item with context to the MXAccess session without error checking.
+ ///
+ /// The ServerHandle from register.
+ /// The item tag address.
+ /// Additional context for the item.
+ /// Cancellation token.
+ /// The raw server reply.
public Task AddItem2RawAsync(
int serverHandle,
string itemDefinition,
@@ -147,6 +205,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
+ ///
+ /// Subscribes to events for an item (advises in MXAccess terminology).
+ ///
+ /// The ServerHandle from register.
+ /// The ItemHandle from add-item.
+ /// Cancellation token.
public async Task AdviseAsync(
int serverHandle,
int itemHandle,
@@ -157,6 +221,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
+ ///
+ /// Subscribes to events for an item without error checking.
+ ///
+ /// The ServerHandle from register.
+ /// The ItemHandle from add-item.
+ /// Cancellation token.
+ /// The raw server reply.
public Task AdviseRawAsync(
int serverHandle,
int itemHandle,
@@ -175,6 +246,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
+ ///
+ /// Unsubscribes from events for an item (unadvises in MXAccess terminology).
+ ///
+ /// The ServerHandle from register.
+ /// The ItemHandle from add-item.
+ /// Cancellation token.
public async Task UnAdviseAsync(
int serverHandle,
int itemHandle,
@@ -185,6 +262,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
+ ///
+ /// Unsubscribes from events for an item without error checking.
+ ///
+ /// The ServerHandle from register.
+ /// The ItemHandle from add-item.
+ /// Cancellation token.
+ /// The raw server reply.
public Task UnAdviseRawAsync(
int serverHandle,
int itemHandle,
@@ -203,6 +287,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
+ ///
+ /// Removes an item from the MXAccess session.
+ ///
+ /// The ServerHandle from register.
+ /// The ItemHandle from add-item.
+ /// Cancellation token.
public async Task RemoveItemAsync(
int serverHandle,
int itemHandle,
@@ -213,6 +303,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
+ ///
+ /// Removes an item from the MXAccess session without error checking.
+ ///
+ /// The ServerHandle from register.
+ /// The ItemHandle from add-item.
+ /// Cancellation token.
+ /// The raw server reply.
public Task RemoveItemRawAsync(
int serverHandle,
int itemHandle,
@@ -231,6 +328,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
+ ///
+ /// Adds multiple items to the MXAccess session in a single command.
+ ///
+ /// The ServerHandle from register.
+ /// The item tag addresses to add.
+ /// Cancellation token.
+ /// Per-item subscription results.
public async Task> AddItemBulkAsync(
int serverHandle,
IReadOnlyList tagAddresses,
@@ -253,6 +357,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.AddItemBulk?.Results.ToArray() ?? [];
}
+ ///
+ /// Advises multiple items in a single command.
+ ///
+ /// The ServerHandle from register.
+ /// The ItemHandles to advise.
+ /// Cancellation token.
+ /// Per-item subscription results.
public async Task> AdviseItemBulkAsync(
int serverHandle,
IReadOnlyList itemHandles,
@@ -275,6 +386,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.AdviseItemBulk?.Results.ToArray() ?? [];
}
+ ///
+ /// Removes multiple items in a single command.
+ ///
+ /// The ServerHandle from register.
+ /// The ItemHandles to remove.
+ /// Cancellation token.
+ /// Per-item subscription results.
public async Task> RemoveItemBulkAsync(
int serverHandle,
IReadOnlyList itemHandles,
@@ -297,6 +415,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.RemoveItemBulk?.Results.ToArray() ?? [];
}
+ ///
+ /// Unadvises multiple items in a single command.
+ ///
+ /// The ServerHandle from register.
+ /// The ItemHandles to unadvise.
+ /// Cancellation token.
+ /// Per-item subscription results.
public async Task> UnAdviseItemBulkAsync(
int serverHandle,
IReadOnlyList itemHandles,
@@ -319,6 +444,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.UnAdviseItemBulk?.Results.ToArray() ?? [];
}
+ ///
+ /// Adds and advises multiple items in a single command.
+ ///
+ /// The ServerHandle from register.
+ /// The item tag addresses to add and advise.
+ /// Cancellation token.
+ /// Per-item subscription results.
public async Task> SubscribeBulkAsync(
int serverHandle,
IReadOnlyList tagAddresses,
@@ -341,6 +473,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.SubscribeBulk?.Results.ToArray() ?? [];
}
+ ///
+ /// Unadvises and removes multiple items in a single command.
+ ///
+ /// The ServerHandle from register.
+ /// The ItemHandles to unsubscribe.
+ /// Cancellation token.
+ /// Per-item subscription results.
public async Task> UnsubscribeBulkAsync(
int serverHandle,
IReadOnlyList itemHandles,
@@ -363,6 +502,14 @@ public sealed class MxGatewaySession : IAsyncDisposable
return reply.UnsubscribeBulk?.Results.ToArray() ?? [];
}
+ ///
+ /// Writes a value to an item on the MXAccess server.
+ ///
+ /// The ServerHandle from register.
+ /// The ItemHandle from add-item.
+ /// The value to write.
+ /// User ID context for the write.
+ /// Cancellation token.
public async Task WriteAsync(
int serverHandle,
int itemHandle,
@@ -375,6 +522,15 @@ public sealed class MxGatewaySession : IAsyncDisposable
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
+ ///
+ /// Writes a value to an item on the MXAccess server without error checking.
+ ///
+ /// The ServerHandle from register.
+ /// The ItemHandle from add-item.
+ /// The value to write.
+ /// User ID context for the write.
+ /// Cancellation token.
+ /// The raw server reply.
public Task WriteRawAsync(
int serverHandle,
int itemHandle,
@@ -399,6 +555,15 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
+ ///
+ /// Writes a value and timestamp to an item on the MXAccess server.
+ ///
+ /// The ServerHandle from register.
+ /// The ItemHandle from add-item.
+ /// The value to write.
+ /// The timestamp to write with the value.
+ /// User ID context for the write.
+ /// Cancellation token.
public async Task Write2Async(
int serverHandle,
int itemHandle,
@@ -418,6 +583,16 @@ public sealed class MxGatewaySession : IAsyncDisposable
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
+ ///
+ /// Writes a value and timestamp to an item on the MXAccess server without error checking.
+ ///
+ /// The ServerHandle from register.
+ /// The ItemHandle from add-item.
+ /// The value to write.
+ /// The timestamp to write with the value.
+ /// User ID context for the write.
+ /// Cancellation token.
+ /// The raw server reply.
public Task Write2RawAsync(
int serverHandle,
int itemHandle,
@@ -445,6 +620,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
+ ///
+ /// Invokes an MXAccess command on this session.
+ ///
+ /// The command request.
+ /// Cancellation token.
+ /// The raw server reply.
public Task InvokeAsync(
MxCommandRequest request,
CancellationToken cancellationToken = default)
@@ -453,6 +634,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
return _client.InvokeAsync(request, cancellationToken);
}
+ ///
+ /// Streams events from the worker for this session, optionally starting after a given sequence number.
+ ///
+ /// The sequence number to stream from. Defaults to 0.
+ /// Cancellation token.
+ /// An async enumerable of events.
public IAsyncEnumerable StreamEventsAsync(
ulong afterWorkerSequence = 0,
CancellationToken cancellationToken = default)
@@ -466,6 +653,9 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
+ ///
+ /// Closes the session and releases resources.
+ ///
public async ValueTask DisposeAsync()
{
await CloseAsync().ConfigureAwait(false);
diff --git a/clients/dotnet/MxGateway.Client/MxGatewaySessionException.cs b/clients/dotnet/MxGateway.Client/MxGatewaySessionException.cs
index f7ae5db..b7d971a 100644
--- a/clients/dotnet/MxGateway.Client/MxGatewaySessionException.cs
+++ b/clients/dotnet/MxGateway.Client/MxGatewaySessionException.cs
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
+/// Exception thrown when a session is not found, not ready, or invalid.
public sealed class MxGatewaySessionException : MxGatewayException
{
+ /// Initializes a new instance with the given details.
+ /// The error message describing the session failure.
+ /// The session ID, if available.
+ /// The correlation ID for tracing, if available.
+ /// The protocol status details, if available.
+ /// The HResult code, if available.
+ /// The MXAccess statuses, if available.
+ /// The underlying exception, if any.
public MxGatewaySessionException(
string message,
string? sessionId = null,
diff --git a/clients/dotnet/MxGateway.Client/MxGatewayWorkerException.cs b/clients/dotnet/MxGateway.Client/MxGatewayWorkerException.cs
index 794a10b..2731c97 100644
--- a/clients/dotnet/MxGateway.Client/MxGatewayWorkerException.cs
+++ b/clients/dotnet/MxGateway.Client/MxGatewayWorkerException.cs
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
+/// Exception thrown when the worker process is unavailable or fails to process a command.
public sealed class MxGatewayWorkerException : MxGatewayException
{
+ /// Initializes a new instance with the given details.
+ /// The error message describing the worker failure.
+ /// The session ID, if available.
+ /// The correlation ID for tracing, if available.
+ /// The protocol status details, if available.
+ /// The HResult code, if available.
+ /// The MXAccess statuses, if available.
+ /// The underlying exception, if any.
public MxGatewayWorkerException(
string message,
string? sessionId = null,
diff --git a/clients/dotnet/MxGateway.Client/MxStatusProxyExtensions.cs b/clients/dotnet/MxGateway.Client/MxStatusProxyExtensions.cs
index a3086b8..07711ee 100644
--- a/clients/dotnet/MxGateway.Client/MxStatusProxyExtensions.cs
+++ b/clients/dotnet/MxGateway.Client/MxStatusProxyExtensions.cs
@@ -2,8 +2,11 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Client;
+/// Extension methods for MxStatusProxy values.
public static class MxStatusProxyExtensions
{
+ /// Returns whether the status indicates success (success flag set and category is Ok).
+ /// The status to check.
public static bool IsSuccess(this MxStatusProxy status)
{
ArgumentNullException.ThrowIfNull(status);
@@ -12,6 +15,8 @@ public static class MxStatusProxyExtensions
&& status.Category is MxStatusCategory.Ok;
}
+ /// Returns a formatted summary of the status for diagnostic output.
+ /// The status to summarize.
public static string ToDiagnosticSummary(this MxStatusProxy status)
{
ArgumentNullException.ThrowIfNull(status);
diff --git a/clients/dotnet/MxGateway.Client/MxValueExtensions.cs b/clients/dotnet/MxGateway.Client/MxValueExtensions.cs
index 5b75d2c..72e6020 100644
--- a/clients/dotnet/MxGateway.Client/MxValueExtensions.cs
+++ b/clients/dotnet/MxGateway.Client/MxValueExtensions.cs
@@ -10,6 +10,10 @@ namespace MxGateway.Client;
///
public static class MxValueExtensions
{
+ ///
+ /// Converts a boolean value to an MxValue with MxDataType.Boolean.
+ ///
+ /// Scalar boolean value to wrap.
public static MxValue ToMxValue(this bool value)
{
return new MxValue
@@ -20,6 +24,10 @@ public static class MxValueExtensions
};
}
+ ///
+ /// Converts a 32-bit integer value to an MxValue with MxDataType.Integer.
+ ///
+ /// 32-bit integer value to wrap.
public static MxValue ToMxValue(this int value)
{
return new MxValue
@@ -30,6 +38,10 @@ public static class MxValueExtensions
};
}
+ ///
+ /// Converts a 64-bit integer value to an MxValue with MxDataType.Integer.
+ ///
+ /// 64-bit integer value to wrap.
public static MxValue ToMxValue(this long value)
{
return new MxValue
@@ -40,6 +52,10 @@ public static class MxValueExtensions
};
}
+ ///
+ /// Converts a single-precision floating-point value to an MxValue with MxDataType.Float.
+ ///
+ /// Single-precision floating-point value to wrap.
public static MxValue ToMxValue(this float value)
{
return new MxValue
@@ -50,6 +66,10 @@ public static class MxValueExtensions
};
}
+ ///
+ /// Converts a double-precision floating-point value to an MxValue with MxDataType.Double.
+ ///
+ /// Double-precision floating-point value to wrap.
public static MxValue ToMxValue(this double value)
{
return new MxValue
@@ -60,6 +80,10 @@ public static class MxValueExtensions
};
}
+ ///
+ /// Converts a string value to an MxValue with MxDataType.String.
+ ///
+ /// String value to wrap.
public static MxValue ToMxValue(this string value)
{
ArgumentNullException.ThrowIfNull(value);
@@ -72,6 +96,10 @@ public static class MxValueExtensions
};
}
+ ///
+ /// Converts a DateTimeOffset value to an MxValue with MxDataType.Time.
+ ///
+ /// DateTimeOffset value to wrap.
public static MxValue ToMxValue(this DateTimeOffset value)
{
return new MxValue
@@ -82,6 +110,10 @@ public static class MxValueExtensions
};
}
+ ///
+ /// Converts a DateTime value to an MxValue with MxDataType.Time.
+ ///
+ /// DateTime value to wrap.
public static MxValue ToMxValue(this DateTime value)
{
return new DateTimeOffset(
@@ -91,6 +123,10 @@ public static class MxValueExtensions
.ToMxValue();
}
+ ///
+ /// Converts a boolean array to an MxValue with MxDataType.Boolean.
+ ///
+ /// Array of boolean values to wrap.
public static MxValue ToMxValue(this IReadOnlyList values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -105,6 +141,10 @@ public static class MxValueExtensions
});
}
+ ///
+ /// Converts a 32-bit integer array to an MxValue with MxDataType.Integer.
+ ///
+ /// Array of 32-bit integer values to wrap.
public static MxValue ToMxValue(this IReadOnlyList values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -119,6 +159,10 @@ public static class MxValueExtensions
});
}
+ ///
+ /// Converts a 64-bit integer array to an MxValue with MxDataType.Integer.
+ ///
+ /// Array of 64-bit integer values to wrap.
public static MxValue ToMxValue(this IReadOnlyList values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -133,6 +177,10 @@ public static class MxValueExtensions
});
}
+ ///
+ /// Converts a single-precision floating-point array to an MxValue with MxDataType.Float.
+ ///
+ /// Array of single-precision floating-point values to wrap.
public static MxValue ToMxValue(this IReadOnlyList values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -147,6 +195,10 @@ public static class MxValueExtensions
});
}
+ ///
+ /// Converts a double-precision floating-point array to an MxValue with MxDataType.Double.
+ ///
+ /// Array of double-precision floating-point values to wrap.
public static MxValue ToMxValue(this IReadOnlyList values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -161,6 +213,10 @@ public static class MxValueExtensions
});
}
+ ///
+ /// Converts a string array to an MxValue with MxDataType.String.
+ ///
+ /// Array of string values to wrap.
public static MxValue ToMxValue(this IReadOnlyList values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -175,6 +231,10 @@ public static class MxValueExtensions
});
}
+ ///
+ /// Converts a DateTimeOffset array to an MxValue with MxDataType.Time.
+ ///
+ /// Array of DateTimeOffset values to wrap.
public static MxValue ToMxValue(this IReadOnlyList values)
{
ArgumentNullException.ThrowIfNull(values);
@@ -189,6 +249,10 @@ public static class MxValueExtensions
});
}
+ ///
+ /// Gets the projection kind (field name) of the given MxValue's current oneof value.
+ ///
+ /// The MxValue whose oneof projection kind is returned.
public static string GetProjectionKind(this MxValue value)
{
ArgumentNullException.ThrowIfNull(value);
@@ -208,6 +272,10 @@ public static class MxValueExtensions
};
}
+ ///
+ /// Converts an MxValue to a CLR object; returns the boxed value or null for null MxValues.
+ ///
+ /// The MxValue to convert.
public static object? ToClrValue(this MxValue value)
{
ArgumentNullException.ThrowIfNull(value);
@@ -227,6 +295,10 @@ public static class MxValueExtensions
};
}
+ ///
+ /// Converts an MxArray to a CLR array; returns null if the array does not have a known element type.
+ ///
+ /// The MxArray to convert.
public static object? ToClrArrayValue(this MxArray array)
{
ArgumentNullException.ThrowIfNull(array);
@@ -249,6 +321,13 @@ public static class MxValueExtensions
};
}
+ ///
+ /// Creates an MxValue with MxDataType.Unknown from raw byte data, variant type, and diagnostic info.
+ ///
+ /// Raw byte data representing the value.
+ /// Variant type string (e.g., "VT_BSTR").
+ /// Diagnostic string describing the raw value.
+ /// Optional MXAccess data type override.
public static MxValue ToRawMxValue(
byte[] value,
string variantType,
diff --git a/src/MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs b/src/MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs
index 34ae67d..731d252 100644
--- a/src/MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs
+++ b/src/MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs
@@ -4,6 +4,7 @@ namespace MxGateway.IntegrationTests.Galaxy;
public sealed class GalaxyRepositoryLiveTests
{
+ /// Verifies that the Galaxy Repository can establish a live connection to the ZB database.
[LiveGalaxyRepositoryFact]
[Trait("Category", "LiveGalaxy")]
public async Task TestConnection_AgainstZb_Succeeds()
@@ -15,6 +16,7 @@ public sealed class GalaxyRepositoryLiveTests
Assert.True(ok, "TestConnectionAsync should return true against the ZB database.");
}
+ /// Verifies that the last deploy time can be retrieved from the ZB database.
[LiveGalaxyRepositoryFact]
[Trait("Category", "LiveGalaxy")]
public async Task GetLastDeployTime_AgainstZb_ReturnsTimestamp()
@@ -26,6 +28,7 @@ public sealed class GalaxyRepositoryLiveTests
Assert.NotNull(lastDeploy);
}
+ /// Verifies that the hierarchy can be retrieved from the ZB database.
[LiveGalaxyRepositoryFact]
[Trait("Category", "LiveGalaxy")]
public async Task GetHierarchy_AgainstZb_ReturnsObjects()
@@ -43,6 +46,7 @@ public sealed class GalaxyRepositoryLiveTests
});
}
+ /// Verifies that object attributes can be retrieved from the ZB database.
[LiveGalaxyRepositoryFact]
[Trait("Category", "LiveGalaxy")]
public async Task GetAttributes_AgainstZb_ReturnsAtLeastOneAttribute()
diff --git a/src/MxGateway.IntegrationTests/Galaxy/LiveGalaxyRepositoryFactAttribute.cs b/src/MxGateway.IntegrationTests/Galaxy/LiveGalaxyRepositoryFactAttribute.cs
index 585b9d8..19896c1 100644
--- a/src/MxGateway.IntegrationTests/Galaxy/LiveGalaxyRepositoryFactAttribute.cs
+++ b/src/MxGateway.IntegrationTests/Galaxy/LiveGalaxyRepositoryFactAttribute.cs
@@ -1,10 +1,14 @@
namespace MxGateway.IntegrationTests.Galaxy;
+/// Fact attribute that skips tests unless live Galaxy Repository tests are explicitly enabled.
public sealed class LiveGalaxyRepositoryFactAttribute : FactAttribute
{
+ /// Environment variable name to enable live Galaxy Repository tests.
public const string EnableVariableName = "MXGATEWAY_RUN_LIVE_GALAXY_TESTS";
+ /// Environment variable name for the Galaxy Repository connection string.
public const string ConnectionStringVariableName = "MXGATEWAY_LIVE_GALAXY_CONN";
+ /// Initializes a new instance of the LiveGalaxyRepositoryFactAttribute class.
public LiveGalaxyRepositoryFactAttribute()
{
if (!Enabled)
@@ -13,12 +17,14 @@ public sealed class LiveGalaxyRepositoryFactAttribute : FactAttribute
}
}
+ /// Gets a value indicating whether live Galaxy Repository tests are enabled.
public static bool Enabled =>
string.Equals(
Environment.GetEnvironmentVariable(EnableVariableName),
"1",
StringComparison.Ordinal);
+ /// Gets the Galaxy Repository connection string from environment or default.
public static string ConnectionString =>
Environment.GetEnvironmentVariable(ConnectionStringVariableName)
?? "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;";
diff --git a/src/MxGateway.IntegrationTests/IntegrationTestEnvironment.cs b/src/MxGateway.IntegrationTests/IntegrationTestEnvironment.cs
index ec7768a..0f2c470 100644
--- a/src/MxGateway.IntegrationTests/IntegrationTestEnvironment.cs
+++ b/src/MxGateway.IntegrationTests/IntegrationTestEnvironment.cs
@@ -8,27 +8,33 @@ public static class IntegrationTestEnvironment
public const string LiveMxAccessClientNameVariableName = "MXGATEWAY_LIVE_MXACCESS_CLIENT_NAME";
public const string LiveMxAccessEventTimeoutSecondsVariableName = "MXGATEWAY_LIVE_MXACCESS_EVENT_TIMEOUT_SECONDS";
+ /// Gets whether live MXAccess tests are enabled.
public static bool LiveMxAccessTestsEnabled =>
string.Equals(
Environment.GetEnvironmentVariable(LiveMxAccessVariableName),
"1",
StringComparison.Ordinal);
+ /// Gets the MXAccess item name for live tests.
public static string LiveMxAccessItem =>
GetOptionalEnvironmentVariable(
LiveMxAccessItemVariableName,
"TestChildObject.TestInt");
+ /// Gets the client name for live tests.
public static string LiveMxAccessClientName =>
GetOptionalEnvironmentVariable(
LiveMxAccessClientNameVariableName,
"MxGateway.IntegrationTests");
+ /// Gets the timeout for waiting on events in live tests.
public static TimeSpan LiveMxAccessEventTimeout =>
TimeSpan.FromSeconds(GetPositiveIntegerEnvironmentVariable(
LiveMxAccessEventTimeoutSecondsVariableName,
defaultValue: 15));
+ /// Resolves the path to the worker executable for live tests.
+ /// Path to MxGateway.Worker.exe.
public static string ResolveLiveMxAccessWorkerExecutablePath()
{
string? configuredPath = Environment.GetEnvironmentVariable(LiveMxAccessWorkerExecutableVariableName);
@@ -74,6 +80,9 @@ public static class IntegrationTestEnvironment
return defaultValue;
}
+ /// Resolves the root directory of the repository by searching for .git and src directories.
+ /// Starting directory to search from.
+ /// The repository root path, or the start directory if not found.
internal static string ResolveRepositoryRoot(string startDirectory)
{
DirectoryInfo? directory = new(startDirectory);
diff --git a/src/MxGateway.IntegrationTests/IntegrationTestEnvironmentTests.cs b/src/MxGateway.IntegrationTests/IntegrationTestEnvironmentTests.cs
index 512f283..871bf57 100644
--- a/src/MxGateway.IntegrationTests/IntegrationTestEnvironmentTests.cs
+++ b/src/MxGateway.IntegrationTests/IntegrationTestEnvironmentTests.cs
@@ -2,6 +2,7 @@ namespace MxGateway.IntegrationTests;
public sealed class IntegrationTestEnvironmentTests
{
+ /// Verifies that live MXAccess tests use correct environment variable name.
[Fact]
public void LiveMxAccessTests_AreOptInByEnvironmentVariable()
{
@@ -10,6 +11,7 @@ public sealed class IntegrationTestEnvironmentTests
IntegrationTestEnvironment.LiveMxAccessVariableName);
}
+ /// Verifies that worker executable uses correct environment variable name.
[Fact]
public void LiveMxAccessWorkerExecutable_UsesDocumentedEnvironmentVariable()
{
@@ -18,6 +20,7 @@ public sealed class IntegrationTestEnvironmentTests
IntegrationTestEnvironment.LiveMxAccessWorkerExecutableVariableName);
}
+ /// Verifies that repository root resolution accepts git worktree files.
[Fact]
public void ResolveRepositoryRoot_AcceptsGitWorktreeFile()
{
diff --git a/src/MxGateway.IntegrationTests/LiveMxAccessFactAttribute.cs b/src/MxGateway.IntegrationTests/LiveMxAccessFactAttribute.cs
index b89cf9c..85263c8 100644
--- a/src/MxGateway.IntegrationTests/LiveMxAccessFactAttribute.cs
+++ b/src/MxGateway.IntegrationTests/LiveMxAccessFactAttribute.cs
@@ -1,7 +1,9 @@
namespace MxGateway.IntegrationTests;
+/// Marks an xUnit test as requiring installed MXAccess COM and live provider state.
public sealed class LiveMxAccessFactAttribute : FactAttribute
{
+ /// Initializes the attribute, skipping the test unless the integration test environment variable is set.
public LiveMxAccessFactAttribute()
{
if (!IntegrationTestEnvironment.LiveMxAccessTestsEnabled)
diff --git a/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs b/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs
index b773604..951fc50 100644
--- a/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs
+++ b/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs
@@ -21,6 +21,9 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
private static readonly TimeSpan CommandTimeout = TimeSpan.FromSeconds(15);
private static readonly TimeSpan StreamShutdownTimeout = TimeSpan.FromSeconds(10);
+ ///
+ /// Verifies that a gateway session can register, add item, advise, and stream events from live MXAccess.
+ ///
[LiveMxAccessFact]
[Trait("Category", "LiveMxAccess")]
public async Task GatewaySession_WithLiveWorker_RegistersAdvisesStreamsDataAndCloses()
@@ -208,12 +211,21 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
$"Event value_type={dataChange.Value?.DataType} raw_status={dataChange.RawStatus}");
}
+ ///
+ /// Test fixture that assembles the gateway service with a worker process factory for live MXAccess testing.
+ ///
private sealed class GatewayServiceFixture : IAsyncDisposable
{
private readonly GatewayMetrics _metrics = new();
private readonly SessionRegistry _registry = new();
private readonly ILoggerFactory _loggerFactory;
+ ///
+ /// Initializes the fixture with worker executable path, factory, and test output helper.
+ ///
+ /// Path to the worker process executable.
+ /// Factory for creating worker processes.
+ /// Test output helper for logging.
public GatewayServiceFixture(
string workerExecutablePath,
IWorkerProcessFactory processFactory,
@@ -255,8 +267,14 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
_loggerFactory.CreateLogger());
}
+ ///
+ /// The assembled gateway service instance.
+ ///
public MxAccessGatewayService Service { get; }
+ ///
+ /// Disposes the fixture resources and closes all sessions.
+ ///
public async ValueTask DisposeAsync()
{
foreach (GatewaySession session in _registry.Snapshot())
@@ -295,12 +313,18 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
}
}
+ ///
+ /// Gathers messages written to a server stream for test inspection.
+ ///
private sealed class RecordingServerStreamWriter : IServerStreamWriter
{
private readonly object syncRoot = new();
private readonly TaskCompletionSource firstMessage = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly List messages = [];
+ ///
+ /// All messages that have been written to the stream.
+ ///
public IReadOnlyList Messages
{
get
@@ -312,8 +336,15 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
}
}
+ ///
+ /// Inherited write options.
+ ///
public WriteOptions? WriteOptions { get; set; }
+ ///
+ /// Records the message and completes the first-message task.
+ ///
+ /// The message to write.
public Task WriteAsync(T message)
{
lock (syncRoot)
@@ -325,12 +356,20 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
return Task.CompletedTask;
}
+ ///
+ /// Waits for the first message up to the specified timeout.
+ ///
+ /// The maximum time to wait.
+ /// The first message written to the stream.
public async Task WaitForFirstMessageAsync(TimeSpan timeout)
{
return await firstMessage.Task.WaitAsync(timeout).ConfigureAwait(false);
}
}
+ ///
+ /// Mock server call context for testing gRPC calls.
+ ///
private sealed class TestServerCallContext(CancellationToken cancellationToken = default) : ServerCallContext
{
private readonly Metadata requestHeaders = [];
@@ -339,43 +378,56 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
private Status status;
private WriteOptions? writeOptions;
+ ///
protected override string MethodCore => "/mxaccess_gateway.v1.MxAccessGateway/Test";
+ ///
protected override string HostCore => "localhost";
+ ///
protected override string PeerCore => "ipv4:127.0.0.1:5000";
+ ///
protected override DateTime DeadlineCore => DateTime.UtcNow.AddMinutes(1);
+ ///
protected override Metadata RequestHeadersCore => requestHeaders;
+ ///
protected override CancellationToken CancellationTokenCore => cancellationToken;
+ ///
protected override Metadata ResponseTrailersCore => responseTrailers;
+ ///
protected override Status StatusCore
{
get => status;
set => status = value;
}
+ ///
protected override WriteOptions? WriteOptionsCore
{
get => writeOptions;
set => writeOptions = value;
}
+ ///
protected override AuthContext AuthContextCore { get; } = new(
string.Empty,
new Dictionary>(StringComparer.Ordinal));
+ ///
protected override IDictionary