From a732030fc88c7edd0d9135e0a97b2861af5ed764 Mon Sep 17 00:00:00 2001 From: Michael Fyffe <6224270+TraGicCode@users.noreply.github.com> Date: Fri, 5 Dec 2025 23:55:13 -0600 Subject: [PATCH 1/2] WIP: Azure Storage Queues --- src/BuslyCLI.Console/BuslyCLI.Console.csproj | 2 + .../AzureStorageQueuesTransportConfig.cs | 6 + .../Config/TransportConfig.cs | 3 +- ...reStorageQueuesTransportConfigValidator.cs | 12 ++ .../ServiceCollectionExtensions.cs | 1 + .../Factories/RawEndpointFactory.cs | 7 ++ .../BuslyCLI.Console.Tests.csproj | 1 + .../AzureStorageQueuesEndToEndTestBase.cs | 20 +++ ...dCommandAzureStorageQueuesEndToEndTests.cs | 119 ++++++++++++++++++ .../Infrastructure/ITestEndpointFactory.cs | 12 +- .../SqlServer/SqlServerEndToEndTestBase.cs | 3 +- 11 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 src/BuslyCLI.Console/Config/AzureStorageQueuesTransportConfig.cs create mode 100644 src/BuslyCLI.Console/Config/Validators/AzureStorageQueuesTransportConfigValidator.cs create mode 100644 tests/BuslyCLI.Console.Tests/EndToEnd/AzureStorageQueues/AzureStorageQueuesEndToEndTestBase.cs create mode 100644 tests/BuslyCLI.Console.Tests/EndToEnd/AzureStorageQueues/SendCommandAzureStorageQueuesEndToEndTests.cs diff --git a/src/BuslyCLI.Console/BuslyCLI.Console.csproj b/src/BuslyCLI.Console/BuslyCLI.Console.csproj index b9b9783..8d3ec80 100644 --- a/src/BuslyCLI.Console/BuslyCLI.Console.csproj +++ b/src/BuslyCLI.Console/BuslyCLI.Console.csproj @@ -33,9 +33,11 @@ + + diff --git a/src/BuslyCLI.Console/Config/AzureStorageQueuesTransportConfig.cs b/src/BuslyCLI.Console/Config/AzureStorageQueuesTransportConfig.cs new file mode 100644 index 0000000..2548d80 --- /dev/null +++ b/src/BuslyCLI.Console/Config/AzureStorageQueuesTransportConfig.cs @@ -0,0 +1,6 @@ +namespace BuslyCLI.Config; + +public class AzureStorageQueuesTransportConfig : 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 7bc8da4..980ca7b 100644 --- a/src/BuslyCLI.Console/Config/TransportConfig.cs +++ b/src/BuslyCLI.Console/Config/TransportConfig.cs @@ -9,7 +9,7 @@ public class TransportConfig public RabbitmqTransportConfig RabbitmqTransportConfig { get; set; } public AmazonsqsTransportConfig AmazonsqsTransportConfig { get; set; } public AzureServiceBusTransportConfig AzureServiceBusTransportConfig { get; set; } - + public AzureStorageQueuesTransportConfig AzureStorageQueuesTransportConfig { get; set; } public SqlServerTransportConfig SqlServerTransportConfig { get; set; } // Helper property to unify config access: @@ -18,5 +18,6 @@ public class TransportConfig ?? (ITransportConfig)RabbitmqTransportConfig ?? (ITransportConfig)AmazonsqsTransportConfig ?? (ITransportConfig)AzureServiceBusTransportConfig + ?? (ITransportConfig)AzureStorageQueuesTransportConfig ?? SqlServerTransportConfig; } \ No newline at end of file diff --git a/src/BuslyCLI.Console/Config/Validators/AzureStorageQueuesTransportConfigValidator.cs b/src/BuslyCLI.Console/Config/Validators/AzureStorageQueuesTransportConfigValidator.cs new file mode 100644 index 0000000..553e7a0 --- /dev/null +++ b/src/BuslyCLI.Console/Config/Validators/AzureStorageQueuesTransportConfigValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; + +namespace BuslyCLI.Config.Validators; + +public class AzureStorageQueuesTransportConfigValidator : AbstractValidator +{ + public AzureStorageQueuesTransportConfigValidator() + { + 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 d1c350e..9e98f4c 100644 --- a/src/BuslyCLI.Console/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/BuslyCLI.Console/DependencyInjection/ServiceCollectionExtensions.cs @@ -35,6 +35,7 @@ private static IServiceCollection AddYamlDeserializer(this IServiceCollection se { "rabbitmq-transport-config", typeof(RabbitmqTransportConfig) }, { "amazonsqs-transport-config", typeof(AmazonsqsTransportConfig) }, { "azure-service-bus-transport-config", typeof(AzureServiceBusTransportConfig) }, + { "azure-storage-queues-transport-config", typeof(AzureStorageQueuesTransportConfig) }, { "sql-server-transport-config", typeof(SqlServerTransportConfig) } }; diff --git a/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs b/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs index a70cc49..d0194f7 100644 --- a/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs +++ b/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs @@ -30,6 +30,8 @@ private TransportDefinition CreateTransport(TransportConfig transportConfig) return CreateRabbitMQTransport(rabbitmqTransportConfig); case AzureServiceBusTransportConfig azureServiceBusTransportConfig: return CreateAzureServiceBusTransport(azureServiceBusTransportConfig.ConnectionString); + case AzureStorageQueuesTransportConfig azureStorageQueuesTransportConfig: + return CreateAzureStorageQueuesTransport(azureStorageQueuesTransportConfig.ConnectionString); case AmazonsqsTransportConfig amazonSqsTransportConfig: return CreateAmazonSQSTransport(amazonSqsTransportConfig); case SqlServerTransportConfig sqlServerTransportConfig: @@ -45,6 +47,11 @@ private TransportDefinition CreateTransport(TransportConfig transportConfig) } } + private TransportDefinition CreateAzureStorageQueuesTransport(string connectionString) + { + return new AzureStorageQueueTransport(connectionString); + } + private TransportDefinition CreateSqlServerTransport(SqlServerTransportConfig sqlServerTransportConfig) { return new SqlServerTransport(sqlServerTransportConfig.ConnectionString); diff --git a/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj b/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj index 89a262b..37f9afa 100644 --- a/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj +++ b/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj @@ -24,6 +24,7 @@ + diff --git a/tests/BuslyCLI.Console.Tests/EndToEnd/AzureStorageQueues/AzureStorageQueuesEndToEndTestBase.cs b/tests/BuslyCLI.Console.Tests/EndToEnd/AzureStorageQueues/AzureStorageQueuesEndToEndTestBase.cs new file mode 100644 index 0000000..d0cf3e5 --- /dev/null +++ b/tests/BuslyCLI.Console.Tests/EndToEnd/AzureStorageQueues/AzureStorageQueuesEndToEndTestBase.cs @@ -0,0 +1,20 @@ +using Testcontainers.Azurite; + +namespace BuslyCLI.Console.Tests.EndToEnd.AzureStorageQueues; + +[TestFixture] +public abstract class AzureStorageQueuesEndToEndTestBase : SingletonTestFixtureBase +{ + protected AzuriteContainer AzuriteContainer => Container; + + protected override AzuriteContainer CreateContainer() + { + return new AzuriteBuilder() + .Build(); + } + + protected override async Task StartContainerAsync(AzuriteContainer container) + { + await container.StartAsync(); + } +} \ No newline at end of file diff --git a/tests/BuslyCLI.Console.Tests/EndToEnd/AzureStorageQueues/SendCommandAzureStorageQueuesEndToEndTests.cs b/tests/BuslyCLI.Console.Tests/EndToEnd/AzureStorageQueues/SendCommandAzureStorageQueuesEndToEndTests.cs new file mode 100644 index 0000000..e1ddd9d --- /dev/null +++ b/tests/BuslyCLI.Console.Tests/EndToEnd/AzureStorageQueues/SendCommandAzureStorageQueuesEndToEndTests.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.AzureStorageQueues; + +[TestFixture] +public class SendCommandAzureStorageQueuesEndToEndTests : AzureStorageQueuesEndToEndTestBase +{ + [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-azure-storage-queues + transports: + - name: local-azure-storage-queues + azure-storage-queues-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-azure-storage-queues + transports: + - name: local-azure-storage-queues + azure-storage-queues-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().CreateAzureStorageQueuesTestEndpoint(Container.GetConnectionString()); + + await testAction(testEndpoint); + await testEndpoint.ShutDownAndCleanUp(); + } +} \ 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 6144c12..a1a7a14 100644 --- a/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs +++ b/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs @@ -1,6 +1,7 @@ using Amazon.Runtime; using Amazon.SimpleNotificationService; using Amazon.SQS; +using NServiceBus.Settings; using NServiceBus.Transport; namespace BuslyCLI.Console.Tests.EndToEnd.Infrastructure; @@ -67,6 +68,7 @@ public async Task CreateAzureServiceBusTestEndpoint(string endpoin private static async Task InternalCreateTestEndpoint(string endpointName, TransportDefinition transport) { + var hostSettings = new HostSettings( endpointName, endpointName, @@ -76,7 +78,8 @@ private static async Task InternalCreateTestEndpoint(string endpoi TestContext.Out.WriteLine("Critical error: " + exception); }, // TODO: This needs to be false for "Azure Service Bus Emulator" tests to pass - transport is not AzureServiceBusTransport); + transport is not AzureServiceBusTransport + ); var infrastructure = await transport.Initialize(hostSettings, [ new ReceiveSettings( @@ -97,4 +100,11 @@ public async Task CreateSqlServerTestEndpoint(string sqlConnection var transport = new SqlServerTransport(sqlConnectionString); return await InternalCreateTestEndpoint(name, transport); } + + public async Task CreateAzureStorageQueuesTestEndpoint(string connectionString) + { + var name = GenerateUniqueEndpointName(); + var transport = new AzureStorageQueueTransport(connectionString); + return await InternalCreateTestEndpoint(name, transport); + } } \ 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 index 23f4b75..09a89f0 100644 --- a/tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SqlServerEndToEndTestBase.cs +++ b/tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SqlServerEndToEndTestBase.cs @@ -1,5 +1,4 @@ -using Microsoft.Data.SqlClient; -using Testcontainers.MsSql; +using Testcontainers.MsSql; namespace BuslyCLI.Console.Tests.EndToEnd.SqlServer; From 7ca9f751310c28715ee341e6b5721a9bd65c4743 Mon Sep 17 00:00:00 2001 From: Michael Fyffe <6224270+TraGicCode@users.noreply.github.com> Date: Sat, 6 Dec 2025 13:51:39 -0600 Subject: [PATCH 2/2] Implement azure storage queues transport --- .../Factories/RawEndpointFactory.cs | 4 +- .../AzureStorageQueuesEndToEndTestBase.cs | 1 + .../Infrastructure/ITestEndpointFactory.cs | 2 + .../docs/transports/azure-storage-queues.md | 40 +++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 website/docs/transports/azure-storage-queues.md diff --git a/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs b/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs index d0194f7..c9125ba 100644 --- a/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs +++ b/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs @@ -49,7 +49,9 @@ private TransportDefinition CreateTransport(TransportConfig transportConfig) private TransportDefinition CreateAzureStorageQueuesTransport(string connectionString) { - return new AzureStorageQueueTransport(connectionString); + var transport = new AzureStorageQueueTransport(connectionString); + transport.MessageWrapperSerializationDefinition = new SystemJsonSerializer(); + return transport; } private TransportDefinition CreateSqlServerTransport(SqlServerTransportConfig sqlServerTransportConfig) diff --git a/tests/BuslyCLI.Console.Tests/EndToEnd/AzureStorageQueues/AzureStorageQueuesEndToEndTestBase.cs b/tests/BuslyCLI.Console.Tests/EndToEnd/AzureStorageQueues/AzureStorageQueuesEndToEndTestBase.cs index d0cf3e5..0b9b1ae 100644 --- a/tests/BuslyCLI.Console.Tests/EndToEnd/AzureStorageQueues/AzureStorageQueuesEndToEndTestBase.cs +++ b/tests/BuslyCLI.Console.Tests/EndToEnd/AzureStorageQueues/AzureStorageQueuesEndToEndTestBase.cs @@ -10,6 +10,7 @@ public abstract class AzureStorageQueuesEndToEndTestBase : SingletonTestFixtureB protected override AzuriteContainer CreateContainer() { return new AzuriteBuilder() + .WithCommand("--skipApiVersionCheck") .Build(); } diff --git a/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs b/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs index a1a7a14..ca849f3 100644 --- a/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs +++ b/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs @@ -105,6 +105,8 @@ public async Task CreateAzureStorageQueuesTestEndpoint(string conn { var name = GenerateUniqueEndpointName(); var transport = new AzureStorageQueueTransport(connectionString); + transport.MessageWrapperSerializationDefinition = new SystemJsonSerializer(); + return await InternalCreateTestEndpoint(name, transport); } } \ No newline at end of file diff --git a/website/docs/transports/azure-storage-queues.md b/website/docs/transports/azure-storage-queues.md new file mode 100644 index 0000000..d0bc085 --- /dev/null +++ b/website/docs/transports/azure-storage-queues.md @@ -0,0 +1,40 @@ +# Azure Storage Queues + +The **Azure Storage Queues Transport** is used to communicate to Azure Storage Queues. It is suitable for development, testing, and production environments. + +## Configuration + +To use the Azure Storage Queues Transport, define it under `transports` and reference it as `current-transport`. + +### Example + +```yaml +current-transport: local-azure-service-bus + +transports: + - name: local-azure-service-bus + azure-storage-queues-transport-config: + connection-string: Endpoint=amqp://127.0.0.1:32799/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true +``` + +--- + +## `azure-storage-queues-transport-config` Fields + +| Field | Required | Type | Default | Description | +| ------------------- | -------- | ------ | ------- | ------------------------------------------ | +| `connection-string` | **Yes** | string | — | Connection string to Azure Storage Queues. | + +--- + +## Field Details + +### `connection-string` (required) + +A standard conection string for Azure Storage Queues. + +Examples: + +```yaml +connection-string: UseDevelopmentStorage=true +```