Skip to content

Commit e08d9a2

Browse files
committed
Refactor - setup per plug-in projects (#32)
* 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]>
1 parent d381767 commit e08d9a2

38 files changed

+931
-155
lines changed

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,52 @@ Currently supported message broker services:
2121

2222
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.
2323

24+
---
25+
26+
## Installation
27+
28+
### 1. Configure the Service
29+
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:
30+
31+
```csharp
32+
Host.CreateDefaultBuilder(args)
33+
.ConfigureServices((hostContext, services) =>
34+
{
35+
...
36+
// Register the subscriber service
37+
services.AddMonaiDeployMessageBrokerSubscriberService(hostContext.Configuration.GetSection("InformaticsGateway:messaging:publisherServiceAssemblyName").Value);
38+
39+
// Register the publisher service
40+
services.AddMonaiDeployMessageBrokerPublisherService(hostContext.Configuration.GetSection("InformaticsGateway:messaging:subscriberServiceAssemblyName").Value);
41+
...
42+
});
43+
```
44+
45+
### 2. Install the Plug-in
46+
47+
1. Create a subdirectory named `plug-ins` in the directory where your main application is installed.
48+
2. Download the zipped plug-in of your choice and extract the files to the `plug-ins` directory.
49+
3. Update `appsettings.json` and set the `publisherServiceAssemblyName` and the `subscriberServiceAssemblyName`, e.g.:
50+
```json
51+
"messaging": {
52+
"publisherServiceAssemblyName": "Monai.Deploy.Messaging.RabbitMQ.RabbitMQMessagePublisherService, Monai.Deploy.Messaging.RabbitMQ",
53+
"publisherSettings": {
54+
...
55+
},
56+
"subscriberServiceAssemblyName": "Monai.Deploy.Messaging.RabbitMQ.RabbitMQMessageSubscriberService, Monai.Deploy.Messaging.RabbitMQ",
57+
"subscriberSettings": {
58+
...
59+
}
60+
},
61+
```
62+
63+
64+
### 3. Restrict Acess to the Plug-ins Directory
65+
66+
To avoid tampering of the plug-ins, it is recommended to set access rights to the plug-ins directory.
67+
68+
---
69+
2470
## Releases
2571

2672
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).

src/.sonarlint/project-monai_monai-deploy-messaging/CSharp/SonarLint.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,4 @@
8686
</Parameters>
8787
</Rule>
8888
</Rules>
89-
</AnalysisInput>
89+
</AnalysisInput>

src/Messaging/API/IMessageBrokerPublisherService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
using Monai.Deploy.Messaging.Messages;
55

