From e08d9a2431852441b24b23269ffb4980812a4967 Mon Sep 17 00:00:00 2001 From: Victor Chang Date: Thu, 23 Jun 2022 09:09:42 -0700 Subject: [PATCH] Refactor - setup per plug-in projects (#32) * Refactor - split into multiple projects Signed-off-by: Victor Chang * Update build pipeline Signed-off-by: Victor Chang * Update unit test and READMEs Signed-off-by: Victor Chang * Exclude test projects from package.sh Signed-off-by: Victor Chang --- README.md | 46 ++++ .../CSharp/SonarLint.xml | 2 +- .../API/IMessageBrokerPublisherService.cs | 2 +- .../API/IMessageBrokerSubscriberService.cs | 2 +- .../MessageBrokerServiceConfiguration.cs | 7 +- src/Messaging/IServiceCollectionExtension.cs | 144 ++++++++++++ src/Messaging/InternalVisible.cs | 3 +- src/Messaging/Messages/Message.cs | 2 +- src/Messaging/Monai.Deploy.Messaging.csproj | 9 +- .../RabbitMq/IServiceCollectionExtension.cs | 17 -- src/Messaging/SR.cs | 11 + src/Messaging/ServiceRegistrationBase.cs | 52 +++++ .../{Test => Tests}/EventBaseTest.cs | 2 +- .../ExportCompleteEventTest.cs | 20 +- .../IServiceCollectionExtensionsTests.cs | 206 ++++++++++++++++++ .../{Test => Tests}/JsonMessageTest.cs | 4 +- .../Monai.Deploy.Messaging.Tests.csproj} | 9 +- .../Tests/ServiceRegistrationBaseTests.cs | 30 +++ .../{Test => Tests}/TaskCallbackEventTest.cs | 2 +- .../{Test => Tests}/TaskDispatchEventTest.cs | 2 +- .../{Test => Tests}/TaskUpdateEventTest.cs | 2 +- .../WorkflowRequestMessageTest.cs | 2 +- src/Monai.Deploy.Messaging.sln | 28 ++- .../RabbitMQ}/ConfigurationKeys.cs | 5 +- src/Plugins/RabbitMQ/InternalVisible.cs | 3 + .../Log.cs => Plugins/RabbitMQ/Logger.cs} | 11 +- .../Monai.Deploy.Messaging.RabbitMQ.csproj | 39 ++++ .../RabbitMQ/PublisherServiceRegistration.cs | 22 ++ src/Plugins/RabbitMQ/README.md | 44 ++++ .../RabbitMQ}/RabbitMqConnectionFactory.cs | 44 ++-- .../RabbitMqMessagePublisherService.cs | 46 ++-- .../RabbitMqMessageSubscriberService.cs | 50 +++-- .../RabbitMQ/SubscriberServiceRegistration.cs | 22 ++ ...nai.Deploy.Messaging.RabbitMQ.Tests.csproj | 33 +++ .../RabbitMqMessagePublisherServiceTest.cs | 23 +- .../RabbitMqMessageSubscriberServiceTest.cs | 36 ++- .../RabbitMQ/Tests/ServiceRegistrationTest.cs | 79 +++++++ src/Plugins/package.sh | 25 +++ 38 files changed, 931 insertions(+), 155 deletions(-) create mode 100644 src/Messaging/IServiceCollectionExtension.cs delete mode 100644 src/Messaging/RabbitMq/IServiceCollectionExtension.cs create mode 100644 src/Messaging/SR.cs create mode 100644 src/Messaging/ServiceRegistrationBase.cs rename src/Messaging/{Test => Tests}/EventBaseTest.cs (98%) rename src/Messaging/{Test => Tests}/ExportCompleteEventTest.cs (81%) create mode 100644 src/Messaging/Tests/IServiceCollectionExtensionsTests.cs rename src/Messaging/{Test => Tests}/JsonMessageTest.cs (98%) rename src/Messaging/{Test/Monai.Deploy.Messaging.Test.csproj => Tests/Monai.Deploy.Messaging.Tests.csproj} (84%) create mode 100644 src/Messaging/Tests/ServiceRegistrationBaseTests.cs rename src/Messaging/{Test => Tests}/TaskCallbackEventTest.cs (97%) rename src/Messaging/{Test => Tests}/TaskDispatchEventTest.cs (99%) rename src/Messaging/{Test => Tests}/TaskUpdateEventTest.cs (96%) rename src/Messaging/{Test => Tests}/WorkflowRequestMessageTest.cs (97%) rename src/{Messaging/Configuration => Plugins/RabbitMQ}/ConfigurationKeys.cs (78%) create mode 100644 src/Plugins/RabbitMQ/InternalVisible.cs rename src/{Messaging/Common/Log.cs => Plugins/RabbitMQ/Logger.cs} (91%) create mode 100644 src/Plugins/RabbitMQ/Monai.Deploy.Messaging.RabbitMQ.csproj create mode 100644 src/Plugins/RabbitMQ/PublisherServiceRegistration.cs create mode 100644 src/Plugins/RabbitMQ/README.md rename src/{Messaging/RabbitMq => Plugins/RabbitMQ}/RabbitMqConnectionFactory.cs (83%) rename src/{Messaging/RabbitMq => Plugins/RabbitMQ}/RabbitMqMessagePublisherService.cs (80%) rename src/{Messaging/RabbitMq => Plugins/RabbitMQ}/RabbitMqMessageSubscriberService.cs (89%) create mode 100644 src/Plugins/RabbitMQ/SubscriberServiceRegistration.cs create mode 100644 src/Plugins/RabbitMQ/Tests/Monai.Deploy.Messaging.RabbitMQ.Tests.csproj rename src/{Messaging/Test/RabbitMq => Plugins/RabbitMQ/Tests}/RabbitMqMessagePublisherServiceTest.cs (82%) rename src/{Messaging/Test/RabbitMq => Plugins/RabbitMQ/Tests}/RabbitMqMessageSubscriberServiceTest.cs (85%) create mode 100644 src/Plugins/RabbitMQ/Tests/ServiceRegistrationTest.cs create mode 100755 src/Plugins/package.sh diff --git a/README.md b/README.md index c12e2e68..912d3915 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,52 @@ Currently supported message broker services: If you would like to use a message broker service not listed above, please file an [issue](https://github.com/Project-MONAI/monai-deploy-messaging/issues) and contribute to the repository. +--- + +## Installation + +### 1. Configure the Service +To use the MONAI Deploy Messaging library, install the [NuGet.Org](https://www.nuget.org/packages/Monai.Deploy.Messaging/) package and call the `AddMonaiDeployMessageBrokerSubscriberService(...)` and/or the `AddMonaiDeployMessageBrokerPublisherService(...)` method to register the dependencies: + +```csharp +Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + ... + // Register the subscriber service + services.AddMonaiDeployMessageBrokerSubscriberService(hostContext.Configuration.GetSection("InformaticsGateway:messaging:publisherServiceAssemblyName").Value); + + // Register the publisher service + services.AddMonaiDeployMessageBrokerPublisherService(hostContext.Configuration.GetSection("InformaticsGateway:messaging:subscriberServiceAssemblyName").Value); + ... + }); +``` + +### 2. Install the Plug-in + +1. Create a subdirectory named `plug-ins` in the directory where your main application is installed. +2. Download the zipped plug-in of your choice and extract the files to the `plug-ins` directory. +3. Update `appsettings.json` and set the `publisherServiceAssemblyName` and the `subscriberServiceAssemblyName`, e.g.: + ```json + "messaging": { + "publisherServiceAssemblyName": "Monai.Deploy.Messaging.RabbitMQ.RabbitMQMessagePublisherService, Monai.Deploy.Messaging.RabbitMQ", + "publisherSettings": { + ... + }, + "subscriberServiceAssemblyName": "Monai.Deploy.Messaging.RabbitMQ.RabbitMQMessageSubscriberService, Monai.Deploy.Messaging.RabbitMQ", + "subscriberSettings": { + ... + } + }, + ``` + + +### 3. Restrict Acess to the Plug-ins Directory + +To avoid tampering of the plug-ins, it is recommended to set access rights to the plug-ins directory. + +--- + ## Releases The MONAI Deploy Messaging library is released in NuGet format, which is available on both [NuGet.Org](https://www.nuget.org/packages/Monai.Deploy.Messaging/) and [GitHub](https://github.com/Project-MONAI/monai-deploy-messaging/packages/1365839). diff --git a/src/.sonarlint/project-monai_monai-deploy-messaging/CSharp/SonarLint.xml b/src/.sonarlint/project-monai_monai-deploy-messaging/CSharp/SonarLint.xml index 90bc98df..c6f7cc2a 100644 --- a/src/.sonarlint/project-monai_monai-deploy-messaging/CSharp/SonarLint.xml +++ b/src/.sonarlint/project-monai_monai-deploy-messaging/CSharp/SonarLint.xml @@ -86,4 +86,4 @@ - \ No newline at end of file + diff --git a/src/Messaging/API/IMessageBrokerPublisherService.cs b/src/Messaging/API/IMessageBrokerPublisherService.cs index a36ac8f7..54019cf1 100644 --- a/src/Messaging/API/IMessageBrokerPublisherService.cs +++ b/src/Messaging/API/IMessageBrokerPublisherService.cs @@ -3,7 +3,7 @@ using Monai.Deploy.Messaging.Messages; -namespace Monai.Deploy.Messaging +namespace Monai.Deploy.Messaging.API { public interface IMessageBrokerPublisherService : IDisposable { diff --git a/src/Messaging/API/IMessageBrokerSubscriberService.cs b/src/Messaging/API/IMessageBrokerSubscriberService.cs index 240d8677..d35b8652 100644 --- a/src/Messaging/API/IMessageBrokerSubscriberService.cs +++ b/src/Messaging/API/IMessageBrokerSubscriberService.cs @@ -4,7 +4,7 @@ using Monai.Deploy.Messaging.Common; using Monai.Deploy.Messaging.Messages; -namespace Monai.Deploy.Messaging +namespace Monai.Deploy.Messaging.API { public interface IMessageBrokerSubscriberService : IDisposable { diff --git a/src/Messaging/Configuration/MessageBrokerServiceConfiguration.cs b/src/Messaging/Configuration/MessageBrokerServiceConfiguration.cs index a055d9ef..34407d1e 100644 --- a/src/Messaging/Configuration/MessageBrokerServiceConfiguration.cs +++ b/src/Messaging/Configuration/MessageBrokerServiceConfiguration.cs @@ -7,13 +7,16 @@ namespace Monai.Deploy.Messaging.Configuration { public class MessageBrokerServiceConfiguration { + public const string DefaultPublisherAssemblyName = "Monai.Deploy.Messaging.RabbitMQ.RabbitMQMessagePublisherService, Monai.Deploy.Messaging.RabbitMQ"; + public const string DefaultSubscriberAssemblyName = "Monai.Deploy.Messaging.RabbitMQ.RabbitMQMessageSubscriberService, Monai.Deploy.Messaging.RabbitMQ"; + /// /// Gets or sets the a fully qualified type name of the message publisher service. /// The spcified type must implement IMessageBrokerPublisherService interface. /// The default message publisher service configured is RabbitMQ. /// [ConfigurationKeyName("publisherServiceAssemblyName")] - public string PublisherServiceAssemblyName { get; set; } = "Monai.Deploy.Messaging.RabbitMq.RabbitMqMessagePublisherService, Monai.Deploy.Messaging"; + public string PublisherServiceAssemblyName { get; set; } = DefaultPublisherAssemblyName; /// /// Gets or sets the a fully qualified type name of the message subscriber service. @@ -21,7 +24,7 @@ public class MessageBrokerServiceConfiguration /// The default message subscriber service configured is RabbitMQ. /// [ConfigurationKeyName("subscriberServiceAssemblyName")] - public string SubscriberServiceAssemblyName { get; set; } = "Monai.Deploy.Messaging.RabbitMq.RabbitMqMessageSubscriberService, Monai.Deploy.Messaging"; + public string SubscriberServiceAssemblyName { get; set; } = DefaultSubscriberAssemblyName; /// /// Gets or sets the message publisher specific settings. diff --git a/src/Messaging/IServiceCollectionExtension.cs b/src/Messaging/IServiceCollectionExtension.cs new file mode 100644 index 00000000..cae473b3 --- /dev/null +++ b/src/Messaging/IServiceCollectionExtension.cs @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: © 2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using System.IO.Abstractions; +using System.Reflection; +using Ardalis.GuardClauses; +using Microsoft.Extensions.DependencyInjection; +using Monai.Deploy.Messaging.API; +using Monai.Deploy.Messaging.Configuration; + +namespace Monai.Deploy.Messaging +{ + public static class IServiceCollectionExtensions + { + private static IFileSystem? s_fileSystem; + + /// + /// Configures all dependencies required for the MONAI Deploy Message Broker Subscriber Service. + /// + /// Instance of . + /// Fully qualified type name of the service to use. + /// Instance of . + /// + public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService(this IServiceCollection services, string fullyQualifiedTypeName) + => AddMonaiDeployMessageBrokerSubscriberService(services, fullyQualifiedTypeName, new FileSystem()); + + /// + /// Configures all dependencies required for the MONAI Deploy Message Broker Subscriber Service. + /// + /// Instance of . + /// Fully qualified type name of the service to use. + /// Instance of . + /// Instance of . + /// + public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService(this IServiceCollection services, string fullyQualifiedTypeName, IFileSystem fileSystem) + => Add(services, fullyQualifiedTypeName, fileSystem); + + /// + /// Configures all dependencies required for the MONAI Deploy Message Broker Publisher Service. + /// + /// Instance of . + /// Fully qualified type name of the service to use. + /// Instance of . + /// + public static IServiceCollection AddMonaiDeployMessageBrokerPublisherService(this IServiceCollection services, string fullyQualifiedTypeName) + => AddMonaiDeployMessageBrokerPublisherService(services, fullyQualifiedTypeName, new FileSystem()); + + /// + /// Configures all dependencies required for the MONAI Deploy Message Broker Publisher Service. + /// + /// Instance of . + /// Fully qualified type name of the service to use. + /// Instance of . + /// Instance of . + /// + public static IServiceCollection AddMonaiDeployMessageBrokerPublisherService(this IServiceCollection services, string fullyQualifiedTypeName, IFileSystem fileSystem) + => Add(services, fullyQualifiedTypeName, fileSystem); + + private static IServiceCollection Add(this IServiceCollection services, string fullyQualifiedTypeName, IFileSystem fileSystem) where U : ServiceRegistrationBase + { + Guard.Against.NullOrWhiteSpace(fullyQualifiedTypeName, nameof(fullyQualifiedTypeName)); + Guard.Against.Null(fileSystem, nameof(fileSystem)); + + s_fileSystem = fileSystem; + + AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + + try + { + var serviceAssembly = LoadAssemblyFromDisk(GetAssemblyName(fullyQualifiedTypeName)); + var serviceRegistrationType = serviceAssembly.GetTypes().FirstOrDefault(p => p.BaseType == typeof(U)); + + if (serviceRegistrationType is null || Activator.CreateInstance(serviceRegistrationType, fullyQualifiedTypeName) is not U serviceRegistrar) + { + throw new ConfigurationException($"Service registrar cannot be found for the configured plug-in '{fullyQualifiedTypeName}'."); + } + + if (!IsSupportedType(fullyQualifiedTypeName, serviceAssembly)) + { + throw new ConfigurationException($"The configured type '{fullyQualifiedTypeName}' does not implement the {typeof(T).Name} interface."); + } + + return serviceRegistrar.Configure(services); + } + finally + { + AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve; + } + } + + private static bool IsSupportedType(string fullyQualifiedTypeName, Assembly storageServiceAssembly) + { + Guard.Against.NullOrWhiteSpace(fullyQualifiedTypeName, nameof(fullyQualifiedTypeName)); + Guard.Against.Null(storageServiceAssembly, nameof(storageServiceAssembly)); + + var storageServiceType = Type.GetType(fullyQualifiedTypeName, assemblyeName => storageServiceAssembly, null, false); + + return storageServiceType is not null && + storageServiceType.GetInterfaces().Contains(typeof(T)); + } + + private static string GetAssemblyName(string fullyQualifiedTypeName) + { + var assemblyNameParts = fullyQualifiedTypeName.Split(',', StringSplitOptions.None); + if (assemblyNameParts.Length < 2 || string.IsNullOrWhiteSpace(assemblyNameParts[1])) + { + throw new ConfigurationException($"The configured service type '{fullyQualifiedTypeName}' is not a valid fully qualified type name. E.g. {MessageBrokerServiceConfiguration.DefaultPublisherAssemblyName}") + { + HelpLink = "https://docs.microsoft.com/en-us/dotnet/standard/assembly/find-fully-qualified-name" + }; + } + + return assemblyNameParts[1].Trim(); + } + + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + Guard.Against.Null(args, nameof(args)); + + var requestedAssemblyName = new AssemblyName(args.Name); + return LoadAssemblyFromDisk(requestedAssemblyName.Name); + } + + private static Assembly LoadAssemblyFromDisk(string assemblyName) + { + Guard.Against.NullOrWhiteSpace(assemblyName, nameof(assemblyName)); + Guard.Against.Null(s_fileSystem, nameof(s_fileSystem)); + + if (!s_fileSystem.Directory.Exists(SR.PlugInDirectoryPath)) + { + throw new ConfigurationException($"Plug-in directory '{SR.PlugInDirectoryPath}' cannot be found."); + } + + var assemblyFilePath = s_fileSystem.Path.Combine(SR.PlugInDirectoryPath, $"{assemblyName}.dll"); + if (!s_fileSystem.File.Exists(assemblyFilePath)) + { + throw new ConfigurationException($"The configured plug-in '{assemblyFilePath}' cannot be found."); + } + + var asesmblyeData = s_fileSystem.File.ReadAllBytes(assemblyFilePath); + return Assembly.Load(asesmblyeData); + } + } +} diff --git a/src/Messaging/InternalVisible.cs b/src/Messaging/InternalVisible.cs index 94266994..7a314342 100644 --- a/src/Messaging/InternalVisible.cs +++ b/src/Messaging/InternalVisible.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Monai.Deploy.Messaging.Test")] +[assembly: InternalsVisibleTo("Monai.Deploy.Messaging.Tests")] +[assembly: InternalsVisibleTo("Monai.Deploy.Messaging.RabbitMQ.Tests")] diff --git a/src/Messaging/Messages/Message.cs b/src/Messaging/Messages/Message.cs index 144d0ba2..8f44750f 100644 --- a/src/Messaging/Messages/Message.cs +++ b/src/Messaging/Messages/Message.cs @@ -40,7 +40,7 @@ public T ConvertTo() var json = Encoding.UTF8.GetString(Body); return JsonConvert.DeserializeObject(json)!; } - catch(Exception ex) + catch (Exception ex) { throw new MessageConversionException($"Error converting message to type {typeof(T)}", ex); } diff --git a/src/Messaging/Monai.Deploy.Messaging.csproj b/src/Messaging/Monai.Deploy.Messaging.csproj index 1ef2b452..ef4b3635 100644 --- a/src/Messaging/Monai.Deploy.Messaging.csproj +++ b/src/Messaging/Monai.Deploy.Messaging.csproj @@ -33,9 +33,9 @@ SPDX-License-Identifier: Apache License 2.0 - - - + + + @@ -54,7 +54,8 @@ SPDX-License-Identifier: Apache License 2.0 - + + \ No newline at end of file diff --git a/src/Messaging/RabbitMq/IServiceCollectionExtension.cs b/src/Messaging/RabbitMq/IServiceCollectionExtension.cs deleted file mode 100644 index bd6f9f51..00000000 --- a/src/Messaging/RabbitMq/IServiceCollectionExtension.cs +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: © 2022 MONAI Consortium -// SPDX-License-Identifier: Apache License 2.0 - -using Microsoft.Extensions.DependencyInjection; - -namespace Monai.Deploy.Messaging.RabbitMq -{ - public static class IServiceCollectionExtension - { - public static IServiceCollection UseRabbitMq(this IServiceCollection services) - { - services.AddSingleton(); - - return services; - } - } -} diff --git a/src/Messaging/SR.cs b/src/Messaging/SR.cs new file mode 100644 index 00000000..5f51387d --- /dev/null +++ b/src/Messaging/SR.cs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: © 2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +namespace Monai.Deploy.Messaging +{ + internal static class SR + { + public const string PlugInDirectoryName = "plug-ins"; + public static readonly string PlugInDirectoryPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, SR.PlugInDirectoryName); + } +} diff --git a/src/Messaging/ServiceRegistrationBase.cs b/src/Messaging/ServiceRegistrationBase.cs new file mode 100644 index 00000000..40654310 --- /dev/null +++ b/src/Messaging/ServiceRegistrationBase.cs @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: © 2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using Ardalis.GuardClauses; +using Microsoft.Extensions.DependencyInjection; +using Monai.Deploy.Messaging.Configuration; + +namespace Monai.Deploy.Messaging +{ + public abstract class SubscriberServiceRegistrationBase : ServiceRegistrationBase + { + protected SubscriberServiceRegistrationBase(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName) + { + } + } + + public abstract class PublisherServiceRegistrationBase : ServiceRegistrationBase + { + protected PublisherServiceRegistrationBase(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName) + { + } + } + + public abstract class ServiceRegistrationBase + { + protected string FullyQualifiedAssemblyName { get; } + protected string AssemblyFilename { get; } + + protected ServiceRegistrationBase(string fullyQualifiedAssemblyName) + { + Guard.Against.NullOrWhiteSpace(fullyQualifiedAssemblyName, nameof(fullyQualifiedAssemblyName)); + FullyQualifiedAssemblyName = fullyQualifiedAssemblyName; + AssemblyFilename = ParseAssemblyName(); + } + + private string ParseAssemblyName() + { + var assemblyNameParts = FullyQualifiedAssemblyName.Split(',', StringSplitOptions.None); + if (assemblyNameParts.Length < 2 || string.IsNullOrWhiteSpace(assemblyNameParts[1])) + { + throw new ConfigurationException($"Storage service '{FullyQualifiedAssemblyName}' is invalid. Please provide a fully qualified name.") + { + HelpLink = "https://docs.microsoft.com/en-us/dotnet/standard/assembly/find-fully-qualified-name" + }; + } + + return assemblyNameParts[1].Trim(); + } + + public abstract IServiceCollection Configure(IServiceCollection services); + } +} diff --git a/src/Messaging/Test/EventBaseTest.cs b/src/Messaging/Tests/EventBaseTest.cs similarity index 98% rename from src/Messaging/Test/EventBaseTest.cs rename to src/Messaging/Tests/EventBaseTest.cs index c082d015..b2773162 100644 --- a/src/Messaging/Test/EventBaseTest.cs +++ b/src/Messaging/Tests/EventBaseTest.cs @@ -7,7 +7,7 @@ using Monai.Deploy.Messaging.Events; using Xunit; -namespace Monai.Deploy.Messaging.Test +namespace Monai.Deploy.Messaging.Tests { internal class StringClass : EventBase { diff --git a/src/Messaging/Test/ExportCompleteEventTest.cs b/src/Messaging/Tests/ExportCompleteEventTest.cs similarity index 81% rename from src/Messaging/Test/ExportCompleteEventTest.cs rename to src/Messaging/Tests/ExportCompleteEventTest.cs index 99364819..d56be9cc 100644 --- a/src/Messaging/Test/ExportCompleteEventTest.cs +++ b/src/Messaging/Tests/ExportCompleteEventTest.cs @@ -7,7 +7,7 @@ using Monai.Deploy.Messaging.Events; using Xunit; -namespace Monai.Deploy.Messaging.Test +namespace Monai.Deploy.Messaging.Tests { public class ExportCompleteEventTest { @@ -22,14 +22,14 @@ public void ShallGenerateExportCompleteMessageTestFromExportRequestMessage() ExportTaskId = Guid.NewGuid().ToString(), MessageId = Guid.NewGuid().ToString(), WorkflowInstanceId = Guid.NewGuid().ToString(), - }; - exportRequestMessage.Files = new List() - { - Guid.NewGuid().ToString(), - Guid.NewGuid().ToString(), - Guid.NewGuid().ToString(), - Guid.NewGuid().ToString(), - Guid.NewGuid().ToString(), + Files = new List() + { + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + } }; var errors = new List() @@ -47,7 +47,7 @@ public void ShallGenerateExportCompleteMessageTestFromExportRequestMessage() Assert.Equal(exportRequestMessage.WorkflowInstanceId, exportCompleteMessage.WorkflowInstanceId); Assert.Equal(exportRequestMessage.ExportTaskId, exportCompleteMessage.ExportTaskId); - Assert.Equal(string.Join(System.Environment.NewLine, errors), exportCompleteMessage.Message); + Assert.Equal(string.Join(Environment.NewLine, errors), exportCompleteMessage.Message); Assert.Equal(ExportStatus.Success, exportCompleteMessage.Status); } diff --git a/src/Messaging/Tests/IServiceCollectionExtensionsTests.cs b/src/Messaging/Tests/IServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..732f35f6 --- /dev/null +++ b/src/Messaging/Tests/IServiceCollectionExtensionsTests.cs @@ -0,0 +1,206 @@ +// SPDX-FileCopyrightText: © 2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using System; +using System.IO; +using System.IO.Abstractions.TestingHelpers; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Monai.Deploy.Messaging.API; +using Monai.Deploy.Messaging.Common; +using Monai.Deploy.Messaging.Configuration; +using Monai.Deploy.Messaging.Messages; +using Moq; +using Xunit; + +namespace Monai.Deploy.Messaging.Tests +{ +#pragma warning disable CS8604 // Possible null reference argument. + + public class IServiceCollectionExtensionsTests + { + [Theory(DisplayName = "AddMonaiDeployMessageBrokerServices throws when type name is invalid")] + [InlineData("mytype")] + [InlineData("mytype,, myversion")] + [InlineData("mytype, myassembly, myversion")] + public void AddMonaiDeployMessageBrokerServices_ThrowsOnInvalidTypeName(string typeName) + { + var serviceCollection = new Mock(); + + Assert.Throws(() => serviceCollection.Object.AddMonaiDeployMessageBrokerPublisherService(typeName, new MockFileSystem())); + Assert.Throws(() => serviceCollection.Object.AddMonaiDeployMessageBrokerSubscriberService(typeName, new MockFileSystem())); + } + + [Fact(DisplayName = "AddMonaiDeployMessageBrokerServices throws if the plug-ins directory is missing")] + public void AddMonaiDeployMessageBrokerServices_ThrowsIfPlugInsDirectoryIsMissing() + { + var typeName = typeof(SomeClass).AssemblyQualifiedName; + var serviceCollection = new Mock(); + var exception = Assert.Throws(() => serviceCollection.Object.AddMonaiDeployMessageBrokerPublisherService(typeName, new MockFileSystem())); + Assert.NotNull(exception); + Assert.Equal($"Plug-in directory '{SR.PlugInDirectoryPath}' cannot be found.", exception.Message); + + exception = Assert.Throws(() => serviceCollection.Object.AddMonaiDeployMessageBrokerSubscriberService(typeName, new MockFileSystem())); + Assert.NotNull(exception); + Assert.Equal($"Plug-in directory '{SR.PlugInDirectoryPath}' cannot be found.", exception.Message); + } + + [Fact(DisplayName = "AddMonaiDeployMessageBrokerServices throws if the plug-in dll is missing")] + public void AddMonaiDeployMessageBrokerServices_ThrowsIfPlugInDllIsMissing() + { + var badType = typeof(SomeClass); + var typeName = badType.AssemblyQualifiedName; + var fileSystem = new MockFileSystem(); + fileSystem.Directory.CreateDirectory(SR.PlugInDirectoryPath); + var serviceCollection = new Mock(); + var exception = Assert.Throws(() => serviceCollection.Object.AddMonaiDeployMessageBrokerPublisherService(typeName, fileSystem)); + Assert.NotNull(exception); + Assert.Equal($"The configured plug-in '{SR.PlugInDirectoryPath}{Path.DirectorySeparatorChar}{badType.Assembly.ManifestModule.Name}' cannot be found.", exception.Message); + + exception = Assert.Throws(() => serviceCollection.Object.AddMonaiDeployMessageBrokerSubscriberService(typeName, fileSystem)); + Assert.NotNull(exception); + Assert.Equal($"The configured plug-in '{SR.PlugInDirectoryPath}{Path.DirectorySeparatorChar}{badType.Assembly.ManifestModule.Name}' cannot be found.", exception.Message); + } + + [Fact(DisplayName = "AddMonaiDeployMessageBrokerServices throws if service registrar cannot be found in the assembly")] + public void AddMonaiDeployMessageBrokerServices_ThrowsIfServiceRegistrarCannotBeFoundInTheAssembly() + { + var badType = typeof(Assert); + var typeName = badType.AssemblyQualifiedName; + var assemblyData = GetAssemblyeBytes(badType.Assembly); + var assemblyFilePath = Path.Combine(SR.PlugInDirectoryPath, badType.Assembly.ManifestModule.Name); + var fileSystem = new MockFileSystem(); + fileSystem.Directory.CreateDirectory(SR.PlugInDirectoryPath); + fileSystem.File.WriteAllBytes(assemblyFilePath, assemblyData); + var serviceCollection = new Mock(); + var exception = Assert.Throws(() => serviceCollection.Object.AddMonaiDeployMessageBrokerPublisherService(typeName, fileSystem)); + Assert.NotNull(exception); + Assert.Equal($"Service registrar cannot be found for the configured plug-in '{typeName}'.", exception.Message); + + exception = Assert.Throws(() => serviceCollection.Object.AddMonaiDeployMessageBrokerSubscriberService(typeName, fileSystem)); + Assert.NotNull(exception); + Assert.Equal($"Service registrar cannot be found for the configured plug-in '{typeName}'.", exception.Message); + } + + [Fact(DisplayName = "AddMonaiDeployMessageBrokerServices throws if service type is not supported")] + public void AddMonaiDeployMessageBrokerServices_ThrowsIfServiceTypeIsNotSupported() + { + var badType = typeof(SomeClass); + var typeName = badType.AssemblyQualifiedName; + var assemblyData = GetAssemblyeBytes(badType.Assembly); + var assemblyFilePath = Path.Combine(SR.PlugInDirectoryPath, badType.Assembly.ManifestModule.Name); + var fileSystem = new MockFileSystem(); + fileSystem.Directory.CreateDirectory(SR.PlugInDirectoryPath); + fileSystem.File.WriteAllBytes(assemblyFilePath, assemblyData); + var serviceCollection = new Mock(); + serviceCollection.Setup(p => p.Clear()); + var exception = Record.Exception(() => serviceCollection.Object.AddMonaiDeployMessageBrokerPublisherService(typeName, fileSystem)); + Assert.NotNull(exception); + Assert.Equal($"The configured type '{typeName}' does not implement the {typeof(IMessageBrokerPublisherService).Name} interface.", exception.Message); + + exception = Record.Exception(() => serviceCollection.Object.AddMonaiDeployMessageBrokerSubscriberService(typeName, fileSystem)); + Assert.NotNull(exception); + Assert.Equal($"The configured type '{typeName}' does not implement the {typeof(IMessageBrokerSubscriberService).Name} interface.", exception.Message); + } + + [Fact(DisplayName = "AddMonaiDeployMessageBrokerPublisherService configures all services as expected")] + public void AddMonaiDeployMessageBrokerPublisherService_ConfiuresServicesAsExpected() + { + var badType = typeof(GoodPublisherService); + var typeName = badType.AssemblyQualifiedName; + var assemblyData = GetAssemblyeBytes(badType.Assembly); + var assemblyFilePath = Path.Combine(SR.PlugInDirectoryPath, badType.Assembly.ManifestModule.Name); + var fileSystem = new MockFileSystem(); + fileSystem.Directory.CreateDirectory(SR.PlugInDirectoryPath); + fileSystem.File.WriteAllBytes(assemblyFilePath, assemblyData); + var serviceCollection = new Mock(); + serviceCollection.Setup(p => p.Clear()); + var exception = Record.Exception(() => serviceCollection.Object.AddMonaiDeployMessageBrokerPublisherService(typeName, fileSystem)); + Assert.Null(exception); + serviceCollection.Verify(p => p.Clear(), Times.Once()); + } + + [Fact(DisplayName = "AddMonaiDeployMessageBrokerSubscriberService configures all services as expected")] + public void AddMonaiDeployMessageBrokerSubscriberService_ConfiuresServicesAsExpected() + { + var badType = typeof(GoodSubscriberService); + var typeName = badType.AssemblyQualifiedName; + var assemblyData = GetAssemblyeBytes(badType.Assembly); + var assemblyFilePath = Path.Combine(SR.PlugInDirectoryPath, badType.Assembly.ManifestModule.Name); + var fileSystem = new MockFileSystem(); + fileSystem.Directory.CreateDirectory(SR.PlugInDirectoryPath); + fileSystem.File.WriteAllBytes(assemblyFilePath, assemblyData); + var serviceCollection = new Mock(); + serviceCollection.Setup(p => p.Clear()); + var exception = Record.Exception(() => serviceCollection.Object.AddMonaiDeployMessageBrokerSubscriberService(typeName, fileSystem)); + Assert.Null(exception); + serviceCollection.Verify(p => p.Clear(), Times.Once()); + } + + private static byte[] GetAssemblyeBytes(Assembly assembly) + { + return File.ReadAllBytes(assembly.Location); + } + } + + internal class TestSubscriberServiceRegistrar : SubscriberServiceRegistrationBase + { + public TestSubscriberServiceRegistrar(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName) + { + } + + public override IServiceCollection Configure(IServiceCollection services) + { + services.Clear(); + return services; + } + } + + internal class TestPublisherServiceRegistrar : PublisherServiceRegistrationBase + { + public TestPublisherServiceRegistrar(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName) + { + } + + public override IServiceCollection Configure(IServiceCollection services) + { + services.Clear(); + return services; + } + } + + internal class GoodPublisherService : IMessageBrokerPublisherService + { + public string Name => throw new NotImplementedException(); + + public void Dispose() => throw new NotImplementedException(); + + public Task Publish(string topic, Message message) => throw new NotImplementedException(); + } + + internal class GoodSubscriberService : IMessageBrokerSubscriberService + { + public string Name => throw new NotImplementedException(); + + public void Acknowledge(MessageBase message) => throw new NotImplementedException(); + + public void Dispose() => throw new NotImplementedException(); + + public void Reject(MessageBase message, bool requeue = true) => throw new NotImplementedException(); + + public void Subscribe(string topic, string queue, Action messageReceivedCallback, ushort prefetchCount = 0) => throw new NotImplementedException(); + + public void Subscribe(string[] topics, string queue, Action messageReceivedCallback, ushort prefetchCount = 0) => throw new NotImplementedException(); + + public void SubscribeAsync(string topic, string queue, Func messageReceivedCallback, ushort prefetchCount = 0) => throw new NotImplementedException(); + + public void SubscribeAsync(string[] topics, string queue, Func messageReceivedCallback, ushort prefetchCount = 0) => throw new NotImplementedException(); + } + + internal class SomeClass + { + } + +#pragma warning restore CS8604 // Possible null reference argument. +} diff --git a/src/Messaging/Test/JsonMessageTest.cs b/src/Messaging/Tests/JsonMessageTest.cs similarity index 98% rename from src/Messaging/Test/JsonMessageTest.cs rename to src/Messaging/Tests/JsonMessageTest.cs index 1bab7b79..2985501c 100644 --- a/src/Messaging/Test/JsonMessageTest.cs +++ b/src/Messaging/Tests/JsonMessageTest.cs @@ -6,7 +6,7 @@ using Monai.Deploy.Messaging.Messages; using Xunit; -namespace Monai.Deploy.Messaging.Test +namespace Monai.Deploy.Messaging.Tests { public class DummyTypeOne { @@ -27,10 +27,10 @@ public void ConvertsThrowsError() var jsonMessage = new JsonMessage(data, Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); var message = jsonMessage.ToMessage(); - Assert.Throws(() => message.ConvertTo()); Assert.Throws(() => message.ConvertToJsonMessage()); } + [Fact(DisplayName = "Converts JsonMessage to Message")] public void ConvertsJsonMessageToMessage() { diff --git a/src/Messaging/Test/Monai.Deploy.Messaging.Test.csproj b/src/Messaging/Tests/Monai.Deploy.Messaging.Tests.csproj similarity index 84% rename from src/Messaging/Test/Monai.Deploy.Messaging.Test.csproj rename to src/Messaging/Tests/Monai.Deploy.Messaging.Tests.csproj index d3833e52..90c93b4b 100644 --- a/src/Messaging/Test/Monai.Deploy.Messaging.Test.csproj +++ b/src/Messaging/Tests/Monai.Deploy.Messaging.Tests.csproj @@ -8,14 +8,15 @@ - - + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Messaging/Tests/ServiceRegistrationBaseTests.cs b/src/Messaging/Tests/ServiceRegistrationBaseTests.cs new file mode 100644 index 00000000..169731c2 --- /dev/null +++ b/src/Messaging/Tests/ServiceRegistrationBaseTests.cs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: © 2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using System; +using Microsoft.Extensions.DependencyInjection; +using Monai.Deploy.Messaging.Configuration; +using Xunit; + +namespace Monai.Deploy.Messaging.Tests +{ + internal class TestServiceRegistration : ServiceRegistrationBase + { + public TestServiceRegistration(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName) + { + } + + public override IServiceCollection Configure(IServiceCollection services) => throw new NotImplementedException(); + } + + public class ServiceRegistrationBaseTests + { + [Theory(DisplayName = "ParseAssemblyName - throws if fully qualified assembly name is invalid")] + [InlineData("mytype")] + [InlineData("mytype,, myversion")] + public void ParseAssemblyName_ThrowIfFullyQualifiedAssemblyNameIsInvalid(string assemblyeName) + { + Assert.Throws(() => new TestPublisherServiceRegistrar(assemblyeName)); + } + } +} diff --git a/src/Messaging/Test/TaskCallbackEventTest.cs b/src/Messaging/Tests/TaskCallbackEventTest.cs similarity index 97% rename from src/Messaging/Test/TaskCallbackEventTest.cs rename to src/Messaging/Tests/TaskCallbackEventTest.cs index ca670a26..9d1eb11b 100644 --- a/src/Messaging/Test/TaskCallbackEventTest.cs +++ b/src/Messaging/Tests/TaskCallbackEventTest.cs @@ -6,7 +6,7 @@ using Monai.Deploy.Messaging.Events; using Xunit; -namespace Monai.Deploy.Messaging.Test +namespace Monai.Deploy.Messaging.Tests { public class TaskCallbackEventTest { diff --git a/src/Messaging/Test/TaskDispatchEventTest.cs b/src/Messaging/Tests/TaskDispatchEventTest.cs similarity index 99% rename from src/Messaging/Test/TaskDispatchEventTest.cs rename to src/Messaging/Tests/TaskDispatchEventTest.cs index 5841c695..ab9573fc 100644 --- a/src/Messaging/Test/TaskDispatchEventTest.cs +++ b/src/Messaging/Tests/TaskDispatchEventTest.cs @@ -7,7 +7,7 @@ using Monai.Deploy.Messaging.Events; using Xunit; -namespace Monai.Deploy.Messaging.Test +namespace Monai.Deploy.Messaging.Tests { public class TaskDispatchEventTest { diff --git a/src/Messaging/Test/TaskUpdateEventTest.cs b/src/Messaging/Tests/TaskUpdateEventTest.cs similarity index 96% rename from src/Messaging/Test/TaskUpdateEventTest.cs rename to src/Messaging/Tests/TaskUpdateEventTest.cs index 4777d13b..76d3282c 100644 --- a/src/Messaging/Test/TaskUpdateEventTest.cs +++ b/src/Messaging/Tests/TaskUpdateEventTest.cs @@ -6,7 +6,7 @@ using Monai.Deploy.Messaging.Events; using Xunit; -namespace Monai.Deploy.Messaging.Test +namespace Monai.Deploy.Messaging.Tests { public class TaskUpdateEventTest { diff --git a/src/Messaging/Test/WorkflowRequestMessageTest.cs b/src/Messaging/Tests/WorkflowRequestMessageTest.cs similarity index 97% rename from src/Messaging/Test/WorkflowRequestMessageTest.cs rename to src/Messaging/Tests/WorkflowRequestMessageTest.cs index faf911d3..faff97a9 100644 --- a/src/Messaging/Test/WorkflowRequestMessageTest.cs +++ b/src/Messaging/Tests/WorkflowRequestMessageTest.cs @@ -7,7 +7,7 @@ using Monai.Deploy.Messaging.Events; using Xunit; -namespace Monai.Deploy.Messaging.Test +namespace Monai.Deploy.Messaging.Tests { public class WorkflowRequestMessageTest { diff --git a/src/Monai.Deploy.Messaging.sln b/src/Monai.Deploy.Messaging.sln index eaf1c488..022c3c41 100644 --- a/src/Monai.Deploy.Messaging.sln +++ b/src/Monai.Deploy.Messaging.sln @@ -10,7 +10,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A73CC5 AssemblyInfo.cs = AssemblyInfo.cs EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.Messaging.Test", "Messaging\Test\Monai.Deploy.Messaging.Test.csproj", "{33516A8E-BCBF-4B63-A358-C3A3B03F63A5}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{50EFF021-4EA8-4D89-8312-306C4EAD8494}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.Messaging.RabbitMQ", "Plugins\RabbitMQ\Monai.Deploy.Messaging.RabbitMQ.csproj", "{AE474A08-0325-486F-A895-6E792EAE8349}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.Messaging.RabbitMQ.Tests", "Plugins\RabbitMQ\Tests\Monai.Deploy.Messaging.RabbitMQ.Tests.csproj", "{64BEE9D2-88B4-4035-A1C8-E00413B5E206}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.Messaging.Tests", "Messaging\Tests\Monai.Deploy.Messaging.Tests.csproj", "{08E0CD28-7CCD-4718-BCFF-15EF416C4D8F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -22,14 +28,26 @@ Global {0EF89B52-A68A-4F5A-B5C6-49D6AC3582C8}.Debug|Any CPU.Build.0 = Debug|Any CPU {0EF89B52-A68A-4F5A-B5C6-49D6AC3582C8}.Release|Any CPU.ActiveCfg = Release|Any CPU {0EF89B52-A68A-4F5A-B5C6-49D6AC3582C8}.Release|Any CPU.Build.0 = Release|Any CPU - {33516A8E-BCBF-4B63-A358-C3A3B03F63A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {33516A8E-BCBF-4B63-A358-C3A3B03F63A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {33516A8E-BCBF-4B63-A358-C3A3B03F63A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {33516A8E-BCBF-4B63-A358-C3A3B03F63A5}.Release|Any CPU.Build.0 = Release|Any CPU + {AE474A08-0325-486F-A895-6E792EAE8349}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE474A08-0325-486F-A895-6E792EAE8349}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE474A08-0325-486F-A895-6E792EAE8349}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE474A08-0325-486F-A895-6E792EAE8349}.Release|Any CPU.Build.0 = Release|Any CPU + {64BEE9D2-88B4-4035-A1C8-E00413B5E206}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64BEE9D2-88B4-4035-A1C8-E00413B5E206}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64BEE9D2-88B4-4035-A1C8-E00413B5E206}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64BEE9D2-88B4-4035-A1C8-E00413B5E206}.Release|Any CPU.Build.0 = Release|Any CPU + {08E0CD28-7CCD-4718-BCFF-15EF416C4D8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08E0CD28-7CCD-4718-BCFF-15EF416C4D8F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08E0CD28-7CCD-4718-BCFF-15EF416C4D8F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08E0CD28-7CCD-4718-BCFF-15EF416C4D8F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {AE474A08-0325-486F-A895-6E792EAE8349} = {50EFF021-4EA8-4D89-8312-306C4EAD8494} + {64BEE9D2-88B4-4035-A1C8-E00413B5E206} = {50EFF021-4EA8-4D89-8312-306C4EAD8494} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E1105263-9CBF-45AA-BAC3-BD8504C1B962} EndGlobalSection diff --git a/src/Messaging/Configuration/ConfigurationKeys.cs b/src/Plugins/RabbitMQ/ConfigurationKeys.cs similarity index 78% rename from src/Messaging/Configuration/ConfigurationKeys.cs rename to src/Plugins/RabbitMQ/ConfigurationKeys.cs index 0bed43ac..93068429 100644 --- a/src/Messaging/Configuration/ConfigurationKeys.cs +++ b/src/Plugins/RabbitMQ/ConfigurationKeys.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium // SPDX-License-Identifier: Apache License 2.0 -namespace Monai.Deploy.Messaging.Configuration +namespace Monai.Deploy.Messaging.RabbitMQ { internal static class ConfigurationKeys { @@ -10,10 +10,9 @@ internal static class ConfigurationKeys public static readonly string Password = "password"; public static readonly string VirtualHost = "virtualHost"; public static readonly string Exchange = "exchange"; - public static readonly string ExportRequestQueue = "exportRequestQueue"; public static readonly string UseSSL = "useSSL"; public static readonly string Port = "port"; public static readonly string[] PublisherRequiredKeys = new[] { EndPoint, Username, Password, VirtualHost, Exchange }; - public static readonly string[] SubscriberRequiredKeys = new[] { EndPoint, Username, Password, VirtualHost, Exchange, ExportRequestQueue }; + public static readonly string[] SubscriberRequiredKeys = new[] { EndPoint, Username, Password, VirtualHost, Exchange }; } } diff --git a/src/Plugins/RabbitMQ/InternalVisible.cs b/src/Plugins/RabbitMQ/InternalVisible.cs new file mode 100644 index 00000000..4c3d09be --- /dev/null +++ b/src/Plugins/RabbitMQ/InternalVisible.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Monai.Deploy.Messaging.RabbitMQ.Tests")] diff --git a/src/Messaging/Common/Log.cs b/src/Plugins/RabbitMQ/Logger.cs similarity index 91% rename from src/Messaging/Common/Log.cs rename to src/Plugins/RabbitMQ/Logger.cs index 0bc9795d..7d4b0b9d 100644 --- a/src/Messaging/Common/Log.cs +++ b/src/Plugins/RabbitMQ/Logger.cs @@ -1,25 +1,26 @@ // SPDX-FileCopyrightText: © 2022 MONAI Consortium // SPDX-License-Identifier: Apache License 2.0 +using System; using Microsoft.Extensions.Logging; -namespace Monai.Deploy.Messaging.Common +namespace Monai.Deploy.Messaging.RabbitMQ { - public static partial class Log + public static partial class Logger { internal static readonly string LoggingScopeMessageApplication = "Message ID={0}. Application ID={1}."; [LoggerMessage(EventId = 10000, Level = LogLevel.Information, Message = "Publishing message to {endpoint}/{virtualHost}. Exchange={exchange}, Routing Key={topic}.")] - public static partial void PublshingRabbitMq(this ILogger logger, string endpoint, string virtualHost, string exchange, string topic); + public static partial void PublshingRabbitMQ(this ILogger logger, string endpoint, string virtualHost, string exchange, string topic); [LoggerMessage(EventId = 10001, Level = LogLevel.Information, Message = "{ServiceName} connecting to {endpoint}/{virtualHost}.")] - public static partial void ConnectingToRabbitMq(this ILogger logger, string serviceNAme, string endpoint, string virtualHost); + public static partial void ConnectingToRabbitMQ(this ILogger logger, string serviceNAme, string endpoint, string virtualHost); [LoggerMessage(EventId = 10002, Level = LogLevel.Information, Message = "Message received from queue {queue} for {topic}.")] public static partial void MessageReceivedFromQueue(this ILogger logger, string queue, string topic); [LoggerMessage(EventId = 10003, Level = LogLevel.Information, Message = "Listening for messages from {endpoint}/{virtualHost}. Exchange={exchange}, Queue={queue}, Routing Key={topic}.")] - public static partial void SubscribeToRabbitMqQueue(this ILogger logger, string endpoint, string virtualHost, string exchange, string queue, string topic); + public static partial void SubscribeToRabbitMQQueue(this ILogger logger, string endpoint, string virtualHost, string exchange, string queue, string topic); [LoggerMessage(EventId = 10004, Level = LogLevel.Information, Message = "Sending message acknowledgement for message {messageId}.")] public static partial void SendingAcknowledgement(this ILogger logger, string messageId); diff --git a/src/Plugins/RabbitMQ/Monai.Deploy.Messaging.RabbitMQ.csproj b/src/Plugins/RabbitMQ/Monai.Deploy.Messaging.RabbitMQ.csproj new file mode 100644 index 00000000..8493e3c5 --- /dev/null +++ b/src/Plugins/RabbitMQ/Monai.Deploy.Messaging.RabbitMQ.csproj @@ -0,0 +1,39 @@ + + + + netstandard2.1 + enable + latest + Monai.Deploy.Messaging.RabbitMQ + false + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + Monai.Deploy.Messaging.RabbitMQ + 0.1.0 + MONAI Consortium + MONAI Consortium + true + RabbitMQ plug-in for MONAI Deploy Messaging Service. + MONAI Consortium + https://github.com/Project-MONAI/monai-deploy-messaging + https://github.com/Project-MONAI/monai-deploy-messaging + Apache-2.0 + True + + + + + + + + + + + + + + + + diff --git a/src/Plugins/RabbitMQ/PublisherServiceRegistration.cs b/src/Plugins/RabbitMQ/PublisherServiceRegistration.cs new file mode 100644 index 00000000..66528fbb --- /dev/null +++ b/src/Plugins/RabbitMQ/PublisherServiceRegistration.cs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: © 2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using Microsoft.Extensions.DependencyInjection; +using Monai.Deploy.Messaging.API; + +namespace Monai.Deploy.Messaging.RabbitMQ +{ + public class PublisherServiceRegistration : PublisherServiceRegistrationBase + { + public PublisherServiceRegistration(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName) + { + } + + public override IServiceCollection Configure(IServiceCollection services) + { + return services + .AddSingleton() + .AddSingleton(); + } + } +} diff --git a/src/Plugins/RabbitMQ/README.md b/src/Plugins/RabbitMQ/README.md new file mode 100644 index 00000000..e67db370 --- /dev/null +++ b/src/Plugins/RabbitMQ/README.md @@ -0,0 +1,44 @@ +# RabbitMQ for MONAI Deploy + +## Overview + +The RabbitMQ plug-in for MONAI Deploy is based on the [RabbitMQ](https://www.rabbitmq.com/) solution. + +## Configuration + + +The following configurations are required to run the MinIO plug-in. + + +### Default Configurations for the Message Publisher + +The `publisherServiceAssemblyName` should be set to `Monai.Deploy.Messaging.RabbitMQ.RabbitMQMessagePublisherService, Monai.Deploy.Messaging.RabbitMQ`. + +The following configurations are required for the publisher service. + +| Key | Description | Sample Value | +| ----------- | -------------------------------------------------------------------------------- | ------------ | +| endpoint | Host name/IP and port. | localhost | +| username | Username | username | +| password | Password | password | +| virtualHost | Name of the virtual host | monaideploy | +| exchange | Name of the exchange | monaideploy | +| useSSL | (optional) use secured connection or not | false | +| port | (optional) port number with default of 5672 for plaint-text and 5671 for secured | 5672 | + + +### Default Configurations for the Message Subscriber + +The `subscriberServiceAssemblyName` should be set to `Monai.Deploy.Messaging.RabbitMQ.RabbitMQMessageSubscriberService, Monai.Deploy.Messaging.RabbitMQ`. + +The following configurations are required for the subscriber service. + +| Key | Description | Sample Value | +| ----------- | -------------------------------------------------------------------------------- | ------------ | +| endpoint | Host name/IP and port. | localhost | +| username | Username | username | +| password | Password | password | +| virtualHost | Name of the virtual host | monaideploy | +| exchange | Name of the exchange | monaideploy | +| useSSL | (optional) use secured connection or not | false | +| port | (optional) port number with default of 5672 for plaint-text and 5671 for secured | 5672 | diff --git a/src/Messaging/RabbitMq/RabbitMqConnectionFactory.cs b/src/Plugins/RabbitMQ/RabbitMqConnectionFactory.cs similarity index 83% rename from src/Messaging/RabbitMq/RabbitMqConnectionFactory.cs rename to src/Plugins/RabbitMQ/RabbitMqConnectionFactory.cs index 9cdda71a..4d38de12 100644 --- a/src/Messaging/RabbitMq/RabbitMqConnectionFactory.cs +++ b/src/Plugins/RabbitMQ/RabbitMqConnectionFactory.cs @@ -1,18 +1,19 @@ // SPDX-FileCopyrightText: © 2022 MONAI Consortium // SPDX-License-Identifier: Apache License 2.0 +using System; using System.Collections.Concurrent; +using System.Linq; +using System.Net.Security; using System.Security.Cryptography; using System.Text; using Ardalis.GuardClauses; using Microsoft.Extensions.Logging; -using Monai.Deploy.Messaging.Common; using RabbitMQ.Client; -using System.Net.Security; -namespace Monai.Deploy.Messaging.RabbitMq +namespace Monai.Deploy.Messaging.RabbitMQ { - public interface IRabbitMqConnectionFactory + public interface IRabbitMQConnectionFactory { /// /// Creates a new channel for RabbitMQ client. @@ -24,39 +25,38 @@ public interface IRabbitMqConnectionFactory /// Password /// Virtual host /// Encrypt communication - /// Port Number + /// Port Number /// Instance of . - IModel CreateChannel(string hostName, string username, string password, string virtualHost, string useSSL, string portnumber); + IModel CreateChannel(string hostName, string username, string password, string virtualHost, string useSSL, string portNumber); } - public class RabbitMqConnectionFactory : IRabbitMqConnectionFactory, IDisposable + public class RabbitMQConnectionFactory : IRabbitMQConnectionFactory, IDisposable { private readonly ConcurrentDictionary> _connectionFactoriess; private readonly ConcurrentDictionary> _connections; - private readonly ILogger _logger; + private readonly ILogger _logger; private bool _disposedValue; - public RabbitMqConnectionFactory(ILogger logger) + public RabbitMQConnectionFactory(ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _connectionFactoriess = new ConcurrentDictionary>(); _connections = new ConcurrentDictionary>(); } - public IModel CreateChannel(string hostName, string username, string password, string virtualHost, string useSSL, string portnumber ) + public IModel CreateChannel(string hostName, string username, string password, string virtualHost, string useSSL, string portNumber) { Guard.Against.NullOrWhiteSpace(hostName, nameof(hostName)); Guard.Against.NullOrWhiteSpace(username, nameof(username)); Guard.Against.NullOrWhiteSpace(password, nameof(password)); Guard.Against.NullOrWhiteSpace(virtualHost, nameof(virtualHost)); - var key = $"{hostName}{username}{HashPassword(password)}{virtualHost}"; var connection = _connections.AddOrUpdate(key, x => { - return CreatConnection(hostName, username, password, virtualHost, key, useSSL, portnumber); + return CreatConnection(hostName, username, password, virtualHost, key, useSSL, portNumber); }, (updateKey, updateConnection) => { @@ -66,26 +66,28 @@ public IModel CreateChannel(string hostName, string username, string password, s } else { - return CreatConnection(hostName, username, password, virtualHost, key, useSSL, portnumber); + return CreatConnection(hostName, username, password, virtualHost, key, useSSL, portNumber); } }); return connection.Value.CreateModel(); } - private Lazy CreatConnection(string hostName, string username, string password, string virtualHost, string key, string useSSL, string portnumber) + private Lazy CreatConnection(string hostName, string username, string password, string virtualHost, string key, string useSSL, string portNumber) { - int port; - Boolean SslEnabled; - Boolean.TryParse(useSSL, out SslEnabled); - if (!Int32.TryParse(portnumber, out port)) + if (!bool.TryParse(useSSL, out var sslEnabled)) + { + sslEnabled = false; + } + + if (!Int32.TryParse(portNumber, out var port)) { - port = SslEnabled ? 5671 : 5672; // 5671 is default port for SSL/TLS , 5672 is default port for PLAIN. + port = sslEnabled ? 5671 : 5672; // 5671 is default port for SSL/TLS , 5672 is default port for PLAIN. } - SslOption sslOptions = new SslOption + var sslOptions = new SslOption { - Enabled = SslEnabled, + Enabled = sslEnabled, ServerName = hostName, AcceptablePolicyErrors = SslPolicyErrors.RemoteCertificateNameMismatch | SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable }; diff --git a/src/Messaging/RabbitMq/RabbitMqMessagePublisherService.cs b/src/Plugins/RabbitMQ/RabbitMqMessagePublisherService.cs similarity index 80% rename from src/Messaging/RabbitMq/RabbitMqMessagePublisherService.cs rename to src/Plugins/RabbitMQ/RabbitMqMessagePublisherService.cs index 0fdf5ec3..8c1e6865 100644 --- a/src/Messaging/RabbitMq/RabbitMqMessagePublisherService.cs +++ b/src/Plugins/RabbitMQ/RabbitMqMessagePublisherService.cs @@ -1,37 +1,39 @@ // SPDX-FileCopyrightText: © 2021-2022 MONAI Consortium // SPDX-License-Identifier: Apache License 2.0 +using System; using System.Globalization; +using System.Threading.Tasks; using Ardalis.GuardClauses; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.Messaging.Common; +using Monai.Deploy.Messaging.API; using Monai.Deploy.Messaging.Configuration; using Monai.Deploy.Messaging.Messages; using RabbitMQ.Client; -namespace Monai.Deploy.Messaging.RabbitMq +namespace Monai.Deploy.Messaging.RabbitMQ { - public class RabbitMqMessagePublisherService : IMessageBrokerPublisherService + public class RabbitMQMessagePublisherService : IMessageBrokerPublisherService { private const int PersistentDeliveryMode = 2; - private readonly ILogger _logger; - private readonly IRabbitMqConnectionFactory _rabbitMqConnectionFactory; + private readonly ILogger _logger; + private readonly IRabbitMQConnectionFactory _rabbitMqConnectionFactory; private readonly string _endpoint; private readonly string _username; private readonly string _password; private readonly string _virtualHost; private readonly string _exchange; - private readonly string _useSSL = string.Empty; - private readonly string _portNumber = string.Empty; + private readonly string _useSSL; + private readonly string _portNumber; private bool _disposedValue; public string Name => "Rabbit MQ Publisher"; - public RabbitMqMessagePublisherService(IOptions options, - ILogger logger, - IRabbitMqConnectionFactory rabbitMqConnectionFactory) + public RabbitMQMessagePublisherService(IOptions options, + ILogger logger, + IRabbitMQConnectionFactory rabbitMqConnectionFactory) { Guard.Against.Null(options, nameof(options)); @@ -46,15 +48,23 @@ public RabbitMqMessagePublisherService(IOptions _logger; + private readonly ILogger _logger; private readonly string _endpoint; private readonly string _virtualHost; private readonly string _exchange; - private readonly string _useSSL = string.Empty; - private readonly string _portNumber = string.Empty; + private readonly string _useSSL; + private readonly string _portNumber; private readonly IModel _channel; private bool _disposedValue; public string Name => "Rabbit MQ Subscriber"; - public RabbitMqMessageSubscriberService(IOptions options, - ILogger logger, - IRabbitMqConnectionFactory rabbitMqConnectionFactory) + public RabbitMQMessageSubscriberService(IOptions options, + ILogger logger, + IRabbitMQConnectionFactory rabbitMqConnectionFactory) { Guard.Against.Null(options, nameof(options)); @@ -42,18 +46,26 @@ public RabbitMqMessageSubscriberService(IOptions { - using var loggerScope = _logger.BeginScope(string.Format(CultureInfo.InvariantCulture, Log.LoggingScopeMessageApplication, eventArgs.BasicProperties.MessageId, eventArgs.BasicProperties.AppId)); + using var loggerScope = _logger.BeginScope(string.Format(CultureInfo.InvariantCulture, Logger.LoggingScopeMessageApplication, eventArgs.BasicProperties.MessageId, eventArgs.BasicProperties.AppId)); _logger.MessageReceivedFromQueue(queueDeclareResult.QueueName, eventArgs.RoutingKey); @@ -116,7 +128,7 @@ public void Subscribe(string[] topics, string queue, Action messageReceivedCallback, ushort prefetchCount = 0) @@ -133,7 +145,7 @@ public void SubscribeAsync(string[] topics, string queue, Func { - using var loggerScope = _logger.BeginScope(string.Format(CultureInfo.InvariantCulture, Log.LoggingScopeMessageApplication, eventArgs.BasicProperties.MessageId, eventArgs.BasicProperties.AppId)); + using var loggerScope = _logger.BeginScope(string.Format(CultureInfo.InvariantCulture, Logger.LoggingScopeMessageApplication, eventArgs.BasicProperties.MessageId, eventArgs.BasicProperties.AppId)); _logger.MessageReceivedFromQueue(queueDeclareResult.QueueName, eventArgs.RoutingKey); @@ -162,7 +174,7 @@ public void SubscribeAsync(string[] topics, string queue, Func(); + services.AddSingleton(); + return services; + } + } +} diff --git a/src/Plugins/RabbitMQ/Tests/Monai.Deploy.Messaging.RabbitMQ.Tests.csproj b/src/Plugins/RabbitMQ/Tests/Monai.Deploy.Messaging.RabbitMQ.Tests.csproj new file mode 100644 index 00000000..5d441c17 --- /dev/null +++ b/src/Plugins/RabbitMQ/Tests/Monai.Deploy.Messaging.RabbitMQ.Tests.csproj @@ -0,0 +1,33 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/src/Messaging/Test/RabbitMq/RabbitMqMessagePublisherServiceTest.cs b/src/Plugins/RabbitMQ/Tests/RabbitMqMessagePublisherServiceTest.cs similarity index 82% rename from src/Messaging/Test/RabbitMq/RabbitMqMessagePublisherServiceTest.cs rename to src/Plugins/RabbitMQ/Tests/RabbitMqMessagePublisherServiceTest.cs index dedb9763..e6fb5915 100644 --- a/src/Messaging/Test/RabbitMq/RabbitMqMessagePublisherServiceTest.cs +++ b/src/Plugins/RabbitMQ/Tests/RabbitMqMessagePublisherServiceTest.cs @@ -1,41 +1,38 @@ // SPDX-FileCopyrightText: © 2022 MONAI Consortium // SPDX-License-Identifier: Apache License 2.0 -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Monai.Deploy.Messaging.Configuration; using Monai.Deploy.Messaging.Messages; -using Monai.Deploy.Messaging.RabbitMq; using Moq; using RabbitMQ.Client; using Xunit; -namespace Monai.Deploy.Messaging.Test.RabbitMq +namespace Monai.Deploy.Messaging.RabbitMQ.Tests { - public class RabbitMqMessagePublisherServiceTest + public class RabbitMQMessagePublisherServiceTest { private readonly IOptions _options; - private readonly Mock> _logger; - private readonly Mock _connectionFactory; + private readonly Mock> _logger; + private readonly Mock _connectionFactory; private readonly Mock _model; - public RabbitMqMessagePublisherServiceTest() + public RabbitMQMessagePublisherServiceTest() { _options = Options.Create(new MessageBrokerServiceConfiguration()); - _logger = new Mock>(); - _connectionFactory = new Mock(); + _logger = new Mock>(); + _connectionFactory = new Mock(); _model = new Mock(); - _connectionFactory.Setup(p => p.CreateChannel(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(),It.IsAny(),It.IsAny())) + _connectionFactory.Setup(p => p.CreateChannel(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_model.Object); } [Fact(DisplayName = "Fails to validate when required keys are missing")] public void FailsToValidateWhenRequiredKeysAreMissing() { - Assert.Throws(() => new RabbitMqMessagePublisherService(_options, _logger.Object, _connectionFactory.Object)); + Assert.Throws(() => new RabbitMQMessagePublisherService(_options, _logger.Object, _connectionFactory.Object)); } [Fact(DisplayName = "Publishes a message")] @@ -56,7 +53,7 @@ public async Task PublishesAMessage() It.IsAny(), It.IsAny>())); - var service = new RabbitMqMessagePublisherService(_options, _logger.Object, _connectionFactory.Object); + var service = new RabbitMQMessagePublisherService(_options, _logger.Object, _connectionFactory.Object); var jsonMessage = new JsonMessage("hello world", Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); var message = jsonMessage.ToMessage(); diff --git a/src/Messaging/Test/RabbitMq/RabbitMqMessageSubscriberServiceTest.cs b/src/Plugins/RabbitMQ/Tests/RabbitMqMessageSubscriberServiceTest.cs similarity index 85% rename from src/Messaging/Test/RabbitMq/RabbitMqMessageSubscriberServiceTest.cs rename to src/Plugins/RabbitMQ/Tests/RabbitMqMessageSubscriberServiceTest.cs index 4c9c0d6e..01da9a6e 100644 --- a/src/Messaging/Test/RabbitMq/RabbitMqMessageSubscriberServiceTest.cs +++ b/src/Plugins/RabbitMQ/Tests/RabbitMqMessageSubscriberServiceTest.cs @@ -1,44 +1,40 @@ // SPDX-FileCopyrightText: © 2022 MONAI Consortium // SPDX-License-Identifier: Apache License 2.0 -using System; -using System.Collections.Generic; using System.Globalization; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Monai.Deploy.Messaging.Configuration; using Monai.Deploy.Messaging.Messages; -using Monai.Deploy.Messaging.RabbitMq; using Moq; using RabbitMQ.Client; using Xunit; -namespace Monai.Deploy.Messaging.Test.RabbitMq +namespace Monai.Deploy.Messaging.RabbitMQ.Tests { - public class RabbitMqMessageSubscriberServiceTest + public class RabbitMQMessageSubscriberServiceTest { private readonly IOptions _options; - private readonly Mock> _logger; - private readonly Mock _connectionFactory; + private readonly Mock> _logger; + private readonly Mock _connectionFactory; private readonly Mock _model; - public RabbitMqMessageSubscriberServiceTest() + public RabbitMQMessageSubscriberServiceTest() { _options = Options.Create(new MessageBrokerServiceConfiguration()); - _logger = new Mock>(); - _connectionFactory = new Mock(); + _logger = new Mock>(); + _connectionFactory = new Mock(); _model = new Mock(); _connectionFactory.Setup(p => p.CreateChannel(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_model.Object); - } [Fact(DisplayName = "Fails to validate when required keys are missing")] public void FailsToValidateWhenRequiredKeysAreMissing() { - Assert.Throws(() => new RabbitMqMessageSubscriberService(_options, _logger.Object, _connectionFactory.Object)); + Assert.Throws(() => new RabbitMQMessageSubscriberService(_options, _logger.Object, _connectionFactory.Object)); } [Fact(DisplayName = "Cleanup connections on Dispose")] @@ -49,9 +45,8 @@ public void CleanupOnDispose() _options.Value.SubscriberSettings.Add(ConfigurationKeys.Password, "password"); _options.Value.SubscriberSettings.Add(ConfigurationKeys.VirtualHost, "virtual-host"); _options.Value.SubscriberSettings.Add(ConfigurationKeys.Exchange, "exchange"); - _options.Value.SubscriberSettings.Add(ConfigurationKeys.ExportRequestQueue, "export-request-queue"); - var service = new RabbitMqMessageSubscriberService(_options, _logger.Object, _connectionFactory.Object); + var service = new RabbitMQMessageSubscriberService(_options, _logger.Object, _connectionFactory.Object); service.Dispose(); _model.Verify(p => p.Close(), Times.Once()); @@ -66,7 +61,6 @@ public void SubscribesToATopic() _options.Value.SubscriberSettings.Add(ConfigurationKeys.Password, "password"); _options.Value.SubscriberSettings.Add(ConfigurationKeys.VirtualHost, "virtual-host"); _options.Value.SubscriberSettings.Add(ConfigurationKeys.Exchange, "exchange"); - _options.Value.SubscriberSettings.Add(ConfigurationKeys.ExportRequestQueue, "export-request-queue"); var jsonMessage = new JsonMessage("hello world", Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "1"); var message = jsonMessage.ToMessage(); @@ -75,7 +69,7 @@ public void SubscribesToATopic() basicProperties.SetupGet(p => p.AppId).Returns(jsonMessage.ApplicationId); basicProperties.SetupGet(p => p.ContentType).Returns(jsonMessage.ContentType); basicProperties.SetupGet(p => p.CorrelationId).Returns(jsonMessage.CorrelationId); - basicProperties.SetupGet(p => p.Headers["CreationDateTime"]).Returns(Encoding.UTF8.GetBytes(jsonMessage.CreationDateTime.ToString("o", System.Globalization.CultureInfo.InvariantCulture))); + basicProperties.SetupGet(p => p.Headers["CreationDateTime"]).Returns(Encoding.UTF8.GetBytes(jsonMessage.CreationDateTime.ToString("o", CultureInfo.InvariantCulture))); _model.Setup(p => p.QueueDeclare( It.IsAny(), @@ -107,7 +101,7 @@ public void SubscribesToATopic() consumer.HandleBasicDeliver(tag, Convert.ToUInt64(jsonMessage.DeliveryTag, CultureInfo.InvariantCulture), false, "exchange", "topic", basicProperties.Object, new ReadOnlyMemory(message.Body)); }); - var service = new RabbitMqMessageSubscriberService(_options, _logger.Object, _connectionFactory.Object); + var service = new RabbitMQMessageSubscriberService(_options, _logger.Object, _connectionFactory.Object); service.Subscribe("topic", "queue", (args) => { @@ -123,7 +117,7 @@ public void SubscribesToATopic() service.SubscribeAsync("topic", "queue", async (args) => { - await System.Threading.Tasks.Task.Run(() => + await Task.Run(() => { Assert.Equal(message.ApplicationId, args.Message.ApplicationId); Assert.Equal(message.ContentType, args.Message.ContentType); @@ -145,7 +139,6 @@ public void AcknowledgeAMessage() _options.Value.SubscriberSettings.Add(ConfigurationKeys.Password, "password"); _options.Value.SubscriberSettings.Add(ConfigurationKeys.VirtualHost, "virtual-host"); _options.Value.SubscriberSettings.Add(ConfigurationKeys.Exchange, "exchange"); - _options.Value.SubscriberSettings.Add(ConfigurationKeys.ExportRequestQueue, "export-request-queue"); var jsonMessage = new JsonMessage("hello world", Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "1"); var message = jsonMessage.ToMessage(); @@ -154,7 +147,7 @@ public void AcknowledgeAMessage() It.IsAny(), It.IsAny())); - var service = new RabbitMqMessageSubscriberService(_options, _logger.Object, _connectionFactory.Object); + var service = new RabbitMQMessageSubscriberService(_options, _logger.Object, _connectionFactory.Object); service.Acknowledge(message); @@ -169,7 +162,6 @@ public void RejectAMessage() _options.Value.SubscriberSettings.Add(ConfigurationKeys.Password, "password"); _options.Value.SubscriberSettings.Add(ConfigurationKeys.VirtualHost, "virtual-host"); _options.Value.SubscriberSettings.Add(ConfigurationKeys.Exchange, "exchange"); - _options.Value.SubscriberSettings.Add(ConfigurationKeys.ExportRequestQueue, "export-request-queue"); var jsonMessage = new JsonMessage("hello world", Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "1"); var message = jsonMessage.ToMessage(); @@ -179,7 +171,7 @@ public void RejectAMessage() It.IsAny(), It.IsAny())); - var service = new RabbitMqMessageSubscriberService(_options, _logger.Object, _connectionFactory.Object); + var service = new RabbitMQMessageSubscriberService(_options, _logger.Object, _connectionFactory.Object); service.Reject(message); diff --git a/src/Plugins/RabbitMQ/Tests/ServiceRegistrationTest.cs b/src/Plugins/RabbitMQ/Tests/ServiceRegistrationTest.cs new file mode 100644 index 00000000..37016998 --- /dev/null +++ b/src/Plugins/RabbitMQ/Tests/ServiceRegistrationTest.cs @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: � 2022 MONAI Consortium +// SPDX-License-Identifier: Apache License 2.0 + +using System.IO.Abstractions.TestingHelpers; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Monai.Deploy.Messaging.RabbitMQ.Tests +{ +#pragma warning disable CS8604 // Possible null reference argument. + + public class PublisherServiceRegistrationTest : ServiceRegistrationTest + { + [Fact(DisplayName = "Shall be able to Add MinIO as default storage service")] + public void ShallAddRabbitMQAsDefaultMessagingService() + { + var serviceCollection = new Mock(); + serviceCollection.Setup(p => p.Add(It.IsAny())); + + var returnedServiceCollection = serviceCollection.Object.AddMonaiDeployMessageBrokerPublisherService(ServiceType.AssemblyQualifiedName, FileSystem); + + Assert.Same(serviceCollection.Object, returnedServiceCollection); + + serviceCollection.Verify(p => p.Add(It.IsAny()), Times.Exactly(2)); + } + } + + public class SubscriberServiceRegistrationTest : ServiceRegistrationTest + { + [Fact(DisplayName = "Shall be able to Add MinIO as default storage service")] + public void ShallAddRabbitMQAsDefaultMessagingService() + { + var serviceCollection = new Mock(); + serviceCollection.Setup(p => p.Add(It.IsAny())); + + var returnedServiceCollection = serviceCollection.Object.AddMonaiDeployMessageBrokerSubscriberService(ServiceType.AssemblyQualifiedName, FileSystem); + + Assert.Same(serviceCollection.Object, returnedServiceCollection); + + serviceCollection.Verify(p => p.Add(It.IsAny()), Times.Exactly(2)); + } + } + + public abstract class ServiceRegistrationTest + { + protected Type ServiceType { get; } + + protected MockFileSystem FileSystem { get; } + + protected ServiceRegistrationTest() + { + ServiceType = typeof(T); + FileSystem = new MockFileSystem(); + var assemblyFilePath = Path.Combine(SR.PlugInDirectoryPath, ServiceType.Assembly.ManifestModule.Name); + var assemblyData = GetAssemblyeBytes(ServiceType.Assembly); + FileSystem.Directory.CreateDirectory(SR.PlugInDirectoryPath); + FileSystem.File.WriteAllBytes(assemblyFilePath, assemblyData); + } + + private static byte[] GetAssemblyeBytes(Assembly assembly) + { + return File.ReadAllBytes(assembly.Location); + } + + protected void AddOptions(Dictionary settings, string[] requiredKeys) + { + foreach (var key in requiredKeys) + { + if (settings.ContainsKey(key)) continue; + + settings.Add(key, Guid.NewGuid().ToString()); + } + } + } + +#pragma warning restore CS8604 // Possible null reference argument. +} diff --git a/src/Plugins/package.sh b/src/Plugins/package.sh new file mode 100755 index 00000000..2d05a95a --- /dev/null +++ b/src/Plugins/package.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: © 2022 MONAI Consortium +# SPDX-License-Identifier: Apache License 2.0 + +export PACKAGEDIR="${PWD}/release" + +[ -d $PACKAGEDIR ] && rm -rf $PACKAGEDIR && echo "Removing $PACKAGEDIR..." + + +find . -type f -name '*.csproj' ! -name '*.Tests.csproj' -exec bash -c ' + for project do + echo Processing $project... + projectName=$(basename -s .csproj $project) + projectPACKAGEDIR="${PACKAGEDIR}/${projectName}" + zipPath="${PACKAGEDIR}/${projectName}.zip" + mkdir -p $projectPACKAGEDIR + echo Publishing $project... + dotnet publish $project -c Release -o $projectPACKAGEDIR --nologo + pushd $projectPACKAGEDIR + rm -f Microsoft*.dll System*.dll JetBrains*.dll + zip -r $zipPath * + popd + done +' _ {} + \ No newline at end of file