From 068def944d572291e5c66ce48b7e387e937b6ac3 Mon Sep 17 00:00:00 2001 From: Vladimir Rodchenko Date: Tue, 23 Mar 2021 23:50:24 +0300 Subject: [PATCH 1/3] initial code for Azure Service Bus sender --- .../AzureServiceBusSender.cs | 51 +++++++ .../Class1.cs | 6 - ...GodelTech.Messaging.AzureServiceBus.csproj | 1 + .../Options/AzureServiceBusOptions.cs | 15 ++ .../AzureServiceBusSenderTests.cs | 140 ++++++++++++++++++ .../Fakes/FakeModel.cs | 9 ++ ...ech.Messaging.AzureServiceBus.Tests.csproj | 1 + .../UnitTest1.cs | 13 -- 8 files changed, 217 insertions(+), 19 deletions(-) create mode 100644 src/GodelTech.Messaging.AzureServiceBus/AzureServiceBusSender.cs delete mode 100644 src/GodelTech.Messaging.AzureServiceBus/Class1.cs create mode 100644 src/GodelTech.Messaging.AzureServiceBus/Options/AzureServiceBusOptions.cs create mode 100644 test/GodelTech.Messaging.AzureServiceBus.Tests/AzureServiceBusSenderTests.cs create mode 100644 test/GodelTech.Messaging.AzureServiceBus.Tests/Fakes/FakeModel.cs delete mode 100644 test/GodelTech.Messaging.AzureServiceBus.Tests/UnitTest1.cs diff --git a/src/GodelTech.Messaging.AzureServiceBus/AzureServiceBusSender.cs b/src/GodelTech.Messaging.AzureServiceBus/AzureServiceBusSender.cs new file mode 100644 index 0000000..772c2c7 --- /dev/null +++ b/src/GodelTech.Messaging.AzureServiceBus/AzureServiceBusSender.cs @@ -0,0 +1,51 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using GodelTech.Messaging.AzureServiceBus.Options; +using Microsoft.Extensions.Options; + +namespace GodelTech.Messaging.AzureServiceBus +{ + /// + /// Azure Service Bus sender + /// + public class AzureServiceBusSender + { + private readonly ServiceBusClient _serviceBusClient; + private readonly AzureServiceBusOptions _azureServiceBusOptions; + + /// + /// Initializes a new instance of the class. + /// + /// Azure Service Bus client. + /// Azure Service Bus options. + public AzureServiceBusSender(ServiceBusClient serviceBusClient, IOptions azureServiceBusOptions) + { + if (azureServiceBusOptions == null) throw new ArgumentNullException(nameof(azureServiceBusOptions)); + + _serviceBusClient = serviceBusClient; + _azureServiceBusOptions = azureServiceBusOptions.Value; + } + + /// + /// Asynchronously sends TModel object as JSON to Azure Service Bus queue. + /// Queue is select by key from options. + /// + /// The type of the T model. + /// Queue key. + /// The model. + /// No queue found with provided key. + public async Task SendAsync(string queueKey, TModel model) + where TModel : class + { + if (!_azureServiceBusOptions.Queues.TryGetValue(queueKey, out var queueName)) throw new ArgumentOutOfRangeException(nameof(queueKey), queueKey, "No queue found with provided key."); + + var sender = _serviceBusClient.CreateSender(queueName); + + var messageToSend = new ServiceBusMessage(JsonSerializer.Serialize(model)); + + await sender.SendMessageAsync(messageToSend); + } + } +} \ No newline at end of file diff --git a/src/GodelTech.Messaging.AzureServiceBus/Class1.cs b/src/GodelTech.Messaging.AzureServiceBus/Class1.cs deleted file mode 100644 index d106583..0000000 --- a/src/GodelTech.Messaging.AzureServiceBus/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace GodelTech.Messaging.AzureServiceBus -{ - public class Class1 - { - } -} diff --git a/src/GodelTech.Messaging.AzureServiceBus/GodelTech.Messaging.AzureServiceBus.csproj b/src/GodelTech.Messaging.AzureServiceBus/GodelTech.Messaging.AzureServiceBus.csproj index 97d1491..e351ef6 100644 --- a/src/GodelTech.Messaging.AzureServiceBus/GodelTech.Messaging.AzureServiceBus.csproj +++ b/src/GodelTech.Messaging.AzureServiceBus/GodelTech.Messaging.AzureServiceBus.csproj @@ -29,6 +29,7 @@ + diff --git a/src/GodelTech.Messaging.AzureServiceBus/Options/AzureServiceBusOptions.cs b/src/GodelTech.Messaging.AzureServiceBus/Options/AzureServiceBusOptions.cs new file mode 100644 index 0000000..6db5771 --- /dev/null +++ b/src/GodelTech.Messaging.AzureServiceBus/Options/AzureServiceBusOptions.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace GodelTech.Messaging.AzureServiceBus.Options +{ + /// + /// Azure Service Bus options + /// + public class AzureServiceBusOptions + { + /// + /// Queue dictionary that stores key value pair of internal queue key and Azure queue name. + /// + public IDictionary Queues { get; set; } + } +} \ No newline at end of file diff --git a/test/GodelTech.Messaging.AzureServiceBus.Tests/AzureServiceBusSenderTests.cs b/test/GodelTech.Messaging.AzureServiceBus.Tests/AzureServiceBusSenderTests.cs new file mode 100644 index 0000000..98a485f --- /dev/null +++ b/test/GodelTech.Messaging.AzureServiceBus.Tests/AzureServiceBusSenderTests.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using GodelTech.Messaging.AzureServiceBus.Options; +using GodelTech.Messaging.AzureServiceBus.Tests.Fakes; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace GodelTech.Messaging.AzureServiceBus.Tests +{ + public class AzureServiceBusSenderTests + { + private readonly Mock _mockServiceBusClient; + + public AzureServiceBusSenderTests() + { + _mockServiceBusClient = new Mock(MockBehavior.Strict); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException() + { + // Arrange & Act & Assert + var exception = Assert.Throws( + () => new AzureServiceBusSender(_mockServiceBusClient.Object, null) + ); + + Assert.Equal("azureServiceBusOptions", exception.ParamName); + } + + [Fact] + public async Task SendAsync_ThrowsArgumentOutOfRangeException() + { + // Arrange + var model = new List(); + + var azureServiceBusOptions = new AzureServiceBusOptions + { + Queues = new Dictionary + { + { + "OtherInternalKey", + "OtherAzureServiceBusQueueName" + } + } + }; + + var mockAzureServiceBusOptions = new Mock>(); + mockAzureServiceBusOptions + .Setup(x => x.Value) + .Returns(azureServiceBusOptions); + + var sender = new AzureServiceBusSender(_mockServiceBusClient.Object, mockAzureServiceBusOptions.Object); + + var expectedException = new ArgumentOutOfRangeException($"queueKey", "InternalKey", "No queue found with provided key."); + + // Act & Assert + var exception = + await Assert.ThrowsAsync( + () => + sender.SendAsync("InternalKey", model) + ); + + Assert.IsType(exception); + Assert.Equal("queueKey", exception.ParamName); + Assert.Equal("InternalKey", exception.ActualValue); + Assert.Equal(expectedException.Message, exception.Message); + } + + [Fact] + public async Task SendAsync_Success() + { + // Arrange + var model = new List + { + new FakeModel + { + Id = 1, + Name = "TestName" + } + }; + + var serializedModel = JsonSerializer.Serialize(model); + + var azureServiceBusOptions = new AzureServiceBusOptions + { + Queues = new Dictionary + { + { + "InternalKey", + "AzureServiceBusQueueName" + } + } + }; + + var mockAzureServiceBusOptions = new Mock>(); + mockAzureServiceBusOptions + .Setup(x => x.Value) + .Returns(azureServiceBusOptions); + + var sender = new AzureServiceBusSender(_mockServiceBusClient.Object, mockAzureServiceBusOptions.Object); + + var mockServiceBusSender = new Mock(MockBehavior.Strict); + mockServiceBusSender + .Setup( + x => x.SendMessageAsync( + It.Is( + serviceBusMessage => + serviceBusMessage.Body.ToString() == serializedModel + ), + It.Is( + cancellationToken => cancellationToken == default + ) + ) + ) + .Returns(Task.CompletedTask) + .Verifiable(); + + _mockServiceBusClient + .Setup( + x => x.CreateSender( + It.Is(queueOrTopicName => queueOrTopicName == "AzureServiceBusQueueName") + ) + ) + .Returns(mockServiceBusSender.Object) + .Verifiable(); + + // Act + await sender.SendAsync("InternalKey", model); + + // Assert + _mockServiceBusClient.VerifyAll(); + mockServiceBusSender.VerifyAll(); + } + } +} \ No newline at end of file diff --git a/test/GodelTech.Messaging.AzureServiceBus.Tests/Fakes/FakeModel.cs b/test/GodelTech.Messaging.AzureServiceBus.Tests/Fakes/FakeModel.cs new file mode 100644 index 0000000..0a4bb1e --- /dev/null +++ b/test/GodelTech.Messaging.AzureServiceBus.Tests/Fakes/FakeModel.cs @@ -0,0 +1,9 @@ +namespace GodelTech.Messaging.AzureServiceBus.Tests.Fakes +{ + public class FakeModel + { + public int Id { get; set; } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/test/GodelTech.Messaging.AzureServiceBus.Tests/GodelTech.Messaging.AzureServiceBus.Tests.csproj b/test/GodelTech.Messaging.AzureServiceBus.Tests/GodelTech.Messaging.AzureServiceBus.Tests.csproj index fac6065..f298ca1 100644 --- a/test/GodelTech.Messaging.AzureServiceBus.Tests/GodelTech.Messaging.AzureServiceBus.Tests.csproj +++ b/test/GodelTech.Messaging.AzureServiceBus.Tests/GodelTech.Messaging.AzureServiceBus.Tests.csproj @@ -10,6 +10,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/GodelTech.Messaging.AzureServiceBus.Tests/UnitTest1.cs b/test/GodelTech.Messaging.AzureServiceBus.Tests/UnitTest1.cs deleted file mode 100644 index b583f60..0000000 --- a/test/GodelTech.Messaging.AzureServiceBus.Tests/UnitTest1.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Xunit; - -namespace GodelTech.Messaging.AzureServiceBus.Tests -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } -} From c9695c72bcc738adc119b4954349e4a60b4bc911 Mon Sep 17 00:00:00 2001 From: Vladimir Rodchenko Date: Wed, 24 Mar 2021 00:39:59 +0300 Subject: [PATCH 2/3] add ServiceCollection extension for sender --- .../{Options => }/AzureServiceBusOptions.cs | 2 +- .../AzureServiceBusSender.cs | 1 - .../ServiceCollectionExtensions.cs | 38 +++++++++++++++++++ ...GodelTech.Messaging.AzureServiceBus.csproj | 1 + .../AzureServiceBusSenderTests.cs | 1 - 5 files changed, 40 insertions(+), 3 deletions(-) rename src/GodelTech.Messaging.AzureServiceBus/{Options => }/AzureServiceBusOptions.cs (87%) create mode 100644 src/GodelTech.Messaging.AzureServiceBus/DependencyInjection/ServiceCollectionExtensions.cs diff --git a/src/GodelTech.Messaging.AzureServiceBus/Options/AzureServiceBusOptions.cs b/src/GodelTech.Messaging.AzureServiceBus/AzureServiceBusOptions.cs similarity index 87% rename from src/GodelTech.Messaging.AzureServiceBus/Options/AzureServiceBusOptions.cs rename to src/GodelTech.Messaging.AzureServiceBus/AzureServiceBusOptions.cs index 6db5771..39525c6 100644 --- a/src/GodelTech.Messaging.AzureServiceBus/Options/AzureServiceBusOptions.cs +++ b/src/GodelTech.Messaging.AzureServiceBus/AzureServiceBusOptions.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace GodelTech.Messaging.AzureServiceBus.Options +namespace GodelTech.Messaging.AzureServiceBus { /// /// Azure Service Bus options diff --git a/src/GodelTech.Messaging.AzureServiceBus/AzureServiceBusSender.cs b/src/GodelTech.Messaging.AzureServiceBus/AzureServiceBusSender.cs index 772c2c7..1677efc 100644 --- a/src/GodelTech.Messaging.AzureServiceBus/AzureServiceBusSender.cs +++ b/src/GodelTech.Messaging.AzureServiceBus/AzureServiceBusSender.cs @@ -2,7 +2,6 @@ using System.Text.Json; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; -using GodelTech.Messaging.AzureServiceBus.Options; using Microsoft.Extensions.Options; namespace GodelTech.Messaging.AzureServiceBus diff --git a/src/GodelTech.Messaging.AzureServiceBus/DependencyInjection/ServiceCollectionExtensions.cs b/src/GodelTech.Messaging.AzureServiceBus/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..ca73970 --- /dev/null +++ b/src/GodelTech.Messaging.AzureServiceBus/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,38 @@ +using System; +using Azure.Messaging.ServiceBus; +using GodelTech.Messaging.AzureServiceBus; +using Microsoft.Extensions.DependencyInjection; + +// ReSharper disable once CheckNamespace +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extensions to register AzureServiceBusSender with the service collection. + /// + public static class ServiceCollectionExtensions + { + /// + /// Register AzureServiceBusSender with the service collection. + /// + /// Service collection. + /// Connection string to Azure Service Bus. + /// Azure Service Bus options action. + /// + public static IServiceCollection AddAzureServiceBusSender( + this IServiceCollection services, + string connectionString, + Action optionsAction) + { + // ServiceBusClient + services.AddTransient(provider => new ServiceBusClient(connectionString)); + + // Options + services.Configure(optionsAction); + + // AzureServiceBusSender + services.AddTransient(); + + return services; + } + } +} \ No newline at end of file diff --git a/src/GodelTech.Messaging.AzureServiceBus/GodelTech.Messaging.AzureServiceBus.csproj b/src/GodelTech.Messaging.AzureServiceBus/GodelTech.Messaging.AzureServiceBus.csproj index e351ef6..94c5f73 100644 --- a/src/GodelTech.Messaging.AzureServiceBus/GodelTech.Messaging.AzureServiceBus.csproj +++ b/src/GodelTech.Messaging.AzureServiceBus/GodelTech.Messaging.AzureServiceBus.csproj @@ -29,6 +29,7 @@ + diff --git a/test/GodelTech.Messaging.AzureServiceBus.Tests/AzureServiceBusSenderTests.cs b/test/GodelTech.Messaging.AzureServiceBus.Tests/AzureServiceBusSenderTests.cs index 98a485f..5e81910 100644 --- a/test/GodelTech.Messaging.AzureServiceBus.Tests/AzureServiceBusSenderTests.cs +++ b/test/GodelTech.Messaging.AzureServiceBus.Tests/AzureServiceBusSenderTests.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; -using GodelTech.Messaging.AzureServiceBus.Options; using GodelTech.Messaging.AzureServiceBus.Tests.Fakes; using Microsoft.Extensions.Options; using Moq; From ecc8d7a6d0d7efde341bab05516310aadaa0025a Mon Sep 17 00:00:00 2001 From: Vladimir Rodchenko Date: Sun, 28 Mar 2021 13:42:08 +0300 Subject: [PATCH 3/3] add ServiceCollectionExtensionsTests --- .../ServiceCollectionExtensions.cs | 1 - .../ServiceCollectionExtensionsTests.cs | 50 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 test/GodelTech.Messaging.AzureServiceBus.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs diff --git a/src/GodelTech.Messaging.AzureServiceBus/DependencyInjection/ServiceCollectionExtensions.cs b/src/GodelTech.Messaging.AzureServiceBus/DependencyInjection/ServiceCollectionExtensions.cs index ca73970..537fd81 100644 --- a/src/GodelTech.Messaging.AzureServiceBus/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/GodelTech.Messaging.AzureServiceBus/DependencyInjection/ServiceCollectionExtensions.cs @@ -3,7 +3,6 @@ using GodelTech.Messaging.AzureServiceBus; using Microsoft.Extensions.DependencyInjection; -// ReSharper disable once CheckNamespace namespace Microsoft.AspNetCore.Builder { /// diff --git a/test/GodelTech.Messaging.AzureServiceBus.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/test/GodelTech.Messaging.AzureServiceBus.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..eabad04 --- /dev/null +++ b/test/GodelTech.Messaging.AzureServiceBus.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Azure.Messaging.ServiceBus; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace GodelTech.Messaging.AzureServiceBus.Tests.DependencyInjection +{ + public class ServiceCollectionExtensionsTests + { + [Fact] + public void AddAzureServiceBusSender_Success() + { + // Arrange + const string connectionString = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=YourAccessKey"; + var queues = new Dictionary + { + { + "InternalKey", + "AzureServiceBusQueueName" + } + }; + Action optionsAction = options => + { + options.Queues = queues; + }; + + var services = new ServiceCollection(); + + // Act + services.AddAzureServiceBusSender(connectionString, optionsAction); + + // Assert + var provider = services.BuildServiceProvider(); + + var resultRequiredService = provider.GetRequiredService(); + Assert.NotNull(resultRequiredService); + + var resultOptionsAction = provider.GetRequiredService>(); + Assert.NotNull(resultOptionsAction); + Assert.NotNull(resultOptionsAction.Value); + Assert.Equal(queues, resultOptionsAction.Value.Queues); + + var resultAzureServiceBusSender = provider.GetRequiredService(); + Assert.NotNull(resultAzureServiceBusSender); + } + } +} \ No newline at end of file