Skip to content

Commit

Permalink
Refactor - setup per plug-in projects (#32)
Browse files Browse the repository at this point in the history
* Refactor - split into multiple projects

Signed-off-by: Victor Chang <[email protected]>

* Update build pipeline

Signed-off-by: Victor Chang <[email protected]>

* Update unit test and READMEs

Signed-off-by: Victor Chang <[email protected]>

* Exclude test projects from package.sh

Signed-off-by: Victor Chang <[email protected]>
  • Loading branch information
mocsharp committed Jun 27, 2022
1 parent d381767 commit e08d9a2
Show file tree
Hide file tree
Showing 38 changed files with 931 additions and 155 deletions.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,4 @@
</Parameters>
</Rule>
</Rules>
</AnalysisInput>
</AnalysisInput>
2 changes: 1 addition & 1 deletion src/Messaging/API/IMessageBrokerPublisherService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using Monai.Deploy.Messaging.Messages;

namespace Monai.Deploy.Messaging
namespace Monai.Deploy.Messaging.API
{
public interface IMessageBrokerPublisherService : IDisposable
{
Expand Down
2 changes: 1 addition & 1 deletion src/Messaging/API/IMessageBrokerSubscriberService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,24 @@ 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";

/// <summary>
/// Gets or sets the a fully qualified type name of the message publisher service.
/// The spcified type must implement <typeparam name="Monai.Deploy.InformaticsGateway.Api.MessageBroker.IMessageBrokerPublisherService">IMessageBrokerPublisherService</typeparam> interface.
/// The default message publisher service configured is RabbitMQ.
/// </summary>
[ConfigurationKeyName("publisherServiceAssemblyName")]
public string PublisherServiceAssemblyName { get; set; } = "Monai.Deploy.Messaging.RabbitMq.RabbitMqMessagePublisherService, Monai.Deploy.Messaging";
public string PublisherServiceAssemblyName { get; set; } = DefaultPublisherAssemblyName;

/// <summary>
/// Gets or sets the a fully qualified type name of the message subscriber service.
/// The spcified type must implement <typeparam name="Monai.Deploy.InformaticsGateway.Api.MessageBroker.IMessageBrokerSubscriberService">IMessageBrokerSubscriberService</typeparam> interface.
/// The default message subscriber service configured is RabbitMQ.
/// </summary>
[ConfigurationKeyName("subscriberServiceAssemblyName")]
public string SubscriberServiceAssemblyName { get; set; } = "Monai.Deploy.Messaging.RabbitMq.RabbitMqMessageSubscriberService, Monai.Deploy.Messaging";
public string SubscriberServiceAssemblyName { get; set; } = DefaultSubscriberAssemblyName;

/// <summary>
/// Gets or sets the message publisher specific settings.
Expand Down
144 changes: 144 additions & 0 deletions src/Messaging/IServiceCollectionExtension.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Configures all dependencies required for the MONAI Deploy Message Broker Subscriber Service.
/// </summary>
/// <param name="services">Instance of <see cref="IServiceCollection"/>.</param>
/// <param name="fullyQualifiedTypeName">Fully qualified type name of the service to use.</param>
/// <returns>Instance of <see cref="IServiceCollection"/>.</returns>
/// <exception cref="ConfigurationException"></exception>
public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService(this IServiceCollection services, string fullyQualifiedTypeName)
=> AddMonaiDeployMessageBrokerSubscriberService(services, fullyQualifiedTypeName, new FileSystem());

/// <summary>
/// Configures all dependencies required for the MONAI Deploy Message Broker Subscriber Service.
/// </summary>
/// <param name="services">Instance of <see cref="IServiceCollection"/>.</param>
/// <param name="fullyQualifiedTypeName">Fully qualified type name of the service to use.</param>
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/>.</param>
/// <returns>Instance of <see cref="IServiceCollection"/>.</returns>
/// <exception cref="ConfigurationException"></exception>
public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService(this IServiceCollection services, string fullyQualifiedTypeName, IFileSystem fileSystem)
=> Add<IMessageBrokerSubscriberService, SubscriberServiceRegistrationBase>(services, fullyQualifiedTypeName, fileSystem);

/// <summary>
/// Configures all dependencies required for the MONAI Deploy Message Broker Publisher Service.
/// </summary>
/// <param name="services">Instance of <see cref="IServiceCollection"/>.</param>
/// <param name="fullyQualifiedTypeName">Fully qualified type name of the service to use.</param>
/// <returns>Instance of <see cref="IServiceCollection"/>.</returns>
/// <exception cref="ConfigurationException"></exception>
public static IServiceCollection AddMonaiDeployMessageBrokerPublisherService(this IServiceCollection services, string fullyQualifiedTypeName)
=> AddMonaiDeployMessageBrokerPublisherService(services, fullyQualifiedTypeName, new FileSystem());

/// <summary>
/// Configures all dependencies required for the MONAI Deploy Message Broker Publisher Service.
/// </summary>
/// <param name="services">Instance of <see cref="IServiceCollection"/>.</param>
/// <param name="fullyQualifiedTypeName">Fully qualified type name of the service to use.</param>
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/>.</param>
/// <returns>Instance of <see cref="IServiceCollection"/>.</returns>
/// <exception cref="ConfigurationException"></exception>
public static IServiceCollection AddMonaiDeployMessageBrokerPublisherService(this IServiceCollection services, string fullyQualifiedTypeName, IFileSystem fileSystem)
=> Add<IMessageBrokerPublisherService, PublisherServiceRegistrationBase>(services, fullyQualifiedTypeName, fileSystem);

private static IServiceCollection Add<T, U>(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<T>(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<T>(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);
}
}
}
3 changes: 2 additions & 1 deletion src/Messaging/InternalVisible.cs
Original file line number Diff line number Diff line change
@@ -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")]
2 changes: 1 addition & 1 deletion src/Messaging/Messages/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public T ConvertTo<T>()
var json = Encoding.UTF8.GetString(Body);
return JsonConvert.DeserializeObject<T>(json)!;
}
catch(Exception ex)
catch (Exception ex)
{
throw new MessageConversionException($"Error converting message to type {typeof(T)}", ex);
}
Expand Down
9 changes: 5 additions & 4 deletions src/Messaging/Monai.Deploy.Messaging.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ SPDX-License-Identifier: Apache License 2.0
</ItemGroup>

<ItemGroup>
<Compile Remove="Test\**" />
<EmbeddedResource Remove="Test\**" />
<None Remove="Test\**" />
<Compile Remove="Tests\**" />
<EmbeddedResource Remove="Tests\**" />
<None Remove="Tests\**" />
</ItemGroup>

<ItemGroup>
Expand All @@ -54,7 +54,8 @@ SPDX-License-Identifier: Apache License 2.0
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="RabbitMQ.Client" Version="6.2.4" />
<PackageReference Include="RabbitMQ.Client" Version="6.4.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.IO.Abstractions" Version="17.0.18" />
</ItemGroup>
</Project>
17 changes: 0 additions & 17 deletions src/Messaging/RabbitMq/IServiceCollectionExtension.cs

This file was deleted.

11 changes: 11 additions & 0 deletions src/Messaging/SR.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
52 changes: 52 additions & 0 deletions src/Messaging/ServiceRegistrationBase.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Loading

0 comments on commit e08d9a2

Please sign in to comment.