diff --git a/src/Api/IInputDataPlugin.cs b/src/Api/IInputDataPlugin.cs
new file mode 100644
index 000000000..c2b5b1f9d
--- /dev/null
+++ b/src/Api/IInputDataPlugin.cs
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 MONAI Consortium
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System.Threading.Tasks;
+using FellowOakDicom;
+using Monai.Deploy.InformaticsGateway.Api.Storage;
+
+namespace Monai.Deploy.InformaticsGateway.Api
+{
+ ///
+ /// IInputDataPlugin enables lightweight data processing over incoming data received from supported data ingestion
+ /// services.
+ /// Refer to for additional details.
+ ///
+ public interface IInputDataPlugin
+ {
+ Task<(DicomFile dicomFile, FileStorageMetadata fileMetadata)> Execute(DicomFile dicomFile, FileStorageMetadata fileMetadata);
+ }
+}
diff --git a/src/Api/IInputDataPluginEngine.cs b/src/Api/IInputDataPluginEngine.cs
new file mode 100644
index 000000000..b84ccfc65
--- /dev/null
+++ b/src/Api/IInputDataPluginEngine.cs
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 MONAI Consortium
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using FellowOakDicom;
+using Monai.Deploy.InformaticsGateway.Api.Storage;
+
+namespace Monai.Deploy.InformaticsGateway.Api
+{
+ ///
+ /// IInputDataPluginEngine processes incoming data receivied from various supported services through
+ /// a list of plug-ins based on .
+ /// Rules:
+ ///
+ /// - A list of plug-ins can be included with each export request, and each plug-in is executed in the order stored, processing one file at a time, enabling piping of the data before each file is exported.
+ /// - Plugins MUST be lightweight and not hinder the export process
+ /// - Plugins SHALL not accumulate files in memory or storage for bulk processing
+ ///
+ ///
+ public interface IInputDataPluginEngine
+ {
+ void Configure(IReadOnlyList pluginAssemblies);
+
+ Task<(DicomFile dicomFile, FileStorageMetadata fileMetadata)> ExecutePlugins(DicomFile dicomFile, FileStorageMetadata fileMetadata);
+ }
+}
diff --git a/src/Api/MonaiApplicationEntity.cs b/src/Api/MonaiApplicationEntity.cs
index f250d1f9d..68050a8ee 100644
--- a/src/Api/MonaiApplicationEntity.cs
+++ b/src/Api/MonaiApplicationEntity.cs
@@ -72,6 +72,8 @@ public class MonaiApplicationEntity : MongoDBEntityBase
///
public List Workflows { get; set; } = default!;
+ public List PluginAssemblies { get; set; } = default!;
+
///
/// Optional field to specify SOP Class UIDs to ignore.
/// and are mutually exclusive.
@@ -128,6 +130,8 @@ public void SetDefaultValues()
IgnoredSopClasses ??= new List();
AllowedSopClasses ??= new List();
+
+ PluginAssemblies ??= new List();
}
public override string ToString()
diff --git a/src/Database/EntityFramework/Configuration/MonaiApplicationEntityConfiguration.cs b/src/Database/EntityFramework/Configuration/MonaiApplicationEntityConfiguration.cs
index ecd8edc7b..264ce54c2 100644
--- a/src/Database/EntityFramework/Configuration/MonaiApplicationEntityConfiguration.cs
+++ b/src/Database/EntityFramework/Configuration/MonaiApplicationEntityConfiguration.cs
@@ -1,5 +1,5 @@
/*
- * Copyright 2021-2022 MONAI Consortium
+ * Copyright 2021-2023 MONAI Consortium
* Copyright 2021 NVIDIA Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,6 +25,7 @@
namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Configuration
{
#pragma warning disable CS8604, CS8603
+
internal class MonaiApplicationEntityConfiguration : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder builder)
@@ -51,6 +52,11 @@ public void Configure(EntityTypeBuilder builder)
v => JsonSerializer.Serialize(v, jsonSerializerSettings),
v => JsonSerializer.Deserialize>(v, jsonSerializerSettings))
.Metadata.SetValueComparer(valueComparer);
+ builder.Property(j => j.PluginAssemblies)
+ .HasConversion(
+ v => JsonSerializer.Serialize(v, jsonSerializerSettings),
+ v => JsonSerializer.Deserialize>(v, jsonSerializerSettings))
+ .Metadata.SetValueComparer(valueComparer);
builder.Property(j => j.IgnoredSopClasses)
.HasConversion(
v => JsonSerializer.Serialize(v, jsonSerializerSettings),
@@ -67,5 +73,6 @@ public void Configure(EntityTypeBuilder builder)
builder.Ignore(p => p.Id);
}
}
+
#pragma warning restore CS8604, CS8603
}
diff --git a/src/Database/EntityFramework/Migrations/20230802003305_R4_0.4.0.Designer.cs b/src/Database/EntityFramework/Migrations/20230802003305_R4_0.4.0.Designer.cs
new file mode 100644
index 000000000..96573a26a
--- /dev/null
+++ b/src/Database/EntityFramework/Migrations/20230802003305_R4_0.4.0.Designer.cs
@@ -0,0 +1,326 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Monai.Deploy.InformaticsGateway.Database.EntityFramework;
+
+#nullable disable
+
+namespace Monai.Deploy.InformaticsGateway.Database.Migrations
+{
+ [DbContext(typeof(InformaticsGatewayContext))]
+ [Migration("20230802003305_R4_0.4.0")]
+ partial class R4_040
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "6.0.15");
+
+ modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntity", b =>
+ {
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("AeTitle")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("DateTimeCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateTimeUpdated")
+ .HasColumnType("TEXT");
+
+ b.Property("HostIp")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Port")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Name");
+
+ b.HasIndex(new[] { "Name" }, "idx_destination_name")
+ .IsUnique();
+
+ b.HasIndex(new[] { "Name", "AeTitle", "HostIp", "Port" }, "idx_source_all")
+ .IsUnique();
+
+ b.ToTable("DestinationApplicationEntities");
+ });
+
+ modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.DicomAssociationInfo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CalledAeTitle")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CallingAeTitle")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CorrelationId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("DateTimeCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateTimeDisconnected")
+ .HasColumnType("TEXT");
+
+ b.Property("Duration")
+ .HasColumnType("TEXT");
+
+ b.Property("Errors")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("FileCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("RemoteHost")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("RemotePort")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("DicomAssociationHistories");
+ });
+
+ modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.MonaiApplicationEntity", b =>
+ {
+ b.Property("Name")
+ .HasColumnType("TEXT")
+ .HasColumnOrder(0);
+
+ b.Property("AeTitle")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("AllowedSopClasses")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("DateTimeCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateTimeUpdated")
+ .HasColumnType("TEXT");
+
+ b.Property("Grouping")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("IgnoredSopClasses")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("PluginAssemblies")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Timeout")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("Workflows")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Name");
+
+ b.HasIndex(new[] { "Name" }, "idx_monaiae_name")
+ .IsUnique();
+
+ b.ToTable("MonaiApplicationEntities");
+ });
+
+ modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Rest.InferenceRequest", b =>
+ {
+ b.Property("InferenceRequestId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("DateTimeCreated")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("InputMetadata")
+ .HasColumnType("TEXT");
+
+ b.Property("InputResources")
+ .HasColumnType("TEXT");
+
+ b.Property("OutputResources")
+ .HasColumnType("TEXT");
+
+ b.Property("Priority")
+ .HasColumnType("INTEGER");
+
+ b.Property("State")
+ .HasColumnType("INTEGER");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER");
+
+ b.Property("TransactionId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("TryCount")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("InferenceRequestId");
+
+ b.HasIndex(new[] { "InferenceRequestId" }, "idx_inferencerequest_inferencerequestid")
+ .IsUnique();
+
+ b.HasIndex(new[] { "State" }, "idx_inferencerequest_state");
+
+ b.HasIndex(new[] { "TransactionId" }, "idx_inferencerequest_transactionid")
+ .IsUnique();
+
+ b.ToTable("InferenceRequests");
+ });
+
+ modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.SourceApplicationEntity", b =>
+ {
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("AeTitle")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property("DateTimeCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateTimeUpdated")
+ .HasColumnType("TEXT");
+
+ b.Property("HostIp")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Name");
+
+ b.HasIndex(new[] { "Name", "AeTitle", "HostIp" }, "idx_source_all")
+ .IsUnique()
+ .HasDatabaseName("idx_source_all1");
+
+ b.HasIndex(new[] { "Name" }, "idx_source_name")
+ .IsUnique();
+
+ b.ToTable("SourceApplicationEntities");
+ });
+
+ modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Storage.Payload", b =>
+ {
+ b.Property("PayloadId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CorrelationId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("DateTimeCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("Files")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("MachineName")
+ .HasColumnType("TEXT");
+
+ b.Property("RetryCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("State")
+ .HasColumnType("INTEGER");
+
+ b.Property("Timeout")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("PayloadId");
+
+ b.HasIndex(new[] { "CorrelationId", "PayloadId" }, "idx_payload_ids")
+ .IsUnique();
+
+ b.HasIndex(new[] { "State" }, "idx_payload_state");
+
+ b.ToTable("Payloads");
+ });
+
+ modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Database.Api.StorageMetadataWrapper", b =>
+ {
+ b.Property("CorrelationId")
+ .HasColumnType("TEXT");
+
+ b.Property("Identity")
+ .HasColumnType("TEXT");
+
+ b.Property("DateTimeCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("IsUploaded")
+ .HasColumnType("INTEGER");
+
+ b.Property("TypeName")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("CorrelationId", "Identity");
+
+ b.HasIndex(new[] { "CorrelationId" }, "idx_storagemetadata_correlation");
+
+ b.HasIndex(new[] { "CorrelationId", "Identity" }, "idx_storagemetadata_ids");
+
+ b.HasIndex(new[] { "IsUploaded" }, "idx_storagemetadata_uploaded");
+
+ b.ToTable("StorageMetadataWrapperEntities");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Database/EntityFramework/Migrations/20230802003305_R4_0.4.0.cs b/src/Database/EntityFramework/Migrations/20230802003305_R4_0.4.0.cs
new file mode 100644
index 000000000..5b0483c71
--- /dev/null
+++ b/src/Database/EntityFramework/Migrations/20230802003305_R4_0.4.0.cs
@@ -0,0 +1,26 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Monai.Deploy.InformaticsGateway.Database.Migrations
+{
+ public partial class R4_040 : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "PluginAssemblies",
+ table: "MonaiApplicationEntities",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: "");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "PluginAssemblies",
+ table: "MonaiApplicationEntities");
+ }
+ }
+}
diff --git a/src/Database/EntityFramework/Migrations/InformaticsGatewayContextModelSnapshot.cs b/src/Database/EntityFramework/Migrations/InformaticsGatewayContextModelSnapshot.cs
index cc545bd79..b6b55d4ac 100644
--- a/src/Database/EntityFramework/Migrations/InformaticsGatewayContextModelSnapshot.cs
+++ b/src/Database/EntityFramework/Migrations/InformaticsGatewayContextModelSnapshot.cs
@@ -133,6 +133,10 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.IsRequired()
.HasColumnType("TEXT");
+ b.Property("PluginAssemblies")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
b.Property("Timeout")
.HasColumnType("INTEGER");
diff --git a/src/Database/EntityFramework/Test/MonaiApplicationEntityRepositoryTest.cs b/src/Database/EntityFramework/Test/MonaiApplicationEntityRepositoryTest.cs
index 372484845..d25eca352 100644
--- a/src/Database/EntityFramework/Test/MonaiApplicationEntityRepositoryTest.cs
+++ b/src/Database/EntityFramework/Test/MonaiApplicationEntityRepositoryTest.cs
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 MONAI Consortium
+ * Copyright 2022-2023 MONAI Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -70,7 +70,8 @@ public async Task GivenAMonaiApplicationEntity_WhenAddingToDatabase_ExpectItToBe
AllowedSopClasses = new List { "1", "2", "3" },
Workflows = new List { "W1", "W2" },
Grouping = "G",
- IgnoredSopClasses = new List { "4", "5" }
+ IgnoredSopClasses = new List { "4", "5" },
+ PluginAssemblies = new List { "AssemblyA", "AssemblyB", "AssemblyC" },
};
var store = new MonaiApplicationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options);
@@ -132,7 +133,7 @@ public async Task GivenAMonaiApplicationEntity_WhenRemoveIsCalled_ExpectItToDele
}
[Fact]
- public async Task GivenDestinationApplicationEntitiesInTheDatabase_WhenToListIsCalled_ExpectAllEntitiesToBeReturned()
+ public async Task GivenMonaiApplicationEntitiesInTheDatabase_WhenToListIsCalled_ExpectAllEntitiesToBeReturned()
{
var store = new MonaiApplicationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options);
diff --git a/src/InformaticsGateway/Common/PlugingLoadingException.cs b/src/InformaticsGateway/Common/PlugingLoadingException.cs
new file mode 100644
index 000000000..dea15d6c1
--- /dev/null
+++ b/src/InformaticsGateway/Common/PlugingLoadingException.cs
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 MONAI Consortium
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+
+namespace Monai.Deploy.InformaticsGateway.Common
+{
+ public class PlugingLoadingException : Exception
+ {
+ public PlugingLoadingException(string message) : base(message)
+ {
+ }
+
+ public PlugingLoadingException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/src/InformaticsGateway/Common/SR.cs b/src/InformaticsGateway/Common/SR.cs
new file mode 100644
index 000000000..e23ec6041
--- /dev/null
+++ b/src/InformaticsGateway/Common/SR.cs
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 MONAI Consortium
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System.IO;
+using System;
+
+namespace Monai.Deploy.InformaticsGateway.Common
+{
+ 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/InformaticsGateway/Common/TypeExtensions.cs b/src/InformaticsGateway/Common/TypeExtensions.cs
new file mode 100644
index 000000000..662e394c8
--- /dev/null
+++ b/src/InformaticsGateway/Common/TypeExtensions.cs
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2022-2023 MONAI Consortium
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Ardalis.GuardClauses;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Monai.Deploy.InformaticsGateway.Common
+{
+ public static class TypeExtensions
+ {
+ public static T CreateInstance(this Type type, IServiceProvider serviceProvider, params object[] parameters)
+ {
+ Guard.Against.Null(type, nameof(type));
+ Guard.Against.Null(serviceProvider, nameof(serviceProvider));
+
+ return (T)ActivatorUtilities.CreateInstance(serviceProvider, type, parameters);
+ }
+
+ public static T CreateInstance(this Type interfaceType, IServiceProvider serviceProvider, string typeString, params object[] parameters)
+ {
+ Guard.Against.Null(interfaceType, nameof(interfaceType));
+ Guard.Against.Null(serviceProvider, nameof(serviceProvider));
+ Guard.Against.NullOrWhiteSpace(typeString, nameof(typeString));
+
+ var type = interfaceType.GetType(typeString);
+ var processor = ActivatorUtilities.CreateInstance(serviceProvider, type, parameters);
+
+ return (T)processor;
+ }
+
+ public static Type GetType(this Type interfaceType, string typeString)
+ {
+ Guard.Against.Null(interfaceType, nameof(interfaceType));
+ Guard.Against.NullOrWhiteSpace(typeString, nameof(typeString));
+
+ var type = Type.GetType(
+ typeString,
+ (name) =>
+ {
+ var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(z => !string.IsNullOrWhiteSpace(z.FullName) && z.FullName.StartsWith(name.FullName));
+
+ assembly ??= Assembly.LoadFile(Path.Combine(SR.PlugInDirectoryPath, $"{name.Name}.dll"));
+
+ return assembly;
+ },
+ null,
+ true);
+
+ if (type is not null &&
+ (type.IsSubclassOf(interfaceType) ||
+ (type.BaseType is not null && type.BaseType.IsAssignableTo(interfaceType)) ||
+ (type.GetInterfaces().Contains(interfaceType))))
+ {
+ return type;
+ }
+
+ throw new NotSupportedException($"{typeString} is not a sub-type of {interfaceType.Name}");
+ }
+ }
+}
diff --git a/src/InformaticsGateway/Program.cs b/src/InformaticsGateway/Program.cs
index d2c37df1d..3c3e04ee4 100644
--- a/src/InformaticsGateway/Program.cs
+++ b/src/InformaticsGateway/Program.cs
@@ -1,5 +1,5 @@
/*
- * Copyright 2021-2022 MONAI Consortium
+ * Copyright 2021-2023 MONAI Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Monai.Deploy.InformaticsGateway.Api;
using Monai.Deploy.InformaticsGateway.Common;
using Monai.Deploy.InformaticsGateway.Configuration;
using Monai.Deploy.InformaticsGateway.Database;
@@ -111,6 +112,7 @@ internal static IHostBuilder CreateHostBuilder(string[] args) =>
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddMonaiDeployStorageService(hostContext.Configuration.GetSection("InformaticsGateway:storage:serviceAssemblyName").Value, Monai.Deploy.Storage.HealthCheckOptions.ServiceHealthCheck);
diff --git a/src/InformaticsGateway/Services/Common/InputDataPluginEngine.cs b/src/InformaticsGateway/Services/Common/InputDataPluginEngine.cs
new file mode 100644
index 000000000..11b714469
--- /dev/null
+++ b/src/InformaticsGateway/Services/Common/InputDataPluginEngine.cs
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023 MONAI Consortium
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using FellowOakDicom;
+using Microsoft.Extensions.Logging;
+using Monai.Deploy.InformaticsGateway.Api;
+using Monai.Deploy.InformaticsGateway.Api.Storage;
+using Monai.Deploy.InformaticsGateway.Common;
+
+namespace Monai.Deploy.InformaticsGateway.Services.Common
+{
+ internal class InputDataPluginEngine : IInputDataPluginEngine
+ {
+ private readonly IServiceProvider _serviceProvider;
+ private readonly ILogger _logger;
+ private IReadOnlyList _plugsins;
+
+ public InputDataPluginEngine(IServiceProvider serviceProvider, ILogger logger)
+ {
+ _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public void Configure(IReadOnlyList pluginAssemblies)
+ {
+ _plugsins = LoadPlugins(_serviceProvider, pluginAssemblies);
+ }
+
+ public async Task<(DicomFile dicomFile, FileStorageMetadata fileMetadata)> ExecutePlugins(DicomFile dicomFile, FileStorageMetadata fileMetadata)
+ {
+ if (_plugsins == null)
+ {
+ throw new ApplicationException("InputDataPluginEngine not configured, please call Configure() first.");
+ }
+
+ foreach (var plugin in _plugsins)
+ {
+ (dicomFile, fileMetadata) = await plugin.Execute(dicomFile, fileMetadata).ConfigureAwait(false);
+ }
+
+ return (dicomFile, fileMetadata);
+ }
+
+ private static IReadOnlyList LoadPlugins(IServiceProvider serviceProvider, IReadOnlyList pluginAssemblies)
+ {
+ var exceptions = new List();
+ var list = new List();
+ foreach (var plugin in pluginAssemblies)
+ {
+ try
+ {
+ list.Add(typeof(IInputDataPlugin).CreateInstance(serviceProvider, typeString: plugin));
+ }
+ catch (Exception ex)
+ {
+ exceptions.Add(new PlugingLoadingException($"Error loading plug-in '{plugin}'.", ex));
+ }
+ }
+
+ if (exceptions.Any())
+ {
+ throw new AggregateException("Error loading plug-in(s).", exceptions);
+ }
+
+ return list;
+ }
+ }
+}
diff --git a/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs b/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs
index b2d9ca5af..a668bdfcc 100644
--- a/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs
+++ b/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs
@@ -35,7 +35,7 @@ namespace Monai.Deploy.InformaticsGateway.Services.Scp
{
internal class ApplicationEntityHandler : IDisposable, IApplicationEntityHandler
{
- private readonly object _lock = new object();
+ private readonly object _lock = new();
private readonly ILogger _logger;
private readonly IOptions _options;
@@ -43,6 +43,7 @@ internal class ApplicationEntityHandler : IDisposable, IApplicationEntityHandler
private readonly IPayloadAssembler _payloadAssembler;
private readonly IObjectUploadQueue _uploadQueue;
private readonly IFileSystem _fileSystem;
+ private readonly IInputDataPluginEngine _pluginEngine;
private MonaiApplicationEntity _configuration;
private DicomJsonOptions _dicomJsonOptions;
private bool _validateDicomValueOnJsonSerialization;
@@ -61,6 +62,7 @@ public ApplicationEntityHandler(
_payloadAssembler = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IPayloadAssembler));
_uploadQueue = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IObjectUploadQueue));
_fileSystem = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IFileSystem));
+ _pluginEngine = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IInputDataPluginEngine));
}
public void Configure(MonaiApplicationEntity monaiApplicationEntity, DicomJsonOptions dicomJsonOptions, bool validateDicomValuesOnJsonSerialization)
@@ -79,6 +81,7 @@ public void Configure(MonaiApplicationEntity monaiApplicationEntity, DicomJsonOp
_configuration = monaiApplicationEntity;
_dicomJsonOptions = dicomJsonOptions;
_validateDicomValueOnJsonSerialization = validateDicomValuesOnJsonSerialization;
+ _pluginEngine.Configure(_configuration.PluginAssemblies);
}
}
@@ -112,11 +115,14 @@ public async Task HandleInstanceAsync(DicomCStoreRequest request, string calledA
dicomInfo.SetWorkflows(_configuration.Workflows.ToArray());
}
- await dicomInfo.SetDataStreams(request.File, request.File.ToJson(_dicomJsonOptions, _validateDicomValueOnJsonSerialization), _options.Value.Storage.TemporaryDataStorage, _fileSystem, _options.Value.Storage.LocalTemporaryStoragePath).ConfigureAwait(false);
+ var (dicomFile, fileMetadata) = await _pluginEngine.ExecutePlugins(request.File, dicomInfo).ConfigureAwait(false);
+
+ dicomInfo = fileMetadata as DicomFileStorageMetadata;
+ await dicomInfo.SetDataStreams(dicomFile, dicomFile.ToJson(_dicomJsonOptions, _validateDicomValueOnJsonSerialization), _options.Value.Storage.TemporaryDataStorage, _fileSystem, _options.Value.Storage.LocalTemporaryStoragePath).ConfigureAwait(false);
var dicomTag = FellowOakDicom.DicomTag.Parse(_configuration.Grouping);
_logger.QueueInstanceUsingDicomTag(dicomTag);
- var key = request.Dataset.GetSingleValue(dicomTag);
+ var key = dicomFile.Dataset.GetSingleValue(dicomTag);
var payloadid = await _payloadAssembler.Queue(key, dicomInfo, _configuration.Timeout).ConfigureAwait(false);
dicomInfo.PayloadId = payloadid.ToString();
diff --git a/src/InformaticsGateway/Services/Scu/ScuService.cs b/src/InformaticsGateway/Services/Scu/ScuService.cs
index 6ca72f240..a23b4ca2a 100644
--- a/src/InformaticsGateway/Services/Scu/ScuService.cs
+++ b/src/InformaticsGateway/Services/Scu/ScuService.cs
@@ -56,7 +56,7 @@ public ScuService(IServiceScopeFactory serviceScopeFactory,
_workQueue = _scope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IScuQueue));
}
- private async Task BackgroundProcessingAsync(CancellationToken cancellationToken)
+ private Task BackgroundProcessingAsync(CancellationToken cancellationToken)
{
_logger.ServiceRunning(ServiceName);
while (!cancellationToken.IsCancellationRequested)
@@ -83,11 +83,12 @@ private async Task BackgroundProcessingAsync(CancellationToken cancellationToken
}
Status = ServiceStatus.Cancelled;
_logger.ServiceCancelled(ServiceName);
+ return Task.CompletedTask;
}
private void ProcessThread(ScuWorkRequest request, CancellationToken cancellationToken)
{
- Task.Run(() => Process(request, cancellationToken));
+ Task.Run(() => Process(request, cancellationToken), cancellationToken);
}
private async Task Process(ScuWorkRequest request, CancellationToken cancellationToken)
diff --git a/src/InformaticsGateway/Test/Monai.Deploy.InformaticsGateway.Test.csproj b/src/InformaticsGateway/Test/Monai.Deploy.InformaticsGateway.Test.csproj
index abc369e6a..52256569c 100644
--- a/src/InformaticsGateway/Test/Monai.Deploy.InformaticsGateway.Test.csproj
+++ b/src/InformaticsGateway/Test/Monai.Deploy.InformaticsGateway.Test.csproj
@@ -1,5 +1,5 @@
+
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/InformaticsGateway/Test/Plugins/TestInputDataPlugins.cs b/src/InformaticsGateway/Test/Plugins/TestInputDataPlugins.cs
new file mode 100644
index 000000000..0bf5afec2
--- /dev/null
+++ b/src/InformaticsGateway/Test/Plugins/TestInputDataPlugins.cs
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 MONAI Consortium
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using FellowOakDicom;
+using Monai.Deploy.InformaticsGateway.Api;
+using Monai.Deploy.InformaticsGateway.Api.Storage;
+
+namespace Monai.Deploy.InformaticsGateway.Test.Plugins
+{
+ public class TestInputDataPluginAddWorkflow : IInputDataPlugin
+ {
+ public static readonly string TestString = "TestInputDataPlugin executed!";
+
+ public Task<(DicomFile dicomFile, FileStorageMetadata fileMetadata)> Execute(DicomFile dicomFile, FileStorageMetadata fileMetadata)
+ {
+ fileMetadata.Workflows.Add(TestString);
+ return Task.FromResult((dicomFile, fileMetadata));
+ }
+ }
+
+ public class TestInputDataPluginModifyDicomFile : IInputDataPlugin
+ {
+ public static readonly DicomTag ExpectedTag = DicomTag.PatientAddress;
+ public static readonly string ExpectedValue = "Aborted by TestInputDataPluginModifyCorrelationId";
+
+ public Task<(DicomFile dicomFile, FileStorageMetadata fileMetadata)> Execute(DicomFile dicomFile, FileStorageMetadata fileMetadata)
+ {
+ dicomFile.Dataset.Add(ExpectedTag, ExpectedValue);
+ return Task.FromResult((dicomFile, fileMetadata));
+ }
+ }
+}
diff --git a/src/InformaticsGateway/Test/Services/Common/InputDataPluginEngineTest.cs b/src/InformaticsGateway/Test/Services/Common/InputDataPluginEngineTest.cs
new file mode 100644
index 000000000..d80cce0ee
--- /dev/null
+++ b/src/InformaticsGateway/Test/Services/Common/InputDataPluginEngineTest.cs
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2023 MONAI Consortium
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using FellowOakDicom;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Monai.Deploy.InformaticsGateway.Api.Storage;
+using Monai.Deploy.InformaticsGateway.Common;
+using Monai.Deploy.InformaticsGateway.Services.Common;
+using Monai.Deploy.InformaticsGateway.Test.Plugins;
+using Moq;
+using Xunit;
+
+namespace Monai.Deploy.InformaticsGateway.Test.Services.Common
+{
+ public class InputDataPluginEngineTest
+ {
+ private readonly Mock> _logger;
+ private readonly Mock _serviceScopeFactory;
+ private readonly Mock _serviceScope;
+ private readonly ServiceProvider _serviceProvider;
+
+ public InputDataPluginEngineTest()
+ {
+ _logger = new Mock>();
+ _serviceScopeFactory = new Mock();
+ _serviceScope = new Mock();
+
+ var services = new ServiceCollection();
+ services.AddScoped(p => _logger.Object);
+
+ _serviceProvider = services.BuildServiceProvider();
+ _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object);
+ _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider);
+
+ _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true);
+ }
+
+ [Fact]
+ public void GivenAnInputDataPluginEngine_WhenInitialized_ExpectParametersToBeValidated()
+ {
+ Assert.Throws(() => new InputDataPluginEngine(null, null));
+ Assert.Throws(() => new InputDataPluginEngine(_serviceProvider, null));
+
+ _ = new InputDataPluginEngine(_serviceProvider, _logger.Object);
+ }
+
+ [Fact]
+ public void GivenAnInputDataPluginEngine_WhenConfigureIsCalledWithBogusAssemblies_ThrowsException()
+ {
+ var pluginEngine = new InputDataPluginEngine(_serviceProvider, _logger.Object);
+ var assemblies = new List() { "SomeBogusAssemblye" };
+
+ var exceptions = Assert.Throws(() => pluginEngine.Configure(assemblies));
+
+ Assert.Single(exceptions.InnerExceptions);
+ Assert.True(exceptions.InnerException is PlugingLoadingException);
+ Assert.Contains("Error loading plug-in 'SomeBogusAssemblye'", exceptions.InnerException.Message);
+ }
+
+ [Fact]
+ public void GivenAnInputDataPluginEngine_WhenConfigureIsCalledWithAValidAssembly_ExpectNoExceptions()
+ {
+ var pluginEngine = new InputDataPluginEngine(_serviceProvider, _logger.Object);
+ var assemblies = new List() { typeof(TestInputDataPluginAddWorkflow).AssemblyQualifiedName };
+
+ pluginEngine.Configure(assemblies);
+ }
+
+ [Fact]
+ public async Task GivenAnInputDataPluginEngine_WhenExecutePluginsIsCalledWithoutConfigure_ThrowsException()
+ {
+ var pluginEngine = new InputDataPluginEngine(_serviceProvider, _logger.Object);
+ var assemblies = new List() { typeof(TestInputDataPluginAddWorkflow).AssemblyQualifiedName };
+
+ var dicomFile = GenerateDicomFile();
+ var dicomInfo = new DicomFileStorageMetadata(
+ Guid.NewGuid().ToString(),
+ Guid.NewGuid().ToString(),
+ dicomFile.Dataset.GetString(DicomTag.StudyInstanceUID),
+ dicomFile.Dataset.GetString(DicomTag.SeriesInstanceUID),
+ dicomFile.Dataset.GetString(DicomTag.SOPInstanceUID));
+
+ await Assert.ThrowsAsync(async () => await pluginEngine.ExecutePlugins(dicomFile, dicomInfo));
+ }
+
+ [Fact]
+ public async Task GivenAnInputDataPluginEngine_WhenExecutePluginsIsCalled_ExpectDataIsProcessedByPluginAsync()
+ {
+ var pluginEngine = new InputDataPluginEngine(_serviceProvider, _logger.Object);
+ var assemblies = new List()
+ {
+ typeof(TestInputDataPluginAddWorkflow).AssemblyQualifiedName,
+ typeof(TestInputDataPluginModifyDicomFile).AssemblyQualifiedName
+ };
+
+ pluginEngine.Configure(assemblies);
+
+ var dicomFile = GenerateDicomFile();
+ var dicomInfo = new DicomFileStorageMetadata(
+ Guid.NewGuid().ToString(),
+ Guid.NewGuid().ToString(),
+ dicomFile.Dataset.GetString(DicomTag.StudyInstanceUID),
+ dicomFile.Dataset.GetString(DicomTag.SeriesInstanceUID),
+ dicomFile.Dataset.GetString(DicomTag.SOPInstanceUID));
+
+ var (resultDicomFile, resultDicomInfo) = await pluginEngine.ExecutePlugins(dicomFile, dicomInfo);
+
+ Assert.Equal(resultDicomFile, dicomFile);
+ Assert.Equal(resultDicomInfo, dicomInfo);
+ Assert.True(dicomInfo.Workflows.Contains(TestInputDataPluginAddWorkflow.TestString));
+ Assert.Equal(TestInputDataPluginModifyDicomFile.ExpectedValue, resultDicomFile.Dataset.GetString(TestInputDataPluginModifyDicomFile.ExpectedTag));
+ }
+
+ private static DicomFile GenerateDicomFile()
+ {
+ var dataset = new DicomDataset
+ {
+ { DicomTag.PatientID, "PID" },
+ { DicomTag.StudyInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() },
+ { DicomTag.SeriesInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() },
+ { DicomTag.SOPInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() },
+ { DicomTag.SOPClassUID, DicomUID.SecondaryCaptureImageStorage.UID }
+ };
+ return new DicomFile(dataset);
+ }
+ }
+}
diff --git a/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs b/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs
index 96e465187..388037aaf 100644
--- a/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs
+++ b/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs
@@ -31,6 +31,7 @@
using Monai.Deploy.InformaticsGateway.Services.Connectors;
using Monai.Deploy.InformaticsGateway.Services.Scp;
using Monai.Deploy.InformaticsGateway.Services.Storage;
+using Monai.Deploy.InformaticsGateway.Test.Plugins;
using Moq;
using xRetry;
using Xunit;
@@ -43,6 +44,7 @@ public class ApplicationEntityHandlerTest
private readonly Mock _serviceScopeFactory;
private readonly Mock _serviceScope;
+ private readonly Mock _inputDataPluginEngine;
private readonly Mock _payloadAssembler;
private readonly Mock _uploadQueue;
private readonly IOptions _options;
@@ -54,6 +56,7 @@ public ApplicationEntityHandlerTest()
_logger = new Mock>();
_serviceScopeFactory = new Mock();
_serviceScope = new Mock();
+ _inputDataPluginEngine = new Mock();
_payloadAssembler = new Mock();
_uploadQueue = new Mock();
@@ -64,6 +67,10 @@ public ApplicationEntityHandlerTest()
services.AddScoped(p => _payloadAssembler.Object);
services.AddScoped(p => _uploadQueue.Object);
services.AddScoped(p => _fileSystem);
+ services.AddScoped(p => _inputDataPluginEngine.Object);
+
+ _inputDataPluginEngine.Setup(p => p.Configure(It.IsAny>()));
+
_serviceProvider = services.BuildServiceProvider();
_serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object);
_serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider);
@@ -157,7 +164,8 @@ public async Task GivenACStoreRequest_WhenHandleInstanceAsyncIsCalled_ExpectADic
{
AeTitle = "TESTAET",
Name = "TESTAET",
- Workflows = new List() { "AppA", "AppB", Guid.NewGuid().ToString() }
+ Workflows = new List() { "AppA", "AppB", Guid.NewGuid().ToString() },
+ PluginAssemblies = new List() { typeof(TestInputDataPluginAddWorkflow).AssemblyQualifiedName }
};
var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options);
@@ -166,11 +174,15 @@ public async Task GivenACStoreRequest_WhenHandleInstanceAsyncIsCalled_ExpectADic
var request = GenerateRequest();
var dicomToolkit = new DicomToolkit();
var uids = dicomToolkit.GetStudySeriesSopInstanceUids(request.File);
+ _inputDataPluginEngine.Setup(p => p.ExecutePlugins(It.IsAny(), It.IsAny()))
+ .Returns((DicomFile dicomFile, FileStorageMetadata fileMetadata) => Task.FromResult((dicomFile, fileMetadata)));
await handler.HandleInstanceAsync(request, aet.AeTitle, "CALLING", Guid.NewGuid(), uids);
_uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Once());
_payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once());
+ _inputDataPluginEngine.Verify(p => p.Configure(It.IsAny>()), Times.Once());
+ _inputDataPluginEngine.Verify(p => p.ExecutePlugins(It.IsAny(), It.IsAny()), Times.Once());
}
[RetryFact(5, 250)]
diff --git a/src/InformaticsGateway/Test/packages.lock.json b/src/InformaticsGateway/Test/packages.lock.json
index 4d2dc7524..c01b8d746 100644
--- a/src/InformaticsGateway/Test/packages.lock.json
+++ b/src/InformaticsGateway/Test/packages.lock.json
@@ -1897,132 +1897,138 @@
"monai.deploy.informaticsgateway": {
"type": "Project",
"dependencies": {
- "Ardalis.GuardClauses": "4.0.1",
- "DotNext.Threading": "4.7.4",
- "HL7-dotnetcore": "2.35.0",
- "Karambolo.Extensions.Logging.File": "3.4.0",
- "Microsoft.EntityFrameworkCore": "6.0.15",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
- "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.15",
- "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "6.0.15",
- "Microsoft.Extensions.Hosting": "6.0.1",
- "Microsoft.Extensions.Logging": "6.0.0",
- "Microsoft.Extensions.Logging.Console": "6.0.0",
- "Microsoft.Extensions.Options": "6.0.0",
- "Monai.Deploy.InformaticsGateway.Api": "1.0.0",
- "Monai.Deploy.InformaticsGateway.Common": "1.0.0",
- "Monai.Deploy.InformaticsGateway.Configuration": "1.0.0",
- "Monai.Deploy.InformaticsGateway.Database": "1.0.0",
- "Monai.Deploy.InformaticsGateway.Database.Api": "1.0.0",
- "Monai.Deploy.InformaticsGateway.Database.EntityFramework": "1.0.0",
- "Monai.Deploy.InformaticsGateway.DicomWeb.Client": "1.0.0",
- "Monai.Deploy.Messaging.RabbitMQ": "0.1.22",
- "Monai.Deploy.Security": "0.1.3",
- "Monai.Deploy.Storage": "0.2.16",
- "Monai.Deploy.Storage.MinIO": "0.2.16",
- "NLog": "5.1.3",
- "NLog.Web.AspNetCore": "5.2.3",
- "Polly": "7.2.3",
- "Swashbuckle.AspNetCore": "6.5.0",
- "fo-dicom": "5.0.3",
- "fo-dicom.NLog": "5.0.3"
+ "Ardalis.GuardClauses": "[4.0.1, )",
+ "DotNext.Threading": "[4.7.4, )",
+ "HL7-dotnetcore": "[2.35.0, )",
+ "Karambolo.Extensions.Logging.File": "[3.4.0, )",
+ "Microsoft.EntityFrameworkCore": "[6.0.15, )",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "[6.0.0, )",
+ "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "[6.0.15, )",
+ "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[6.0.15, )",
+ "Microsoft.Extensions.Hosting": "[6.0.1, )",
+ "Microsoft.Extensions.Logging": "[6.0.0, )",
+ "Microsoft.Extensions.Logging.Console": "[6.0.0, )",
+ "Microsoft.Extensions.Options": "[6.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Database": "[1.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Database.Api": "[1.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Database.EntityFramework": "[1.0.0, )",
+ "Monai.Deploy.InformaticsGateway.DicomWeb.Client": "[1.0.0, )",
+ "Monai.Deploy.Messaging.RabbitMQ": "[0.1.22, )",
+ "Monai.Deploy.Security": "[0.1.3, )",
+ "Monai.Deploy.Storage": "[0.2.16, )",
+ "Monai.Deploy.Storage.MinIO": "[0.2.16, )",
+ "NLog": "[5.1.3, )",
+ "NLog.Web.AspNetCore": "[5.2.3, )",
+ "Polly": "[7.2.3, )",
+ "Swashbuckle.AspNetCore": "[6.5.0, )",
+ "fo-dicom": "[5.0.3, )",
+ "fo-dicom.NLog": "[5.0.3, )"
}
},
"monai.deploy.informaticsgateway.api": {
"type": "Project",
"dependencies": {
- "Macross.Json.Extensions": "3.0.0",
- "Microsoft.EntityFrameworkCore.Abstractions": "6.0.15",
- "Monai.Deploy.InformaticsGateway.Common": "1.0.0",
- "Monai.Deploy.Messaging": "0.1.22",
- "Monai.Deploy.Storage": "0.2.16"
+ "Macross.Json.Extensions": "[3.0.0, )",
+ "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.15, )",
+ "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )",
+ "Monai.Deploy.Messaging": "[0.1.22, )",
+ "Monai.Deploy.Storage": "[0.2.16, )"
}
},
"monai.deploy.informaticsgateway.client.common": {
"type": "Project",
"dependencies": {
- "Ardalis.GuardClauses": "4.0.1",
- "System.Text.Json": "6.0.7"
+ "Ardalis.GuardClauses": "[4.0.1, )",
+ "System.Text.Json": "[6.0.7, )"
}
},
"monai.deploy.informaticsgateway.common": {
"type": "Project",
"dependencies": {
- "Ardalis.GuardClauses": "4.0.1",
- "System.IO.Abstractions": "17.2.3",
- "System.Threading.Tasks.Dataflow": "6.0.0",
- "fo-dicom": "5.0.3"
+ "Ardalis.GuardClauses": "[4.0.1, )",
+ "System.IO.Abstractions": "[17.2.3, )",
+ "System.Threading.Tasks.Dataflow": "[6.0.0, )",
+ "fo-dicom": "[5.0.3, )"
}
},
"monai.deploy.informaticsgateway.configuration": {
"type": "Project",
"dependencies": {
- "Microsoft.Extensions.Logging.Abstractions": "6.0.3",
- "Microsoft.Extensions.Options": "6.0.0",
- "Monai.Deploy.InformaticsGateway.Api": "1.0.0",
- "Monai.Deploy.InformaticsGateway.Common": "1.0.0",
- "Monai.Deploy.Messaging": "0.1.22",
- "Monai.Deploy.Storage": "0.2.16",
- "System.IO.Abstractions": "17.2.3"
+ "Microsoft.Extensions.Logging.Abstractions": "[6.0.3, )",
+ "Microsoft.Extensions.Options": "[6.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )",
+ "Monai.Deploy.Messaging": "[0.1.22, )",
+ "Monai.Deploy.Storage": "[0.2.16, )",
+ "System.IO.Abstractions": "[17.2.3, )"
}
},
"monai.deploy.informaticsgateway.database": {
"type": "Project",
"dependencies": {
- "AspNetCore.HealthChecks.MongoDb": "6.0.2",
- "Microsoft.EntityFrameworkCore": "6.0.15",
- "Microsoft.Extensions.Configuration": "6.0.1",
- "Microsoft.Extensions.Configuration.FileExtensions": "6.0.0",
- "Microsoft.Extensions.Configuration.Json": "6.0.0",
- "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "6.0.15",
- "Microsoft.Extensions.Options.ConfigurationExtensions": "6.0.0",
- "Monai.Deploy.InformaticsGateway.Api": "1.0.0",
- "Monai.Deploy.InformaticsGateway.Configuration": "1.0.0",
- "Monai.Deploy.InformaticsGateway.Database.Api": "1.0.0",
- "Monai.Deploy.InformaticsGateway.Database.EntityFramework": "1.0.0",
- "Monai.Deploy.InformaticsGateway.Database.MongoDB": "1.0.0"
+ "AspNetCore.HealthChecks.MongoDb": "[6.0.2, )",
+ "Microsoft.EntityFrameworkCore": "[6.0.15, )",
+ "Microsoft.Extensions.Configuration": "[6.0.1, )",
+ "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )",
+ "Microsoft.Extensions.Configuration.Json": "[6.0.0, )",
+ "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[6.0.15, )",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "[6.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Database.Api": "[1.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Database.EntityFramework": "[1.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Database.MongoDB": "[1.0.0, )"
}
},
"monai.deploy.informaticsgateway.database.api": {
"type": "Project",
"dependencies": {
- "Microsoft.EntityFrameworkCore": "6.0.15",
- "Monai.Deploy.InformaticsGateway.Api": "1.0.0",
- "Monai.Deploy.InformaticsGateway.Configuration": "1.0.0",
- "Polly": "7.2.3"
+ "Microsoft.EntityFrameworkCore": "[6.0.15, )",
+ "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )",
+ "Polly": "[7.2.3, )"
}
},
"monai.deploy.informaticsgateway.database.entityframework": {
"type": "Project",
"dependencies": {
- "Microsoft.EntityFrameworkCore": "6.0.15",
- "Microsoft.EntityFrameworkCore.Sqlite": "6.0.15",
- "Microsoft.Extensions.Configuration": "6.0.1",
- "Microsoft.Extensions.Configuration.FileExtensions": "6.0.0",
- "Microsoft.Extensions.Configuration.Json": "6.0.0",
- "Monai.Deploy.InformaticsGateway.Api": "1.0.0",
- "Monai.Deploy.InformaticsGateway.Configuration": "1.0.0",
- "Monai.Deploy.InformaticsGateway.Database.Api": "1.0.0"
+ "Microsoft.EntityFrameworkCore": "[6.0.15, )",
+ "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.15, )",
+ "Microsoft.Extensions.Configuration": "[6.0.1, )",
+ "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )",
+ "Microsoft.Extensions.Configuration.Json": "[6.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )",
+ "Monai.Deploy.InformaticsGateway.Database.Api": "[1.0.0, )"
}
},
"monai.deploy.informaticsgateway.database.mongodb": {
"type": "Project",
"dependencies": {
- "Monai.Deploy.InformaticsGateway.Database.Api": "1.0.0",
- "MongoDB.Driver": "2.19.1",
- "MongoDB.Driver.Core": "2.19.1"
+ "Monai.Deploy.InformaticsGateway.Database.Api": "[1.0.0, )",
+ "MongoDB.Driver": "[2.19.1, )",
+ "MongoDB.Driver.Core": "[2.19.1, )"
}
},
"monai.deploy.informaticsgateway.dicomweb.client": {
"type": "Project",
"dependencies": {
- "Ardalis.GuardClauses": "4.0.1",
- "Microsoft.AspNet.WebApi.Client": "5.2.9",
- "Microsoft.Extensions.Http": "6.0.0",
- "Microsoft.Net.Http.Headers": "2.2.8",
- "Monai.Deploy.InformaticsGateway.Client.Common": "1.0.0",
- "System.Linq.Async": "6.0.1",
- "fo-dicom": "5.0.3"
+ "Ardalis.GuardClauses": "[4.0.1, )",
+ "Microsoft.AspNet.WebApi.Client": "[5.2.9, )",
+ "Microsoft.Extensions.Http": "[6.0.0, )",
+ "Microsoft.Net.Http.Headers": "[2.2.8, )",
+ "Monai.Deploy.InformaticsGateway.Client.Common": "[1.0.0, )",
+ "System.Linq.Async": "[6.0.1, )",
+ "fo-dicom": "[5.0.3, )"
+ }
+ },
+ "monai.deploy.informaticsgateway.test.plugins": {
+ "type": "Project",
+ "dependencies": {
+ "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )"
}
}
}
diff --git a/src/Monai.Deploy.InformaticsGateway.sln b/src/Monai.Deploy.InformaticsGateway.sln
index 1bf686dba..5e7a9e4b3 100644
--- a/src/Monai.Deploy.InformaticsGateway.sln
+++ b/src/Monai.Deploy.InformaticsGateway.sln
@@ -54,7 +54,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.InformaticsGat
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.InformaticsGateway.Database.MongoDB", "Database\MongoDB\Monai.Deploy.InformaticsGateway.Database.MongoDB.csproj", "{5ED73EEA-4DFA-426D-82E8-AA24D3CB4C31}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monai.Deploy.InformaticsGateway.Database.MongoDB.Integration.Test", "Database\MongoDB\Integration.Test\Monai.Deploy.InformaticsGateway.Database.MongoDB.Integration.Test.csproj", "{2F849556-44B6-484A-B612-CB0FA5D29AC6}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.InformaticsGateway.Database.MongoDB.Integration.Test", "Database\MongoDB\Integration.Test\Monai.Deploy.InformaticsGateway.Database.MongoDB.Integration.Test.csproj", "{2F849556-44B6-484A-B612-CB0FA5D29AC6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.InformaticsGateway.Test.Plugins", "InformaticsGateway\Test\Plugins\Monai.Deploy.InformaticsGateway.Test.Plugins.csproj", "{7E735CE9-CE74-450E-A4E8-060D185DDEF1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -366,6 +368,18 @@ Global
{2F849556-44B6-484A-B612-CB0FA5D29AC6}.Release|x64.Build.0 = Release|Any CPU
{2F849556-44B6-484A-B612-CB0FA5D29AC6}.Release|x86.ActiveCfg = Release|Any CPU
{2F849556-44B6-484A-B612-CB0FA5D29AC6}.Release|x86.Build.0 = Release|Any CPU
+ {7E735CE9-CE74-450E-A4E8-060D185DDEF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7E735CE9-CE74-450E-A4E8-060D185DDEF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7E735CE9-CE74-450E-A4E8-060D185DDEF1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7E735CE9-CE74-450E-A4E8-060D185DDEF1}.Debug|x64.Build.0 = Debug|Any CPU
+ {7E735CE9-CE74-450E-A4E8-060D185DDEF1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7E735CE9-CE74-450E-A4E8-060D185DDEF1}.Debug|x86.Build.0 = Debug|Any CPU
+ {7E735CE9-CE74-450E-A4E8-060D185DDEF1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7E735CE9-CE74-450E-A4E8-060D185DDEF1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7E735CE9-CE74-450E-A4E8-060D185DDEF1}.Release|x64.ActiveCfg = Release|Any CPU
+ {7E735CE9-CE74-450E-A4E8-060D185DDEF1}.Release|x64.Build.0 = Release|Any CPU
+ {7E735CE9-CE74-450E-A4E8-060D185DDEF1}.Release|x86.ActiveCfg = Release|Any CPU
+ {7E735CE9-CE74-450E-A4E8-060D185DDEF1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -387,6 +401,7 @@ Global
{EA930DE2-33C4-447C-9E26-31387652D408} = {B8E99EF7-84EA-4D11-B722-9EE81B89CD86}
{5ED73EEA-4DFA-426D-82E8-AA24D3CB4C31} = {290E4C9B-841D-4E2C-91A0-5A69BAB122F3}
{2F849556-44B6-484A-B612-CB0FA5D29AC6} = {B8E99EF7-84EA-4D11-B722-9EE81B89CD86}
+ {7E735CE9-CE74-450E-A4E8-060D185DDEF1} = {B8E99EF7-84EA-4D11-B722-9EE81B89CD86}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E23DC856-D033-49F6-9BC6-9F1D0ECD05CB}
diff --git a/tests/Integration.Test/Common/Assertions.cs b/tests/Integration.Test/Common/Assertions.cs
index 33787d549..4d9eea3ee 100644
--- a/tests/Integration.Test/Common/Assertions.cs
+++ b/tests/Integration.Test/Common/Assertions.cs
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 MONAI Consortium
+ * Copyright 2023 MONAI Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -48,7 +48,7 @@ public Assertions(Configurations configurations, InformaticsGatewayConfiguration
_retryPolicy = Policy.Handle().WaitAndRetryAsync(retryCount: 5, sleepDurationProvider: _ => TimeSpan.FromMilliseconds(500));
}
- internal async Task ShouldHaveUploadedDicomDataToMinio(IReadOnlyList messages, Dictionary fileHashes)
+ internal async Task ShouldHaveUploadedDicomDataToMinio(IReadOnlyList messages, Dictionary fileHashes, Action additionalChecks = null)
{
Guard.Against.Null(messages);
Guard.Against.NullOrEmpty(fileHashes);
@@ -75,6 +75,12 @@ await _retryPolicy.ExecuteAsync(async () =>
memoryStream.Position = 0;
var dicomFile = DicomFile.Open(memoryStream);
dicomValidationKey = dicomFile.GenerateFileName();
+
+ if (additionalChecks is not null)
+ {
+ additionalChecks(dicomFile);
+ }
+
fileHashes.Should().ContainKey(dicomValidationKey).WhoseValue.Should().Be(dicomFile.CalculateHash());
});
await minioClient.GetObjectAsync(getObjectArgs);
diff --git a/tests/Integration.Test/Drivers/RabbitMqConsumer.cs b/tests/Integration.Test/Drivers/RabbitMqConsumer.cs
index e82f53d1a..70b34da43 100644
--- a/tests/Integration.Test/Drivers/RabbitMqConsumer.cs
+++ b/tests/Integration.Test/Drivers/RabbitMqConsumer.cs
@@ -48,12 +48,13 @@ public RabbitMqConsumer(RabbitMQMessageSubscriberService subscriberService, stri
subscriberService.SubscribeAsync(
queueName,
queueName,
- async (eventArgs) =>
+ (eventArgs) =>
{
_outputHelper.WriteLine($"Message received from queue {queueName} for {queueName}.");
_messages.Add(eventArgs.Message);
subscriberService.Acknowledge(eventArgs.Message);
_outputHelper.WriteLine($"{DateTime.UtcNow} - {queueName} message received with correlation ID={eventArgs.Message.CorrelationId}, delivery tag={eventArgs.Message.DeliveryTag}");
+ return Task.CompletedTask;
});
}
diff --git a/tests/Integration.Test/Features/DicomDimseScp.feature b/tests/Integration.Test/Features/DicomDimseScp.feature
index 1c4116a16..7741e62ed 100644
--- a/tests/Integration.Test/Features/DicomDimseScp.feature
+++ b/tests/Integration.Test/Features/DicomDimseScp.feature
@@ -44,7 +44,7 @@ Feature: DICOM DIMSE SCP Services
When a C-STORE-RQ is sent to 'Informatics Gateway' with AET '' from 'TEST-RUNNER'
Then a successful response should be received
And workflow requests sent to message broker
- And studies are uploaded to storage service
+ And studies are uploaded to storage service with data input plugins
Examples:
| modality | count | aet | timeout |
diff --git a/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj b/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj
index eea628e76..e7db9ec11 100644
--- a/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj
+++ b/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj
@@ -1,5 +1,5 @@