diff --git a/clients/dotnet/MxGateway.Client.Cli/MxGateway.Client.Cli.csproj b/clients/dotnet/MxGateway.Client.Cli/MxGateway.Client.Cli.csproj
new file mode 100644
index 0000000..b04f285
--- /dev/null
+++ b/clients/dotnet/MxGateway.Client.Cli/MxGateway.Client.Cli.csproj
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs b/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs
new file mode 100644
index 0000000..8b64cd6
--- /dev/null
+++ b/clients/dotnet/MxGateway.Client.Cli/MxGatewayClientCli.cs
@@ -0,0 +1,48 @@
+using MxGateway.Client;
+
+namespace MxGateway.Client.Cli;
+
+public static class MxGatewayClientCli
+{
+ public static int Run(
+ string[] args,
+ TextWriter standardOutput,
+ TextWriter standardError)
+ {
+ ArgumentNullException.ThrowIfNull(args);
+ ArgumentNullException.ThrowIfNull(standardOutput);
+ ArgumentNullException.ThrowIfNull(standardError);
+
+ if (args.Length is 0 || IsHelp(args[0]))
+ {
+ WriteUsage(standardOutput);
+ return 0;
+ }
+
+ if (string.Equals(args[0], "version", StringComparison.OrdinalIgnoreCase))
+ {
+ standardOutput.WriteLine(
+ $"gateway-protocol={MxGatewayClientContractInfo.GatewayProtocolVersion}");
+ standardOutput.WriteLine(
+ $"worker-protocol={MxGatewayClientContractInfo.WorkerProtocolVersion}");
+ return 0;
+ }
+
+ standardError.WriteLine($"Unknown command '{args[0]}'.");
+ WriteUsage(standardError);
+ return 2;
+ }
+
+ private static bool IsHelp(string value)
+ {
+ return string.Equals(value, "-h", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(value, "--help", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(value, "help", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static void WriteUsage(TextWriter writer)
+ {
+ writer.WriteLine("mxgw-dotnet version");
+ writer.WriteLine("mxgw-dotnet --help");
+ }
+}
diff --git a/clients/dotnet/MxGateway.Client.Cli/Program.cs b/clients/dotnet/MxGateway.Client.Cli/Program.cs
new file mode 100644
index 0000000..3f1db75
--- /dev/null
+++ b/clients/dotnet/MxGateway.Client.Cli/Program.cs
@@ -0,0 +1,3 @@
+using MxGateway.Client.Cli;
+
+return MxGatewayClientCli.Run(args, Console.Out, Console.Error);
diff --git a/clients/dotnet/MxGateway.Client.Tests/MxGateway.Client.Tests.csproj b/clients/dotnet/MxGateway.Client.Tests/MxGateway.Client.Tests.csproj
new file mode 100644
index 0000000..d02ed8e
--- /dev/null
+++ b/clients/dotnet/MxGateway.Client.Tests/MxGateway.Client.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs
new file mode 100644
index 0000000..5ee9ebc
--- /dev/null
+++ b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientCliTests.cs
@@ -0,0 +1,20 @@
+using MxGateway.Client.Cli;
+
+namespace MxGateway.Client.Tests;
+
+public sealed class MxGatewayClientCliTests
+{
+ [Fact]
+ public void Run_Version_PrintsCompiledProtocolVersions()
+ {
+ using var output = new StringWriter();
+ using var error = new StringWriter();
+
+ var exitCode = MxGatewayClientCli.Run(["version"], output, error);
+
+ Assert.Equal(0, exitCode);
+ Assert.Contains("gateway-protocol=1", output.ToString());
+ Assert.Contains("worker-protocol=1", output.ToString());
+ Assert.Equal(string.Empty, error.ToString());
+ }
+}
diff --git a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientContractInfoTests.cs b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientContractInfoTests.cs
new file mode 100644
index 0000000..1458347
--- /dev/null
+++ b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientContractInfoTests.cs
@@ -0,0 +1,22 @@
+using MxGateway.Contracts;
+
+namespace MxGateway.Client.Tests;
+
+public sealed class MxGatewayClientContractInfoTests
+{
+ [Fact]
+ public void GatewayProtocolVersion_MatchesSharedContract()
+ {
+ Assert.Equal(
+ GatewayContractInfo.GatewayProtocolVersion,
+ MxGatewayClientContractInfo.GatewayProtocolVersion);
+ }
+
+ [Fact]
+ public void WorkerProtocolVersion_MatchesSharedContract()
+ {
+ Assert.Equal(
+ GatewayContractInfo.WorkerProtocolVersion,
+ MxGatewayClientContractInfo.WorkerProtocolVersion);
+ }
+}
diff --git a/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientOptionsTests.cs b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientOptionsTests.cs
new file mode 100644
index 0000000..8c91227
--- /dev/null
+++ b/clients/dotnet/MxGateway.Client.Tests/MxGatewayClientOptionsTests.cs
@@ -0,0 +1,28 @@
+namespace MxGateway.Client.Tests;
+
+public sealed class MxGatewayClientOptionsTests
+{
+ [Fact]
+ public void Validate_WithAbsoluteEndpointAndApiKey_Succeeds()
+ {
+ var options = new MxGatewayClientOptions
+ {
+ Endpoint = new Uri("http://localhost:5000"),
+ ApiKey = "test-api-key",
+ };
+
+ options.Validate();
+ }
+
+ [Fact]
+ public void Validate_WithEmptyApiKey_Throws()
+ {
+ var options = new MxGatewayClientOptions
+ {
+ Endpoint = new Uri("http://localhost:5000"),
+ ApiKey = "",
+ };
+
+ Assert.Throws(options.Validate);
+ }
+}
diff --git a/clients/dotnet/MxGateway.Client.Tests/MxGatewayGeneratedContractTests.cs b/clients/dotnet/MxGateway.Client.Tests/MxGatewayGeneratedContractTests.cs
new file mode 100644
index 0000000..846d9e3
--- /dev/null
+++ b/clients/dotnet/MxGateway.Client.Tests/MxGatewayGeneratedContractTests.cs
@@ -0,0 +1,18 @@
+namespace MxGateway.Client.Tests;
+
+public sealed class MxGatewayGeneratedContractTests
+{
+ [Fact]
+ public async Task GeneratedGrpcClient_CanBeConstructedFromClientFactory()
+ {
+ var options = new MxGatewayClientOptions
+ {
+ Endpoint = new Uri("http://localhost:5000"),
+ ApiKey = "test-api-key",
+ };
+
+ await using var client = MxGatewayClient.Create(options);
+
+ Assert.NotNull(client.RawClient);
+ }
+}
diff --git a/clients/dotnet/MxGateway.Client.sln b/clients/dotnet/MxGateway.Client.sln
new file mode 100644
index 0000000..07a6a8c
--- /dev/null
+++ b/clients/dotnet/MxGateway.Client.sln
@@ -0,0 +1,76 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Client", "MxGateway.Client\MxGateway.Client.csproj", "{7CF9ED88-1F32-4040-BEB1-D0902E304C70}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Contracts", "..\..\src\MxGateway.Contracts\MxGateway.Contracts.csproj", "{9AB807A8-0469-40F7-A000-D240F36B6E5D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Client.Cli", "MxGateway.Client.Cli\MxGateway.Client.Cli.csproj", "{EB061E77-2475-4322-9257-3F2456DD141C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Client.Tests", "MxGateway.Client.Tests\MxGateway.Client.Tests.csproj", "{B77B5A8E-0C53-4419-9BCD-227C9753A074}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|x64.Build.0 = Debug|Any CPU
+ {7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|x86.Build.0 = Debug|Any CPU
+ {7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|x64.ActiveCfg = Release|Any CPU
+ {7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|x64.Build.0 = Release|Any CPU
+ {7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|x86.ActiveCfg = Release|Any CPU
+ {7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|x86.Build.0 = Release|Any CPU
+ {9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|x64.Build.0 = Debug|Any CPU
+ {9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|x86.Build.0 = Debug|Any CPU
+ {9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|x64.ActiveCfg = Release|Any CPU
+ {9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|x64.Build.0 = Release|Any CPU
+ {9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|x86.ActiveCfg = Release|Any CPU
+ {9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|x86.Build.0 = Release|Any CPU
+ {EB061E77-2475-4322-9257-3F2456DD141C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EB061E77-2475-4322-9257-3F2456DD141C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EB061E77-2475-4322-9257-3F2456DD141C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {EB061E77-2475-4322-9257-3F2456DD141C}.Debug|x64.Build.0 = Debug|Any CPU
+ {EB061E77-2475-4322-9257-3F2456DD141C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EB061E77-2475-4322-9257-3F2456DD141C}.Debug|x86.Build.0 = Debug|Any CPU
+ {EB061E77-2475-4322-9257-3F2456DD141C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EB061E77-2475-4322-9257-3F2456DD141C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EB061E77-2475-4322-9257-3F2456DD141C}.Release|x64.ActiveCfg = Release|Any CPU
+ {EB061E77-2475-4322-9257-3F2456DD141C}.Release|x64.Build.0 = Release|Any CPU
+ {EB061E77-2475-4322-9257-3F2456DD141C}.Release|x86.ActiveCfg = Release|Any CPU
+ {EB061E77-2475-4322-9257-3F2456DD141C}.Release|x86.Build.0 = Release|Any CPU
+ {B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|x64.Build.0 = Debug|Any CPU
+ {B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|x86.Build.0 = Debug|Any CPU
+ {B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|x64.ActiveCfg = Release|Any CPU
+ {B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|x64.Build.0 = Release|Any CPU
+ {B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|x86.ActiveCfg = Release|Any CPU
+ {B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/clients/dotnet/MxGateway.Client/MxGateway.Client.csproj b/clients/dotnet/MxGateway.Client/MxGateway.Client.csproj
new file mode 100644
index 0000000..d505aa4
--- /dev/null
+++ b/clients/dotnet/MxGateway.Client/MxGateway.Client.csproj
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/clients/dotnet/MxGateway.Client/MxGatewayClient.cs b/clients/dotnet/MxGateway.Client/MxGatewayClient.cs
new file mode 100644
index 0000000..efa8ce8
--- /dev/null
+++ b/clients/dotnet/MxGateway.Client/MxGatewayClient.cs
@@ -0,0 +1,41 @@
+using Grpc.Net.Client;
+using MxGateway.Contracts.Proto;
+
+namespace MxGateway.Client;
+
+///
+/// Provides the initial .NET client entry point and raw generated gRPC client.
+///
+public sealed class MxGatewayClient : IAsyncDisposable
+{
+ private readonly GrpcChannel _channel;
+
+ private MxGatewayClient(GrpcChannel channel)
+ {
+ _channel = channel;
+ RawClient = new MxAccessGateway.MxAccessGatewayClient(channel);
+ }
+
+ public MxAccessGateway.MxAccessGatewayClient RawClient { get; }
+
+ public static MxGatewayClient Create(MxGatewayClientOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+ options.Validate();
+
+ var channel = GrpcChannel.ForAddress(
+ options.Endpoint,
+ new GrpcChannelOptions
+ {
+ LoggerFactory = options.LoggerFactory,
+ });
+
+ return new MxGatewayClient(channel);
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ _channel.Dispose();
+ return ValueTask.CompletedTask;
+ }
+}
diff --git a/clients/dotnet/MxGateway.Client/MxGatewayClientContractInfo.cs b/clients/dotnet/MxGateway.Client/MxGatewayClientContractInfo.cs
new file mode 100644
index 0000000..4869faa
--- /dev/null
+++ b/clients/dotnet/MxGateway.Client/MxGatewayClientContractInfo.cs
@@ -0,0 +1,15 @@
+using MxGateway.Contracts;
+
+namespace MxGateway.Client;
+
+///
+/// Exposes the protocol versions compiled into this client package.
+///
+public static class MxGatewayClientContractInfo
+{
+ public const uint GatewayProtocolVersion =
+ GatewayContractInfo.GatewayProtocolVersion;
+
+ public const uint WorkerProtocolVersion =
+ GatewayContractInfo.WorkerProtocolVersion;
+}
diff --git a/clients/dotnet/MxGateway.Client/MxGatewayClientOptions.cs b/clients/dotnet/MxGateway.Client/MxGatewayClientOptions.cs
new file mode 100644
index 0000000..b6863c5
--- /dev/null
+++ b/clients/dotnet/MxGateway.Client/MxGatewayClientOptions.cs
@@ -0,0 +1,58 @@
+using Microsoft.Extensions.Logging;
+
+namespace MxGateway.Client;
+
+///
+/// Configures the gRPC channel used by the .NET MXAccess Gateway client.
+///
+public sealed class MxGatewayClientOptions
+{
+ public required Uri Endpoint { get; init; }
+
+ public required string ApiKey { get; init; }
+
+ public bool UseTls { get; init; }
+
+ public string? CaCertificatePath { get; init; }
+
+ public string? ServerNameOverride { get; init; }
+
+ public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(10);
+
+ public TimeSpan DefaultCallTimeout { get; init; } = TimeSpan.FromSeconds(30);
+
+ public ILoggerFactory? LoggerFactory { get; init; }
+
+ public void Validate()
+ {
+ ArgumentNullException.ThrowIfNull(Endpoint);
+
+ if (!Endpoint.IsAbsoluteUri)
+ {
+ throw new ArgumentException(
+ "The gateway endpoint must be an absolute URI.",
+ nameof(Endpoint));
+ }
+
+ if (string.IsNullOrWhiteSpace(ApiKey))
+ {
+ throw new ArgumentException(
+ "The gateway API key must not be empty.",
+ nameof(ApiKey));
+ }
+
+ if (ConnectTimeout <= TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(ConnectTimeout),
+ "The connect timeout must be greater than zero.");
+ }
+
+ if (DefaultCallTimeout <= TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(DefaultCallTimeout),
+ "The default call timeout must be greater than zero.");
+ }
+ }
+}
diff --git a/clients/dotnet/README.md b/clients/dotnet/README.md
new file mode 100644
index 0000000..a606185
--- /dev/null
+++ b/clients/dotnet/README.md
@@ -0,0 +1,24 @@
+# .NET Client Projects
+
+The .NET client workspace contains the MXAccess Gateway client library, test
+CLI, and unit tests.
+
+## Projects
+
+| Project | Purpose |
+|---------|---------|
+| `MxGateway.Client` | .NET 10 library entry point and raw gRPC client access. |
+| `MxGateway.Client.Cli` | Test CLI for smoke and diagnostic commands. |
+| `MxGateway.Client.Tests` | Unit tests for the scaffold and generated contract wiring. |
+
+The projects reference `src/MxGateway.Contracts/MxGateway.Contracts.csproj` so
+the client compiles against the same generated protobuf and gRPC types as the
+gateway. `clients/dotnet/generated` remains reserved for generator output if a
+future client build switches to client-local `Grpc.Tools` generation.
+
+## Build And Test
+
+```powershell
+dotnet build clients/dotnet/MxGateway.Client.sln
+dotnet test clients/dotnet/MxGateway.Client.sln --no-build
+```
diff --git a/docs/clients-dotnet-csharp-design.md b/docs/clients-dotnet-csharp-design.md
index 900b63c..8b35036 100644
--- a/docs/clients-dotnet-csharp-design.md
+++ b/docs/clients-dotnet-csharp-design.md
@@ -16,6 +16,7 @@ Recommended layout:
```text
clients/dotnet/
+ MxGateway.Client.sln
MxGateway.Client/
MxGateway.Client.csproj
GatewayClient.cs
@@ -41,6 +42,12 @@ Target framework:
net10.0
```
+The scaffold uses a project reference to
+`src/MxGateway.Contracts/MxGateway.Contracts.csproj` for generated protobuf and
+gRPC types. `clients/dotnet/generated` remains reserved for client-local
+generator output if the .NET client later needs to decouple from the contracts
+project.
+
Expected packages:
- `Grpc.Net.Client`