diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 0000000..0b8cc13
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,11 @@
+
+
+ latest
+ enable
+ enable
+ true
+ latest
+ true
+ true
+
+
diff --git a/src/MxGateway.Contracts/GatewayContractInfo.cs b/src/MxGateway.Contracts/GatewayContractInfo.cs
new file mode 100644
index 0000000..22bcb20
--- /dev/null
+++ b/src/MxGateway.Contracts/GatewayContractInfo.cs
@@ -0,0 +1,12 @@
+namespace MxGateway.Contracts;
+
+///
+/// Exposes version metadata shared by gateway components before generated
+/// protobuf contracts are introduced.
+///
+public static class GatewayContractInfo
+{
+ public const uint WorkerProtocolVersion = 1;
+
+ public const string DefaultBackendName = "mxaccess-worker";
+}
diff --git a/src/MxGateway.Contracts/MxGateway.Contracts.csproj b/src/MxGateway.Contracts/MxGateway.Contracts.csproj
new file mode 100644
index 0000000..00b9359
--- /dev/null
+++ b/src/MxGateway.Contracts/MxGateway.Contracts.csproj
@@ -0,0 +1,7 @@
+
+
+
+ net10.0
+
+
+
diff --git a/src/MxGateway.IntegrationTests/IntegrationTestEnvironment.cs b/src/MxGateway.IntegrationTests/IntegrationTestEnvironment.cs
new file mode 100644
index 0000000..608101a
--- /dev/null
+++ b/src/MxGateway.IntegrationTests/IntegrationTestEnvironment.cs
@@ -0,0 +1,12 @@
+namespace MxGateway.IntegrationTests;
+
+public static class IntegrationTestEnvironment
+{
+ public const string LiveMxAccessVariableName = "MXGATEWAY_RUN_LIVE_MXACCESS_TESTS";
+
+ public static bool LiveMxAccessTestsEnabled =>
+ string.Equals(
+ Environment.GetEnvironmentVariable(LiveMxAccessVariableName),
+ "1",
+ StringComparison.Ordinal);
+}
diff --git a/src/MxGateway.IntegrationTests/IntegrationTestEnvironmentTests.cs b/src/MxGateway.IntegrationTests/IntegrationTestEnvironmentTests.cs
new file mode 100644
index 0000000..b199231
--- /dev/null
+++ b/src/MxGateway.IntegrationTests/IntegrationTestEnvironmentTests.cs
@@ -0,0 +1,12 @@
+namespace MxGateway.IntegrationTests;
+
+public sealed class IntegrationTestEnvironmentTests
+{
+ [Fact]
+ public void LiveMxAccessTests_AreOptInByEnvironmentVariable()
+ {
+ Assert.Equal(
+ "MXGATEWAY_RUN_LIVE_MXACCESS_TESTS",
+ IntegrationTestEnvironment.LiveMxAccessVariableName);
+ }
+}
diff --git a/src/MxGateway.IntegrationTests/MxGateway.IntegrationTests.csproj b/src/MxGateway.IntegrationTests/MxGateway.IntegrationTests.csproj
new file mode 100644
index 0000000..6a9cfe2
--- /dev/null
+++ b/src/MxGateway.IntegrationTests/MxGateway.IntegrationTests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net10.0
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MxGateway.Server/GatewayApplication.cs b/src/MxGateway.Server/GatewayApplication.cs
new file mode 100644
index 0000000..648b537
--- /dev/null
+++ b/src/MxGateway.Server/GatewayApplication.cs
@@ -0,0 +1,40 @@
+using MxGateway.Contracts;
+
+namespace MxGateway.Server;
+
+public static class GatewayApplication
+{
+ public static WebApplication Build(string[] args)
+ {
+ WebApplicationBuilder builder = CreateBuilder(args);
+ WebApplication app = builder.Build();
+
+ app.MapGatewayEndpoints();
+
+ return app;
+ }
+
+ public static WebApplicationBuilder CreateBuilder(string[] args)
+ {
+ WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
+
+ builder.Services.AddHealthChecks();
+
+ return builder;
+ }
+
+ public static IEndpointRouteBuilder MapGatewayEndpoints(this IEndpointRouteBuilder endpoints)
+ {
+ endpoints.MapGet("/", () => Results.Redirect("/health/live"));
+
+ endpoints.MapGet(
+ "/health/live",
+ () => Results.Ok(new GatewayHealthReply(
+ Status: "Healthy",
+ DefaultBackend: GatewayContractInfo.DefaultBackendName,
+ WorkerProtocolVersion: GatewayContractInfo.WorkerProtocolVersion)))
+ .WithName("LiveHealth");
+
+ return endpoints;
+ }
+}
diff --git a/src/MxGateway.Server/GatewayHealthReply.cs b/src/MxGateway.Server/GatewayHealthReply.cs
new file mode 100644
index 0000000..c4a7ad7
--- /dev/null
+++ b/src/MxGateway.Server/GatewayHealthReply.cs
@@ -0,0 +1,6 @@
+namespace MxGateway.Server;
+
+public sealed record GatewayHealthReply(
+ string Status,
+ string DefaultBackend,
+ uint WorkerProtocolVersion);
diff --git a/src/MxGateway.Server/MxGateway.Server.csproj b/src/MxGateway.Server/MxGateway.Server.csproj
new file mode 100644
index 0000000..dc18b08
--- /dev/null
+++ b/src/MxGateway.Server/MxGateway.Server.csproj
@@ -0,0 +1,11 @@
+
+
+
+ net10.0
+
+
+
+
+
+
+
diff --git a/src/MxGateway.Server/Program.cs b/src/MxGateway.Server/Program.cs
new file mode 100644
index 0000000..1dd9812
--- /dev/null
+++ b/src/MxGateway.Server/Program.cs
@@ -0,0 +1,7 @@
+using MxGateway.Server;
+
+var app = GatewayApplication.Build(args);
+
+app.Run();
+
+public partial class Program;
diff --git a/src/MxGateway.Server/Properties/launchSettings.json b/src/MxGateway.Server/Properties/launchSettings.json
new file mode 100644
index 0000000..94f07e8
--- /dev/null
+++ b/src/MxGateway.Server/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5120",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7121;http://localhost:5120",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/MxGateway.Server/appsettings.Development.json b/src/MxGateway.Server/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/src/MxGateway.Server/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/src/MxGateway.Server/appsettings.json b/src/MxGateway.Server/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/src/MxGateway.Server/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/MxGateway.Tests/Contracts/GatewayContractInfoTests.cs b/src/MxGateway.Tests/Contracts/GatewayContractInfoTests.cs
new file mode 100644
index 0000000..93c0acf
--- /dev/null
+++ b/src/MxGateway.Tests/Contracts/GatewayContractInfoTests.cs
@@ -0,0 +1,18 @@
+using MxGateway.Contracts;
+
+namespace MxGateway.Tests.Contracts;
+
+public sealed class GatewayContractInfoTests
+{
+ [Fact]
+ public void DefaultBackendName_IsMxAccessWorker()
+ {
+ Assert.Equal("mxaccess-worker", GatewayContractInfo.DefaultBackendName);
+ }
+
+ [Fact]
+ public void WorkerProtocolVersion_StartsAtVersionOne()
+ {
+ Assert.Equal(1u, GatewayContractInfo.WorkerProtocolVersion);
+ }
+}
diff --git a/src/MxGateway.Tests/Gateway/GatewayApplicationTests.cs b/src/MxGateway.Tests/Gateway/GatewayApplicationTests.cs
new file mode 100644
index 0000000..006798f
--- /dev/null
+++ b/src/MxGateway.Tests/Gateway/GatewayApplicationTests.cs
@@ -0,0 +1,22 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Routing;
+using MxGateway.Server;
+
+namespace MxGateway.Tests.Gateway;
+
+public sealed class GatewayApplicationTests
+{
+ [Fact]
+ public void Build_MapsLiveHealthEndpoint()
+ {
+ WebApplication app = GatewayApplication.Build([]);
+
+ RouteEndpoint endpoint = Assert.Single(
+ ((IEndpointRouteBuilder)app).DataSources
+ .SelectMany(dataSource => dataSource.Endpoints)
+ .OfType(),
+ candidate => candidate.RoutePattern.RawText == "/health/live");
+
+ Assert.Equal("LiveHealth", endpoint.Metadata.GetMetadata()?.EndpointName);
+ }
+}
diff --git a/src/MxGateway.Tests/MxGateway.Tests.csproj b/src/MxGateway.Tests/MxGateway.Tests.csproj
new file mode 100644
index 0000000..27e10c0
--- /dev/null
+++ b/src/MxGateway.Tests/MxGateway.Tests.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net10.0
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MxGateway.Tests/ProjectStructure/GatewayProjectReferenceTests.cs b/src/MxGateway.Tests/ProjectStructure/GatewayProjectReferenceTests.cs
new file mode 100644
index 0000000..b9711e8
--- /dev/null
+++ b/src/MxGateway.Tests/ProjectStructure/GatewayProjectReferenceTests.cs
@@ -0,0 +1,64 @@
+using System.Xml.Linq;
+
+namespace MxGateway.Tests.ProjectStructure;
+
+public sealed class GatewayProjectReferenceTests
+{
+ [Fact]
+ public void GatewayProject_TargetsNet10()
+ {
+ XDocument project = LoadProject("MxGateway.Server");
+
+ Assert.Equal("net10.0", ElementValue(project, "TargetFramework"));
+ }
+
+ [Fact]
+ public void GatewayProject_DoesNotReferenceMxAccessCom()
+ {
+ XDocument project = LoadProject("MxGateway.Server");
+
+ IReadOnlyList referenceNames = project
+ .Descendants()
+ .Where(element => element.Name.LocalName is "Reference" or "COMReference" or "COMFileReference" or "PackageReference")
+ .Select(element => (string?)element.Attribute("Include") ?? string.Empty)
+ .ToArray();
+
+ Assert.DoesNotContain(referenceNames, reference =>
+ reference.Contains("MxAccess", StringComparison.OrdinalIgnoreCase)
+ || reference.Contains("ArchestrA.MXAccess", StringComparison.OrdinalIgnoreCase)
+ || reference.Contains("LMXProxy", StringComparison.OrdinalIgnoreCase));
+ }
+
+ private static XDocument LoadProject(string projectName)
+ {
+ DirectoryInfo repositoryRoot = FindRepositoryRoot();
+ string projectPath = Path.Combine(repositoryRoot.FullName, projectName, $"{projectName}.csproj");
+
+ return XDocument.Load(projectPath);
+ }
+
+ private static string ElementValue(XDocument project, string elementName)
+ {
+ return project
+ .Descendants()
+ .Single(element => element.Name.LocalName == elementName)
+ .Value;
+ }
+
+ private static DirectoryInfo FindRepositoryRoot()
+ {
+ DirectoryInfo? current = new(AppContext.BaseDirectory);
+
+ while (current is not null)
+ {
+ if (File.Exists(Path.Combine(current.FullName, "MxGateway.sln")))
+ {
+ return current;
+ }
+
+ current = current.Parent;
+ }
+
+ throw new DirectoryNotFoundException("Could not locate src/MxGateway.sln from the test output directory.");
+ }
+}
diff --git a/src/MxGateway.sln b/src/MxGateway.sln
new file mode 100644
index 0000000..4426136
--- /dev/null
+++ b/src/MxGateway.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.Contracts", "MxGateway.Contracts\MxGateway.Contracts.csproj", "{484053B1-30E8-4411-9ACE-E3AE5EE65EB8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Server", "MxGateway.Server\MxGateway.Server.csproj", "{2752A666-898C-4D2A-A5A6-4F2FD17F64AE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Tests", "MxGateway.Tests\MxGateway.Tests.csproj", "{6E069780-A892-487E-AEED-051E26C829A4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.IntegrationTests", "MxGateway.IntegrationTests\MxGateway.IntegrationTests.csproj", "{6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}"
+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
+ {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|x64.Build.0 = Debug|Any CPU
+ {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Debug|x86.Build.0 = Debug|Any CPU
+ {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|x64.ActiveCfg = Release|Any CPU
+ {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|x64.Build.0 = Release|Any CPU
+ {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|x86.ActiveCfg = Release|Any CPU
+ {484053B1-30E8-4411-9ACE-E3AE5EE65EB8}.Release|x86.Build.0 = Release|Any CPU
+ {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|x64.Build.0 = Debug|Any CPU
+ {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Debug|x86.Build.0 = Debug|Any CPU
+ {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|x64.ActiveCfg = Release|Any CPU
+ {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|x64.Build.0 = Release|Any CPU
+ {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|x86.ActiveCfg = Release|Any CPU
+ {2752A666-898C-4D2A-A5A6-4F2FD17F64AE}.Release|x86.Build.0 = Release|Any CPU
+ {6E069780-A892-487E-AEED-051E26C829A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6E069780-A892-487E-AEED-051E26C829A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6E069780-A892-487E-AEED-051E26C829A4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6E069780-A892-487E-AEED-051E26C829A4}.Debug|x64.Build.0 = Debug|Any CPU
+ {6E069780-A892-487E-AEED-051E26C829A4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6E069780-A892-487E-AEED-051E26C829A4}.Debug|x86.Build.0 = Debug|Any CPU
+ {6E069780-A892-487E-AEED-051E26C829A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6E069780-A892-487E-AEED-051E26C829A4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6E069780-A892-487E-AEED-051E26C829A4}.Release|x64.ActiveCfg = Release|Any CPU
+ {6E069780-A892-487E-AEED-051E26C829A4}.Release|x64.Build.0 = Release|Any CPU
+ {6E069780-A892-487E-AEED-051E26C829A4}.Release|x86.ActiveCfg = Release|Any CPU
+ {6E069780-A892-487E-AEED-051E26C829A4}.Release|x86.Build.0 = Release|Any CPU
+ {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|x64.Build.0 = Debug|Any CPU
+ {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Debug|x86.Build.0 = Debug|Any CPU
+ {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|x64.ActiveCfg = Release|Any CPU
+ {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|x64.Build.0 = Release|Any CPU
+ {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|x86.ActiveCfg = Release|Any CPU
+ {6D0BDEA5-F3F5-4F7C-9152-040BF88E4F2D}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal