From da23851cf5d435822a3632941d3fb0c2076681f3 Mon Sep 17 00:00:00 2001
From: Michael Fyffe <6224270+TraGicCode@users.noreply.github.com>
Date: Fri, 5 Dec 2025 23:08:45 -0600
Subject: [PATCH] Add support for SqlServer Transport
---
src/BuslyCLI.Console/BuslyCLI.Console.csproj | 1 +
.../Transport/ListTransportsCommand.cs | 2 +
.../Config/SqlServerTransportConfig.cs | 6 +
.../Config/TransportConfig.cs | 5 +-
.../SqlServerTransportConfigValidator.cs | 12 ++
.../ServiceCollectionExtensions.cs | 4 +-
.../Factories/RawEndpointFactory.cs | 7 ++
.../BuslyCLI.Console.Tests.csproj | 1 +
...ServiceBusTransportConfigValidatorTests.cs | 2 +-
.../SqlServerTransportConfigValidatorTests.cs | 47 +++++++
.../Infrastructure/ITestEndpointFactory.cs | 7 ++
.../SendCommandSqlServerEndToEndTests.cs | 119 ++++++++++++++++++
.../SqlServer/SqlServerEndToEndTestBase.cs | 21 ++++
website/docs/transports/sql-server.md | 40 ++++++
14 files changed, 271 insertions(+), 3 deletions(-)
create mode 100644 src/BuslyCLI.Console/Config/SqlServerTransportConfig.cs
create mode 100644 src/BuslyCLI.Console/Config/Validators/SqlServerTransportConfigValidator.cs
create mode 100644 tests/BuslyCLI.Console.Tests/Config/Validators/SqlServerTransportConfigValidatorTests.cs
create mode 100644 tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SendCommandSqlServerEndToEndTests.cs
create mode 100644 tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SqlServerEndToEndTestBase.cs
create mode 100644 website/docs/transports/sql-server.md
diff --git a/src/BuslyCLI.Console/BuslyCLI.Console.csproj b/src/BuslyCLI.Console/BuslyCLI.Console.csproj
index 2c9d913..b9b9783 100644
--- a/src/BuslyCLI.Console/BuslyCLI.Console.csproj
+++ b/src/BuslyCLI.Console/BuslyCLI.Console.csproj
@@ -33,6 +33,7 @@
+
diff --git a/src/BuslyCLI.Console/Commands/Transport/ListTransportsCommand.cs b/src/BuslyCLI.Console/Commands/Transport/ListTransportsCommand.cs
index 358f590..861a4ca 100644
--- a/src/BuslyCLI.Console/Commands/Transport/ListTransportsCommand.cs
+++ b/src/BuslyCLI.Console/Commands/Transport/ListTransportsCommand.cs
@@ -59,6 +59,8 @@ public string TransportConfigTypeToString(ITransportConfig transportConfig)
return "azure-service-bus";
case AmazonsqsTransportConfig amazonsqsTransportConfig:
return "amazon-sqs";
+ case SqlServerTransportConfig sqlServerTransportConfig:
+ return "sql-server";
default:
throw new ApplicationException("Unknown transport type");
}
diff --git a/src/BuslyCLI.Console/Config/SqlServerTransportConfig.cs b/src/BuslyCLI.Console/Config/SqlServerTransportConfig.cs
new file mode 100644
index 0000000..5992bfd
--- /dev/null
+++ b/src/BuslyCLI.Console/Config/SqlServerTransportConfig.cs
@@ -0,0 +1,6 @@
+namespace BuslyCLI.Config;
+
+public class SqlServerTransportConfig : ITransportConfig
+{
+ public string ConnectionString { get; set; }
+}
\ No newline at end of file
diff --git a/src/BuslyCLI.Console/Config/TransportConfig.cs b/src/BuslyCLI.Console/Config/TransportConfig.cs
index 7b5a938..7bc8da4 100644
--- a/src/BuslyCLI.Console/Config/TransportConfig.cs
+++ b/src/BuslyCLI.Console/Config/TransportConfig.cs
@@ -10,10 +10,13 @@ public class TransportConfig
public AmazonsqsTransportConfig AmazonsqsTransportConfig { get; set; }
public AzureServiceBusTransportConfig AzureServiceBusTransportConfig { get; set; }
+ public SqlServerTransportConfig SqlServerTransportConfig { get; set; }
+
// Helper property to unify config access:
[YamlIgnore]
public ITransportConfig Config => (ITransportConfig)LearningTransportConfig
?? (ITransportConfig)RabbitmqTransportConfig
?? (ITransportConfig)AmazonsqsTransportConfig
- ?? (ITransportConfig)AzureServiceBusTransportConfig;
+ ?? (ITransportConfig)AzureServiceBusTransportConfig
+ ?? SqlServerTransportConfig;
}
\ No newline at end of file
diff --git a/src/BuslyCLI.Console/Config/Validators/SqlServerTransportConfigValidator.cs b/src/BuslyCLI.Console/Config/Validators/SqlServerTransportConfigValidator.cs
new file mode 100644
index 0000000..aed2082
--- /dev/null
+++ b/src/BuslyCLI.Console/Config/Validators/SqlServerTransportConfigValidator.cs
@@ -0,0 +1,12 @@
+using FluentValidation;
+
+namespace BuslyCLI.Config.Validators;
+
+public class SqlServerTransportConfigValidator : AbstractValidator
+{
+ public SqlServerTransportConfigValidator()
+ {
+ RuleFor(x => x.ConnectionString)
+ .NotEmpty();
+ }
+}
\ No newline at end of file
diff --git a/src/BuslyCLI.Console/DependencyInjection/ServiceCollectionExtensions.cs b/src/BuslyCLI.Console/DependencyInjection/ServiceCollectionExtensions.cs
index e1ed09f..d1c350e 100644
--- a/src/BuslyCLI.Console/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/src/BuslyCLI.Console/DependencyInjection/ServiceCollectionExtensions.cs
@@ -33,7 +33,9 @@ private static IServiceCollection AddYamlDeserializer(this IServiceCollection se
{
{ "learning-transport-config", typeof(LearningTransportConfig) },
{ "rabbitmq-transport-config", typeof(RabbitmqTransportConfig) },
- { "amazonsqs-transport-config", typeof(AmazonsqsTransportConfig) }
+ { "amazonsqs-transport-config", typeof(AmazonsqsTransportConfig) },
+ { "azure-service-bus-transport-config", typeof(AzureServiceBusTransportConfig) },
+ { "sql-server-transport-config", typeof(SqlServerTransportConfig) }
};
o.AddUniqueKeyTypeDiscriminator(keyMappings);
diff --git a/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs b/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs
index 3ef3137..d0ad5db 100644
--- a/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs
+++ b/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs
@@ -32,6 +32,8 @@ private TransportDefinition CreateTransport(TransportConfig transportConfig)
return CreateAzureServiceBusTransport(azureServiceBusTransportConfig.ConnectionString);
case AmazonsqsTransportConfig amazonSqsTransportConfig:
return CreateAmazonSQSTransport(amazonSqsTransportConfig);
+ case SqlServerTransportConfig sqlServerTransportConfig:
+ return CreateSqlServerTransport(sqlServerTransportConfig);
case LearningTransportConfig learningTransportConfig:
return new LearningTransport
{
@@ -43,6 +45,11 @@ private TransportDefinition CreateTransport(TransportConfig transportConfig)
}
}
+ private TransportDefinition CreateSqlServerTransport(SqlServerTransportConfig sqlServerTransportConfig)
+ {
+ return new SqlServerTransport(sqlServerTransportConfig.ConnectionString);
+ }
+
private RabbitMQTransport CreateRabbitMQTransport(RabbitmqTransportConfig rabbitmqTransportConfig)
{
var t = new RabbitMQTransport(RoutingTopology.Conventional(QueueType.Quorum), rabbitmqTransportConfig.AmqpConnectionString);
diff --git a/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj b/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj
index f9de6b7..89a262b 100644
--- a/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj
+++ b/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj
@@ -25,6 +25,7 @@
+
diff --git a/tests/BuslyCLI.Console.Tests/Config/Validators/AzureServiceBusTransportConfigValidatorTests.cs b/tests/BuslyCLI.Console.Tests/Config/Validators/AzureServiceBusTransportConfigValidatorTests.cs
index d5cf0cc..efa0d26 100644
--- a/tests/BuslyCLI.Console.Tests/Config/Validators/AzureServiceBusTransportConfigValidatorTests.cs
+++ b/tests/BuslyCLI.Console.Tests/Config/Validators/AzureServiceBusTransportConfigValidatorTests.cs
@@ -31,7 +31,7 @@ public async Task ShouldErrorWhenConnectionStringIsNotPassed()
}
[Test]
- public async Task ShouldNotErrorStorageDirectoryIsPassed()
+ public async Task ShouldNotErrorConnectionStringIsPassed()
{
// Arrange
var azureServiceBusTransportConfig = new AzureServiceBusTransportConfig
diff --git a/tests/BuslyCLI.Console.Tests/Config/Validators/SqlServerTransportConfigValidatorTests.cs b/tests/BuslyCLI.Console.Tests/Config/Validators/SqlServerTransportConfigValidatorTests.cs
new file mode 100644
index 0000000..5bf4553
--- /dev/null
+++ b/tests/BuslyCLI.Console.Tests/Config/Validators/SqlServerTransportConfigValidatorTests.cs
@@ -0,0 +1,47 @@
+using BuslyCLI.Config;
+using BuslyCLI.Config.Validators;
+using FluentValidation.TestHelper;
+
+namespace BuslyCLI.Console.Tests.Config.Validators;
+
+[TestFixture]
+public class SqlServerTransportConfigValidatorTests
+{
+ private readonly SqlServerTransportConfigValidator _validator;
+
+ public SqlServerTransportConfigValidatorTests()
+ {
+ _validator = new SqlServerTransportConfigValidator();
+ }
+
+ [Test]
+ public async Task ShouldErrorWhenConnectionStringIsNotPassed()
+ {
+ // Arrange
+ var sqlServerTransportConfig = new SqlServerTransportConfig
+ {
+ ConnectionString = null
+ };
+ // Act
+ var result = await _validator.TestValidateAsync(sqlServerTransportConfig);
+
+ // Assert
+ result.ShouldHaveValidationErrorFor(c => c.ConnectionString)
+ .WithErrorMessage("'Connection String' must not be empty.");
+ }
+
+ [Test]
+ public async Task ShouldNotErrorConnectionStringIsPassed()
+ {
+ // Arrange
+ var sqlServerTransportConfig = new SqlServerTransportConfig
+ {
+ ConnectionString = "Data Source=(local);Initial Catalog=Ordering;Integrated Security=SSPI;Application Name=Busly-CLI;TrustServerCertificate=true"
+ };
+ // Act
+ var result = await _validator.TestValidateAsync(sqlServerTransportConfig);
+
+ // Assert
+ result.ShouldNotHaveValidationErrorFor(c => c.ConnectionString);
+ }
+}
\ No newline at end of file
diff --git a/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs b/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs
index 35f7dbe..8052360 100644
--- a/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs
+++ b/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs
@@ -90,4 +90,11 @@ private static async Task InternalCreateTestEndpoint(string endpoi
return new TestEndpoint(infrastructure);
}
+
+ public async Task CreateSqlServerTestEndpoint(string sqlConnectionString)
+ {
+ var name = GenerateUniqueEndpointName();
+ var transport = new SqlServerTransport(sqlConnectionString);
+ return await InternalCreateTestEndpoint(name, transport);
+ }
}
\ No newline at end of file
diff --git a/tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SendCommandSqlServerEndToEndTests.cs b/tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SendCommandSqlServerEndToEndTests.cs
new file mode 100644
index 0000000..e3e4d85
--- /dev/null
+++ b/tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SendCommandSqlServerEndToEndTests.cs
@@ -0,0 +1,119 @@
+using System.Text;
+using System.Text.Json;
+using BuslyCLI.Console.Tests.EndToEnd.Infrastructure;
+using BuslyCLI.Console.Tests.TestHelpers;
+using BuslyCLI.DependencyInjection;
+using BuslyCLI.Spectre;
+using Microsoft.Extensions.DependencyInjection;
+using Spectre.Console.Cli.Extensions.DependencyInjection;
+using Spectre.Console.Cli.Testing;
+
+namespace BuslyCLI.Console.Tests.EndToEnd.SqlServer;
+
+[TestFixture]
+public class SendCommandSqlServerEndToEndTests : SqlServerEndToEndTestBase
+{
+ [SetUp]
+ public void Setup()
+ {
+ var registrations = new ServiceCollection();
+ registrations.AddBuslyCLIServices();
+ using var registrar = new DependencyInjectionRegistrar(registrations);
+ _sut = new CommandAppTester(registrar);
+ _sut.Configure(AppConfiguration.GetSpectreCommandConfiguration());
+ }
+
+ private CommandAppTester _sut;
+
+ private readonly JsonSerializerOptions _jsonObjectOptions =
+ new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true };
+
+ [Test]
+ public async Task ShouldSendCommand()
+ {
+ await RunWithTestEndpoint(async testEndpoint =>
+ {
+ // Arrange
+ await testEndpoint.StartEndpoint();
+ var messageBody = new { OrderNumber = Guid.NewGuid() };
+ var json = JsonSerializer.Serialize(messageBody, _jsonObjectOptions);
+ var yamlFile = $"""
+ ---
+ current-transport: local-sql-server
+ transports:
+ - name: local-sql-server
+ sql-server-transport-config:
+ connection-string: {Container.GetConnectionString()}
+ """;
+ using var configFile = new TestableNServiceBusConfigurationFile(yamlFile);
+
+ // Act
+ var result = _sut.Run(
+ "command",
+ "send",
+ "--content-type", "application/json",
+ "--enclosed-message-type", "MessageContracts.Commands.CreateOrder",
+ "--destination-endpoint", testEndpoint.EndpointName,
+ "--message-body", json,
+ "--config", configFile.FilePath);
+
+ // Assert
+ Assert.That(result.ExitCode, Is.EqualTo(0));
+ var message = testEndpoint.TryReceiveMessage();
+ Assert.That(message.Headers["NServiceBus.EnclosedMessageTypes"],
+ Is.EqualTo("MessageContracts.Commands.CreateOrder"));
+ Assert.That(message.Headers["NServiceBus.ContentType"], Is.EqualTo("application/json"));
+ Assert.That(Encoding.UTF8.GetString(message.Body.Span), Is.EqualTo(json));
+ });
+ }
+
+ [Test]
+ public async Task ShouldPublishEvent()
+ {
+ await RunWithTestEndpoint(async testEndpoint =>
+ {
+ // Arrange
+ await testEndpoint.StartEndpoint();
+ await testEndpoint.Subscribe("MessageContracts.Events.OrderCreated");
+ var messageBody = new { OrderNumber = Guid.NewGuid() };
+ var json = JsonSerializer.Serialize(messageBody, _jsonObjectOptions);
+ var yamlFile = $"""
+ ---
+ current-transport: local-sql-server
+ transports:
+ - name: local-sql-server
+ sql-server-transport-config:
+ connection-string: {Container.GetConnectionString()}
+ """;
+ using var configFile = new TestableNServiceBusConfigurationFile(yamlFile);
+
+ // Act
+ var result = _sut.Run(
+ "event",
+ "publish",
+ "--content-type", "application/json",
+ "--enclosed-message-type", "MessageContracts.Events.OrderCreated",
+ "--message-body", json,
+ "--config", configFile.FilePath);
+
+ // Assert
+ Assert.That(result.ExitCode, Is.EqualTo(0));
+ var message = testEndpoint.TryReceiveMessage();
+ Assert.That(message.Headers["NServiceBus.EnclosedMessageTypes"],
+ Is.EqualTo("MessageContracts.Events.OrderCreated"));
+ Assert.That(message.Headers["NServiceBus.ContentType"], Is.EqualTo("application/json"));
+ Assert.That(Encoding.UTF8.GetString(message.Body.Span), Is.EqualTo(json));
+ });
+ }
+
+ // Test Endpoint
+ // Example of how to wait for and get messages
+ // https://github.com/Particular/NServiceBus.RabbitMQ/blob/dba627a5a2c50519d7a2466efe3f76c8d5c8828d/src/NServiceBus.Transport.RabbitMQ.Tests/RabbitMqContext.cs#L41
+ private async Task RunWithTestEndpoint(Func testAction)
+ {
+ var testEndpoint = await new TestEndpointFactory().CreateSqlServerTestEndpoint(Container.GetConnectionString());
+
+ await testAction(testEndpoint);
+ await testEndpoint.ShutDownAndCleanUp();
+ }
+}
\ No newline at end of file
diff --git a/tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SqlServerEndToEndTestBase.cs b/tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SqlServerEndToEndTestBase.cs
new file mode 100644
index 0000000..23f4b75
--- /dev/null
+++ b/tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SqlServerEndToEndTestBase.cs
@@ -0,0 +1,21 @@
+using Microsoft.Data.SqlClient;
+using Testcontainers.MsSql;
+
+namespace BuslyCLI.Console.Tests.EndToEnd.SqlServer;
+
+[TestFixture]
+public abstract class SqlServerEndToEndTestBase : SingletonTestFixtureBase
+{
+ protected MsSqlContainer SqlServerContainer => Container;
+
+ protected override MsSqlContainer CreateContainer()
+ {
+ return new MsSqlBuilder()
+ .Build();
+ }
+
+ protected override async Task StartContainerAsync(MsSqlContainer container)
+ {
+ await container.StartAsync();
+ }
+}
\ No newline at end of file
diff --git a/website/docs/transports/sql-server.md b/website/docs/transports/sql-server.md
new file mode 100644
index 0000000..c6101e3
--- /dev/null
+++ b/website/docs/transports/sql-server.md
@@ -0,0 +1,40 @@
+# Sql Server
+
+The **Sql Server Transport** is used to communicate to Microsoft Sql Server. It is suitable for development, testing, and production environments.
+
+## Configuration
+
+To use the Sql Server Transport, define it under `transports` and reference it as `current-transport`.
+
+### Example
+
+```yaml
+current-transport: local-sql-server
+
+transports:
+ - name: local-sql-server
+ sql-server-transport-config:
+ connection-string: Data Source=(local);Initial Catalog=Ordering;Integrated Security=SSPI;Application Name=Busly-CLI;TrustServerCertificate=true
+```
+
+---
+
+## `sql-server-transport-config` Fields
+
+| Field | Required | Type | Default | Description |
+| ------------------- | -------- | ------ | ------- | ---------------------------------- |
+| `connection-string` | **Yes** | string | — | Full Sql Server Connection string. |
+
+---
+
+## Field Details
+
+### `connection-string` (required)
+
+Sql Server connection string used to connect to Microsoft SQL Server.
+
+Examples:
+
+```yaml
+connection-string: Data Source=(local);Initial Catalog=Ordering;Integrated Security=SSPI;Application Name=Busly-CLI;TrustServerCertificate=true
+```