6-
namespace Monai.Deploy.Messaging
6+
namespace Monai.Deploy.Messaging.API
77
{
88
public interface IMessageBrokerPublisherService : IDisposable
99
{

src/Messaging/API/IMessageBrokerSubscriberService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using Monai.Deploy.Messaging.Common;
55
using Monai.Deploy.Messaging.Messages;
66

7-
namespace Monai.Deploy.Messaging
7+
namespace Monai.Deploy.Messaging.API
88
{
99
public interface IMessageBrokerSubscriberService : IDisposable
1010
{

src/Messaging/Configuration/MessageBrokerServiceConfiguration.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,24 @@ namespace Monai.Deploy.Messaging.Configuration
77
{
88
public class MessageBrokerServiceConfiguration
99
{
10+
public const string DefaultPublisherAssemblyName = "Monai.Deploy.Messaging.RabbitMQ.RabbitMQMessagePublisherService, Monai.Deploy.Messaging.RabbitMQ";
11+
public const string DefaultSubscriberAssemblyName = "Monai.Deploy.Messaging.RabbitMQ.RabbitMQMessageSubscriberService, Monai.Deploy.Messaging.RabbitMQ";
12+
1013
/// <summary>
1114
/// Gets or sets the a fully qualified type name of the message publisher service.
1215
/// The spcified type must implement <typeparam name="Monai.Deploy.InformaticsGateway.Api.MessageBroker.IMessageBrokerPublisherService">IMessageBrokerPublisherService</typeparam> interface.
1316
/// The default message publisher service configured is RabbitMQ.
1417
/// </summary>
1518
[ConfigurationKeyName("publisherServiceAssemblyName")]
16-
public string PublisherServiceAssemblyName { get; set; } = "Monai.Deploy.Messaging.RabbitMq.RabbitMqMessagePublisherService, Monai.Deploy.Messaging";
19+
public string PublisherServiceAssemblyName { get; set; } = DefaultPublisherAssemblyName;
1720

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

2629
/// <summary>
2730
/// Gets or sets the message publisher specific settings.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// SPDX-FileCopyrightText: © 2022 MONAI Consortium
2+
// SPDX-License-Identifier: Apache License 2.0
3+
4+
using System.IO.Abstractions;
5+
using System.Reflection;
6+
using Ardalis.GuardClauses;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Monai.Deploy.Messaging.API;
9+
using Monai.Deploy.Messaging.Configuration;
10+
11+
namespace Monai.Deploy.Messaging
12+
{
13+
public static class IServiceCollectionExtensions
14+
{
15+
private static IFileSystem? s_fileSystem;
16+
17+
/// <summary>
18+
/// Configures all dependencies required for the MONAI Deploy Message Broker Subscriber Service.
19+
/// </summary>
20+
/// <param name="services">Instance of <see cref="IServiceCollection"/>.</param>
21+
/// <param name="fullyQualifiedTypeName">Fully qualified type name of the service to use.</param>
22+
/// <returns>Instance of <see cref="IServiceCollection"/>.</returns>
23+
/// <exception cref="ConfigurationException"></exception>
24+
public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService(this IServiceCollection services, string fullyQualifiedTypeName)
25+
=> AddMonaiDeployMessageBrokerSubscriberService(services, fullyQualifiedTypeName, new FileSystem());
26+
27+
/// <summary>
28+
/// Configures all dependencies required for the MONAI Deploy Message Broker Subscriber Service.
29+
/// </summary>
30+
/// <param name="services">Instance of <see cref="IServiceCollection"/>.</param>
31+
/// <param name="fullyQualifiedTypeName">Fully qualified type name of the service to use.</param>
32+
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/>.</param>
33+
/// <returns>Instance of <see cref="IServiceCollection"/>.</returns>
34+
/// <exception cref="ConfigurationException"></exception>
35+
public static IServiceCollection AddMonaiDeployMessageBrokerSubscriberService(this IServiceCollection services, string fullyQualifiedTypeName, IFileSystem fileSystem)
36+
=> Add<IMessageBrokerSubscriberService, SubscriberServiceRegistrationBase>(services, fullyQualifiedTypeName, fileSystem);
37+
38+
/// <summary>
39+
/// Configures all dependencies required for the MONAI Deploy Message Broker Publisher Service.
40+
/// </summary>
41+
/// <param name="services">Instance of <see cref="IServiceCollection"/>.</param>
42+
/// <param name="fullyQualifiedTypeName">Fully qualified type name of the service to use.</param>
43+
/// <returns>Instance of <see cref="IServiceCollection"/>.</returns>
44+
/// <exception cref="ConfigurationException"></exception>
45+
public static IServiceCollection AddMonaiDeployMessageBrokerPublisherService(this IServiceCollection services, string fullyQualifiedTypeName)
46+
=> AddMonaiDeployMessageBrokerPublisherService(services, fullyQualifiedTypeName, new FileSystem());
47+
48+
/// <summary>
49+
/// Configures all dependencies required for the MONAI Deploy Message Broker Publisher Service.
50+
/// </summary>
51+
/// <param name="services">Instance of <see cref="IServiceCollection"/>.</param>
52+
/// <param name="fullyQualifiedTypeName">Fully qualified type name of the service to use.</param>
53+
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/>.</param>
54+
/// <returns>Instance of <see cref="IServiceCollection"/>.</returns>
55+
/// <exception cref="ConfigurationException"></exception>
56+
public static IServiceCollection AddMonaiDeployMessageBrokerPublisherService(this IServiceCollection services, string fullyQualifiedTypeName, IFileSystem fileSystem)
57+
=> Add<IMessageBrokerPublisherService, PublisherServiceRegistrationBase>(services, fullyQualifiedTypeName, fileSystem);
58+
59+
private static IServiceCollection Add<T, U>(this IServiceCollection services, string fullyQualifiedTypeName, IFileSystem fileSystem) where U : ServiceRegistrationBase
60+
{
61+
Guard.Against.NullOrWhiteSpace(fullyQualifiedTypeName, nameof(fullyQualifiedTypeName));
62+
Guard.Against.Null(fileSystem, nameof(fileSystem));
63+
64+
s_fileSystem = fileSystem;
65+
66+
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
67+
68+
try
69+
{
70+
var serviceAssembly = LoadAssemblyFromDisk(GetAssemblyName(fullyQualifiedTypeName));
71+
var serviceRegistrationType = serviceAssembly.GetTypes().FirstOrDefault(p => p.BaseType == typeof(U));
72+
73+
if (serviceRegistrationType is null || Activator.CreateInstance(serviceRegistrationType, fullyQualifiedTypeName) is not U serviceRegistrar)
74+
{
75+
throw new ConfigurationException($"Service registrar cannot be found for the configured plug-in '{fullyQualifiedTypeName}'.");
76+
}
77+
78+
if (!IsSupportedType<T>(fullyQualifiedTypeName, serviceAssembly))
79+
{
80+
throw new ConfigurationException($"The configured type '{fullyQualifiedTypeName}' does not implement the {typeof(T).Name} interface.");
81+
}
82+
83+
return serviceRegistrar.Configure(services);
84+
}
85+
finally
86+
{
87+
AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
88+
}
89+
}
90+
91+
private static bool IsSupportedType<T>(string fullyQualifiedTypeName, Assembly storageServiceAssembly)
92+
{
93+
Guard.Against.NullOrWhiteSpace(fullyQualifiedTypeName, nameof(fullyQualifiedTypeName));
94+
Guard.Against.Null(storageServiceAssembly, nameof(storageServiceAssembly));
95+
96+
var storageServiceType = Type.GetType(fullyQualifiedTypeName, assemblyeName => storageServiceAssembly, null, false);
97+
98+
return storageServiceType is not null &&
99+
storageServiceType.GetInterfaces().Contains(typeof(T));
100+
}
101+
102+
private static string GetAssemblyName(string fullyQualifiedTypeName)
103+
{
104+
var assemblyNameParts = fullyQualifiedTypeName.Split(',', StringSplitOptions.None);
105+
if (assemblyNameParts.Length < 2 || string.IsNullOrWhiteSpace(assemblyNameParts[1]))
106+
{
107+
throw new ConfigurationException($"The configured service type '{fullyQualifiedTypeName}' is not a valid fully qualified type name. E.g. {MessageBrokerServiceConfiguration.DefaultPublisherAssemblyName}")
108+
{
109+
HelpLink = "https://docs.microsoft.com/en-us/dotnet/standard/assembly/find-fully-qualified-name"
110+
};
111+
}
112+
113+
return assemblyNameParts[1].Trim();
114+
}
115+
116+
private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
117+
{
118+
Guard.Against.Null(args, nameof(args));
119+
120+
var requestedAssemblyName = new AssemblyName(args.Name);
121+
return LoadAssemblyFromDisk(requestedAssemblyName.Name);
122+
}
123+
124+
private static Assembly LoadAssemblyFromDisk(string assemblyName)
125+
{
126+
Guard.Against.NullOrWhiteSpace(assemblyName, nameof(assemblyName));
127+
Guard.Against.Null(s_fileSystem, nameof(s_fileSystem));
128+
129+
if (!s_fileSystem.Directory.Exists(SR.PlugInDirectoryPath))
130+
{
131+
throw new ConfigurationException($"Plug-in directory '{SR.PlugInDirectoryPath}' cannot be found.");
132+
}
133+
134+
var assemblyFilePath = s_fileSystem.Path.Combine(SR.PlugInDirectoryPath, $"{assemblyName}.dll");
135+
if (!s_fileSystem.File.Exists(assemblyFilePath))
136+
{
137+
throw new ConfigurationException($"The configured plug-in '{assemblyFilePath}' cannot be found.");
138+
}
139+
140+
var asesmblyeData = s_fileSystem.File.ReadAllBytes(assemblyFilePath);
141+
return Assembly.Load(asesmblyeData);
142+
}
143+
}
144+
}

src/Messaging/InternalVisible.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
using System.Runtime.CompilerServices;
22

3-
[assembly: InternalsVisibleTo("Monai.Deploy.Messaging.Test")]
3+
[assembly: InternalsVisibleTo("Monai.Deploy.Messaging.Tests")]
4+
[assembly: InternalsVisibleTo("Monai.Deploy.Messaging.RabbitMQ.Tests")]

src/Messaging/Messages/Message.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public T ConvertTo<T>()
4040
var json = Encoding.UTF8.GetString(Body);
4141
return JsonConvert.DeserializeObject<T>(json)!;
4242
}
43-
catch(Exception ex)
43+
catch (Exception ex)
4444
{
4545
throw new MessageConversionException($"Error converting message to type {typeof(T)}", ex);
4646
}

src/Messaging/Monai.Deploy.Messaging.csproj

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ SPDX-License-Identifier: Apache License 2.0
3333
</ItemGroup>
3434

3535
<ItemGroup>
36-
<Compile Remove="Test\**" />
37-
<EmbeddedResource Remove="Test\**" />
38-
<None Remove="Test\**" />
36+
<Compile Remove="Tests\**" />
37+
<EmbeddedResource Remove="Tests\**" />
38+
<None Remove="Tests\**" />
3939
</ItemGroup>
4040

4141
<ItemGroup>
@@ -54,7 +54,8 @@ SPDX-License-Identifier: Apache License 2.0
5454
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
5555
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
5656
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
57-
<PackageReference Include="RabbitMQ.Client" Version="6.2.4" />
57+
<PackageReference Include="RabbitMQ.Client" Version="6.4.0" />
5858
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
59+
<PackageReference Include="System.IO.Abstractions" Version="17.0.18" />
5960
</ItemGroup>
6061
</Project>

src/Messaging/RabbitMq/IServiceCollectionExtension.cs

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/Messaging/SR.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// SPDX-FileCopyrightText: © 2022 MONAI Consortium
2+
// SPDX-License-Identifier: Apache License 2.0
3+
4+
namespace Monai.Deploy.Messaging
5+
{
6+
internal static class SR
7+
{
8+
public const string PlugInDirectoryName = "plug-ins";
9+
public static readonly string PlugInDirectoryPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, SR.PlugInDirectoryName);
10+
}
11+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// SPDX-FileCopyrightText: © 2022 MONAI Consortium
2+
// SPDX-License-Identifier: Apache License 2.0
3+
4+
using Ardalis.GuardClauses;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Monai.Deploy.Messaging.Configuration;
7+
8+
namespace Monai.Deploy.Messaging
9+
{
10+
public abstract class SubscriberServiceRegistrationBase : ServiceRegistrationBase
11+
{
12+
protected SubscriberServiceRegistrationBase(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName)
13+
{
14+
}
15+
}
16+
17+
public abstract class PublisherServiceRegistrationBase : ServiceRegistrationBase
18+
{
19+
protected PublisherServiceRegistrationBase(string fullyQualifiedAssemblyName) : base(fullyQualifiedAssemblyName)
20+
{
21+
}
22+
}
23+
24+
public abstract class ServiceRegistrationBase
25+
{
26+
protected string FullyQualifiedAssemblyName { get; }
27+
protected string AssemblyFilename { get; }
28+
29+
protected ServiceRegistrationBase(string fullyQualifiedAssemblyName)
30+
{
31+
Guard.Against.NullOrWhiteSpace(fullyQualifiedAssemblyName, nameof(fullyQualifiedAssemblyName));
32+
FullyQualifiedAssemblyName = fullyQualifiedAssemblyName;
33+
AssemblyFilename = ParseAssemblyName();
34+
}
35+
36+
private string ParseAssemblyName()
37+
{
38+
var assemblyNameParts = FullyQualifiedAssemblyName.Split(',', StringSplitOptions.None);
39+
if (assemblyNameParts.Length < 2 || string.IsNullOrWhiteSpace(assemblyNameParts[1]))
40+
{
41+
throw new ConfigurationException($"Storage service '{FullyQualifiedAssemblyName}' is invalid. Please provide a fully qualified name.")
42+
{
43+
HelpLink = "https://docs.microsoft.com/en-us/dotnet/standard/assembly/find-fully-qualified-name"
44+
};
45+
}
46+
47+
return assemblyNameParts[1].Trim();
48+
}
49+
50+
public abstract IServiceCollection Configure(IServiceCollection services);
51+
}
52+
}

src/Messaging/Test/EventBaseTest.cs renamed to src/Messaging/Tests/EventBaseTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
using Monai.Deploy.Messaging.Events;
88
using Xunit;
99

10-
namespace Monai.Deploy.Messaging.Test
10+
namespace Monai.Deploy.Messaging.Tests
1111
{
1212
internal class StringClass : EventBase
1313
{

0 commit comments

Comments
 (0)