Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/BuslyCLI.Console/BuslyCLI.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<PackageReference Include="NServiceBus.AmazonSQS" Version="8.0.0" />
<PackageReference Include="NServiceBus.RabbitMQ" Version="10.1.6" />
<PackageReference Include="NServiceBus.Transport.AzureServiceBus" Version="5.1.2" />
<PackageReference Include="NServiceBus.Transport.SqlServer" Version="8.1.10" />
<PackageReference Include="Spectre.Console.Cli" Version="0.53.1" />
<PackageReference Include="Spectre.Console.Cli.Extensions.DependencyInjection" Version="0.20.0" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
6 changes: 6 additions & 0 deletions src/BuslyCLI.Console/Config/SqlServerTransportConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace BuslyCLI.Config;

public class SqlServerTransportConfig : ITransportConfig
{
public string ConnectionString { get; set; }
}
5 changes: 4 additions & 1 deletion src/BuslyCLI.Console/Config/TransportConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using FluentValidation;

namespace BuslyCLI.Config.Validators;

public class SqlServerTransportConfigValidator : AbstractValidator<SqlServerTransportConfig>
{
public SqlServerTransportConfigValidator()
{
RuleFor(x => x.ConnectionString)
.NotEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ITransportConfig>(keyMappings);
Expand Down
7 changes: 7 additions & 0 deletions src/BuslyCLI.Console/Factories/RawEndpointFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
<PackageReference Include="Spectre.Console.Cli.Testing" Version="1.0.0-alpha.0.11" />
<PackageReference Include="Testcontainers.LocalStack" Version="4.9.0" />
<PackageReference Include="Testcontainers.MsSql" Version="4.9.0" />
<PackageReference Include="Testcontainers.RabbitMq" Version="4.9.0" />
<PackageReference Include="Testcontainers.ServiceBus" Version="4.9.0" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public async Task ShouldErrorWhenConnectionStringIsNotPassed()
}

[Test]
public async Task ShouldNotErrorStorageDirectoryIsPassed()
public async Task ShouldNotErrorConnectionStringIsPassed()
{
// Arrange
var azureServiceBusTransportConfig = new AzureServiceBusTransportConfig
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,11 @@ private static async Task<TestEndpoint> InternalCreateTestEndpoint(string endpoi

return new TestEndpoint(infrastructure);
}

public async Task<TestEndpoint> CreateSqlServerTestEndpoint(string sqlConnectionString)
{
var name = GenerateUniqueEndpointName();
var transport = new SqlServerTransport(sqlConnectionString);
return await InternalCreateTestEndpoint(name, transport);
}
}
Original file line number Diff line number Diff line change
@@ -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<TestEndpoint, Task> testAction)
{
var testEndpoint = await new TestEndpointFactory().CreateSqlServerTestEndpoint(Container.GetConnectionString());

await testAction(testEndpoint);
await testEndpoint.ShutDownAndCleanUp();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.Data.SqlClient;
using Testcontainers.MsSql;

namespace BuslyCLI.Console.Tests.EndToEnd.SqlServer;

[TestFixture]
public abstract class SqlServerEndToEndTestBase : SingletonTestFixtureBase<MsSqlContainer>
{
protected MsSqlContainer SqlServerContainer => Container;

protected override MsSqlContainer CreateContainer()
{
return new MsSqlBuilder()
.Build();
}

protected override async Task StartContainerAsync(MsSqlContainer container)
{
await container.StartAsync();
}
}
40 changes: 40 additions & 0 deletions website/docs/transports/sql-server.md
Original file line number Diff line number Diff line change
@@ -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
```