From fb6f76412b71b9133409bbbb44da4c9c0e22cfec Mon Sep 17 00:00:00 2001 From: Victor Chang Date: Fri, 18 Aug 2023 21:19:45 -0700 Subject: [PATCH] Refactor RemoteAppExecution plug-ins to a new project - New APIs to allow plug-in projects to extend database setups and configurations - Add integration test feature for end-to-end RemoteAppExecution - Rename Plugin to PlugIn Signed-off-by: Victor Chang --- .github/workflows/ci.yml | 2 +- src/Api/DestinationApplicationEntity.cs | 8 - src/Api/ExportRequestDataMessage.cs | 8 +- src/Api/IsExternalinit.cs | 2 +- src/Api/MonaiApplicationEntity.cs | 7 +- src/Api/{ => PlugIns}/IInputDataPlugin.cs | 10 +- .../{ => PlugIns}/IInputDataPluginEngine.cs | 10 +- src/Api/{ => PlugIns}/IOutputDataPlugin.cs | 10 +- .../{ => PlugIns}/IOutputDataPluginEngine.cs | 14 +- src/Api/{ => PlugIns}/PluginNameAttribute.cs | 6 +- .../Common => Api/PlugIns}/SR.cs | 6 +- src/Api/RemoteAppExecution.cs | 58 - src/Api/Storage/DicomFileStorageMetadata.cs | 6 +- src/Api/Storage/FileStorageMetadata.cs | 11 +- src/Api/VirtualApplicationEntity.cs | 19 +- src/AssemblyInfo.cs | 1 - src/CLI/Commands/AetCommand.cs | 24 +- src/CLI/Commands/DestinationCommand.cs | 14 +- src/CLI/ExitCodes.cs | 4 +- src/CLI/Logging/Log.cs | 8 +- src/CLI/Services/ConfigurationService.cs | 1 - src/CLI/Services/DockerRunner.cs | 2 +- src/CLI/Test/AetCommandTest.cs | 40 +- src/CLI/Test/DestinationCommandTest.cs | 24 +- .../NLogConfigurationOptionAccessorTest.cs | 1 - src/CLI/Test/ProgramTest.cs | 2 +- src/Client/Services/AeTitle{T}Service.cs | 4 +- src/Client/Test/AeTitleServiceTest.cs | 12 +- src/Configuration/DicomWebConfiguration.cs | 6 +- .../InformaticsGatewayConfiguration.cs | 6 +- src/Configuration/PluginConfiguration.cs | 2 +- src/Configuration/ValidationExtensions.cs | 1 + .../DatabaseOptions.cs} | 4 +- .../Api/DatabaseRegistrationBase.cs} | 11 +- src/Database/Api/DatabaseTypes.cs | 24 + src/Database/Api/IDatabaseMigrationManager.cs | 11 + src/Database/Api/SR.cs | 2 +- src/Database/DatabaseManager.cs | 77 +- src/Database/DatabaseMigrationManager.cs | 44 +- ...onEntityRemoteAppExecutionConfiguration.cs | 39 - .../InferenceRequestConfiguration.cs | 2 + .../MonaiApplicationEntityConfiguration.cs | 2 +- .../VirtualApplicationEntityConfiguration.cs | 5 +- .../InformaticsGatewayContext.cs | 4 - .../Migrations/20230327190827_R3_0.3.15.cs | 3 +- .../Migrations/20230816201637_R4_0.4.0.cs | 166 -- ...cs => 20230818160639_R4_0.4.0.Designer.cs} | 126 +- .../Migrations/20230818160639_R4_0.4.0.cs | 73 + .../InformaticsGatewayContextModelSnapshot.cs | 124 +- .../MonaiApplicationEntityRepositoryTest.cs | 2 +- .../Test/RemoteAppRepositoryTest.cs | 124 -- .../Test/SqliteDatabaseFixture.cs | 1 + .../StorageMetadataWrapperRepositoryTest.cs | 4 +- .../VirtualApplicationEntityRepositoryTest.cs | 4 +- ...tinationApplicationEntityRepositoryTest.cs | 1 - .../DicomAssociationInfoRepositoryTest.cs | 1 - .../InferenceRequestRepositoryTest.cs | 1 - .../MonaiApplicationEntityRepositoryTest.cs | 1 - .../Integration.Test/MongoDatabaseFixture.cs | 6 +- .../Integration.Test/PayloadRepositoryTest.cs | 1 - .../RemoteAppRepositoryTest.cs | 124 -- .../SourceApplicationEntityRepositoryTest.cs | 3 - .../StorageMetadataWrapperRepositoryTest.cs | 5 +- .../VirtualApplicationEntityRepositoryTest.cs | 6 +- .../DestinationApplicationEntityRepository.cs | 3 +- .../DicomAssociationInfoRepository.cs | 4 +- .../InferenceRequestRepository.cs | 5 +- .../MonaiApplicationEntityRepository.cs | 3 +- .../MongoDB/Repositories/PayloadRepository.cs | 4 +- .../RemoteAppExecutionRepository.cs | 125 -- .../SourceApplicationEntityRepository.cs | 3 +- .../StorageMetadataWrapperRepository.cs | 4 +- .../VirtualApplicationEntityRepository.cs | 3 +- src/InformaticsGateway/Common/DicomToolkit.cs | 1 - .../Common/FileStorageMetadataExtensions.cs | 1 - .../Common/IDicomToolkit.cs | 48 - ...Exception.cs => PlugInLoadingException.cs} | 6 +- .../Common/TypeExtensions.cs | 1 + .../ExecutionPlugins/ExternalAppOutgoing.cs | 143 -- .../Logging/Log.5000.DataPlugins.cs | 16 +- .../Logging/Log.8000.HttpServices.cs | 4 +- src/InformaticsGateway/Program.cs | 12 +- .../Common/IInputDataPluginEngineFactory.cs | 51 +- .../Services/Common/InputDataPluginEngine.cs | 30 +- .../Services/Common/OutputDataPluginEngine.cs | 29 +- .../Services/Connectors/PayloadAssembler.cs | 1 + .../Connectors/PayloadMoveActionHandler.cs | 2 - .../Connectors/PayloadNotificationService.cs | 1 - .../Services/DicomWeb/IStreamsWriter.cs | 13 +- .../Services/DicomWeb/StowService.cs | 1 - .../Services/Export/ExportServiceBase.cs | 10 +- .../Fhir/FhirResourceTypesRouteConstraint.cs | 1 - .../Services/HealthLevel7/MllpService.cs | 1 - .../Http/DestinationAeTitleController.cs | 13 +- .../Services/Http/DicomWeb/StowController.cs | 1 - .../Services/Http/InferenceController.cs | 1 - .../Services/Http/MonaiAeTitleController.cs | 15 +- .../Services/Http/VirtualAeTitleController.cs | 17 +- .../Services/Scp/ApplicationEntityHandler.cs | 9 +- .../Services/Scp/IApplicationEntityManager.cs | 1 - .../Scp/MonaiAeChangedNotificationService.cs | 1 - .../Services/Scp/ScpService.cs | 1 + .../Services/Scp/ScpServiceInternal.cs | 1 - ...onai.Deploy.InformaticsGateway.Test.csproj | 10 +- ...loy.InformaticsGateway.Test.Plugins.csproj | 2 + .../Test/Plugins/TestInputDataPlugins.cs | 42 +- .../Test/Plugins/TestOutputDataPlugins.cs | 27 +- src/InformaticsGateway/Test/ProgramTest.cs | 8 - .../Services/Common/ExternalAppPluginTest.cs | 408 ----- .../InputDataPluginEngineFactoryTest.cs | 41 +- .../Common/InputDataPluginEngineTest.cs | 58 +- .../OutputDataPluginEngineFactoryTest.cs | 33 +- .../Common/OutputDataPluginEngineTest.cs | 54 +- .../PayloadMoveActionHandlerTest.cs | 1 - .../Services/DicomWeb/StreamsWriterTest.cs | 32 +- .../Export/DicomWebExportServiceTest.cs | 13 +- .../Services/Export/ExportServiceBaseTest.cs | 17 +- .../Services/Export/ScuExportServiceTest.cs | 14 +- .../Http/DestinationAeTitleControllerTest.cs | 29 +- .../Http/MonaiAeTitleControllerTest.cs | 37 +- .../Http/VirtualAeTitleControllerTest.cs | 39 +- .../Scp/ApplicationEntityHandlerTest.cs | 19 +- src/InformaticsGateway/Test/appsettings.json | 2 +- src/Monai.Deploy.InformaticsGateway.sln | 31 +- .../Database/DatabaseRegistrar.cs | 50 + .../EntityFramework/MigrationManager.cs | 48 + .../RemoteAppExecutionConfiguration.cs | 36 +- .../RemoteAppExecutionDbContext.cs | 70 + .../RemoteAppExecutionDbContextFactory.cs | 44 + .../RemoteAppExecutionRepository.cs | 70 +- .../IRemoteAppExecutionRepository.cs | 12 +- .../Database/MongoDb/MigrationManager.cs | 30 + .../RemoteAppExecutionConfiguration.cs | 35 + .../MongoDb/RemoteAppExecutionRepository.cs | 167 ++ .../ExternalAppIncoming.cs | 42 +- .../RemoteAppExecution/ExternalAppOutgoing.cs | 101 ++ .../RemoteAppExecution/InternalVisibleTo.cs | 19 + .../Log.10000.DataPlugins.cs | 29 + .../20230818161328_R4_0.4.0.Designer.cs | 72 + .../Migrations/20230818161328_R4_0.4.0.cs | 52 + ...emoteAppExecutionDbContextModelSnapshot.cs | 70 + ...sGateway.PlugIns.RemoteAppExecution.csproj | 64 + .../RemoteAppExecution/RemoteAppExecution.cs | 85 + src/Plug-ins/RemoteAppExecution/SR.cs | 23 + .../Test/Database/DatabaseRegistrarTest.cs | 66 + .../EntityFramework/MigrationManagerTest.cs | 57 + .../RemoteAppExecutionRepositoryTest.cs | 169 ++ .../EntityFramework/SqliteDatabaseFixture.cs | 87 + .../Database/MongoDb/MongoDatabaseFixture.cs | 90 + .../RemoteAppExecutionRepositoryTest.cs | 173 ++ .../Test/ExternalAppIncomingTest.cs | 133 ++ .../Test/ExternalAppOutgoingTest.cs | 262 +++ ...way.PlugIns.RemoteAppExecution.Test.csproj | 64 + .../Test/packages.lock.json | 1601 +++++++++++++++++ src/Plug-ins/RemoteAppExecution/Utilities.cs | 59 + .../RemoteAppExecution/appsettings.json | 5 + .../RemoteAppExecution/packages.lock.json | 577 ++++++ src/Shared/Test/InstanceGenerator.cs | 5 +- tests/Integration.Test/Common/Assertions.cs | 47 + tests/Integration.Test/Common/DataProvider.cs | 139 +- .../Common/DicomCStoreDataClient.cs | 4 +- .../Integration.Test/Common/DicomDataSpecs.cs | 2 +- tests/Integration.Test/Common/DicomScp.cs | 30 +- .../Integration.Test/Common/MinioDataSink.cs | 2 +- .../Drivers/DicomInstanceGenerator.cs | 4 +- .../Features/RemoteAppExecutionPlugIn.feature | 27 + ...InformaticsGateway.Integration.Test.csproj | 9 +- .../DicomDimseScpServicesStepDefinitions.cs | 2 +- .../DicomWebStowServiceStepDefinitions.cs | 16 +- .../ExportServicesStepDefinitions.cs | 11 +- ...emoteAppExecutionPlugInsStepDefinitions.cs | 228 +++ .../StepDefinitions/SharedDefinitions.cs | 8 +- tests/Integration.Test/appsettings.json | 7 +- tests/Integration.Test/packages.lock.json | 22 + 174 files changed, 5680 insertions(+), 2178 deletions(-) rename src/Api/{ => PlugIns}/IInputDataPlugin.cs (76%) rename src/Api/{ => PlugIns}/IInputDataPluginEngine.cs (80%) rename src/Api/{ => PlugIns}/IOutputDataPlugin.cs (73%) rename src/Api/{ => PlugIns}/IOutputDataPluginEngine.cs (71%) rename src/Api/{ => PlugIns}/PluginNameAttribute.cs (85%) rename src/{InformaticsGateway/Common => Api/PlugIns}/SR.cs (91%) delete mode 100755 src/Api/RemoteAppExecution.cs rename src/Database/{MongoDB/Configurations/MongoDBOptions.cs => Api/DatabaseOptions.cs} (87%) mode change 100755 => 100644 rename src/{Api/DestinationApplicationEntityRemoteAppExecution.cs => Database/Api/DatabaseRegistrationBase.cs} (58%) mode change 100755 => 100644 create mode 100644 src/Database/Api/DatabaseTypes.cs delete mode 100644 src/Database/EntityFramework/Configuration/DestinationApplicationEntityRemoteAppExecutionConfiguration.cs delete mode 100644 src/Database/EntityFramework/Migrations/20230816201637_R4_0.4.0.cs rename src/Database/EntityFramework/Migrations/{20230816201637_R4_0.4.0.Designer.cs => 20230818160639_R4_0.4.0.Designer.cs} (71%) create mode 100644 src/Database/EntityFramework/Migrations/20230818160639_R4_0.4.0.cs delete mode 100755 src/Database/EntityFramework/Test/RemoteAppRepositoryTest.cs delete mode 100755 src/Database/MongoDB/Integration.Test/RemoteAppRepositoryTest.cs delete mode 100755 src/Database/MongoDB/Repositories/RemoteAppExecutionRepository.cs rename src/InformaticsGateway/Common/{PlugingLoadingException.cs => PlugInLoadingException.cs} (75%) delete mode 100755 src/InformaticsGateway/ExecutionPlugins/ExternalAppOutgoing.cs delete mode 100755 src/InformaticsGateway/Test/Services/Common/ExternalAppPluginTest.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Database/DatabaseRegistrar.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Database/EntityFramework/MigrationManager.cs rename src/{Database/EntityFramework/Configuration => Plug-ins/RemoteAppExecution/Database/EntityFramework}/RemoteAppExecutionConfiguration.cs (58%) mode change 100755 => 100644 create mode 100644 src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionDbContext.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionDbContextFactory.cs rename src/{Database/EntityFramework/Repositories => Plug-ins/RemoteAppExecution/Database/EntityFramework}/RemoteAppExecutionRepository.cs (56%) mode change 100755 => 100644 rename src/{Database/Api/Repositories => Plug-ins/RemoteAppExecution/Database}/IRemoteAppExecutionRepository.cs (57%) mode change 100755 => 100644 create mode 100644 src/Plug-ins/RemoteAppExecution/Database/MongoDb/MigrationManager.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Database/MongoDb/RemoteAppExecutionConfiguration.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Database/MongoDb/RemoteAppExecutionRepository.cs rename src/{InformaticsGateway/ExecutionPlugins => Plug-ins/RemoteAppExecution}/ExternalAppIncoming.cs (53%) mode change 100755 => 100644 create mode 100644 src/Plug-ins/RemoteAppExecution/ExternalAppOutgoing.cs create mode 100644 src/Plug-ins/RemoteAppExecution/InternalVisibleTo.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Log.10000.DataPlugins.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Migrations/20230818161328_R4_0.4.0.Designer.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Migrations/20230818161328_R4_0.4.0.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Migrations/RemoteAppExecutionDbContextModelSnapshot.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.csproj create mode 100644 src/Plug-ins/RemoteAppExecution/RemoteAppExecution.cs create mode 100644 src/Plug-ins/RemoteAppExecution/SR.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/Database/DatabaseRegistrarTest.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/MigrationManagerTest.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/RemoteAppExecutionRepositoryTest.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/SqliteDatabaseFixture.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/MongoDatabaseFixture.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/RemoteAppExecutionRepositoryTest.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/ExternalAppIncomingTest.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/ExternalAppOutgoingTest.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.csproj create mode 100644 src/Plug-ins/RemoteAppExecution/Test/packages.lock.json create mode 100644 src/Plug-ins/RemoteAppExecution/Utilities.cs create mode 100644 src/Plug-ins/RemoteAppExecution/appsettings.json create mode 100644 src/Plug-ins/RemoteAppExecution/packages.lock.json create mode 100644 tests/Integration.Test/Features/RemoteAppExecutionPlugIn.feature create mode 100644 tests/Integration.Test/StepDefinitions/RemoteAppExecutionPlugInsStepDefinitions.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c7df54e9..875e527a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -255,7 +255,7 @@ jobs: timeout-minutes: 30 strategy: matrix: - feature: [AcrApi, DicomDimseScp, DicomDimseScu, DicomWebExport, DicomWebStow, HealthLevel7, Fhir] + feature: [AcrApi, DicomDimseScp, DicomDimseScu, DicomWebExport, DicomWebStow, HealthLevel7, Fhir, RemoteAppExecutionPlugIn] database: [ef, mongodb] fail-fast: false env: diff --git a/src/Api/DestinationApplicationEntity.cs b/src/Api/DestinationApplicationEntity.cs index fcc7a6a02..6599591fa 100644 --- a/src/Api/DestinationApplicationEntity.cs +++ b/src/Api/DestinationApplicationEntity.cs @@ -15,8 +15,6 @@ * limitations under the License. */ -using System.Collections.Generic; - namespace Monai.Deploy.InformaticsGateway.Api { /// @@ -38,11 +36,5 @@ public class DestinationApplicationEntity : BaseApplicationEntity /// Gets or sets the port to connect to. /// public int Port { get; set; } - - /// - /// Gets or sets remote application executions. - /// - public virtual List RemoteAppExecutions { get; set; } = new(); - public virtual List DestinationApplicationEntityRemoteAppExecutions { get; set; } = new(); } } diff --git a/src/Api/ExportRequestDataMessage.cs b/src/Api/ExportRequestDataMessage.cs index 6246f93db..e7dfec0fe 100755 --- a/src/Api/ExportRequestDataMessage.cs +++ b/src/Api/ExportRequestDataMessage.cs @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 MONAI Consortium + * Copyright 2021-2023 MONAI Consortium * Copyright 2019-2021 NVIDIA Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ using System.Collections.Generic; using Ardalis.GuardClauses; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.Messaging.Events; namespace Monai.Deploy.InformaticsGateway.Api @@ -32,9 +33,9 @@ public class ExportRequestDataMessage public string Filename { get; } /// - /// Optional list of data output plug-in type names to be executed by the . + /// Optional list of data output plug-in type names to be executed by the . /// - public List PluginAssemblies + public List PlugInAssemblies { get { @@ -62,7 +63,6 @@ public string[] Destinations get { return _exportRequest.Destinations; } } - public ExportRequestDataMessage(ExportRequestEvent exportRequest, string filename) { IsFailed = false; diff --git a/src/Api/IsExternalinit.cs b/src/Api/IsExternalinit.cs index 2de6d1984..04a3f0728 100644 --- a/src/Api/IsExternalinit.cs +++ b/src/Api/IsExternalinit.cs @@ -16,7 +16,7 @@ using System.ComponentModel; -namespace System.Runtime.CompilerServices +namespace Monai.Deploy.InformaticsGateway.Api { /// /// Reserved to be used by the compiler for tracking metadata. diff --git a/src/Api/MonaiApplicationEntity.cs b/src/Api/MonaiApplicationEntity.cs index abf1fba67..9e2929147 100644 --- a/src/Api/MonaiApplicationEntity.cs +++ b/src/Api/MonaiApplicationEntity.cs @@ -20,6 +20,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Security.Claims; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; namespace Monai.Deploy.InformaticsGateway.Api { @@ -73,9 +74,9 @@ public class MonaiApplicationEntity : MongoDBEntityBase public List Workflows { get; set; } = default!; /// - /// Optional list of data input plug-in type names to be executed by the . + /// Optional list of data input plug-in type names to be executed by the . /// - public List PluginAssemblies { get; set; } = default!; + public List PlugInAssemblies { get; set; } = default!; /// /// Optional field to specify SOP Class UIDs to ignore. @@ -134,7 +135,7 @@ public void SetDefaultValues() AllowedSopClasses ??= new List(); - PluginAssemblies ??= new List(); + PlugInAssemblies ??= new List(); } public override string ToString() diff --git a/src/Api/IInputDataPlugin.cs b/src/Api/PlugIns/IInputDataPlugin.cs similarity index 76% rename from src/Api/IInputDataPlugin.cs rename to src/Api/PlugIns/IInputDataPlugin.cs index 1272044a5..a6b5a50c8 100644 --- a/src/Api/IInputDataPlugin.cs +++ b/src/Api/PlugIns/IInputDataPlugin.cs @@ -18,17 +18,17 @@ using FellowOakDicom; using Monai.Deploy.InformaticsGateway.Api.Storage; -namespace Monai.Deploy.InformaticsGateway.Api +namespace Monai.Deploy.InformaticsGateway.Api.PlugIns { /// - /// IInputDataPlugin enables lightweight data processing over incoming data received from supported data ingestion + /// IInputDataPlugIn enables lightweight data processing over incoming data received from supported data ingestion /// services. - /// Refer to for additional details. + /// Refer to for additional details. /// - public interface IInputDataPlugin + public interface IInputDataPlugIn { string Name { get; } - Task<(DicomFile dicomFile, FileStorageMetadata fileMetadata)> Execute(DicomFile dicomFile, FileStorageMetadata fileMetadata); + Task<(DicomFile dicomFile, FileStorageMetadata fileMetadata)> ExecuteAsync(DicomFile dicomFile, FileStorageMetadata fileMetadata); } } diff --git a/src/Api/IInputDataPluginEngine.cs b/src/Api/PlugIns/IInputDataPluginEngine.cs similarity index 80% rename from src/Api/IInputDataPluginEngine.cs rename to src/Api/PlugIns/IInputDataPluginEngine.cs index f2469fe3c..dc349822a 100644 --- a/src/Api/IInputDataPluginEngine.cs +++ b/src/Api/PlugIns/IInputDataPluginEngine.cs @@ -20,11 +20,11 @@ using FellowOakDicom; using Monai.Deploy.InformaticsGateway.Api.Storage; -namespace Monai.Deploy.InformaticsGateway.Api +namespace Monai.Deploy.InformaticsGateway.Api.PlugIns { /// - /// IInputDataPluginEngine processes incoming data receivied from various supported services through - /// a list of plug-ins based on . + /// IInputDataPlugInEngine processes incoming data receivied from various supported services through + /// a list of plug-ins based on . /// Rules: /// /// SCP: A list of plug-ins can be configured with each AET, and each plug-in is executed in the order stored, enabling piping of the incoming data before each file is uploaded to the storage service. @@ -33,10 +33,10 @@ namespace Monai.Deploy.InformaticsGateway.Api /// Plug-ins SHALL not accumulate files in memory or storage for bulk processing. /// /// - public interface IInputDataPluginEngine + public interface IInputDataPlugInEngine { void Configure(IReadOnlyList pluginAssemblies); - Task> ExecutePlugins(DicomFile dicomFile, FileStorageMetadata fileMetadata); + Task> ExecutePlugInsAsync(DicomFile dicomFile, FileStorageMetadata fileMetadata); } } diff --git a/src/Api/IOutputDataPlugin.cs b/src/Api/PlugIns/IOutputDataPlugin.cs similarity index 73% rename from src/Api/IOutputDataPlugin.cs rename to src/Api/PlugIns/IOutputDataPlugin.cs index 81415cee0..47d36da12 100644 --- a/src/Api/IOutputDataPlugin.cs +++ b/src/Api/PlugIns/IOutputDataPlugin.cs @@ -17,17 +17,17 @@ using System.Threading.Tasks; using FellowOakDicom; -namespace Monai.Deploy.InformaticsGateway.Api +namespace Monai.Deploy.InformaticsGateway.Api.PlugIns { /// - /// IOutputDataPlugin enables lightweight data processing over incoming data received from supported data ingestion + /// IOutputDataPlugIn enables lightweight data processing over incoming data received from supported data ingestion /// services. - /// Refer to for additional details. + /// Refer to for additional details. /// - public interface IOutputDataPlugin + public interface IOutputDataPlugIn { string Name { get; } - Task<(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage)> Execute(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage); + Task<(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage)> ExecuteAsync(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage); } } diff --git a/src/Api/IOutputDataPluginEngine.cs b/src/Api/PlugIns/IOutputDataPluginEngine.cs similarity index 71% rename from src/Api/IOutputDataPluginEngine.cs rename to src/Api/PlugIns/IOutputDataPluginEngine.cs index 8d4189ebd..07e62ccd0 100644 --- a/src/Api/IOutputDataPluginEngine.cs +++ b/src/Api/PlugIns/IOutputDataPluginEngine.cs @@ -17,22 +17,22 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace Monai.Deploy.InformaticsGateway.Api +namespace Monai.Deploy.InformaticsGateway.Api.PlugIns { /// - /// IOutputDataPluginEngine processes each file before exporting to its destination - /// through a list of plug-ins based on . + /// IOutputDataPlugInEngine processes each file before exporting to its destination + /// 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. + /// Plug-ins MUST be lightweight and not hinder the export process. + /// Plug-ins SHALL not accumulate files in memory or storage for bulk processing. /// /// - public interface IOutputDataPluginEngine + public interface IOutputDataPlugInEngine { void Configure(IReadOnlyList pluginAssemblies); - Task ExecutePlugins(ExportRequestDataMessage exportRequestDataMessage); + Task ExecutePlugInsAsync(ExportRequestDataMessage exportRequestDataMessage); } } diff --git a/src/Api/PluginNameAttribute.cs b/src/Api/PlugIns/PluginNameAttribute.cs similarity index 85% rename from src/Api/PluginNameAttribute.cs rename to src/Api/PlugIns/PluginNameAttribute.cs index 5e15c015b..46ae56869 100644 --- a/src/Api/PluginNameAttribute.cs +++ b/src/Api/PlugIns/PluginNameAttribute.cs @@ -17,14 +17,14 @@ using System; using Ardalis.GuardClauses; -namespace Monai.Deploy.InformaticsGateway.Api +namespace Monai.Deploy.InformaticsGateway.Api.PlugIns { [AttributeUsage(AttributeTargets.Class)] - public class PluginNameAttribute : Attribute + public class PlugInNameAttribute : Attribute { public string Name { get; set; } - public PluginNameAttribute(string name) + public PlugInNameAttribute(string name) { Guard.Against.NullOrWhiteSpace(name, nameof(name)); diff --git a/src/InformaticsGateway/Common/SR.cs b/src/Api/PlugIns/SR.cs similarity index 91% rename from src/InformaticsGateway/Common/SR.cs rename to src/Api/PlugIns/SR.cs index e23ec6041..57471e027 100644 --- a/src/InformaticsGateway/Common/SR.cs +++ b/src/Api/PlugIns/SR.cs @@ -14,12 +14,12 @@ * limitations under the License. */ -using System.IO; using System; +using System.IO; -namespace Monai.Deploy.InformaticsGateway.Common +namespace Monai.Deploy.InformaticsGateway.Api.PlugIns { - internal static class SR + public 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/Api/RemoteAppExecution.cs b/src/Api/RemoteAppExecution.cs deleted file mode 100755 index b57da21be..000000000 --- a/src/Api/RemoteAppExecution.cs +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.Text.Json.Serialization; - -namespace Monai.Deploy.InformaticsGateway.Api -{ - /// - /// TODO: include description of class and all properties - /// /// - public class RemoteAppExecution - { - [JsonPropertyName("_id")] - public string Id { get; set; } = default!; - - /// - /// Gets or sets exported destinations - /// - public virtual List ExportDetails { get; set; } = new(); - public virtual List DestinationApplicationEntityRemoteAppExecutions { get; set; } = new(); - - public DateTime RequestTime { get; set; } = DateTime.UtcNow; - public string ExportTaskId { get; set; } = string.Empty; - public string WorkflowInstanceId { get; set; } = string.Empty; - public string CorrelationId { get; set; } = string.Empty; - public string? StudyUid { get; set; } - public string? OutgoingUid { get { return Id; } set { Id = value ?? ""; } } - - public List Files { get; set; } = new(); - public Dictionary OriginalValues { get; set; } = new(); - public Dictionary ProxyValues { get; set; } = new(); - } - - /// - /// TODO: maybe use internal for testing? - /// - public class RemoteAppExecutionTest - { - [JsonPropertyName("_id")] - public string Id { get; set; } = default!; - public DateTime RequestTime { get; set; } = DateTime.UtcNow; - } -} diff --git a/src/Api/Storage/DicomFileStorageMetadata.cs b/src/Api/Storage/DicomFileStorageMetadata.cs index d00aeabad..9341a308c 100644 --- a/src/Api/Storage/DicomFileStorageMetadata.cs +++ b/src/Api/Storage/DicomFileStorageMetadata.cs @@ -100,9 +100,9 @@ public DicomFileStorageMetadata(string associationId, string identifier, string { Guard.Against.NullOrWhiteSpace(associationId, nameof(associationId)); Guard.Against.NullOrWhiteSpace(identifier, nameof(identifier)); - Guard.Against.NullOrWhiteSpace(identifier, nameof(identifier)); - Guard.Against.NullOrWhiteSpace(identifier, nameof(identifier)); - Guard.Against.NullOrWhiteSpace(identifier, nameof(identifier)); + Guard.Against.NullOrWhiteSpace(studyInstanceUid, nameof(studyInstanceUid)); + Guard.Against.NullOrWhiteSpace(seriesInstanceUid, nameof(seriesInstanceUid)); + Guard.Against.NullOrWhiteSpace(sopInstanceUid, nameof(sopInstanceUid)); StudyInstanceUid = studyInstanceUid; SeriesInstanceUid = seriesInstanceUid; diff --git a/src/Api/Storage/FileStorageMetadata.cs b/src/Api/Storage/FileStorageMetadata.cs index 67ee4aa25..56ddf70dd 100644 --- a/src/Api/Storage/FileStorageMetadata.cs +++ b/src/Api/Storage/FileStorageMetadata.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. @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.Text.Json.Serialization; using Ardalis.GuardClauses; +using Microsoft.Extensions.Logging; namespace Monai.Deploy.InformaticsGateway.Api.Storage { @@ -60,7 +61,7 @@ public abstract record FileStorageMetadata /// For ACR retrieved DICOM/FHIR files: use the original transaction ID embedded in the request. /// [JsonPropertyName("correlationId")] - public string CorrelationId { get; init; } = default!; + public string CorrelationId { get; set; } = default!; /// /// Gets or sets the source of the file. @@ -131,6 +132,12 @@ public virtual void SetFailed() File.SetFailed(); } + public void ChangeCorrelationId(ILogger logger, string correlationId) + { + logger.LogWarning($"Changing correlation ID from {CorrelationId} to {correlationId}."); + CorrelationId = correlationId; + } + public string? PayloadId { get; set; } } } diff --git a/src/Api/VirtualApplicationEntity.cs b/src/Api/VirtualApplicationEntity.cs index 97a9848e2..9a6545999 100644 --- a/src/Api/VirtualApplicationEntity.cs +++ b/src/Api/VirtualApplicationEntity.cs @@ -14,25 +14,26 @@ * limitations under the License. */ +using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using System.Security.Claims; -using System; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; namespace Monai.Deploy.InformaticsGateway.Api { /// /// Virtual Application Entity (VAE). - /// + /// /// A VAE identifies a service or application similar to a DIMSE AE but is /// designed to be used for DICOMWeb. /// /// For example, users can configure VAEs on DICOMWeb STOW-RS endpoints to enable - /// data input plug-ins, . This allows different plug-ins + /// data input plug-ins, . This allows different plug-ins /// to be associated with each VAE for data manipulation, etc... - /// - /// In addition, one can trigger a new workflow request with the workflows defined in + /// + /// In addition, one can trigger a new workflow request with the workflows defined in /// when studies are posted to a VAE-enabled STOW-RS endpoint. /// public class VirtualApplicationEntity : MongoDBEntityBase @@ -56,9 +57,9 @@ public class VirtualApplicationEntity : MongoDBEntityBase public List Workflows { get; set; } = default!; /// - /// Optional list of data input plug-in type names to be executed by the . + /// Optional list of data input plug-in type names to be executed by the . /// - public List PluginAssemblies { get; set; } = default!; + public List PlugInAssemblies { get; set; } = default!; /// /// Gets or set the user who created the DICOM entity. @@ -89,7 +90,7 @@ public void SetDefaultValues() Workflows ??= new List(); - PluginAssemblies ??= new List(); + PlugInAssemblies ??= new List(); } public override string ToString() diff --git a/src/AssemblyInfo.cs b/src/AssemblyInfo.cs index 739ae2c87..1ae63f2a5 100644 --- a/src/AssemblyInfo.cs +++ b/src/AssemblyInfo.cs @@ -19,4 +19,3 @@ [assembly: AssemblyVersion("0.0.0.0")] [assembly: AssemblyFileVersion("0.0.0.0")] [assembly: AssemblyInformationalVersion("0.0.0")] - diff --git a/src/CLI/Commands/AetCommand.cs b/src/CLI/Commands/AetCommand.cs index 3879b15f5..3c2f27c04 100644 --- a/src/CLI/Commands/AetCommand.cs +++ b/src/CLI/Commands/AetCommand.cs @@ -43,7 +43,7 @@ public AetCommand() : base("aet", "Configure SCP Application Entities") SetupEditAetCommand(); SetupRemoveAetCommand(); SetupListAetCommand(); - SetupPluginsCommand(); + SetupPlugInsCommand(); } private void SetupListAetCommand() @@ -147,12 +147,12 @@ private void SetupEditAetCommand() addCommand.Handler = CommandHandler.Create(EditAeTitleHandlerAsync); } - private void SetupPluginsCommand() + private void SetupPlugInsCommand() { var pluginsCommand = new Command("plugins", "List all available plug-ins for SCP Application Entities"); AddCommand(pluginsCommand); - pluginsCommand.Handler = CommandHandler.Create(ListPluginsHandlerAsync); + pluginsCommand.Handler = CommandHandler.Create(ListPlugInsHandlerAsync); } private async Task ListAeTitlehandlerAsync(IHost host, bool verbose, CancellationToken cancellationToken) @@ -295,7 +295,11 @@ private async Task AddAeTitlehandlerAsync(MonaiApplicationEntity entity, IH } if (result.AllowedSopClasses.Any()) { - logger.MonaiAePlugins(string.Join(',', result.AllowedSopClasses)); + logger.MonaiAeAllowedSops(string.Join(',', result.AllowedSopClasses)); + } + if (result.PlugInAssemblies.Any()) + { + logger.MonaiAePlugIns(string.Join(',', result.PlugInAssemblies)); } } catch (ConfigurationException ex) @@ -350,9 +354,9 @@ private async Task EditAeTitleHandlerAsync(MonaiApplicationEntity entity, I logger.MonaiAeAllowedSops(string.Join(',', result.AllowedSopClasses)); logger.AcceptedSopClassesWarning(); } - if (result.AllowedSopClasses.Any()) + if (result.PlugInAssemblies.Any()) { - logger.MonaiAePlugins(string.Join(',', result.AllowedSopClasses)); + logger.MonaiAePlugIns(string.Join(',', result.PlugInAssemblies)); } } catch (ConfigurationException ex) @@ -368,7 +372,7 @@ private async Task EditAeTitleHandlerAsync(MonaiApplicationEntity entity, I return ExitCodes.Success; } - private async Task ListPluginsHandlerAsync(IHost host, bool verbose, CancellationToken cancellationToken) + private async Task ListPlugInsHandlerAsync(IHost host, bool verbose, CancellationToken cancellationToken) { Guard.Against.Null(host, nameof(host)); @@ -393,7 +397,7 @@ private async Task ListPluginsHandlerAsync(IHost host, bool verbose, Cancel client.ConfigureServiceUris(configService.Configurations.InformaticsGatewayServerUri); LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {configService.Configurations.InformaticsGatewayServerEndpoint}..."); LogVerbose(verbose, host, $"Retrieving MONAI SCP AE Titles..."); - items = await client.MonaiScpAeTitle.Plugins(cancellationToken).ConfigureAwait(false); + items = await client.MonaiScpAeTitle.PlugIns(cancellationToken).ConfigureAwait(false); } catch (ConfigurationException ex) { @@ -402,8 +406,8 @@ private async Task ListPluginsHandlerAsync(IHost host, bool verbose, Cancel } catch (Exception ex) { - logger.ErrorListingDataInputPlugins(ex.Message); - return ExitCodes.MonaiScp_ErrorPlugins; + logger.ErrorListingDataInputPlugIns(ex.Message); + return ExitCodes.MonaiScp_ErrorPlugIns; } if (items.IsNullOrEmpty()) diff --git a/src/CLI/Commands/DestinationCommand.cs b/src/CLI/Commands/DestinationCommand.cs index 7c0c19d25..0205153f8 100644 --- a/src/CLI/Commands/DestinationCommand.cs +++ b/src/CLI/Commands/DestinationCommand.cs @@ -45,7 +45,7 @@ public DestinationCommand() : base("dst", "Configure DICOM destinations") SetupRemoveDestinationCommand(); SetupListDestinationCommand(); SetupCEchoCommand(); - SetupPluginsCommand(); + SetupPlugInsCommand(); } private void SetupCEchoCommand() @@ -115,12 +115,12 @@ private void SetupAddDestinationCommand() addCommand.Handler = CommandHandler.Create(AddDestinationHandlerAsync); } - private void SetupPluginsCommand() + private void SetupPlugInsCommand() { var pluginsCommand = new Command("plugins", "List all available plug-ins for DICOM destinations"); AddCommand(pluginsCommand); - pluginsCommand.Handler = CommandHandler.Create(ListPluginsHandlerAsync); + pluginsCommand.Handler = CommandHandler.Create(ListPlugInsHandlerAsync); } private async Task ListDestinationHandlerAsync(DestinationApplicationEntity entity, IHost host, bool verbose, CancellationToken cancellationToken) @@ -336,7 +336,7 @@ private async Task AddDestinationHandlerAsync(DestinationApplicationEntity return ExitCodes.Success; } - private async Task ListPluginsHandlerAsync(IHost host, bool verbose, CancellationToken cancellationToken) + private async Task ListPlugInsHandlerAsync(IHost host, bool verbose, CancellationToken cancellationToken) { Guard.Against.Null(host, nameof(host)); @@ -361,7 +361,7 @@ private async Task ListPluginsHandlerAsync(IHost host, bool verbose, Cancel client.ConfigureServiceUris(configService.Configurations.InformaticsGatewayServerUri); LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {configService.Configurations.InformaticsGatewayServerEndpoint}..."); LogVerbose(verbose, host, $"Retrieving MONAI SCP AE Titles..."); - items = await client.DicomDestinations.Plugins(cancellationToken).ConfigureAwait(false); + items = await client.DicomDestinations.PlugIns(cancellationToken).ConfigureAwait(false); } catch (ConfigurationException ex) { @@ -370,8 +370,8 @@ private async Task ListPluginsHandlerAsync(IHost host, bool verbose, Cancel } catch (Exception ex) { - logger.ErrorListingDataOutputPlugins(ex.Message); - return ExitCodes.DestinationAe_ErrorPlugins; + logger.ErrorListingDataOutputPlugIns(ex.Message); + return ExitCodes.DestinationAe_ErrorPlugIns; } if (items.IsNullOrEmpty()) diff --git a/src/CLI/ExitCodes.cs b/src/CLI/ExitCodes.cs index 95701e4b8..f16cf5c73 100644 --- a/src/CLI/ExitCodes.cs +++ b/src/CLI/ExitCodes.cs @@ -29,14 +29,14 @@ public static class ExitCodes public const int MonaiScp_ErrorDelete = 201; public const int MonaiScp_ErrorCreate = 202; public const int MonaiScp_ErrorUpdate = 203; - public const int MonaiScp_ErrorPlugins = 204; + public const int MonaiScp_ErrorPlugIns = 204; public const int DestinationAe_ErrorList = 300; public const int DestinationAe_ErrorDelete = 301; public const int DestinationAe_ErrorCreate = 302; public const int DestinationAe_ErrorCEcho = 303; public const int DestinationAe_ErrorUpdate = 304; - public const int DestinationAe_ErrorPlugins = 305; + public const int DestinationAe_ErrorPlugIns = 305; public const int SourceAe_ErrorList = 400; public const int SourceAe_ErrorDelete = 401; diff --git a/src/CLI/Logging/Log.cs b/src/CLI/Logging/Log.cs index 7641374c5..48bee6b91 100644 --- a/src/CLI/Logging/Log.cs +++ b/src/CLI/Logging/Log.cs @@ -188,13 +188,13 @@ public static partial class Log public static partial void ErrorUpdatingMonaiApplicationEntity(this ILogger logger, string aeTitle, string message); [LoggerMessage(EventId = 30062, Level = LogLevel.Information, Message = "\tPlug-ins: {plugins}")] - public static partial void MonaiAePlugins(this ILogger logger, string plugins); + public static partial void MonaiAePlugIns(this ILogger logger, string plugins); [LoggerMessage(EventId = 30063, Level = LogLevel.Critical, Message = "Error retrieving data input plug-ins: {message}.")] - public static partial void ErrorListingDataInputPlugins(this ILogger logger, string message); + public static partial void ErrorListingDataInputPlugIns(this ILogger logger, string message); [LoggerMessage(EventId = 30064, Level = LogLevel.Critical, Message = "Error retrieving data output plug-ins: {message}.")] - public static partial void ErrorListingDataOutputPlugins(this ILogger logger, string message); + public static partial void ErrorListingDataOutputPlugIns(this ILogger logger, string message); // Docker Runner [LoggerMessage(EventId = 31000, Level = LogLevel.Debug, Message = "Checking for existing {applicationName} ({version}) containers...")] @@ -246,6 +246,6 @@ public static partial class Log public static partial void DockerCreateWarnings(this ILogger logger, string warnings); [LoggerMessage(EventId = 31016, Level = LogLevel.Information, Message = "\tMount (plug-ins): {hostPath} => {containerPath}")] - public static partial void DockerMountPlugins(this ILogger logger, string hostPath, string containerPath); + public static partial void DockerMountPlugIns(this ILogger logger, string hostPath, string containerPath); } } diff --git a/src/CLI/Services/ConfigurationService.cs b/src/CLI/Services/ConfigurationService.cs index 7e71c6afd..c989d9325 100644 --- a/src/CLI/Services/ConfigurationService.cs +++ b/src/CLI/Services/ConfigurationService.cs @@ -88,7 +88,6 @@ public async Task WriteConfigFile(string resourceName, string outputPath, Cancel await fileStream.FlushAsync(cancellationToken).ConfigureAwait(false); } _logger.AppSettingUpdated(outputPath); - } } } diff --git a/src/CLI/Services/DockerRunner.cs b/src/CLI/Services/DockerRunner.cs index f3eb2abfb..a3eef166e 100644 --- a/src/CLI/Services/DockerRunner.cs +++ b/src/CLI/Services/DockerRunner.cs @@ -140,7 +140,7 @@ public async Task StartApplication(ImageVersion imageVersion, Cancellation _fileSystem.Directory.CreateDirectoryIfNotExists(_configurationService.Configurations.HostLogsStorageMount); createContainerParams.HostConfig.Mounts.Add(new Mount { Type = "bind", ReadOnly = false, Source = _configurationService.Configurations.HostLogsStorageMount, Target = _configurationService.NLogConfigurations.LogStoragePath }); - _logger.DockerMountPlugins(_configurationService.Configurations.HostPlugInsStorageMount, Common.MountedPlugInsPath); + _logger.DockerMountPlugIns(_configurationService.Configurations.HostPlugInsStorageMount, Common.MountedPlugInsPath); _fileSystem.Directory.CreateDirectoryIfNotExists(_configurationService.Configurations.HostPlugInsStorageMount); createContainerParams.HostConfig.Mounts.Add(new Mount { Type = "bind", ReadOnly = false, Source = _configurationService.Configurations.HostPlugInsStorageMount, Target = Common.MountedPlugInsPath }); diff --git a/src/CLI/Test/AetCommandTest.cs b/src/CLI/Test/AetCommandTest.cs index 39fcaca20..473f02426 100644 --- a/src/CLI/Test/AetCommandTest.cs +++ b/src/CLI/Test/AetCommandTest.cs @@ -124,9 +124,9 @@ public async Task AetAdd_Command() } [Fact(DisplayName = "aet add comand with plug-ins")] - public async Task AetAdd_Command_WithPlugins() + public async Task AetAdd_Command_WithPlugIns() { - var command = "aet add -n MyName -a MyAET --workflows App MyCoolApp TheApp --plugins \"PluginTypeA\" \"PluginTypeB\""; + var command = "aet add -n MyName -a MyAET --workflows App MyCoolApp TheApp --plugins \"PlugInTypeA\" \"PlugInTypeB\""; var result = _paser.Parse(command); Assert.Equal(ExitCodes.Success, result.Errors.Count); @@ -135,7 +135,7 @@ public async Task AetAdd_Command_WithPlugins() Name = result.CommandResult.Children[0].Tokens[0].Value, AeTitle = result.CommandResult.Children[1].Tokens[0].Value, Workflows = result.CommandResult.Children[2].Tokens.Select(p => p.Value).ToList(), - PluginAssemblies = result.CommandResult.Children[3].Tokens.Select(p => p.Value).ToList(), + PlugInAssemblies = result.CommandResult.Children[3].Tokens.Select(p => p.Value).ToList(), }; Assert.Equal("MyName", entity.Name); Assert.Equal("MyAET", entity.AeTitle); @@ -143,9 +143,9 @@ public async Task AetAdd_Command_WithPlugins() item => item.Equals("App"), item => item.Equals("MyCoolApp"), item => item.Equals("TheApp")); - Assert.Collection(entity.PluginAssemblies, - item => item.Equals("PluginTypeA"), - item => item.Equals("PluginTypeB")); + Assert.Collection(entity.PlugInAssemblies, + item => item.Equals("PlugInTypeA"), + item => item.Equals("PlugInTypeB")); _informaticsGatewayClient.Setup(p => p.MonaiScpAeTitle.Create(It.IsAny(), It.IsAny())) .ReturnsAsync(entity); @@ -384,7 +384,7 @@ public async Task AetUpdate_Command() Workflows = result.CommandResult.Children[1].Tokens.Select(p => p.Value).ToList(), IgnoredSopClasses = result.CommandResult.Children[2].Tokens.Select(p => p.Value).ToList(), AllowedSopClasses = result.CommandResult.Children[3].Tokens.Select(p => p.Value).ToList(), - PluginAssemblies = result.CommandResult.Children[4].Tokens.Select(p => p.Value).ToList(), + PlugInAssemblies = result.CommandResult.Children[4].Tokens.Select(p => p.Value).ToList(), }; Assert.Equal("MyName", entity.Name); @@ -401,7 +401,7 @@ public async Task AetUpdate_Command() item => item.Equals("A"), item => item.Equals("B"), item => item.Equals("C")); - Assert.Collection(entity.PluginAssemblies, + Assert.Collection(entity.PlugInAssemblies, item => item.Equals("PlugInAssemblyA"), item => item.Equals("PlugInAssemblyB")); @@ -458,7 +458,7 @@ public async Task AetUpdate_Command_ConfigurationException() } [Fact(DisplayName = "aet plugins comand")] - public async Task AetPlugins_Command() + public async Task AetPlugIns_Command() { var command = "aet plugins"; var result = _paser.Parse(command); @@ -466,7 +466,7 @@ public async Task AetPlugins_Command() var entries = new Dictionary { { "A", "1" }, { "B", "2" } }; - _informaticsGatewayClient.Setup(p => p.MonaiScpAeTitle.Plugins(It.IsAny())) + _informaticsGatewayClient.Setup(p => p.MonaiScpAeTitle.PlugIns(It.IsAny())) .ReturnsAsync(entries); int exitCode = await _paser.InvokeAsync(command); @@ -474,28 +474,28 @@ public async Task AetPlugins_Command() Assert.Equal(ExitCodes.Success, exitCode); _informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Once()); - _informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.Plugins(It.IsAny()), Times.Once()); + _informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.PlugIns(It.IsAny()), Times.Once()); } [Fact(DisplayName = "aet plugins comand exception")] - public async Task AetPlugins_Command_Exception() + public async Task AetPlugIns_Command_Exception() { var command = "aet plugins"; - _informaticsGatewayClient.Setup(p => p.MonaiScpAeTitle.Plugins(It.IsAny())) + _informaticsGatewayClient.Setup(p => p.MonaiScpAeTitle.PlugIns(It.IsAny())) .Throws(new Exception("error")); int exitCode = await _paser.InvokeAsync(command); - Assert.Equal(ExitCodes.MonaiScp_ErrorPlugins, exitCode); + Assert.Equal(ExitCodes.MonaiScp_ErrorPlugIns, exitCode); _informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Once()); - _informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.Plugins(It.IsAny()), Times.Once()); + _informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.PlugIns(It.IsAny()), Times.Once()); _logger.VerifyLoggingMessageBeginsWith("Error retrieving data input plug-ins", LogLevel.Critical, Times.Once()); } [Fact(DisplayName = "aet plugins comand configuration exception")] - public async Task AetPlugins_Command_ConfigurationException() + public async Task AetPlugIns_Command_ConfigurationException() { var command = "aet plugins"; _configurationService.SetupGet(p => p.IsInitialized).Returns(false); @@ -505,16 +505,16 @@ public async Task AetPlugins_Command_ConfigurationException() Assert.Equal(ExitCodes.Config_NotConfigured, exitCode); _informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Never()); - _informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.Plugins(It.IsAny()), Times.Never()); + _informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.PlugIns(It.IsAny()), Times.Never()); _logger.VerifyLoggingMessageBeginsWith("Please execute `testhost config init` to intialize Informatics Gateway.", LogLevel.Critical, Times.Once()); } [Fact(DisplayName = "aet plugins comand empty")] - public async Task AetPlugins_Command_Empty() + public async Task AetPlugIns_Command_Empty() { var command = "aet plugins"; - _informaticsGatewayClient.Setup(p => p.MonaiScpAeTitle.Plugins(It.IsAny())) + _informaticsGatewayClient.Setup(p => p.MonaiScpAeTitle.PlugIns(It.IsAny())) .ReturnsAsync(new Dictionary()); int exitCode = await _paser.InvokeAsync(command); @@ -522,7 +522,7 @@ public async Task AetPlugins_Command_Empty() Assert.Equal(ExitCodes.Success, exitCode); _informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Once()); - _informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.Plugins(It.IsAny()), Times.Once()); + _informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.PlugIns(It.IsAny()), Times.Once()); _logger.VerifyLogging("No MONAI SCP Application Entities configured.", LogLevel.Warning, Times.Once()); } diff --git a/src/CLI/Test/DestinationCommandTest.cs b/src/CLI/Test/DestinationCommandTest.cs index adb1d9e8d..4212a7e0d 100644 --- a/src/CLI/Test/DestinationCommandTest.cs +++ b/src/CLI/Test/DestinationCommandTest.cs @@ -407,7 +407,7 @@ public async Task DstUpdate_Command_ConfigurationException() } [Fact(DisplayName = "dst plugins comand")] - public async Task DstPlugins_Command() + public async Task DstPlugIns_Command() { var command = "dst plugins"; var result = _paser.Parse(command); @@ -415,7 +415,7 @@ public async Task DstPlugins_Command() var entries = new Dictionary { { "A", "1" }, { "B", "2" } }; - _informaticsGatewayClient.Setup(p => p.DicomDestinations.Plugins(It.IsAny())) + _informaticsGatewayClient.Setup(p => p.DicomDestinations.PlugIns(It.IsAny())) .ReturnsAsync(entries); int exitCode = await _paser.InvokeAsync(command); @@ -423,28 +423,28 @@ public async Task DstPlugins_Command() Assert.Equal(ExitCodes.Success, exitCode); _informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Once()); - _informaticsGatewayClient.Verify(p => p.DicomDestinations.Plugins(It.IsAny()), Times.Once()); + _informaticsGatewayClient.Verify(p => p.DicomDestinations.PlugIns(It.IsAny()), Times.Once()); } [Fact(DisplayName = "dst plugins comand exception")] - public async Task DstPlugins_Command_Exception() + public async Task DstPlugIns_Command_Exception() { var command = "dst plugins"; - _informaticsGatewayClient.Setup(p => p.DicomDestinations.Plugins(It.IsAny())) + _informaticsGatewayClient.Setup(p => p.DicomDestinations.PlugIns(It.IsAny())) .Throws(new Exception("error")); int exitCode = await _paser.InvokeAsync(command); - Assert.Equal(ExitCodes.DestinationAe_ErrorPlugins, exitCode); + Assert.Equal(ExitCodes.DestinationAe_ErrorPlugIns, exitCode); _informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Once()); - _informaticsGatewayClient.Verify(p => p.DicomDestinations.Plugins(It.IsAny()), Times.Once()); + _informaticsGatewayClient.Verify(p => p.DicomDestinations.PlugIns(It.IsAny()), Times.Once()); _logger.VerifyLoggingMessageBeginsWith("Error retrieving data output plug-ins", LogLevel.Critical, Times.Once()); } [Fact(DisplayName = "dst plugins comand configuration exception")] - public async Task DstPlugins_Command_ConfigurationException() + public async Task DstPlugIns_Command_ConfigurationException() { var command = "dst plugins"; _configurationService.SetupGet(p => p.IsInitialized).Returns(false); @@ -454,16 +454,16 @@ public async Task DstPlugins_Command_ConfigurationException() Assert.Equal(ExitCodes.Config_NotConfigured, exitCode); _informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Never()); - _informaticsGatewayClient.Verify(p => p.DicomDestinations.Plugins(It.IsAny()), Times.Never()); + _informaticsGatewayClient.Verify(p => p.DicomDestinations.PlugIns(It.IsAny()), Times.Never()); _logger.VerifyLoggingMessageBeginsWith("Please execute `testhost config init` to intialize Informatics Gateway.", LogLevel.Critical, Times.Once()); } [Fact(DisplayName = "dst plugins comand empty")] - public async Task DstPlugins_Command_Empty() + public async Task DstPlugIns_Command_Empty() { var command = "dst plugins"; - _informaticsGatewayClient.Setup(p => p.DicomDestinations.Plugins(It.IsAny())) + _informaticsGatewayClient.Setup(p => p.DicomDestinations.PlugIns(It.IsAny())) .ReturnsAsync(new Dictionary()); int exitCode = await _paser.InvokeAsync(command); @@ -471,7 +471,7 @@ public async Task DstPlugins_Command_Empty() Assert.Equal(ExitCodes.Success, exitCode); _informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Once()); - _informaticsGatewayClient.Verify(p => p.DicomDestinations.Plugins(It.IsAny()), Times.Once()); + _informaticsGatewayClient.Verify(p => p.DicomDestinations.PlugIns(It.IsAny()), Times.Once()); _logger.VerifyLogging("No MONAI SCP Application Entities configured.", LogLevel.Warning, Times.Once()); } diff --git a/src/CLI/Test/NLogConfigurationOptionAccessorTest.cs b/src/CLI/Test/NLogConfigurationOptionAccessorTest.cs index 5d060f736..4da277490 100644 --- a/src/CLI/Test/NLogConfigurationOptionAccessorTest.cs +++ b/src/CLI/Test/NLogConfigurationOptionAccessorTest.cs @@ -35,7 +35,6 @@ public void DockerRunner_Constructor() Assert.Throws(() => new NLogConfigurationOptionAccessor(null)); } - [Fact] public void DicomListeningPort_Get_ReturnsValue() { diff --git a/src/CLI/Test/ProgramTest.cs b/src/CLI/Test/ProgramTest.cs index 8ef0bc856..82f8761f8 100644 --- a/src/CLI/Test/ProgramTest.cs +++ b/src/CLI/Test/ProgramTest.cs @@ -25,7 +25,7 @@ public class ProgramTest public void Startup_RunsProperly() { var host = Program.BuildParser(); - + Assert.NotNull(host); } } diff --git a/src/Client/Services/AeTitle{T}Service.cs b/src/Client/Services/AeTitle{T}Service.cs index 877fdb17e..43a2e7f3d 100644 --- a/src/Client/Services/AeTitle{T}Service.cs +++ b/src/Client/Services/AeTitle{T}Service.cs @@ -41,7 +41,7 @@ public interface IAeTitleService Task CEcho(string name, CancellationToken cancellationToken); - Task> Plugins(CancellationToken cancellationToken); + Task> PlugIns(CancellationToken cancellationToken); } internal class AeTitleService : ServiceBase, IAeTitleService @@ -119,7 +119,7 @@ public async Task Update(T item, CancellationToken cancellationToken) return await response.Content.ReadAsAsync(cancellationToken).ConfigureAwait(false); } - public async Task> Plugins(CancellationToken cancellationToken) + public async Task> PlugIns(CancellationToken cancellationToken) { if (typeof(T) != typeof(MonaiApplicationEntity) && typeof(T) != typeof(DestinationApplicationEntity)) diff --git a/src/Client/Test/AeTitleServiceTest.cs b/src/Client/Test/AeTitleServiceTest.cs index 3e9f7b89f..646f03a44 100644 --- a/src/Client/Test/AeTitleServiceTest.cs +++ b/src/Client/Test/AeTitleServiceTest.cs @@ -461,8 +461,8 @@ public async Task CEcho_ReturnsAProblem() Assert.Equal($"HTTP Status: {problem.Status}. {problem.Detail}", result.Message); } - [Fact(DisplayName = "AET - Plugins")] - public async Task Plugins() + [Fact(DisplayName = "AET - Plug-ins")] + public async Task PlugIns() { var plugins = new Dictionary { @@ -486,12 +486,12 @@ public async Task Plugins() var service = new AeTitleService(uriPath, httpClient, _logger.Object); - var exception = await Record.ExceptionAsync(async () => await service.Plugins(CancellationToken.None)); + var exception = await Record.ExceptionAsync(async () => await service.PlugIns(CancellationToken.None)); Assert.Null(exception); } - [Fact(DisplayName = "AET - Plugins returns a problem")] - public async Task Plugins_ReturnsAProblem() + [Fact(DisplayName = "AET - Plug-ins returns a problem")] + public async Task PlugIns_ReturnsAProblem() { var problem = new ProblemDetails { @@ -515,7 +515,7 @@ public async Task Plugins_ReturnsAProblem() var service = new AeTitleService(uriPath, httpClient, _logger.Object); - var result = await Assert.ThrowsAsync(async () => await service.Plugins(CancellationToken.None)); + var result = await Assert.ThrowsAsync(async () => await service.PlugIns(CancellationToken.None)); Assert.Equal($"HTTP Status: {problem.Status}. {problem.Detail}", result.Message); } diff --git a/src/Configuration/DicomWebConfiguration.cs b/src/Configuration/DicomWebConfiguration.cs index b5cde5aa5..8fde9b334 100644 --- a/src/Configuration/DicomWebConfiguration.cs +++ b/src/Configuration/DicomWebConfiguration.cs @@ -61,7 +61,7 @@ public class DicomWebConfiguration public uint Timeout { get; set; } = 10; /// - /// Optional list of data input plug-in type names to be executed by the *IInputDataPluginEngine* + /// Optional list of data input plug-in type names to be executed by the *IInputDataPlugInEngine* /// on the data received using default DICOMWeb STOW-RS endpoints: /// /// POST /dicomweb/studies/[{study-instance-uid}] @@ -69,11 +69,11 @@ public class DicomWebConfiguration /// /// [ConfigurationKeyName("plugins")] - public List PluginAssemblies { get; set; } = default!; + public List PlugInAssemblies { get; set; } = default!; public DicomWebConfiguration() { - PluginAssemblies ??= new List(); + PlugInAssemblies ??= new List(); } } } diff --git a/src/Configuration/InformaticsGatewayConfiguration.cs b/src/Configuration/InformaticsGatewayConfiguration.cs index e77c65a37..e87229134 100644 --- a/src/Configuration/InformaticsGatewayConfiguration.cs +++ b/src/Configuration/InformaticsGatewayConfiguration.cs @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 MONAI Consortium + * Copyright 2021-2023 MONAI Consortium * Copyright 2019-2021 NVIDIA Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -80,7 +80,7 @@ public class InformaticsGatewayConfiguration /// Represents the pluginConfiguration section of the configuration file. /// [ConfigurationKeyName("plugins")] - public PluginConfiguration PluginConfigurations { get; set; } + public PlugInConfiguration PlugInConfigurations { get; set; } public InformaticsGatewayConfiguration() { @@ -92,7 +92,7 @@ public InformaticsGatewayConfiguration() Messaging = new MessageBrokerConfiguration(); Database = new DatabaseConfiguration(); Hl7 = new Hl7Configuration(); - PluginConfigurations = new PluginConfiguration(); + PlugInConfigurations = new PlugInConfiguration(); } } } diff --git a/src/Configuration/PluginConfiguration.cs b/src/Configuration/PluginConfiguration.cs index e6a4063b1..e2294c37b 100755 --- a/src/Configuration/PluginConfiguration.cs +++ b/src/Configuration/PluginConfiguration.cs @@ -19,7 +19,7 @@ namespace Monai.Deploy.InformaticsGateway.Configuration { - public class PluginConfiguration + public class PlugInConfiguration { [ConfigurationKeyName("remoteApp")] public Dictionary RemoteAppConfigurations { get; set; } = new(); diff --git a/src/Configuration/ValidationExtensions.cs b/src/Configuration/ValidationExtensions.cs index c612227df..a652e89b4 100644 --- a/src/Configuration/ValidationExtensions.cs +++ b/src/Configuration/ValidationExtensions.cs @@ -69,6 +69,7 @@ public static bool IsValid(this SourceApplicationEntity sourceApplicationEntity, return valid; } + public static bool IsValid(this VirtualApplicationEntity virtualApplicationEntity, out IList validationErrors) { Guard.Against.Null(virtualApplicationEntity, nameof(virtualApplicationEntity)); diff --git a/src/Database/MongoDB/Configurations/MongoDBOptions.cs b/src/Database/Api/DatabaseOptions.cs old mode 100755 new mode 100644 similarity index 87% rename from src/Database/MongoDB/Configurations/MongoDBOptions.cs rename to src/Database/Api/DatabaseOptions.cs index 70e0d35bc..9951e44ce --- a/src/Database/MongoDB/Configurations/MongoDBOptions.cs +++ b/src/Database/Api/DatabaseOptions.cs @@ -16,9 +16,9 @@ using Microsoft.Extensions.Configuration; -namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations +namespace Monai.Deploy.InformaticsGateway.Database.Api { - public class MongoDBOptions + public class DatabaseOptions { [ConfigurationKeyName("DatabaseName")] public string DatabaseName { get; set; } = string.Empty; diff --git a/src/Api/DestinationApplicationEntityRemoteAppExecution.cs b/src/Database/Api/DatabaseRegistrationBase.cs old mode 100755 new mode 100644 similarity index 58% rename from src/Api/DestinationApplicationEntityRemoteAppExecution.cs rename to src/Database/Api/DatabaseRegistrationBase.cs index 2ca6e4fe0..eeb4bad37 --- a/src/Api/DestinationApplicationEntityRemoteAppExecution.cs +++ b/src/Database/Api/DatabaseRegistrationBase.cs @@ -14,15 +14,12 @@ * limitations under the License. */ +using Microsoft.Extensions.DependencyInjection; -namespace Monai.Deploy.InformaticsGateway.Api +namespace Monai.Deploy.InformaticsGateway.Database.Api { - public class DestinationApplicationEntityRemoteAppExecution + public abstract class DatabaseRegistrationBase { - public string DestinationApplicationEntityName { get; set; } = default!; - public DestinationApplicationEntity DestinationApplicationEntity { get; set; } = default!; - - public string RemoteAppExecutionId { get; set; } = default!; - public RemoteAppExecution RemoteAppExecution { get; set; } = default!; + public abstract IServiceCollection Configure(IServiceCollection services, DatabaseType databaseType, string? connectionString); } } diff --git a/src/Database/Api/DatabaseTypes.cs b/src/Database/Api/DatabaseTypes.cs new file mode 100644 index 000000000..166c8f5d1 --- /dev/null +++ b/src/Database/Api/DatabaseTypes.cs @@ -0,0 +1,24 @@ +/* + * 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. + */ + +namespace Monai.Deploy.InformaticsGateway.Database.Api +{ + public enum DatabaseType + { + EntityFramework, + MongoDb + } +} diff --git a/src/Database/Api/IDatabaseMigrationManager.cs b/src/Database/Api/IDatabaseMigrationManager.cs index 3de2f9abf..cf2fc1861 100644 --- a/src/Database/Api/IDatabaseMigrationManager.cs +++ b/src/Database/Api/IDatabaseMigrationManager.cs @@ -18,8 +18,19 @@ namespace Monai.Deploy.InformaticsGateway.Database.Api { + /// + /// Interface for the main application to migrate and configure databases + /// public interface IDatabaseMigrationManager { IHost Migrate(IHost host); } + + /// + /// Interface for the plug-ins to migrate and configure databases + /// + public interface IDatabaseMigrationManagerForPlugIns + { + IHost Migrate(IHost host); + } } diff --git a/src/Database/Api/SR.cs b/src/Database/Api/SR.cs index 05f83bc94..268b6f425 100644 --- a/src/Database/Api/SR.cs +++ b/src/Database/Api/SR.cs @@ -23,7 +23,7 @@ public static class SR /// Name of the key for retrieve database connection string. /// public const string DatabaseConnectionStringKey = "InformaticsGatewayDatabase"; - public const string DatabaseNameKey = "DatabaseName"; + public const string DatabaseNameKey = "DatabaseName"; } } diff --git a/src/Database/DatabaseManager.cs b/src/Database/DatabaseManager.cs index 56b4d062c..ab55003e1 100755 --- a/src/Database/DatabaseManager.cs +++ b/src/Database/DatabaseManager.cs @@ -14,16 +14,21 @@ * limitations under the License. */ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using System.Reflection; +using Ardalis.GuardClauses; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Database.EntityFramework; using Monai.Deploy.InformaticsGateway.Database.MongoDB; -using Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations; using MongoDB.Driver; +using ConfigurationException = Monai.Deploy.InformaticsGateway.Configuration.ConfigurationException; namespace Monai.Deploy.InformaticsGateway.Database { @@ -57,6 +62,9 @@ public static IHealthChecksBuilder AddDatabaseHealthCheck(this IHealthChecksBuil } public static IServiceCollection ConfigureDatabase(this IServiceCollection services, IConfigurationSection? connectionStringConfigurationSection) + => services.ConfigureDatabase(connectionStringConfigurationSection, new FileSystem()); + + public static IServiceCollection ConfigureDatabase(this IServiceCollection services, IConfigurationSection? connectionStringConfigurationSection, IFileSystem fileSystem) { if (connectionStringConfigurationSection is null) { @@ -79,12 +87,14 @@ public static IServiceCollection ConfigureDatabase(this IServiceCollection servi services.AddScoped(typeof(IPayloadRepository), typeof(EntityFramework.Repositories.PayloadRepository)); services.AddScoped(typeof(IDicomAssociationInfoRepository), typeof(EntityFramework.Repositories.DicomAssociationInfoRepository)); services.AddScoped(typeof(IVirtualApplicationEntityRepository), typeof(EntityFramework.Repositories.VirtualApplicationEntityRepository)); - services.AddScoped(typeof(IRemoteAppExecutionRepository), typeof(EntityFramework.Repositories.RemoteAppExecutionRepository)); + + services.ConfigureDatabaseFromPlugIns(DatabaseType.EntityFramework, fileSystem, connectionStringConfigurationSection); return services; case DbType_MongoDb: services.AddSingleton(s => new MongoClient(connectionStringConfigurationSection[SR.DatabaseConnectionStringKey])); - services.Configure(connectionStringConfigurationSection); + services.Configure(connectionStringConfigurationSection); + services.AddScoped(); services.AddScoped(typeof(IDestinationApplicationEntityRepository), typeof(MongoDB.Repositories.DestinationApplicationEntityRepository)); services.AddScoped(typeof(IInferenceRequestRepository), typeof(MongoDB.Repositories.InferenceRequestRepository)); @@ -94,7 +104,8 @@ public static IServiceCollection ConfigureDatabase(this IServiceCollection servi services.AddScoped(typeof(IPayloadRepository), typeof(MongoDB.Repositories.PayloadRepository)); services.AddScoped(typeof(IDicomAssociationInfoRepository), typeof(MongoDB.Repositories.DicomAssociationInfoRepository)); services.AddScoped(typeof(IVirtualApplicationEntityRepository), typeof(MongoDB.Repositories.VirtualApplicationEntityRepository)); - services.AddScoped(typeof(IRemoteAppExecutionRepository), typeof(MongoDB.Repositories.RemoteAppExecutionRepository)); + + services.ConfigureDatabaseFromPlugIns(DatabaseType.MongoDb, fileSystem, connectionStringConfigurationSection); return services; @@ -102,5 +113,61 @@ public static IServiceCollection ConfigureDatabase(this IServiceCollection servi throw new ConfigurationException($"Unsupported database type defined: '{databaseType}'"); } } + + public static IServiceCollection ConfigureDatabaseFromPlugIns(this IServiceCollection services, + DatabaseType databaseType, + IFileSystem fileSystem, + IConfigurationSection? connectionStringConfigurationSection) + { + Guard.Against.Null(fileSystem, nameof(fileSystem)); + + var assemblies = LoadAssemblyFromPlugInsDirectory(fileSystem); + var matchingTypes = FindMatchingTypesFromAssemblies(assemblies); + + foreach (var type in matchingTypes) + { + if (Activator.CreateInstance(type) is not DatabaseRegistrationBase registrar) + { + throw new ConfigurationException($"Error activating database registration from type '{type.FullName}'."); + } + registrar.Configure(services, databaseType, connectionStringConfigurationSection?[SR.DatabaseConnectionStringKey]); + } + return services; + } + + internal static Type[] FindMatchingTypesFromAssemblies(Assembly[] assemblies) + { + var matchingTypes = new List(); + foreach (var assembly in assemblies) + { + var types = assembly.ExportedTypes.Where(p => p.IsSubclassOf(typeof(DatabaseRegistrationBase))); + if (types.Any()) + { + matchingTypes.AddRange(types); + } + } + + return matchingTypes.ToArray(); + } + + internal static Assembly[] LoadAssemblyFromPlugInsDirectory(IFileSystem fileSystem) + { + Guard.Against.Null(fileSystem, nameof(fileSystem)); + + if (!fileSystem.Directory.Exists(InformaticsGateway.Api.PlugIns.SR.PlugInDirectoryPath)) + { + throw new ConfigurationException($"Plug-in directory '{InformaticsGateway.Api.PlugIns.SR.PlugInDirectoryPath}' cannot be found."); + } + + var assemblies = new List(); + var plugins = fileSystem.Directory.GetFiles(InformaticsGateway.Api.PlugIns.SR.PlugInDirectoryPath, "*.dll"); + + foreach (var plugin in plugins) + { + var asesmblyeData = fileSystem.File.ReadAllBytes(plugin); + assemblies.Add(Assembly.Load(asesmblyeData)); + } + return assemblies.ToArray(); + } } } diff --git a/src/Database/DatabaseMigrationManager.cs b/src/Database/DatabaseMigrationManager.cs index 111cce122..da35c17a4 100644 --- a/src/Database/DatabaseMigrationManager.cs +++ b/src/Database/DatabaseMigrationManager.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. @@ -14,8 +14,14 @@ * limitations under the License. */ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api; namespace Monai.Deploy.InformaticsGateway.Database @@ -26,9 +32,43 @@ public static IHost MigrateDatabase(this IHost host) { using (var scope = host.Services.CreateScope()) { - scope.ServiceProvider.GetService()?.Migrate(host); + scope.ServiceProvider.GetRequiredService()?.Migrate(host); + var fileSystem = scope.ServiceProvider.GetRequiredService(); + + host.MigrateDatabaseFromExternalPlugIns(fileSystem); + } + return host; + } + + private static IHost MigrateDatabaseFromExternalPlugIns(this IHost host, IFileSystem fileSystem) + { + var assemblies = DatabaseManager.LoadAssemblyFromPlugInsDirectory(fileSystem); + var matchingTypes = FindMatchingTypesFromAssemblies(assemblies); + + foreach (var type in matchingTypes) + { + if (Activator.CreateInstance(type) is not IDatabaseMigrationManager migrationManager) + { + throw new ConfigurationException($"Error activating IDatabaseMigrationManager from type '{type.FullName}'."); + } + migrationManager.Migrate(host); } return host; } + + private static Type[] FindMatchingTypesFromAssemblies(Assembly[] assemblies) + { + var matchingTypes = new List(); + foreach (var assembly in assemblies) + { + var types = assembly.ExportedTypes.Where(p => p.IsAssignableFrom(typeof(IDatabaseMigrationManager))); + if (types.Any()) + { + matchingTypes.AddRange(types); + } + } + + return matchingTypes.ToArray(); + } } } diff --git a/src/Database/EntityFramework/Configuration/DestinationApplicationEntityRemoteAppExecutionConfiguration.cs b/src/Database/EntityFramework/Configuration/DestinationApplicationEntityRemoteAppExecutionConfiguration.cs deleted file mode 100644 index d8536fde1..000000000 --- a/src/Database/EntityFramework/Configuration/DestinationApplicationEntityRemoteAppExecutionConfiguration.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2021-2022 MONAI Consortium - * Copyright 2021 NVIDIA Corporation - * - * 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 Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Monai.Deploy.InformaticsGateway.Api; - -namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Configuration -{ - - internal class DestinationApplicationEntityRemoteAppExecutionConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) - { - builder.HasKey(dr => new { dr.DestinationApplicationEntityName, dr.RemoteAppExecutionId }); - builder.HasOne(bc => bc.RemoteAppExecution) - .WithMany(b => b.DestinationApplicationEntityRemoteAppExecutions) - .HasForeignKey(bc => bc.RemoteAppExecutionId); - builder.HasOne(bc => bc.DestinationApplicationEntity) - .WithMany(c => c.DestinationApplicationEntityRemoteAppExecutions) - .HasForeignKey(bc => bc.DestinationApplicationEntityName); - - } - } -} diff --git a/src/Database/EntityFramework/Configuration/InferenceRequestConfiguration.cs b/src/Database/EntityFramework/Configuration/InferenceRequestConfiguration.cs index 486d15a90..ba58ba66e 100644 --- a/src/Database/EntityFramework/Configuration/InferenceRequestConfiguration.cs +++ b/src/Database/EntityFramework/Configuration/InferenceRequestConfiguration.cs @@ -26,6 +26,7 @@ namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Configuration { #pragma warning disable CS8603 // Possible null reference return. #pragma warning disable CS8604 // Possible null reference argument. + internal class InferenceRequestConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) @@ -77,6 +78,7 @@ public void Configure(EntityTypeBuilder builder) builder.HasIndex(p => p.TransactionId, "idx_inferencerequest_transactionid").IsUnique(); } } + #pragma warning restore CS8604 // Possible null reference argument. #pragma warning restore CS8603 // Possible null reference return. } diff --git a/src/Database/EntityFramework/Configuration/MonaiApplicationEntityConfiguration.cs b/src/Database/EntityFramework/Configuration/MonaiApplicationEntityConfiguration.cs index 264ce54c2..3e78d3ad9 100644 --- a/src/Database/EntityFramework/Configuration/MonaiApplicationEntityConfiguration.cs +++ b/src/Database/EntityFramework/Configuration/MonaiApplicationEntityConfiguration.cs @@ -52,7 +52,7 @@ public void Configure(EntityTypeBuilder builder) v => JsonSerializer.Serialize(v, jsonSerializerSettings), v => JsonSerializer.Deserialize>(v, jsonSerializerSettings)) .Metadata.SetValueComparer(valueComparer); - builder.Property(j => j.PluginAssemblies) + builder.Property(j => j.PlugInAssemblies) .HasConversion( v => JsonSerializer.Serialize(v, jsonSerializerSettings), v => JsonSerializer.Deserialize>(v, jsonSerializerSettings)) diff --git a/src/Database/EntityFramework/Configuration/VirtualApplicationEntityConfiguration.cs b/src/Database/EntityFramework/Configuration/VirtualApplicationEntityConfiguration.cs index 5d91c8d0e..3d52e751d 100644 --- a/src/Database/EntityFramework/Configuration/VirtualApplicationEntityConfiguration.cs +++ b/src/Database/EntityFramework/Configuration/VirtualApplicationEntityConfiguration.cs @@ -1,6 +1,5 @@ /* - * Copyright 2021-2023 MONAI Consortium - * Copyright 2021 NVIDIA Corporation + * 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. @@ -50,7 +49,7 @@ public void Configure(EntityTypeBuilder builder) v => JsonSerializer.Serialize(v, jsonSerializerSettings), v => JsonSerializer.Deserialize>(v, jsonSerializerSettings)) .Metadata.SetValueComparer(valueComparer); - builder.Property(j => j.PluginAssemblies) + builder.Property(j => j.PlugInAssemblies) .HasConversion( v => JsonSerializer.Serialize(v, jsonSerializerSettings), v => JsonSerializer.Deserialize>(v, jsonSerializerSettings)) diff --git a/src/Database/EntityFramework/InformaticsGatewayContext.cs b/src/Database/EntityFramework/InformaticsGatewayContext.cs index 98c5b691c..52b410c39 100755 --- a/src/Database/EntityFramework/InformaticsGatewayContext.cs +++ b/src/Database/EntityFramework/InformaticsGatewayContext.cs @@ -41,8 +41,6 @@ public InformaticsGatewayContext(DbContextOptions opt public virtual DbSet StorageMetadataWrapperEntities { get; set; } public virtual DbSet DicomAssociationHistories { get; set; } public virtual DbSet VirtualApplicationEntities { get; set; } - public virtual DbSet RemoteAppExecutions { get; set; } - public virtual DbSet RemtoeAppExecutionDestinations { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -56,8 +54,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfiguration(new StorageMetadataWrapperEntityConfiguration()); modelBuilder.ApplyConfiguration(new DicomAssociationInfoConfiguration()); modelBuilder.ApplyConfiguration(new VirtualApplicationEntityConfiguration()); - modelBuilder.ApplyConfiguration(new RemoteAppExecutionConfiguration()); - modelBuilder.ApplyConfiguration(new DestinationApplicationEntityRemoteAppExecutionConfiguration()); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) diff --git a/src/Database/EntityFramework/Migrations/20230327190827_R3_0.3.15.cs b/src/Database/EntityFramework/Migrations/20230327190827_R3_0.3.15.cs index 21919bc5d..a190f7719 100644 --- a/src/Database/EntityFramework/Migrations/20230327190827_R3_0.3.15.cs +++ b/src/Database/EntityFramework/Migrations/20230327190827_R3_0.3.15.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Database/EntityFramework/Migrations/20230816201637_R4_0.4.0.cs b/src/Database/EntityFramework/Migrations/20230816201637_R4_0.4.0.cs deleted file mode 100644 index aebc3fc58..000000000 --- a/src/Database/EntityFramework/Migrations/20230816201637_R4_0.4.0.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -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: "TaskId", - table: "Payloads", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "WorkflowInstanceId", - table: "Payloads", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "PluginAssemblies", - table: "MonaiApplicationEntities", - type: "TEXT", - nullable: false, - defaultValue: ""); - - migrationBuilder.CreateTable( - name: "RemoteAppExecutions", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - RequestTime = table.Column(type: "TEXT", nullable: false), - ExportTaskId = table.Column(type: "TEXT", nullable: false), - WorkflowInstanceId = table.Column(type: "TEXT", nullable: false), - CorrelationId = table.Column(type: "TEXT", nullable: false), - StudyUid = table.Column(type: "TEXT", nullable: true), - OutgoingUid = table.Column(type: "TEXT", nullable: false), - Files = table.Column(type: "TEXT", nullable: false), - OriginalValues = table.Column(type: "TEXT", nullable: false), - ProxyValues = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_RemoteAppExecutions", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "VirtualApplicationEntities", - columns: table => new - { - Name = table.Column(type: "TEXT", nullable: false), - VirtualAeTitle = table.Column(type: "TEXT", nullable: false), - Workflows = table.Column(type: "TEXT", nullable: false), - PluginAssemblies = table.Column(type: "TEXT", nullable: false), - CreatedBy = table.Column(type: "TEXT", nullable: true), - UpdatedBy = table.Column(type: "TEXT", nullable: true), - DateTimeUpdated = table.Column(type: "TEXT", nullable: true), - DateTimeCreated = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_VirtualApplicationEntities", x => x.Name); - }); - - migrationBuilder.CreateTable( - name: "DestinationApplicationEntityRemoteAppExecution", - columns: table => new - { - ExportDetailsName = table.Column(type: "TEXT", nullable: false), - RemoteAppExecutionsId = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_DestinationApplicationEntityRemoteAppExecution", x => new { x.ExportDetailsName, x.RemoteAppExecutionsId }); - table.ForeignKey( - name: "FK_DestinationApplicationEntityRemoteAppExecution_DestinationApplicationEntities_ExportDetailsName", - column: x => x.ExportDetailsName, - principalTable: "DestinationApplicationEntities", - principalColumn: "Name", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_DestinationApplicationEntityRemoteAppExecution_RemoteAppExecutions_RemoteAppExecutionsId", - column: x => x.RemoteAppExecutionsId, - principalTable: "RemoteAppExecutions", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "RemtoeAppExecutionDestinations", - columns: table => new - { - DestinationApplicationEntityName = table.Column(type: "TEXT", nullable: false), - RemoteAppExecutionId = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_RemtoeAppExecutionDestinations", x => new { x.DestinationApplicationEntityName, x.RemoteAppExecutionId }); - table.ForeignKey( - name: "FK_RemtoeAppExecutionDestinations_DestinationApplicationEntities_DestinationApplicationEntityName", - column: x => x.DestinationApplicationEntityName, - principalTable: "DestinationApplicationEntities", - principalColumn: "Name", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_RemtoeAppExecutionDestinations_RemoteAppExecutions_RemoteAppExecutionId", - column: x => x.RemoteAppExecutionId, - principalTable: "RemoteAppExecutions", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_DestinationApplicationEntityRemoteAppExecution_RemoteAppExecutionsId", - table: "DestinationApplicationEntityRemoteAppExecution", - column: "RemoteAppExecutionsId"); - - migrationBuilder.CreateIndex( - name: "idx_outgoing_key", - table: "RemoteAppExecutions", - column: "OutgoingUid"); - - migrationBuilder.CreateIndex( - name: "IX_RemtoeAppExecutionDestinations_RemoteAppExecutionId", - table: "RemtoeAppExecutionDestinations", - column: "RemoteAppExecutionId"); - - migrationBuilder.CreateIndex( - name: "idx_virtualae_name", - table: "VirtualApplicationEntities", - column: "Name", - unique: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "DestinationApplicationEntityRemoteAppExecution"); - - migrationBuilder.DropTable( - name: "RemtoeAppExecutionDestinations"); - - migrationBuilder.DropTable( - name: "VirtualApplicationEntities"); - - migrationBuilder.DropTable( - name: "RemoteAppExecutions"); - - migrationBuilder.DropColumn( - name: "TaskId", - table: "Payloads"); - - migrationBuilder.DropColumn( - name: "WorkflowInstanceId", - table: "Payloads"); - - migrationBuilder.DropColumn( - name: "PluginAssemblies", - table: "MonaiApplicationEntities"); - } - } -} diff --git a/src/Database/EntityFramework/Migrations/20230816201637_R4_0.4.0.Designer.cs b/src/Database/EntityFramework/Migrations/20230818160639_R4_0.4.0.Designer.cs similarity index 71% rename from src/Database/EntityFramework/Migrations/20230816201637_R4_0.4.0.Designer.cs rename to src/Database/EntityFramework/Migrations/20230818160639_R4_0.4.0.Designer.cs index 2d24518ef..a1ce08d6e 100644 --- a/src/Database/EntityFramework/Migrations/20230816201637_R4_0.4.0.Designer.cs +++ b/src/Database/EntityFramework/Migrations/20230818160639_R4_0.4.0.Designer.cs @@ -11,7 +11,7 @@ namespace Monai.Deploy.InformaticsGateway.Database.Migrations { [DbContext(typeof(InformaticsGatewayContext))] - [Migration("20230816201637_R4_0.4.0")] + [Migration("20230818160639_R4_0.4.0")] partial class R4_040 { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -19,21 +19,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "6.0.21"); - modelBuilder.Entity("DestinationApplicationEntityRemoteAppExecution", b => - { - b.Property("ExportDetailsName") - .HasColumnType("TEXT"); - - b.Property("RemoteAppExecutionsId") - .HasColumnType("TEXT"); - - b.HasKey("ExportDetailsName", "RemoteAppExecutionsId"); - - b.HasIndex("RemoteAppExecutionsId"); - - b.ToTable("DestinationApplicationEntityRemoteAppExecution"); - }); - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntity", b => { b.Property("Name") @@ -73,21 +58,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("DestinationApplicationEntities"); }); - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntityRemoteAppExecution", b => - { - b.Property("DestinationApplicationEntityName") - .HasColumnType("TEXT"); - - b.Property("RemoteAppExecutionId") - .HasColumnType("TEXT"); - - b.HasKey("DestinationApplicationEntityName", "RemoteAppExecutionId"); - - b.HasIndex("RemoteAppExecutionId"); - - b.ToTable("RemtoeAppExecutionDestinations"); - }); - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.DicomAssociationInfo", b => { b.Property("Id") @@ -165,7 +135,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); - b.Property("PluginAssemblies") + b.Property("PlugInAssemblies") .IsRequired() .HasColumnType("TEXT"); @@ -187,52 +157,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("MonaiApplicationEntities"); }); - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.RemoteAppExecution", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("CorrelationId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ExportTaskId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Files") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("OriginalValues") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("OutgoingUid") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ProxyValues") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("RequestTime") - .HasColumnType("TEXT"); - - b.Property("StudyUid") - .HasColumnType("TEXT"); - - b.Property("WorkflowInstanceId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex(new[] { "OutgoingUid" }, "idx_outgoing_key"); - - b.ToTable("RemoteAppExecutions"); - }); - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Rest.InferenceRequest", b => { b.Property("InferenceRequestId") @@ -385,7 +309,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("DateTimeUpdated") .HasColumnType("TEXT"); - b.Property("PluginAssemblies") + b.Property("PlugInAssemblies") .IsRequired() .HasColumnType("TEXT"); @@ -440,50 +364,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("StorageMetadataWrapperEntities"); }); - - modelBuilder.Entity("DestinationApplicationEntityRemoteAppExecution", b => - { - b.HasOne("Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntity", null) - .WithMany() - .HasForeignKey("ExportDetailsName") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Monai.Deploy.InformaticsGateway.Api.RemoteAppExecution", null) - .WithMany() - .HasForeignKey("RemoteAppExecutionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntityRemoteAppExecution", b => - { - b.HasOne("Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntity", "DestinationApplicationEntity") - .WithMany("DestinationApplicationEntityRemoteAppExecutions") - .HasForeignKey("DestinationApplicationEntityName") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Monai.Deploy.InformaticsGateway.Api.RemoteAppExecution", "RemoteAppExecution") - .WithMany("DestinationApplicationEntityRemoteAppExecutions") - .HasForeignKey("RemoteAppExecutionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("DestinationApplicationEntity"); - - b.Navigation("RemoteAppExecution"); - }); - - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntity", b => - { - b.Navigation("DestinationApplicationEntityRemoteAppExecutions"); - }); - - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.RemoteAppExecution", b => - { - b.Navigation("DestinationApplicationEntityRemoteAppExecutions"); - }); #pragma warning restore 612, 618 } } diff --git a/src/Database/EntityFramework/Migrations/20230818160639_R4_0.4.0.cs b/src/Database/EntityFramework/Migrations/20230818160639_R4_0.4.0.cs new file mode 100644 index 000000000..1c938b626 --- /dev/null +++ b/src/Database/EntityFramework/Migrations/20230818160639_R4_0.4.0.cs @@ -0,0 +1,73 @@ +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: "TaskId", + table: "Payloads", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "WorkflowInstanceId", + table: "Payloads", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "PlugInAssemblies", + table: "MonaiApplicationEntities", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateTable( + name: "VirtualApplicationEntities", + columns: table => new + { + Name = table.Column(type: "TEXT", nullable: false), + VirtualAeTitle = table.Column(type: "TEXT", nullable: false), + Workflows = table.Column(type: "TEXT", nullable: false), + PlugInAssemblies = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true), + DateTimeUpdated = table.Column(type: "TEXT", nullable: true), + DateTimeCreated = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_VirtualApplicationEntities", x => x.Name); + }); + + migrationBuilder.CreateIndex( + name: "idx_virtualae_name", + table: "VirtualApplicationEntities", + column: "Name", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "VirtualApplicationEntities"); + + migrationBuilder.DropColumn( + name: "TaskId", + table: "Payloads"); + + migrationBuilder.DropColumn( + name: "WorkflowInstanceId", + table: "Payloads"); + + migrationBuilder.DropColumn( + name: "PlugInAssemblies", + table: "MonaiApplicationEntities"); + } + } +} diff --git a/src/Database/EntityFramework/Migrations/InformaticsGatewayContextModelSnapshot.cs b/src/Database/EntityFramework/Migrations/InformaticsGatewayContextModelSnapshot.cs index c4624dbe4..623715e8f 100644 --- a/src/Database/EntityFramework/Migrations/InformaticsGatewayContextModelSnapshot.cs +++ b/src/Database/EntityFramework/Migrations/InformaticsGatewayContextModelSnapshot.cs @@ -17,21 +17,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "6.0.21"); - modelBuilder.Entity("DestinationApplicationEntityRemoteAppExecution", b => - { - b.Property("ExportDetailsName") - .HasColumnType("TEXT"); - - b.Property("RemoteAppExecutionsId") - .HasColumnType("TEXT"); - - b.HasKey("ExportDetailsName", "RemoteAppExecutionsId"); - - b.HasIndex("RemoteAppExecutionsId"); - - b.ToTable("DestinationApplicationEntityRemoteAppExecution"); - }); - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntity", b => { b.Property("Name") @@ -71,21 +56,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("DestinationApplicationEntities"); }); - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntityRemoteAppExecution", b => - { - b.Property("DestinationApplicationEntityName") - .HasColumnType("TEXT"); - - b.Property("RemoteAppExecutionId") - .HasColumnType("TEXT"); - - b.HasKey("DestinationApplicationEntityName", "RemoteAppExecutionId"); - - b.HasIndex("RemoteAppExecutionId"); - - b.ToTable("RemtoeAppExecutionDestinations"); - }); - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.DicomAssociationInfo", b => { b.Property("Id") @@ -163,7 +133,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); - b.Property("PluginAssemblies") + b.Property("PlugInAssemblies") .IsRequired() .HasColumnType("TEXT"); @@ -185,52 +155,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("MonaiApplicationEntities"); }); - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.RemoteAppExecution", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("CorrelationId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ExportTaskId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Files") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("OriginalValues") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("OutgoingUid") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ProxyValues") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("RequestTime") - .HasColumnType("TEXT"); - - b.Property("StudyUid") - .HasColumnType("TEXT"); - - b.Property("WorkflowInstanceId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex(new[] { "OutgoingUid" }, "idx_outgoing_key"); - - b.ToTable("RemoteAppExecutions"); - }); - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.Rest.InferenceRequest", b => { b.Property("InferenceRequestId") @@ -383,7 +307,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DateTimeUpdated") .HasColumnType("TEXT"); - b.Property("PluginAssemblies") + b.Property("PlugInAssemblies") .IsRequired() .HasColumnType("TEXT"); @@ -438,50 +362,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("StorageMetadataWrapperEntities"); }); - - modelBuilder.Entity("DestinationApplicationEntityRemoteAppExecution", b => - { - b.HasOne("Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntity", null) - .WithMany() - .HasForeignKey("ExportDetailsName") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Monai.Deploy.InformaticsGateway.Api.RemoteAppExecution", null) - .WithMany() - .HasForeignKey("RemoteAppExecutionsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntityRemoteAppExecution", b => - { - b.HasOne("Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntity", "DestinationApplicationEntity") - .WithMany("DestinationApplicationEntityRemoteAppExecutions") - .HasForeignKey("DestinationApplicationEntityName") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Monai.Deploy.InformaticsGateway.Api.RemoteAppExecution", "RemoteAppExecution") - .WithMany("DestinationApplicationEntityRemoteAppExecutions") - .HasForeignKey("RemoteAppExecutionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("DestinationApplicationEntity"); - - b.Navigation("RemoteAppExecution"); - }); - - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntity", b => - { - b.Navigation("DestinationApplicationEntityRemoteAppExecutions"); - }); - - modelBuilder.Entity("Monai.Deploy.InformaticsGateway.Api.RemoteAppExecution", b => - { - b.Navigation("DestinationApplicationEntityRemoteAppExecutions"); - }); #pragma warning restore 612, 618 } } diff --git a/src/Database/EntityFramework/Test/MonaiApplicationEntityRepositoryTest.cs b/src/Database/EntityFramework/Test/MonaiApplicationEntityRepositoryTest.cs index d25eca352..ab50f74b2 100644 --- a/src/Database/EntityFramework/Test/MonaiApplicationEntityRepositoryTest.cs +++ b/src/Database/EntityFramework/Test/MonaiApplicationEntityRepositoryTest.cs @@ -71,7 +71,7 @@ public async Task GivenAMonaiApplicationEntity_WhenAddingToDatabase_ExpectItToBe Workflows = new List { "W1", "W2" }, Grouping = "G", IgnoredSopClasses = new List { "4", "5" }, - PluginAssemblies = new List { "AssemblyA", "AssemblyB", "AssemblyC" }, + PlugInAssemblies = new List { "AssemblyA", "AssemblyB", "AssemblyC" }, }; var store = new MonaiApplicationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options); diff --git a/src/Database/EntityFramework/Test/RemoteAppRepositoryTest.cs b/src/Database/EntityFramework/Test/RemoteAppRepositoryTest.cs deleted file mode 100755 index 740bbe6ab..000000000 --- a/src/Database/EntityFramework/Test/RemoteAppRepositoryTest.cs +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2022 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 Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.VisualBasic; -using Monai.Deploy.InformaticsGateway.Api; -using Monai.Deploy.InformaticsGateway.Api.Storage; -using Monai.Deploy.InformaticsGateway.Configuration; -using Monai.Deploy.InformaticsGateway.Database.EntityFramework.Repositories; -using Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test; -using Moq; -namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Integration.Test -{ - [Collection("SqliteDatabase")] - public class RemoteAppRepositoryTest - { - private readonly SqliteDatabaseFixture _databaseFixture; - - private readonly Mock _serviceScopeFactory; - private readonly Mock> _logger; - private readonly IOptions _options; - - private readonly Mock _serviceScope; - private readonly IServiceProvider _serviceProvider; - - public RemoteAppRepositoryTest(SqliteDatabaseFixture databaseFixture) - { - - _databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture)); - - _serviceScopeFactory = new Mock(); - _logger = new Mock>(); - _options = Microsoft.Extensions.Options.Options.Create(new InformaticsGatewayConfiguration()); - - _serviceScope = new Mock(); - var services = new ServiceCollection(); - services.AddScoped(p => _logger.Object); - services.AddScoped(p => databaseFixture.DatabaseContext); - - _serviceProvider = services.BuildServiceProvider(); - _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); - _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); - - _options.Value.Database.Retries.DelaysMilliseconds = new[] { 1, 1, 1 }; - _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); - databaseFixture.DatabaseContext.Set(); - databaseFixture.DatabaseContext.SaveChanges(); - } - - [Fact] - public async Task GivenAexecution_WhenAddingToDatabase_ExpectItToBeSaved() - { - var outgoingUid = Guid.NewGuid().ToString(); - var dataset = new DicomDataset(); - var date = new DateTime( - DateTime.Now.Year, - DateTime.Now.Month, - DateTime.Now.Day, - DateTime.Now.Hour, - DateTime.Now.Minute, - DateTime.Now.Second).ToUniversalTime(); - - var execution = new RemoteAppExecution - { - CorrelationId = Guid.NewGuid().ToString(), - ExportTaskId = "ExportTaskId", - WorkflowInstanceId = "WorkflowInstanceId", - OutgoingUid = outgoingUid, - StudyUid = Guid.NewGuid().ToString(), - RequestTime = date, - OriginalValues = new Dictionary { - { DicomTag.StudyInstanceUID.ToString(), "StudyInstanceUID" }, - { DicomTag.SeriesInstanceUID.ToString(), "SeriesInstanceUID" } - }, - ProxyValues = new Dictionary { - { DicomTag.StudyInstanceUID.ToString(), "StudyInstanceUID" }, - { DicomTag.SeriesInstanceUID.ToString(), "SeriesInstanceUID" } - } - }; - - - var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options); - await store.AddAsync(execution).ConfigureAwait(false); - - var actual = await store.GetAsync(execution.OutgoingUid).ConfigureAwait(false); - - Task.Delay(1000).Wait(); - Assert.NotNull(actual); - Assert.Equal(execution.CorrelationId, actual!.CorrelationId); - Assert.Equal(execution.ExportTaskId, actual!.ExportTaskId); - Assert.Equal(execution.WorkflowInstanceId, actual!.WorkflowInstanceId); - Assert.Equal(execution.OutgoingUid, actual!.OutgoingUid); - Assert.Equal(execution.CorrelationId, actual!.CorrelationId); - Assert.Equal(execution.WorkflowInstanceId, actual!.WorkflowInstanceId); - Assert.Equal(execution.StudyUid, actual!.StudyUid); - Assert.Equal(execution.RequestTime, actual!.RequestTime); - Assert.Equal(execution.OriginalValues, actual!.OriginalValues); - Assert.Equal(execution.ProxyValues, actual!.ProxyValues); - Assert.Equal(2, actual!.OriginalValues.Count); - - await store.RemoveAsync(execution.OutgoingUid).ConfigureAwait(false); - - actual = await _databaseFixture.DatabaseContext.Set().FirstOrDefaultAsync(p => p.OutgoingUid == execution.OutgoingUid).ConfigureAwait(false); - Assert.Null(actual); - } - } -} diff --git a/src/Database/EntityFramework/Test/SqliteDatabaseFixture.cs b/src/Database/EntityFramework/Test/SqliteDatabaseFixture.cs index 183cfb59b..7dd549b73 100644 --- a/src/Database/EntityFramework/Test/SqliteDatabaseFixture.cs +++ b/src/Database/EntityFramework/Test/SqliteDatabaseFixture.cs @@ -77,6 +77,7 @@ public void InitDatabaseWithMonaiApplicationEntities() DatabaseContext.SaveChanges(); } + public void InitDatabaseWithVirtualApplicationEntities() { var aet1 = new VirtualApplicationEntity { VirtualAeTitle = "AET1", Name = "AET1" }; diff --git a/src/Database/EntityFramework/Test/StorageMetadataWrapperRepositoryTest.cs b/src/Database/EntityFramework/Test/StorageMetadataWrapperRepositoryTest.cs index 61241f224..4cc712168 100644 --- a/src/Database/EntityFramework/Test/StorageMetadataWrapperRepositoryTest.cs +++ b/src/Database/EntityFramework/Test/StorageMetadataWrapperRepositoryTest.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. @@ -163,7 +163,7 @@ public async Task GivenACorrelationId_WhenGetFileStorageMetdadataIsCalled_Expect } [Fact] - public async Task GivenACorrelationIdAndAnIdentity_WhenGetFileStorageMetdadataIsCalled_ExpectMatchingFileStorageMetadataToBeReturned() + public async Task GivenACorrelationIdAndAnIdentity_WhenGetFileStorageMetadadataIsCalled_ExpectMatchingFileStorageMetadataToBeReturned() { var correlationId = Guid.NewGuid().ToString(); var identifier = Guid.NewGuid().ToString(); diff --git a/src/Database/EntityFramework/Test/VirtualApplicationEntityRepositoryTest.cs b/src/Database/EntityFramework/Test/VirtualApplicationEntityRepositoryTest.cs index 3a51384be..fa677b5a4 100644 --- a/src/Database/EntityFramework/Test/VirtualApplicationEntityRepositoryTest.cs +++ b/src/Database/EntityFramework/Test/VirtualApplicationEntityRepositoryTest.cs @@ -67,7 +67,7 @@ public async Task GivenAVirtualApplicationEntity_WhenAddingToDatabase_ExpectItTo VirtualAeTitle = "AET", Name = "AET", Workflows = new List { "W1", "W2" }, - PluginAssemblies = new List { "AssemblyA", "AssemblyB", "AssemblyC" }, + PlugInAssemblies = new List { "AssemblyA", "AssemblyB", "AssemblyC" }, }; var store = new VirtualApplicationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options); @@ -78,7 +78,7 @@ public async Task GivenAVirtualApplicationEntity_WhenAddingToDatabase_ExpectItTo Assert.Equal(aet.VirtualAeTitle, actual!.VirtualAeTitle); Assert.Equal(aet.Name, actual!.Name); Assert.Equal(aet.Workflows, actual!.Workflows); - Assert.Equal(aet.PluginAssemblies, actual!.PluginAssemblies); + Assert.Equal(aet.PlugInAssemblies, actual!.PlugInAssemblies); } [Fact] diff --git a/src/Database/MongoDB/Integration.Test/DestinationApplicationEntityRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/DestinationApplicationEntityRepositoryTest.cs index 4ac0b0c09..5d4756844 100644 --- a/src/Database/MongoDB/Integration.Test/DestinationApplicationEntityRepositoryTest.cs +++ b/src/Database/MongoDB/Integration.Test/DestinationApplicationEntityRepositoryTest.cs @@ -15,7 +15,6 @@ */ using FluentAssertions; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/src/Database/MongoDB/Integration.Test/DicomAssociationInfoRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/DicomAssociationInfoRepositoryTest.cs index d52b1c161..d668abd17 100644 --- a/src/Database/MongoDB/Integration.Test/DicomAssociationInfoRepositoryTest.cs +++ b/src/Database/MongoDB/Integration.Test/DicomAssociationInfoRepositoryTest.cs @@ -15,7 +15,6 @@ */ using FluentAssertions; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/src/Database/MongoDB/Integration.Test/InferenceRequestRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/InferenceRequestRepositoryTest.cs index bb561992f..ae9c47dfe 100644 --- a/src/Database/MongoDB/Integration.Test/InferenceRequestRepositoryTest.cs +++ b/src/Database/MongoDB/Integration.Test/InferenceRequestRepositoryTest.cs @@ -14,7 +14,6 @@ * limitations under the License. */ -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/src/Database/MongoDB/Integration.Test/MonaiApplicationEntityRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/MonaiApplicationEntityRepositoryTest.cs index 7da5804ac..5a6d0776b 100644 --- a/src/Database/MongoDB/Integration.Test/MonaiApplicationEntityRepositoryTest.cs +++ b/src/Database/MongoDB/Integration.Test/MonaiApplicationEntityRepositoryTest.cs @@ -15,7 +15,6 @@ */ using FluentAssertions; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/src/Database/MongoDB/Integration.Test/MongoDatabaseFixture.cs b/src/Database/MongoDB/Integration.Test/MongoDatabaseFixture.cs index 64f015bb5..e55753a8c 100755 --- a/src/Database/MongoDB/Integration.Test/MongoDatabaseFixture.cs +++ b/src/Database/MongoDB/Integration.Test/MongoDatabaseFixture.cs @@ -17,8 +17,8 @@ using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Api; using Monai.Deploy.InformaticsGateway.Api.Rest; +using Monai.Deploy.InformaticsGateway.Database.Api; using Monai.Deploy.InformaticsGateway.Database.MongoDB; -using Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations; using MongoDB.Driver; namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test @@ -35,12 +35,12 @@ public class MongoDatabaseFixture { public IMongoClient Client { get; set; } public IMongoDatabase Database { get; set; } - public IOptions Options { get; set; } + public IOptions Options { get; set; } public MongoDatabaseFixture() { Client = new MongoClient("mongodb://root:rootpassword@localhost:27017"); - Options = Microsoft.Extensions.Options.Options.Create(new MongoDBOptions { DatabaseName = $"IGTest" }); + Options = Microsoft.Extensions.Options.Options.Create(new DatabaseOptions { DatabaseName = $"IGTest" }); Database = Client.GetDatabase(Options.Value.DatabaseName); var migration = new MongoDatabaseMigrationManager(); diff --git a/src/Database/MongoDB/Integration.Test/PayloadRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/PayloadRepositoryTest.cs index b14fa796e..164e624fe 100644 --- a/src/Database/MongoDB/Integration.Test/PayloadRepositoryTest.cs +++ b/src/Database/MongoDB/Integration.Test/PayloadRepositoryTest.cs @@ -15,7 +15,6 @@ */ using FluentAssertions; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/src/Database/MongoDB/Integration.Test/RemoteAppRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/RemoteAppRepositoryTest.cs deleted file mode 100755 index 828aca142..000000000 --- a/src/Database/MongoDB/Integration.Test/RemoteAppRepositoryTest.cs +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2022 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 Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.VisualBasic; -using Monai.Deploy.InformaticsGateway.Api; -using Monai.Deploy.InformaticsGateway.Configuration; -using Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test; -using Monai.Deploy.InformaticsGateway.Database.MongoDB.Repositories; -using MongoDB.Driver; -using Moq; -namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Integration.Test -{ - [Collection("MongoDatabase")] - public class RemoteAppRepositoryTest - { - private readonly MongoDatabaseFixture _databaseFixture; - - private readonly Mock _serviceScopeFactory; - private readonly Mock> _logger; - private readonly IOptions _options; - - private readonly Mock _serviceScope; - private readonly IServiceProvider _serviceProvider; - - public RemoteAppRepositoryTest(MongoDatabaseFixture databaseFixture) - { - - _databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture)); - - _serviceScopeFactory = new Mock(); - _logger = new Mock>(); - _options = Options.Create(new InformaticsGatewayConfiguration()); - - _serviceScope = new Mock(); - var services = new ServiceCollection(); - services.AddScoped(p => _logger.Object); - services.AddScoped(p => databaseFixture.Client); - - _serviceProvider = services.BuildServiceProvider(); - _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); - _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); - - _options.Value.Database.Retries.DelaysMilliseconds = new[] { 1, 1, 1 }; - _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); - } - - [Fact] - public async Task GivenAexecution_WhenAddingToDatabase_ExpectItToBeSaved() - { - var outgoingUid = Guid.NewGuid().ToString(); - var dataset = new DicomDataset(); - var date = new DateTime( - DateTime.Now.Year, - DateTime.Now.Month, - DateTime.Now.Day, - DateTime.Now.Hour, - DateTime.Now.Minute, - DateTime.Now.Second).ToUniversalTime(); - - var execution = new RemoteAppExecution - { - CorrelationId = Guid.NewGuid().ToString(), - ExportTaskId = "ExportTaskId", - WorkflowInstanceId = "WorkflowInstanceId", - OutgoingUid = outgoingUid, - StudyUid = Guid.NewGuid().ToString(), - RequestTime = date, - OriginalValues = new Dictionary { - { DicomTag.StudyInstanceUID.ToString(), "StudyInstanceUID" }, - { DicomTag.SeriesInstanceUID.ToString(), "SeriesInstanceUID" } - }, - ProxyValues = new Dictionary { - { DicomTag.StudyInstanceUID.ToString(), "StudyInstanceUID" }, - { DicomTag.SeriesInstanceUID.ToString(), "SeriesInstanceUID" } - } - }; - - - var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); - await store.AddAsync(execution).ConfigureAwait(false); - - var collection = _databaseFixture.Database.GetCollection(nameof(RemoteAppExecution)); - - var actual = await store.GetAsync(execution.OutgoingUid).ConfigureAwait(false); - - Task.Delay(1000).Wait(); - Assert.NotNull(actual); - Assert.Equal(execution.CorrelationId, actual!.CorrelationId); - Assert.Equal(execution.ExportTaskId, actual!.ExportTaskId); - Assert.Equal(execution.WorkflowInstanceId, actual!.WorkflowInstanceId); - Assert.Equal(execution.OutgoingUid, actual!.OutgoingUid); - Assert.Equal(execution.CorrelationId, actual!.CorrelationId); - Assert.Equal(execution.WorkflowInstanceId, actual!.WorkflowInstanceId); - Assert.Equal(execution.StudyUid, actual!.StudyUid); - Assert.Equal(execution.RequestTime, actual!.RequestTime); - Assert.Equal(execution.OriginalValues, actual!.OriginalValues); - Assert.Equal(execution.ProxyValues, actual!.ProxyValues); - Assert.Equal(2, actual!.OriginalValues.Count); - - await store.RemoveAsync(execution.OutgoingUid).ConfigureAwait(false); - - actual = await collection.Find(p => p.OutgoingUid == execution.OutgoingUid).FirstOrDefaultAsync().ConfigureAwait(false); - Assert.Null(actual); - } - } -} diff --git a/src/Database/MongoDB/Integration.Test/SourceApplicationEntityRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/SourceApplicationEntityRepositoryTest.cs index 83ec01f0f..9529fd6b1 100644 --- a/src/Database/MongoDB/Integration.Test/SourceApplicationEntityRepositoryTest.cs +++ b/src/Database/MongoDB/Integration.Test/SourceApplicationEntityRepositoryTest.cs @@ -15,7 +15,6 @@ */ using FluentAssertions; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -173,7 +172,5 @@ public async Task GivenASourceApplicationEntity_WhenUpdatedIsCalled_ExpectItToSa Assert.NotNull(dbResult); Assert.Equal(expected.AeTitle, dbResult!.AeTitle); } - - } } diff --git a/src/Database/MongoDB/Integration.Test/StorageMetadataWrapperRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/StorageMetadataWrapperRepositoryTest.cs index f0e460297..053e06738 100644 --- a/src/Database/MongoDB/Integration.Test/StorageMetadataWrapperRepositoryTest.cs +++ b/src/Database/MongoDB/Integration.Test/StorageMetadataWrapperRepositoryTest.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. @@ -15,7 +15,6 @@ */ using System.Text.Json; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -170,7 +169,7 @@ public async Task GivenACorrelationId_WhenGetFileStorageMetdadataIsCalled_Expect } [Fact] - public async Task GivenACorrelationIdAndAnIdentity_WhenGetFileStorageMetdadataIsCalled_ExpectMatchingFileStorageMetadataToBeReturned() + public async Task GivenACorrelationIdAndAnIdentity_WhenGetFileStorageMetadadataIsCalled_ExpectMatchingFileStorageMetadataToBeReturned() { var correlationId = Guid.NewGuid().ToString(); var identifier = Guid.NewGuid().ToString(); diff --git a/src/Database/MongoDB/Integration.Test/VirtualApplicationEntityRepositoryTest.cs b/src/Database/MongoDB/Integration.Test/VirtualApplicationEntityRepositoryTest.cs index 628ac0fcf..c526884b8 100644 --- a/src/Database/MongoDB/Integration.Test/VirtualApplicationEntityRepositoryTest.cs +++ b/src/Database/MongoDB/Integration.Test/VirtualApplicationEntityRepositoryTest.cs @@ -15,7 +15,6 @@ */ using FluentAssertions; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -70,7 +69,7 @@ public async Task GivenAVirtualApplicationEntity_WhenAddingToDatabase_ExpectItTo VirtualAeTitle = "AET", Name = "AET", Workflows = new List { "W1", "W2" }, - PluginAssemblies = new List { "AssemblyA", "AssemblyB", "AssemblyC" }, + PlugInAssemblies = new List { "AssemblyA", "AssemblyB", "AssemblyC" }, }; var store = new VirtualApplicationEntityRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); @@ -83,7 +82,7 @@ public async Task GivenAVirtualApplicationEntity_WhenAddingToDatabase_ExpectItTo Assert.Equal(aet.VirtualAeTitle, actual!.VirtualAeTitle); Assert.Equal(aet.Name, actual!.Name); Assert.Equal(aet.Workflows, actual!.Workflows); - Assert.Equal(aet.PluginAssemblies, actual!.PluginAssemblies); + Assert.Equal(aet.PlugInAssemblies, actual!.PlugInAssemblies); } [Fact] @@ -100,6 +99,7 @@ public async Task GivenAExpressionFilter_WhenContainsAsyncIsCalled_ExpectItToRet result = await store.ContainsAsync(p => p.Name == "AET6").ConfigureAwait(false); Assert.False(result); } + [Fact] public async Task GivenAAETitleName_WhenFindByNameAsyncIsCalled_ExpectItToReturnMatchingEntity() { diff --git a/src/Database/MongoDB/Repositories/DestinationApplicationEntityRepository.cs b/src/Database/MongoDB/Repositories/DestinationApplicationEntityRepository.cs index 0fd5f8fac..97a554dd0 100755 --- a/src/Database/MongoDB/Repositories/DestinationApplicationEntityRepository.cs +++ b/src/Database/MongoDB/Repositories/DestinationApplicationEntityRepository.cs @@ -24,7 +24,6 @@ using Monai.Deploy.InformaticsGateway.Database.Api; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; -using Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations; using MongoDB.Driver; using Polly; using Polly.Retry; @@ -43,7 +42,7 @@ public DestinationApplicationEntityRepository( IServiceScopeFactory serviceScopeFactory, ILogger logger, IOptions options, - IOptions mongoDbOptions) + IOptions mongoDbOptions) { Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); Guard.Against.Null(options, nameof(options)); diff --git a/src/Database/MongoDB/Repositories/DicomAssociationInfoRepository.cs b/src/Database/MongoDB/Repositories/DicomAssociationInfoRepository.cs index 87b4e82eb..bc376631b 100755 --- a/src/Database/MongoDB/Repositories/DicomAssociationInfoRepository.cs +++ b/src/Database/MongoDB/Repositories/DicomAssociationInfoRepository.cs @@ -20,9 +20,9 @@ using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Api; using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; -using Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations; using MongoDB.Driver; using Polly; using Polly.Retry; @@ -41,7 +41,7 @@ public DicomAssociationInfoRepository( IServiceScopeFactory serviceScopeFactory, ILogger logger, IOptions options, - IOptions mongoDbOptions) + IOptions mongoDbOptions) { Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); Guard.Against.Null(options, nameof(options)); diff --git a/src/Database/MongoDB/Repositories/InferenceRequestRepository.cs b/src/Database/MongoDB/Repositories/InferenceRequestRepository.cs index 7deb0a3e0..d8eff03a9 100755 --- a/src/Database/MongoDB/Repositories/InferenceRequestRepository.cs +++ b/src/Database/MongoDB/Repositories/InferenceRequestRepository.cs @@ -16,16 +16,15 @@ */ using Ardalis.GuardClauses; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Api; using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; -using Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations; using MongoDB.Driver; using Polly; using Polly.Retry; @@ -47,7 +46,7 @@ public InferenceRequestRepository( IServiceScopeFactory serviceScopeFactory, ILogger logger, IOptions options, - IOptions mongoDbOptions) : base(logger, options) + IOptions mongoDbOptions) : base(logger, options) { Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); diff --git a/src/Database/MongoDB/Repositories/MonaiApplicationEntityRepository.cs b/src/Database/MongoDB/Repositories/MonaiApplicationEntityRepository.cs index 7a9f2996f..d8395992d 100755 --- a/src/Database/MongoDB/Repositories/MonaiApplicationEntityRepository.cs +++ b/src/Database/MongoDB/Repositories/MonaiApplicationEntityRepository.cs @@ -24,7 +24,6 @@ using Monai.Deploy.InformaticsGateway.Database.Api; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; -using Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations; using MongoDB.Driver; using Polly; using Polly.Retry; @@ -43,7 +42,7 @@ public MonaiApplicationEntityRepository( IServiceScopeFactory serviceScopeFactory, ILogger logger, IOptions options, - IOptions mongoDbOptions) + IOptions mongoDbOptions) { Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); Guard.Against.Null(options, nameof(options)); diff --git a/src/Database/MongoDB/Repositories/PayloadRepository.cs b/src/Database/MongoDB/Repositories/PayloadRepository.cs index 0a67a585b..e675c3bc2 100755 --- a/src/Database/MongoDB/Repositories/PayloadRepository.cs +++ b/src/Database/MongoDB/Repositories/PayloadRepository.cs @@ -15,7 +15,6 @@ */ using Ardalis.GuardClauses; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -24,7 +23,6 @@ using Monai.Deploy.InformaticsGateway.Database.Api; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; -using Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations; using MongoDB.Driver; using Polly; using Polly.Retry; @@ -43,7 +41,7 @@ public PayloadRepository( IServiceScopeFactory serviceScopeFactory, ILogger logger, IOptions options, - IOptions mongoDbOptions) + IOptions mongoDbOptions) { Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); Guard.Against.Null(options, nameof(options)); diff --git a/src/Database/MongoDB/Repositories/RemoteAppExecutionRepository.cs b/src/Database/MongoDB/Repositories/RemoteAppExecutionRepository.cs deleted file mode 100755 index 16c921de2..000000000 --- a/src/Database/MongoDB/Repositories/RemoteAppExecutionRepository.cs +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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. - * 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 Ardalis.GuardClauses; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; -using Monai.Deploy.InformaticsGateway.Configuration; -using Monai.Deploy.InformaticsGateway.Database.Api.Logging; -using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; -using Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations; -using MongoDB.Driver; -using Polly; -using Polly.Retry; - -namespace Monai.Deploy.InformaticsGateway.Database.MongoDB.Repositories -{ - public class RemoteAppExecutionRepository : IRemoteAppExecutionRepository, IDisposable - { - private readonly ILogger _logger; - private readonly IServiceScope _scope; - private readonly AsyncRetryPolicy _retryPolicy; - private readonly IMongoCollection _collection; - private bool _disposedValue; - - public RemoteAppExecutionRepository(IServiceScopeFactory serviceScopeFactory, - ILogger logger, - IOptions options, - IOptions mongoDbOptions) - { - Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); - Guard.Against.Null(options, nameof(options)); - Guard.Against.Null(mongoDbOptions, nameof(mongoDbOptions)); - - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - _scope = serviceScopeFactory.CreateScope(); - _retryPolicy = Policy.Handle().WaitAndRetryAsync( - options.Value.Database.Retries.RetryDelays, - (exception, timespan, count, context) => _logger.DatabaseErrorRetry(timespan, count, exception)); - - var mongoDbClient = _scope.ServiceProvider.GetRequiredService(); - var mongoDatabase = mongoDbClient.GetDatabase(mongoDbOptions.Value.DatabaseName); - _collection = mongoDatabase.GetCollection(nameof(RemoteAppExecution)); - CreateIndexes(); - } - - private void CreateIndexes() - { - var options = new CreateIndexOptions { Unique = true }; - var indexDefinitionState = Builders.IndexKeys.Ascending(_ => _.OutgoingUid); - var indexModel = new CreateIndexModel(indexDefinitionState, options); - - _collection.Indexes.CreateOne(indexModel); - - options = new CreateIndexOptions { ExpireAfter = TimeSpan.FromDays(7), Name = "RequestTime" }; - indexDefinitionState = Builders.IndexKeys.Ascending(_ => _.RequestTime); - indexModel = new CreateIndexModel(indexDefinitionState, options); - - _collection.Indexes.CreateOne(indexModel); - } - - public async Task AddAsync(RemoteAppExecution item, CancellationToken cancellationToken = default) - { - Guard.Against.Null(item, nameof(item)); - - return await _retryPolicy.ExecuteAsync(async () => - { - await _collection.InsertOneAsync(item, cancellationToken: cancellationToken).ConfigureAwait(false); - return true; - }).ConfigureAwait(false); - } - - public async Task RemoveAsync(string OutgoingStudyUid, CancellationToken cancellationToken = default) - { - return await _retryPolicy.ExecuteAsync(async () => - { - var results = await _collection.DeleteManyAsync(Builders.Filter.Where(p => p.OutgoingUid == OutgoingStudyUid), cancellationToken).ConfigureAwait(false); - return Convert.ToInt32(results.DeletedCount); - }).ConfigureAwait(false); - } - - public async Task GetAsync(string OutgoingStudyUid, CancellationToken cancellationToken = default) - { - return await _retryPolicy.ExecuteAsync(async () => - { - return await _collection.Find(p => p.OutgoingUid == OutgoingStudyUid).FirstOrDefaultAsync().ConfigureAwait(false); - }).ConfigureAwait(false); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - _scope.Dispose(); - } - - _disposedValue = true; - } - } - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - } -} diff --git a/src/Database/MongoDB/Repositories/SourceApplicationEntityRepository.cs b/src/Database/MongoDB/Repositories/SourceApplicationEntityRepository.cs index 9f747928f..5df8c678b 100755 --- a/src/Database/MongoDB/Repositories/SourceApplicationEntityRepository.cs +++ b/src/Database/MongoDB/Repositories/SourceApplicationEntityRepository.cs @@ -24,7 +24,6 @@ using Monai.Deploy.InformaticsGateway.Database.Api; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; -using Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations; using MongoDB.Driver; using Polly; using Polly.Retry; @@ -43,7 +42,7 @@ public SourceApplicationEntityRepository( IServiceScopeFactory serviceScopeFactory, ILogger logger, IOptions options, - IOptions mongoDbOptions) + IOptions mongoDbOptions) { Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); Guard.Against.Null(options, nameof(options)); diff --git a/src/Database/MongoDB/Repositories/StorageMetadataWrapperRepository.cs b/src/Database/MongoDB/Repositories/StorageMetadataWrapperRepository.cs index 48427a9f6..bdcc9a504 100755 --- a/src/Database/MongoDB/Repositories/StorageMetadataWrapperRepository.cs +++ b/src/Database/MongoDB/Repositories/StorageMetadataWrapperRepository.cs @@ -17,7 +17,6 @@ using System.Data; using Ardalis.GuardClauses; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -26,7 +25,6 @@ using Monai.Deploy.InformaticsGateway.Database.Api; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; -using Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations; using MongoDB.Driver; using Polly; using Polly.Retry; @@ -45,7 +43,7 @@ public StorageMetadataWrapperRepository( IServiceScopeFactory serviceScopeFactory, ILogger logger, IOptions options, - IOptions mongoDbOptions) : base(logger) + IOptions mongoDbOptions) : base(logger) { Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); Guard.Against.Null(options, nameof(options)); diff --git a/src/Database/MongoDB/Repositories/VirtualApplicationEntityRepository.cs b/src/Database/MongoDB/Repositories/VirtualApplicationEntityRepository.cs index 462803fd0..d895b016a 100644 --- a/src/Database/MongoDB/Repositories/VirtualApplicationEntityRepository.cs +++ b/src/Database/MongoDB/Repositories/VirtualApplicationEntityRepository.cs @@ -24,7 +24,6 @@ using Monai.Deploy.InformaticsGateway.Database.Api; using Monai.Deploy.InformaticsGateway.Database.Api.Logging; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; -using Monai.Deploy.InformaticsGateway.Database.MongoDB.Configurations; using MongoDB.Driver; using Polly; using Polly.Retry; @@ -43,7 +42,7 @@ public VirtualApplicationEntityRepository( IServiceScopeFactory serviceScopeFactory, ILogger logger, IOptions options, - IOptions mongoDbOptions) + IOptions mongoDbOptions) { Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); Guard.Against.Null(options, nameof(options)); diff --git a/src/InformaticsGateway/Common/DicomToolkit.cs b/src/InformaticsGateway/Common/DicomToolkit.cs index 6f861731b..1ba2a872f 100644 --- a/src/InformaticsGateway/Common/DicomToolkit.cs +++ b/src/InformaticsGateway/Common/DicomToolkit.cs @@ -19,7 +19,6 @@ using System.Threading.Tasks; using Ardalis.GuardClauses; using FellowOakDicom; -using SharpCompress.Compressors.Xz; namespace Monai.Deploy.InformaticsGateway.Common { diff --git a/src/InformaticsGateway/Common/FileStorageMetadataExtensions.cs b/src/InformaticsGateway/Common/FileStorageMetadataExtensions.cs index c610680fe..a3f7518a5 100644 --- a/src/InformaticsGateway/Common/FileStorageMetadataExtensions.cs +++ b/src/InformaticsGateway/Common/FileStorageMetadataExtensions.cs @@ -19,7 +19,6 @@ using System.Threading.Tasks; using Ardalis.GuardClauses; using FellowOakDicom; -using FellowOakDicom.Serialization; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Configuration; diff --git a/src/InformaticsGateway/Common/IDicomToolkit.cs b/src/InformaticsGateway/Common/IDicomToolkit.cs index 469ac4734..dc3d797d8 100755 --- a/src/InformaticsGateway/Common/IDicomToolkit.cs +++ b/src/InformaticsGateway/Common/IDicomToolkit.cs @@ -15,10 +15,7 @@ * limitations under the License. */ -using System; using System.IO; -using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; using FellowOakDicom; @@ -31,50 +28,5 @@ public interface IDicomToolkit DicomFile Load(byte[] fileContent); StudySerieSopUids GetStudySeriesSopInstanceUids(DicomFile dicomFile); - - static DicomTag GetDicomTagByName(string tag) => DicomDictionary.Default[tag] ?? DicomDictionary.Default[Regex.Replace(tag, @"\s+", "", RegexOptions.None, TimeSpan.FromSeconds(1))]; - - static DicomTag[] GetTagArrayFromStringArray(string values) - { - var names = values.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - return names.Select(n => IDicomToolkit.GetDicomTagByName(n)).ToArray(); - } - - static T GetTagProxyValue(DicomTag tag) where T : class - { - // partial implementation for now see - // https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html - // for full list - var output = ""; - switch (tag.DictionaryEntry.ValueRepresentations[0].Code) - { - case "UI": - case "LO": - case "LT": - { - output = DicomUIDGenerator.GenerateDerivedFromUUID().UID; - return output as T; - } - case "SH": - case "AE": - case "CS": - case "PN": - case "ST": - case "UT": - { - output = "no Value"; - break; - } - } - return output as T; - } - - static (ushort group, ushort element) ParseDicomTagStringToTwoShorts(string input) - { - var trim = input.Substring(1, input.Length - 2); - var array = trim.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - return (Convert.ToUInt16(array[0], 16), Convert.ToUInt16(array[1], 16)); - } - } } diff --git a/src/InformaticsGateway/Common/PlugingLoadingException.cs b/src/InformaticsGateway/Common/PlugInLoadingException.cs similarity index 75% rename from src/InformaticsGateway/Common/PlugingLoadingException.cs rename to src/InformaticsGateway/Common/PlugInLoadingException.cs index dea15d6c1..6deac7d93 100644 --- a/src/InformaticsGateway/Common/PlugingLoadingException.cs +++ b/src/InformaticsGateway/Common/PlugInLoadingException.cs @@ -18,13 +18,13 @@ namespace Monai.Deploy.InformaticsGateway.Common { - public class PlugingLoadingException : Exception + public class PlugInLoadingException : Exception { - public PlugingLoadingException(string message) : base(message) + public PlugInLoadingException(string message) : base(message) { } - public PlugingLoadingException(string message, Exception innerException) : base(message, innerException) + public PlugInLoadingException(string message, Exception innerException) : base(message, innerException) { } } diff --git a/src/InformaticsGateway/Common/TypeExtensions.cs b/src/InformaticsGateway/Common/TypeExtensions.cs index 15fa9a17d..04821bb14 100644 --- a/src/InformaticsGateway/Common/TypeExtensions.cs +++ b/src/InformaticsGateway/Common/TypeExtensions.cs @@ -20,6 +20,7 @@ using System.Reflection; using Ardalis.GuardClauses; using Microsoft.Extensions.DependencyInjection; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; namespace Monai.Deploy.InformaticsGateway.Common { diff --git a/src/InformaticsGateway/ExecutionPlugins/ExternalAppOutgoing.cs b/src/InformaticsGateway/ExecutionPlugins/ExternalAppOutgoing.cs deleted file mode 100755 index f0e1f59e9..000000000 --- a/src/InformaticsGateway/ExecutionPlugins/ExternalAppOutgoing.cs +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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. - * 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.Reflection; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Extensions.DependencyInjection; -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.Api.Repositories; -using Monai.Deploy.InformaticsGateway.Logging; - -namespace Monai.Deploy.InformaticsGateway.ExecutionPlugins -{ - [PluginName("Remote App Execution Outgoing")] - public class ExternalAppOutgoing : IOutputDataPlugin - { - private readonly ILogger _logger; - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly PluginConfiguration _options; - - public string Name => GetType().GetCustomAttribute()?.Name ?? GetType().Name; - - public ExternalAppOutgoing( - ILogger logger, - IServiceScopeFactory serviceScopeFactory, - IOptions configuration) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); - _options = configuration.Value ?? throw new ArgumentNullException(nameof(configuration)); - if (_options.RemoteAppConfigurations.ContainsKey("ReplaceTags") is false) { throw new ArgumentNullException(nameof(configuration)); } - } - - public async Task<(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage)> Execute(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage) - { - var tags = IDicomToolkit.GetTagArrayFromStringArray(_options.RemoteAppConfigurations["ReplaceTags"]); - var outgoingUid = dicomFile.Dataset.GetString(tags[0]); - - var scope = _serviceScopeFactory.CreateScope(); - var repository = scope.ServiceProvider.GetRequiredService(); - - var existing = await repository.GetAsync(outgoingUid).ConfigureAwait(false); - - if (existing is not null) - { - PopulateWithStoredProxyValues(dicomFile, tags, existing); - } - else - { - var remoteAppExecution = await PopulateWithNewProxyValues(dicomFile, exportRequestDataMessage, tags).ConfigureAwait(false); - await repository.AddAsync(remoteAppExecution).ConfigureAwait(false); - } - return (dicomFile, exportRequestDataMessage); - } - - private async Task PopulateWithNewProxyValues(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage, DicomTag[] tags) - { - var remoteAppExecution = await GetRemoteAppExecution(exportRequestDataMessage).ConfigureAwait(false); - remoteAppExecution.StudyUid = dicomFile.Dataset.GetString(DicomTag.StudyInstanceUID); - - foreach (var tag in tags) - { - if (dicomFile.Dataset.TryGetString(tag, out var value)) - { - remoteAppExecution.OriginalValues.Add(tag.ToString(), value); - var newValue = IDicomToolkit.GetTagProxyValue(tag); - dicomFile.Dataset.AddOrUpdate(tag, newValue); - remoteAppExecution.ProxyValues.Add(tag.ToString(), newValue); - } - } - - remoteAppExecution.OutgoingUid = dicomFile.Dataset.GetString(tags[0]); - _logger.LogStudyUidChanged(remoteAppExecution.StudyUid, remoteAppExecution.OutgoingUid); - return remoteAppExecution; - } - - private static void PopulateWithStoredProxyValues(DicomFile dicomFile, DicomTag[] tags, RemoteAppExecution existing) - { - foreach (var tag in tags) - { - if (dicomFile.Dataset.TryGetString(tag, out _)) - { - dicomFile.Dataset.AddOrUpdate(tag, existing.ProxyValues[tag.ToString()]); - } - } - } - - private async Task GetRemoteAppExecution(ExportRequestDataMessage request) - { - var remoteAppExecution = new RemoteAppExecution - { - CorrelationId = request.CorrelationId, - WorkflowInstanceId = request.WorkflowInstanceId, - ExportTaskId = request.ExportTaskId, - Files = new System.Collections.Generic.List { request.Filename }, - }; - - - var outgoingStudyUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; - remoteAppExecution.OutgoingUid = outgoingStudyUid; - - foreach (var destination in request.Destinations) - { - remoteAppExecution.ExportDetails.Add(await LookupDestinationAsync(destination, new CancellationToken()).ConfigureAwait(false)); - } - - return remoteAppExecution; - } - - private async Task LookupDestinationAsync(string destinationName, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(destinationName)) - { - throw new ConfigurationException("Export task does not have destination set."); - } - - using var scope = _serviceScopeFactory.CreateScope(); - var repository = scope.ServiceProvider.GetRequiredService(); - var destination = await repository.FindByNameAsync(destinationName, cancellationToken).ConfigureAwait(false); - - return destination ?? throw new ConfigurationException($"Specified destination '{destinationName}' does not exist."); - } - } -} diff --git a/src/InformaticsGateway/Logging/Log.5000.DataPlugins.cs b/src/InformaticsGateway/Logging/Log.5000.DataPlugins.cs index 5b96b2f27..2e2425b63 100644 --- a/src/InformaticsGateway/Logging/Log.5000.DataPlugins.cs +++ b/src/InformaticsGateway/Logging/Log.5000.DataPlugins.cs @@ -24,24 +24,18 @@ public static partial class Log public static partial void LoadingAssembly(this ILogger logger, string filename); [LoggerMessage(EventId = 5001, Level = LogLevel.Information, Message = "{type} data plug-in found {name}: {plugin}.")] - public static partial void DataPluginFound(this ILogger logger, string type, string name, string plugin); + public static partial void DataPlugInFound(this ILogger logger, string type, string name, string plugin); [LoggerMessage(EventId = 5002, Level = LogLevel.Debug, Message = "Adding input data plug-in: {plugin}.")] - public static partial void AddingInputDataPlugin(this ILogger logger, string plugin); + public static partial void AddingInputDataPlugIn(this ILogger logger, string plugin); [LoggerMessage(EventId = 5003, Level = LogLevel.Information, Message = "Executing input data plug-in: {plugin}.")] - public static partial void ExecutingInputDataPlugin(this ILogger logger, string plugin); + public static partial void ExecutingInputDataPlugIn(this ILogger logger, string plugin); [LoggerMessage(EventId = 5004, Level = LogLevel.Debug, Message = "Adding output data plug-in: {plugin}.")] - public static partial void AddingOutputDataPlugin(this ILogger logger, string plugin); + public static partial void AddingOutputDataPlugIn(this ILogger logger, string plugin); [LoggerMessage(EventId = 5005, Level = LogLevel.Information, Message = "Executing output data plug-in: {plugin}.")] - public static partial void ExecutingOutputDataPlugin(this ILogger logger, string plugin); - [LoggerMessage(EventId = 5006, Level = LogLevel.Debug, Message = "Changed the StudyUid from {OriginalStudyUid} to {NewStudyUid}")] - public static partial void LogStudyUidChanged(this ILogger logger, string OriginalStudyUid, string NewStudyUid); - - [LoggerMessage(EventId = 5007, Level = LogLevel.Error, Message = "Cannot find entry for OriginalStudyUid {OriginalStudyUid} ")] - public static partial void LogOriginalUidNotFound(this ILogger logger, string OriginalStudyUid); - + public static partial void ExecutingOutputDataPlugIn(this ILogger logger, string plugin); } } diff --git a/src/InformaticsGateway/Logging/Log.8000.HttpServices.cs b/src/InformaticsGateway/Logging/Log.8000.HttpServices.cs index 46d19a4ab..3dd8d10bb 100644 --- a/src/InformaticsGateway/Logging/Log.8000.HttpServices.cs +++ b/src/InformaticsGateway/Logging/Log.8000.HttpServices.cs @@ -43,7 +43,7 @@ public static partial class Log public static partial void MonaiApplicationEntityUpdated(this ILogger logger, string name, string aeTitle); [LoggerMessage(EventId = 8006, Level = LogLevel.Error, Message = "Error reading data input plug-ins.")] - public static partial void ErrorReadingDataInputPlugins(this ILogger logger, Exception ex); + public static partial void ErrorReadingDataInputPlugIns(this ILogger logger, Exception ex); // Destination AE Title Controller [LoggerMessage(EventId = 8010, Level = LogLevel.Information, Message = "DICOM destination added AE Title={aeTitle}, Host/IP={hostIp}.")] @@ -100,8 +100,6 @@ public static partial class Log [LoggerMessage(EventId = 8040, Level = LogLevel.Error, Message = "Error collecting system status.")] public static partial void ErrorCollectingSystemStatus(this ILogger logger, Exception ex); - - // Virtual AE Title Controller [LoggerMessage(EventId = 8050, Level = LogLevel.Error, Message = "Error querying Virtual Application Entity.")] public static partial void ErrorListingVirtualApplicationEntities(this ILogger logger, Exception ex); diff --git a/src/InformaticsGateway/Program.cs b/src/InformaticsGateway/Program.cs index 3a4645168..7ffcd2454 100755 --- a/src/InformaticsGateway/Program.cs +++ b/src/InformaticsGateway/Program.cs @@ -25,7 +25,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database; @@ -98,7 +98,7 @@ internal static IHostBuilder CreateHostBuilder(string[] args) => services.AddOptions().Bind(hostContext.Configuration.GetSection("InformaticsGateway:messaging")); services.AddOptions().Bind(hostContext.Configuration.GetSection("InformaticsGateway:storage")); services.AddOptions().Bind(hostContext.Configuration.GetSection("MonaiDeployAuthentication")); - services.AddOptions().Bind(hostContext.Configuration.GetSection("InformaticsGateway:plugins")); + services.AddOptions().Bind(hostContext.Configuration.GetSection("InformaticsGateway:plugins")); services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigurationValidator>()); services.ConfigureDatabase(hostContext.Configuration?.GetSection("ConnectionStrings")); @@ -112,10 +112,10 @@ internal static IHostBuilder CreateHostBuilder(string[] args) => services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped, InputDataPluginEngineFactory>(); - services.AddScoped, OutputDataPluginEngineFactory>(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped, InputDataPlugInEngineFactory>(); + services.AddScoped, OutputDataPlugInEngineFactory>(); services.AddMonaiDeployStorageService(hostContext.Configuration.GetSection("InformaticsGateway:storage:serviceAssemblyName").Value, Monai.Deploy.Storage.HealthCheckOptions.ServiceHealthCheck); diff --git a/src/InformaticsGateway/Services/Common/IInputDataPluginEngineFactory.cs b/src/InformaticsGateway/Services/Common/IInputDataPluginEngineFactory.cs index 0da756fd2..5295ddf79 100644 --- a/src/InformaticsGateway/Services/Common/IInputDataPluginEngineFactory.cs +++ b/src/InformaticsGateway/Services/Common/IInputDataPluginEngineFactory.cs @@ -21,31 +21,32 @@ using System.Reflection; using Ardalis.GuardClauses; using Microsoft.Extensions.Logging; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Logging; namespace Monai.Deploy.InformaticsGateway.Services.Common { - public interface IDataPluginEngineFactory + public interface IDataPlugInEngineFactory { - IReadOnlyDictionary RegisteredPlugins(); + IReadOnlyDictionary RegisteredPlugIns(); } - public abstract class DataPluginEngineFactoryBase : IDataPluginEngineFactory + public abstract class DataPlugInEngineFactoryBase : IDataPlugInEngineFactory { + private static readonly object SyncLock = new(); private readonly IFileSystem _fileSystem; - private readonly ILogger> _logger; + private readonly ILogger> _logger; private readonly Type _type; /// /// A dictionary mapping of input data plug-ins where: - /// key: if available or name of the class. + /// key: if available or name of the class. /// value: fully qualified assembly type /// private readonly Dictionary _cachedTypeNames; - public DataPluginEngineFactoryBase(IFileSystem fileSystem, ILogger> logger) + public DataPlugInEngineFactoryBase(IFileSystem fileSystem, ILogger> logger) { _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); @@ -53,9 +54,9 @@ public DataPluginEngineFactoryBase(IFileSystem fileSystem, ILogger(); } - public IReadOnlyDictionary RegisteredPlugins() + public IReadOnlyDictionary RegisteredPlugIns() { - LoadAssembliesFromPluginDirectory(); + LoadAssembliesFromPlugInsDirectory(); var types = AppDomain.CurrentDomain.GetAssemblies() .Where(p => !p.FullName.Contains("DynamicProxyGenAssembly2")) @@ -77,40 +78,44 @@ private void AddToCache(List types) { if (!_cachedTypeNames.ContainsValue(p.GetShortTypeAssemblyName())) { - var nameAttribute = p.GetCustomAttribute(); + var nameAttribute = p.GetCustomAttribute(); var name = nameAttribute is null ? p.Name : nameAttribute.Name; _cachedTypeNames.Add(name, p.GetShortTypeAssemblyName()); - _logger.DataPluginFound(_type.Name, name, p.GetShortTypeAssemblyName()); + _logger.DataPlugInFound(_type.Name, name, p.GetShortTypeAssemblyName()); } }); } } - private void LoadAssembliesFromPluginDirectory() + private void LoadAssembliesFromPlugInsDirectory() { - var files = _fileSystem.Directory.GetFiles(SR.PlugInDirectoryPath, "*.dll", System.IO.SearchOption.TopDirectoryOnly); - - foreach (var file in files) + lock (SyncLock) { - _logger.LoadingAssembly(file); - var assembly = Assembly.LoadFile(file); - var matchingTypes = assembly.GetTypes().Where(p => _type.IsAssignableFrom(p) && p != _type).ToList(); - AddToCache(matchingTypes); + var files = _fileSystem.Directory.GetFiles(SR.PlugInDirectoryPath, "*.dll", System.IO.SearchOption.TopDirectoryOnly); + + foreach (var file in files) + { + _logger.LoadingAssembly(file); + var assembly = Assembly.LoadFile(file); + var matchingTypes = assembly.GetTypes().Where(p => _type.IsAssignableFrom(p) && p != _type).ToList(); + + AddToCache(matchingTypes); + } } } } - public class InputDataPluginEngineFactory : DataPluginEngineFactoryBase + public class InputDataPlugInEngineFactory : DataPlugInEngineFactoryBase { - public InputDataPluginEngineFactory(IFileSystem fileSystem, ILogger> logger) : base(fileSystem, logger) + public InputDataPlugInEngineFactory(IFileSystem fileSystem, ILogger> logger) : base(fileSystem, logger) { } } - public class OutputDataPluginEngineFactory : DataPluginEngineFactoryBase + public class OutputDataPlugInEngineFactory : DataPlugInEngineFactoryBase { - public OutputDataPluginEngineFactory(IFileSystem fileSystem, ILogger> logger) : base(fileSystem, logger) + public OutputDataPlugInEngineFactory(IFileSystem fileSystem, ILogger> logger) : base(fileSystem, logger) { } } diff --git a/src/InformaticsGateway/Services/Common/InputDataPluginEngine.cs b/src/InformaticsGateway/Services/Common/InputDataPluginEngine.cs index afe821818..68f302a03 100644 --- a/src/InformaticsGateway/Services/Common/InputDataPluginEngine.cs +++ b/src/InformaticsGateway/Services/Common/InputDataPluginEngine.cs @@ -20,20 +20,20 @@ using System.Threading.Tasks; using FellowOakDicom; using Microsoft.Extensions.Logging; -using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Logging; namespace Monai.Deploy.InformaticsGateway.Services.Common { - internal class InputDataPluginEngine : IInputDataPluginEngine + internal class InputDataPlugInEngine : IInputDataPlugInEngine { private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private IReadOnlyList _plugsins; + private readonly ILogger _logger; + private IReadOnlyList _plugsins; - public InputDataPluginEngine(IServiceProvider serviceProvider, ILogger logger) + public InputDataPlugInEngine(IServiceProvider serviceProvider, ILogger logger) { _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -41,39 +41,39 @@ public InputDataPluginEngine(IServiceProvider serviceProvider, ILogger pluginAssemblies) { - _plugsins = LoadPlugins(_serviceProvider, pluginAssemblies); + _plugsins = LoadPlugIns(_serviceProvider, pluginAssemblies); } - public async Task> ExecutePlugins(DicomFile dicomFile, FileStorageMetadata fileMetadata) + public async Task> ExecutePlugInsAsync(DicomFile dicomFile, FileStorageMetadata fileMetadata) { if (_plugsins == null) { - throw new ApplicationException("InputDataPluginEngine not configured, please call Configure() first."); + throw new ApplicationException("InputDataPlugInEngine not configured, please call Configure() first."); } foreach (var plugin in _plugsins) { - _logger.ExecutingInputDataPlugin(plugin.Name); - (dicomFile, fileMetadata) = await plugin.Execute(dicomFile, fileMetadata).ConfigureAwait(false); + _logger.ExecutingInputDataPlugIn(plugin.Name); + (dicomFile, fileMetadata) = await plugin.ExecuteAsync(dicomFile, fileMetadata).ConfigureAwait(false); } return new Tuple(dicomFile, fileMetadata); } - private IReadOnlyList LoadPlugins(IServiceProvider serviceProvider, IReadOnlyList pluginAssemblies) + private IReadOnlyList LoadPlugIns(IServiceProvider serviceProvider, IReadOnlyList pluginAssemblies) { var exceptions = new List(); - var list = new List(); + var list = new List(); foreach (var plugin in pluginAssemblies) { try { - _logger.AddingInputDataPlugin(plugin); - list.Add(typeof(IInputDataPlugin).CreateInstance(serviceProvider, typeString: plugin)); + _logger.AddingInputDataPlugIn(plugin); + list.Add(typeof(IInputDataPlugIn).CreateInstance(serviceProvider, typeString: plugin)); } catch (Exception ex) { - exceptions.Add(new PlugingLoadingException($"Error loading plug-in '{plugin}'.", ex)); + exceptions.Add(new PlugInLoadingException($"Error loading plug-in '{plugin}'.", ex)); } } diff --git a/src/InformaticsGateway/Services/Common/OutputDataPluginEngine.cs b/src/InformaticsGateway/Services/Common/OutputDataPluginEngine.cs index 4c40313b5..888fd97e5 100644 --- a/src/InformaticsGateway/Services/Common/OutputDataPluginEngine.cs +++ b/src/InformaticsGateway/Services/Common/OutputDataPluginEngine.cs @@ -21,19 +21,20 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Logging; namespace Monai.Deploy.InformaticsGateway.Services.Common { - internal class OutputDataPluginEngine : IOutputDataPluginEngine + internal class OutputDataPlugInEngine : IOutputDataPlugInEngine { private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IDicomToolkit _dicomToolkit; - private IReadOnlyList _plugsins; + private IReadOnlyList _plugsins; - public OutputDataPluginEngine(IServiceProvider serviceProvider, ILogger logger, IDicomToolkit dicomToolkit) + public OutputDataPlugInEngine(IServiceProvider serviceProvider, ILogger logger, IDicomToolkit dicomToolkit) { _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -42,21 +43,21 @@ public OutputDataPluginEngine(IServiceProvider serviceProvider, ILogger pluginAssemblies) { - _plugsins = LoadPlugins(_serviceProvider, pluginAssemblies); + _plugsins = LoadPlugIns(_serviceProvider, pluginAssemblies); } - public async Task ExecutePlugins(ExportRequestDataMessage exportRequestDataMessage) + public async Task ExecutePlugInsAsync(ExportRequestDataMessage exportRequestDataMessage) { if (_plugsins == null) { - throw new ApplicationException("InputDataPluginEngine not configured, please call Configure() first."); + throw new ApplicationException("InputDataPlugInEngine not configured, please call Configure() first."); } var dicomFile = _dicomToolkit.Load(exportRequestDataMessage.FileContent); foreach (var plugin in _plugsins) { - _logger.ExecutingOutputDataPlugin(plugin.Name); - (dicomFile, exportRequestDataMessage) = await plugin.Execute(dicomFile, exportRequestDataMessage).ConfigureAwait(false); + _logger.ExecutingOutputDataPlugIn(plugin.Name); + (dicomFile, exportRequestDataMessage) = await plugin.ExecuteAsync(dicomFile, exportRequestDataMessage).ConfigureAwait(false); } using var ms = new MemoryStream(); await dicomFile.SaveAsync(ms); @@ -65,20 +66,20 @@ public async Task ExecutePlugins(ExportRequestDataMess return exportRequestDataMessage; } - private IReadOnlyList LoadPlugins(IServiceProvider serviceProvider, IReadOnlyList pluginAssemblies) + private IReadOnlyList LoadPlugIns(IServiceProvider serviceProvider, IReadOnlyList pluginAssemblies) { var exceptions = new List(); - var list = new List(); + var list = new List(); foreach (var plugin in pluginAssemblies) { try { - _logger.AddingOutputDataPlugin(plugin); - list.Add(typeof(IOutputDataPlugin).CreateInstance(serviceProvider, typeString: plugin)); + _logger.AddingOutputDataPlugIn(plugin); + list.Add(typeof(IOutputDataPlugIn).CreateInstance(serviceProvider, typeString: plugin)); } catch (Exception ex) { - exceptions.Add(new PlugingLoadingException($"Error loading plug-in '{plugin}'.", ex)); + exceptions.Add(new PlugInLoadingException($"Error loading plug-in '{plugin}'.", ex)); } } diff --git a/src/InformaticsGateway/Services/Connectors/PayloadAssembler.cs b/src/InformaticsGateway/Services/Connectors/PayloadAssembler.cs index 237fc0ec1..5bded84e7 100755 --- a/src/InformaticsGateway/Services/Connectors/PayloadAssembler.cs +++ b/src/InformaticsGateway/Services/Connectors/PayloadAssembler.cs @@ -28,6 +28,7 @@ using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Logging; + #nullable enable namespace Monai.Deploy.InformaticsGateway.Services.Connectors diff --git a/src/InformaticsGateway/Services/Connectors/PayloadMoveActionHandler.cs b/src/InformaticsGateway/Services/Connectors/PayloadMoveActionHandler.cs index a951dc21e..64d291e24 100644 --- a/src/InformaticsGateway/Services/Connectors/PayloadMoveActionHandler.cs +++ b/src/InformaticsGateway/Services/Connectors/PayloadMoveActionHandler.cs @@ -15,7 +15,6 @@ */ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; @@ -32,7 +31,6 @@ using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Logging; using Monai.Deploy.Storage.API; -using Polly; namespace Monai.Deploy.InformaticsGateway.Services.Connectors { diff --git a/src/InformaticsGateway/Services/Connectors/PayloadNotificationService.cs b/src/InformaticsGateway/Services/Connectors/PayloadNotificationService.cs index a190ddfad..492c6a821 100644 --- a/src/InformaticsGateway/Services/Connectors/PayloadNotificationService.cs +++ b/src/InformaticsGateway/Services/Connectors/PayloadNotificationService.cs @@ -274,7 +274,6 @@ private static void ResetIfFaultedOrCancelled(ActionBlock queue, Action } } - private async Task RestoreFromDatabaseAsync(CancellationToken cancellationToken) { _logger.StartupRestoreFromDatabase(); diff --git a/src/InformaticsGateway/Services/DicomWeb/IStreamsWriter.cs b/src/InformaticsGateway/Services/DicomWeb/IStreamsWriter.cs index 9018c6c96..3a18ce119 100644 --- a/src/InformaticsGateway/Services/DicomWeb/IStreamsWriter.cs +++ b/src/InformaticsGateway/Services/DicomWeb/IStreamsWriter.cs @@ -29,6 +29,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; @@ -87,17 +88,17 @@ public async Task Save(IList streams, string studyInstanceUi Guard.Against.NullOrWhiteSpace(correlationId, nameof(correlationId)); Guard.Against.NullOrWhiteSpace(dataSource, nameof(dataSource)); - var inputDataPluginEngine = _serviceScopeFactory.CreateScope().ServiceProvider.GetService(); + var inputDataPlugInEngine = _serviceScopeFactory.CreateScope().ServiceProvider.GetService(); string[] workflows = null; if (virtualApplicationEntity is not null) { - inputDataPluginEngine.Configure(virtualApplicationEntity.PluginAssemblies); + inputDataPlugInEngine.Configure(virtualApplicationEntity.PlugInAssemblies); workflows = virtualApplicationEntity.Workflows.ToArray(); } else { - inputDataPluginEngine.Configure(_configuration.Value.DicomWeb.PluginAssemblies); + inputDataPlugInEngine.Configure(_configuration.Value.DicomWeb.PlugInAssemblies); } // If a workflow is specified, it will overwrite ones specified in a virtual AE. @@ -120,7 +121,7 @@ public async Task Save(IList streams, string studyInstanceUi _logger.ZeroLengthDicomWebStowStream(); continue; } - await SaveInstance(stream, studyInstanceUid, inputDataPluginEngine, correlationId, dataSource, cancellationToken, workflows).ConfigureAwait(false); + await SaveInstance(stream, studyInstanceUid, inputDataPlugInEngine, correlationId, dataSource, cancellationToken, workflows).ConfigureAwait(false); } catch (Exception ex) { @@ -152,7 +153,7 @@ private int GetStatusCode(int instancesReceived) } } - private async Task SaveInstance(Stream stream, string studyInstanceUid, IInputDataPluginEngine inputDataPluginEngine, string correlationId, string dataSource, CancellationToken cancellationToken = default, params string[] workflows) + private async Task SaveInstance(Stream stream, string studyInstanceUid, IInputDataPlugInEngine inputDataPlugInEngine, string correlationId, string dataSource, CancellationToken cancellationToken = default, params string[] workflows) { Guard.Against.Null(stream, nameof(stream)); Guard.Against.NullOrWhiteSpace(correlationId, nameof(correlationId)); @@ -196,7 +197,7 @@ private async Task SaveInstance(Stream stream, string studyInstanceUid, IInputDa dicomInfo.SetWorkflows(workflows); } - var result = await inputDataPluginEngine.ExecutePlugins(dicomFile, dicomInfo).ConfigureAwait(false); + var result = await inputDataPlugInEngine.ExecutePlugInsAsync(dicomFile, dicomInfo).ConfigureAwait(false); dicomFile = result.Item1; dicomInfo = result.Item2 as DicomFileStorageMetadata; diff --git a/src/InformaticsGateway/Services/DicomWeb/StowService.cs b/src/InformaticsGateway/Services/DicomWeb/StowService.cs index f6164ec09..01d6e7dbf 100644 --- a/src/InformaticsGateway/Services/DicomWeb/StowService.cs +++ b/src/InformaticsGateway/Services/DicomWeb/StowService.cs @@ -115,6 +115,5 @@ private async Task ValidateVirtualAet(string aet) { return await _repository.FindByAeTitleAsync(aet).ConfigureAwait(false); } - } } diff --git a/src/InformaticsGateway/Services/Export/ExportServiceBase.cs b/src/InformaticsGateway/Services/Export/ExportServiceBase.cs index 082b4ddb7..5eb688b19 100644 --- a/src/InformaticsGateway/Services/Export/ExportServiceBase.cs +++ b/src/InformaticsGateway/Services/Export/ExportServiceBase.cs @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 MONAI Consortium + * Copyright 2021-2023 MONAI Consortium * Copyright 2019-2021 NVIDIA Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; @@ -271,12 +272,13 @@ private IEnumerable DownloadPayloadActionCallback(Expo yield return exportRequestData; } } + private async Task ExecuteOutputDataEngineCallback(ExportRequestDataMessage exportDataRequest, CancellationToken token) { - var outputDataEngine = _scope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IOutputDataPluginEngine)); + var outputDataEngine = _scope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IOutputDataPlugInEngine)); - outputDataEngine.Configure(exportDataRequest.PluginAssemblies); - return await outputDataEngine.ExecutePlugins(exportDataRequest).ConfigureAwait(false); + outputDataEngine.Configure(exportDataRequest.PlugInAssemblies); + return await outputDataEngine.ExecutePlugInsAsync(exportDataRequest).ConfigureAwait(false); } private void ReportingActionBlock(ExportRequestDataMessage exportRequestData) diff --git a/src/InformaticsGateway/Services/Fhir/FhirResourceTypesRouteConstraint.cs b/src/InformaticsGateway/Services/Fhir/FhirResourceTypesRouteConstraint.cs index 7a7a54190..d962c125b 100644 --- a/src/InformaticsGateway/Services/Fhir/FhirResourceTypesRouteConstraint.cs +++ b/src/InformaticsGateway/Services/Fhir/FhirResourceTypesRouteConstraint.cs @@ -15,7 +15,6 @@ */ using Ardalis.GuardClauses; -using FellowOakDicom; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; diff --git a/src/InformaticsGateway/Services/HealthLevel7/MllpService.cs b/src/InformaticsGateway/Services/HealthLevel7/MllpService.cs index cb2cde24f..b26c58614 100644 --- a/src/InformaticsGateway/Services/HealthLevel7/MllpService.cs +++ b/src/InformaticsGateway/Services/HealthLevel7/MllpService.cs @@ -25,7 +25,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Minio.DataModel; using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Common; diff --git a/src/InformaticsGateway/Services/Http/DestinationAeTitleController.cs b/src/InformaticsGateway/Services/Http/DestinationAeTitleController.cs index d9629ab15..53716df35 100644 --- a/src/InformaticsGateway/Services/Http/DestinationAeTitleController.cs +++ b/src/InformaticsGateway/Services/Http/DestinationAeTitleController.cs @@ -23,6 +23,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; @@ -38,19 +39,19 @@ public class DestinationAeTitleController : ControllerBase { private readonly ILogger _logger; private readonly IDestinationApplicationEntityRepository _repository; - private readonly IDataPluginEngineFactory _outputDataPluginEngineFactory; + private readonly IDataPlugInEngineFactory _outputDataPlugInEngineFactory; private readonly IScuQueue _scuQueue; public DestinationAeTitleController( ILogger logger, IDestinationApplicationEntityRepository repository, IScuQueue scuQueue, - IDataPluginEngineFactory outputDataPluginEngineFactory) + IDataPlugInEngineFactory outputDataPlugInEngineFactory) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _scuQueue = scuQueue ?? throw new ArgumentNullException(nameof(scuQueue)); - _outputDataPluginEngineFactory = outputDataPluginEngineFactory ?? throw new ArgumentNullException(nameof(outputDataPluginEngineFactory)); + _outputDataPlugInEngineFactory = outputDataPlugInEngineFactory ?? throw new ArgumentNullException(nameof(outputDataPlugInEngineFactory)); } [HttpGet] @@ -261,15 +262,15 @@ public async Task> Delete(string name [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public ActionResult GetPlugins() + public ActionResult GetPlugIns() { try { - return Ok(_outputDataPluginEngineFactory.RegisteredPlugins()); + return Ok(_outputDataPlugInEngineFactory.RegisteredPlugIns()); } catch (Exception ex) { - _logger.ErrorReadingDataInputPlugins(ex); + _logger.ErrorReadingDataInputPlugIns(ex); return Problem(title: "Error reading data input plug-ins.", statusCode: (int)System.Net.HttpStatusCode.InternalServerError, detail: ex.Message); } } diff --git a/src/InformaticsGateway/Services/Http/DicomWeb/StowController.cs b/src/InformaticsGateway/Services/Http/DicomWeb/StowController.cs index 1401ddae4..8037b721c 100644 --- a/src/InformaticsGateway/Services/Http/DicomWeb/StowController.cs +++ b/src/InformaticsGateway/Services/Http/DicomWeb/StowController.cs @@ -120,7 +120,6 @@ private async Task StoreInstances(string studyInstanceUid, string return StatusCode( StatusCodes.Status400BadRequest, Problem(title: $"Invalid virtual application entity '{aet}'.", statusCode: StatusCodes.Status400BadRequest, detail: ex.Message)); - } catch (Exception ex) { diff --git a/src/InformaticsGateway/Services/Http/InferenceController.cs b/src/InformaticsGateway/Services/Http/InferenceController.cs index 0d599795c..981031a69 100644 --- a/src/InformaticsGateway/Services/Http/InferenceController.cs +++ b/src/InformaticsGateway/Services/Http/InferenceController.cs @@ -19,7 +19,6 @@ using System.Net; using System.Threading.Tasks; using Ardalis.GuardClauses; -using DotNext; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; diff --git a/src/InformaticsGateway/Services/Http/MonaiAeTitleController.cs b/src/InformaticsGateway/Services/Http/MonaiAeTitleController.cs index 518eec3f1..22810f4a3 100644 --- a/src/InformaticsGateway/Services/Http/MonaiAeTitleController.cs +++ b/src/InformaticsGateway/Services/Http/MonaiAeTitleController.cs @@ -24,6 +24,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; @@ -39,18 +40,18 @@ public class MonaiAeTitleController : ControllerBase { private readonly ILogger _logger; private readonly IMonaiApplicationEntityRepository _repository; - private readonly IDataPluginEngineFactory _inputDataPluginEngineFactory; + private readonly IDataPlugInEngineFactory _inputDataPlugInEngineFactory; private readonly IMonaiAeChangedNotificationService _monaiAeChangedNotificationService; public MonaiAeTitleController( ILogger logger, IMonaiAeChangedNotificationService monaiAeChangedNotificationService, IMonaiApplicationEntityRepository repository, - IDataPluginEngineFactory inputDataPluginEngineFactory) + IDataPlugInEngineFactory inputDataPlugInEngineFactory) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _repository = repository ?? throw new ArgumentNullException(nameof(repository)); - _inputDataPluginEngineFactory = inputDataPluginEngineFactory ?? throw new ArgumentNullException(nameof(inputDataPluginEngineFactory)); + _inputDataPlugInEngineFactory = inputDataPlugInEngineFactory ?? throw new ArgumentNullException(nameof(inputDataPlugInEngineFactory)); _monaiAeChangedNotificationService = monaiAeChangedNotificationService ?? throw new ArgumentNullException(nameof(monaiAeChangedNotificationService)); } @@ -160,7 +161,7 @@ public async Task> Edit(MonaiApplicationEnt applicationEntity.Timeout = item.Timeout; applicationEntity.IgnoredSopClasses = item.IgnoredSopClasses ?? new List(); applicationEntity.Workflows = item.Workflows ?? new List(); - applicationEntity.PluginAssemblies = item.PluginAssemblies ?? new List(); + applicationEntity.PlugInAssemblies = item.PlugInAssemblies ?? new List(); applicationEntity.SetAuthor(User, EditMode.Update); await ValidateUpdateAsync(applicationEntity).ConfigureAwait(false); @@ -213,15 +214,15 @@ public async Task> Delete(string name) [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public ActionResult GetPlugins() + public ActionResult GetPlugIns() { try { - return Ok(_inputDataPluginEngineFactory.RegisteredPlugins()); + return Ok(_inputDataPlugInEngineFactory.RegisteredPlugIns()); } catch (Exception ex) { - _logger.ErrorReadingDataInputPlugins(ex); + _logger.ErrorReadingDataInputPlugIns(ex); return Problem(title: "Error reading data input plug-ins.", statusCode: (int)System.Net.HttpStatusCode.InternalServerError, detail: ex.Message); } } diff --git a/src/InformaticsGateway/Services/Http/VirtualAeTitleController.cs b/src/InformaticsGateway/Services/Http/VirtualAeTitleController.cs index aba9c208e..9c11d5084 100644 --- a/src/InformaticsGateway/Services/Http/VirtualAeTitleController.cs +++ b/src/InformaticsGateway/Services/Http/VirtualAeTitleController.cs @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 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. @@ -24,6 +24,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; @@ -38,16 +39,16 @@ public class VirtualAeTitleController : ControllerBase { private readonly ILogger _logger; private readonly IVirtualApplicationEntityRepository _repository; - private readonly IDataPluginEngineFactory _inputDataPluginEngineFactory; + private readonly IDataPlugInEngineFactory _inputDataPlugInEngineFactory; public VirtualAeTitleController( ILogger logger, IVirtualApplicationEntityRepository repository, - IDataPluginEngineFactory inputDataPluginEngineFactory) + IDataPlugInEngineFactory inputDataPlugInEngineFactory) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _repository = repository ?? throw new ArgumentNullException(nameof(repository)); - _inputDataPluginEngineFactory = inputDataPluginEngineFactory ?? throw new ArgumentNullException(nameof(inputDataPluginEngineFactory)); + _inputDataPlugInEngineFactory = inputDataPlugInEngineFactory ?? throw new ArgumentNullException(nameof(inputDataPlugInEngineFactory)); } [HttpGet] @@ -151,7 +152,7 @@ public async Task> Edit(VirtualApplicatio item.SetDefaultValues(); applicationEntity.Workflows = item.Workflows ?? new List(); - applicationEntity.PluginAssemblies = item.PluginAssemblies ?? new List(); + applicationEntity.PlugInAssemblies = item.PlugInAssemblies ?? new List(); applicationEntity.SetAuthor(User, EditMode.Update); await ValidateUpdateAsync(applicationEntity).ConfigureAwait(false); @@ -202,15 +203,15 @@ public async Task> Delete(string name) [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public ActionResult GetPlugins() + public ActionResult GetPlugIns() { try { - return Ok(_inputDataPluginEngineFactory.RegisteredPlugins()); + return Ok(_inputDataPlugInEngineFactory.RegisteredPlugIns()); } catch (Exception ex) { - _logger.ErrorReadingDataInputPlugins(ex); + _logger.ErrorReadingDataInputPlugIns(ex); return Problem(title: "Error reading data input plug-ins.", statusCode: (int)System.Net.HttpStatusCode.InternalServerError, detail: ex.Message); } } diff --git a/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs b/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs index 107c4f515..44fb24878 100644 --- a/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs +++ b/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs @@ -24,6 +24,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; @@ -43,7 +44,7 @@ internal class ApplicationEntityHandler : IDisposable, IApplicationEntityHandler private readonly IPayloadAssembler _payloadAssembler; private readonly IObjectUploadQueue _uploadQueue; private readonly IFileSystem _fileSystem; - private readonly IInputDataPluginEngine _pluginEngine; + private readonly IInputDataPlugInEngine _pluginEngine; private MonaiApplicationEntity _configuration; private DicomJsonOptions _dicomJsonOptions; private bool _validateDicomValueOnJsonSerialization; @@ -62,7 +63,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)); + _pluginEngine = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IInputDataPlugInEngine)); } public void Configure(MonaiApplicationEntity monaiApplicationEntity, DicomJsonOptions dicomJsonOptions, bool validateDicomValuesOnJsonSerialization) @@ -81,7 +82,7 @@ public void Configure(MonaiApplicationEntity monaiApplicationEntity, DicomJsonOp _configuration = monaiApplicationEntity; _dicomJsonOptions = dicomJsonOptions; _validateDicomValueOnJsonSerialization = validateDicomValuesOnJsonSerialization; - _pluginEngine.Configure(_configuration.PluginAssemblies); + _pluginEngine.Configure(_configuration.PlugInAssemblies); } } @@ -115,7 +116,7 @@ public async Task HandleInstanceAsync(DicomCStoreRequest request, string calledA dicomInfo.SetWorkflows(_configuration.Workflows.ToArray()); } - var result = await _pluginEngine.ExecutePlugins(request.File, dicomInfo).ConfigureAwait(false); + var result = await _pluginEngine.ExecutePlugInsAsync(request.File, dicomInfo).ConfigureAwait(false); dicomInfo = result.Item2 as DicomFileStorageMetadata; var dicomFile = result.Item1; diff --git a/src/InformaticsGateway/Services/Scp/IApplicationEntityManager.cs b/src/InformaticsGateway/Services/Scp/IApplicationEntityManager.cs index 098473f90..1f385b159 100644 --- a/src/InformaticsGateway/Services/Scp/IApplicationEntityManager.cs +++ b/src/InformaticsGateway/Services/Scp/IApplicationEntityManager.cs @@ -18,7 +18,6 @@ using System; using System.Threading.Tasks; using FellowOakDicom.Network; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Configuration; diff --git a/src/InformaticsGateway/Services/Scp/MonaiAeChangedNotificationService.cs b/src/InformaticsGateway/Services/Scp/MonaiAeChangedNotificationService.cs index 970e1e635..79669794b 100644 --- a/src/InformaticsGateway/Services/Scp/MonaiAeChangedNotificationService.cs +++ b/src/InformaticsGateway/Services/Scp/MonaiAeChangedNotificationService.cs @@ -18,7 +18,6 @@ using System; using System.Collections.Generic; using Ardalis.GuardClauses; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Logging; diff --git a/src/InformaticsGateway/Services/Scp/ScpService.cs b/src/InformaticsGateway/Services/Scp/ScpService.cs index 757515d9e..b491741f8 100644 --- a/src/InformaticsGateway/Services/Scp/ScpService.cs +++ b/src/InformaticsGateway/Services/Scp/ScpService.cs @@ -29,6 +29,7 @@ using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Logging; using Monai.Deploy.InformaticsGateway.Services.Common; + using FoDicomNetwork = FellowOakDicom.Network; namespace Monai.Deploy.InformaticsGateway.Services.Scp diff --git a/src/InformaticsGateway/Services/Scp/ScpServiceInternal.cs b/src/InformaticsGateway/Services/Scp/ScpServiceInternal.cs index 60dd79958..6caedf44e 100644 --- a/src/InformaticsGateway/Services/Scp/ScpServiceInternal.cs +++ b/src/InformaticsGateway/Services/Scp/ScpServiceInternal.cs @@ -46,7 +46,6 @@ internal class ScpServiceInternal : private Guid _associationId; private DateTimeOffset? _associationReceived; - public ScpServiceInternal(INetworkStream stream, Encoding fallbackEncoding, ILogger logger, DicomServiceDependencies dicomServiceDependencies) : base(stream, fallbackEncoding, logger, dicomServiceDependencies) { diff --git a/src/InformaticsGateway/Test/Monai.Deploy.InformaticsGateway.Test.csproj b/src/InformaticsGateway/Test/Monai.Deploy.InformaticsGateway.Test.csproj index 7e38304a4..fe8346b52 100644 --- a/src/InformaticsGateway/Test/Monai.Deploy.InformaticsGateway.Test.csproj +++ b/src/InformaticsGateway/Test/Monai.Deploy.InformaticsGateway.Test.csproj @@ -1,4 +1,4 @@ - + + + + + Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution + net6.0 + enable + enable + Apache-2.0 + true + True + latest + ..\..\.sonarlint\project-monai_monai-deploy-informatics-gatewaycsharp.ruleset + false + true + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/Plug-ins/RemoteAppExecution/RemoteAppExecution.cs b/src/Plug-ins/RemoteAppExecution/RemoteAppExecution.cs new file mode 100644 index 000000000..1af5ce355 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/RemoteAppExecution.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.Text.Json.Serialization; +using FellowOakDicom; +using Monai.Deploy.InformaticsGateway.Api; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution +{ + public class RemoteAppExecution + { + /// + /// Gets the ID of this record. + /// + [JsonPropertyName("_id")] + public Guid Id { get; init; } = Guid.NewGuid(); + + /// + /// Gets the date time this record is created. + /// + public DateTimeOffset RequestTime { get; init; } = DateTime.UtcNow; + + /// + /// Gets or sets the workflow instance ID of the original request. + /// + public string WorkflowInstanceId { get; set; } = string.Empty; + + /// + /// Gets or sets the export task ID of the original request. + /// + public string ExportTaskId { get; set; } = string.Empty; + + /// + /// Gets or sets the correlation ID of the original request. + /// + public string CorrelationId { get; set; } = string.Empty; + + ///// + ///// Gets or sets the proxy value of Study Instance UID. + ///// + public string StudyInstanceUid { get; set; } = string.Empty; + + ///// + ///// Gets or sets the proxy value of Series Instance UID. + ///// + public string SeriesInstanceUid { get; set; } = string.Empty; + + ///// + ///// Gets or sets the proxy value of SOP Instance UID. + ///// + public string SopInstanceUid { get; set; } = string.Empty; + + /// + /// Gets or sets the original values of a given DICOM tag. + /// + public Dictionary OriginalValues { get; init; } = new(); + + public RemoteAppExecution() + { } + + public RemoteAppExecution(ExportRequestDataMessage exportRequestDataMessage, string? studyInstanceUid, string? seriesInstanceUid) + { + WorkflowInstanceId = exportRequestDataMessage.WorkflowInstanceId; + ExportTaskId = exportRequestDataMessage.ExportTaskId; + CorrelationId = exportRequestDataMessage.CorrelationId; + + StudyInstanceUid = studyInstanceUid ?? Utilities.GetTagProxyValue(DicomTag.StudyInstanceUID); + SeriesInstanceUid = seriesInstanceUid ?? Utilities.GetTagProxyValue(DicomTag.SeriesInstanceUID); + SopInstanceUid = Utilities.GetTagProxyValue(DicomTag.SOPInstanceUID); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/SR.cs b/src/Plug-ins/RemoteAppExecution/SR.cs new file mode 100644 index 000000000..1ae61b7a5 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/SR.cs @@ -0,0 +1,23 @@ +/* + * 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. + */ + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution +{ + internal static class SR + { + public static string ConfigKey_ReplaceTags = "ReplaceTags"; + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/Database/DatabaseRegistrarTest.cs b/src/Plug-ins/RemoteAppExecution/Test/Database/DatabaseRegistrarTest.cs new file mode 100644 index 000000000..9464cc8db --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/Database/DatabaseRegistrarTest.cs @@ -0,0 +1,66 @@ +/* + * 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 Microsoft.Extensions.DependencyInjection; +using Monai.Deploy.InformaticsGateway.Database.Api; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework; +using Moq; +using Xunit; +using MongoDbTypes = Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.MongoDb; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.Database +{ + public class DatabaseRegistrarTest + { + [Fact] + public void GivenEntityFrameworkDatabaseType_WhenConfigureIsCalled_AddsDependencies() + { + var serviceDescriptors = new List(); + var serviceCollection = new Mock(); + serviceCollection.Setup(p => p.Add(It.IsAny())); + serviceCollection.Setup(p => p.GetEnumerator()).Returns(serviceDescriptors.GetEnumerator()); + + var registrar = new DatabaseRegistrar(); + var returnedServiceCollection = registrar.Configure(serviceCollection.Object, DatabaseType.EntityFramework, "DataSource=file::memory:?cache=shared"); + + Assert.Same(serviceCollection.Object, returnedServiceCollection); + + serviceCollection.Verify(p => p.Add(It.IsAny()), Times.Exactly(5)); + serviceCollection.Verify(p => p.Add(It.Is(p => p.ServiceType == typeof(RemoteAppExecutionDbContext))), Times.Once()); + serviceCollection.Verify(p => p.Add(It.Is(p => p.ServiceType == typeof(IDatabaseMigrationManagerForPlugIns) && p.ImplementationType == typeof(MigrationManager))), Times.Once()); + serviceCollection.Verify(p => p.Add(It.Is(p => p.ServiceType == typeof(IRemoteAppExecutionRepository) && p.ImplementationType == typeof(RemoteAppExecutionRepository))), Times.Once()); + } + + [Fact] + public void GivenMongoDatabaseType_WhenConfigureIsCalled_AddsDependencies() + { + var serviceDescriptors = new List(); + var serviceCollection = new Mock(); + serviceCollection.Setup(p => p.Add(It.IsAny())); + serviceCollection.Setup(p => p.GetEnumerator()).Returns(serviceDescriptors.GetEnumerator()); + + var registrar = new DatabaseRegistrar(); + var returnedServiceCollection = registrar.Configure(serviceCollection.Object, DatabaseType.MongoDb, "DataSource=file::memory:?cache=shared"); + + Assert.Same(serviceCollection.Object, returnedServiceCollection); + + serviceCollection.Verify(p => p.Add(It.IsAny()), Times.Exactly(2)); + serviceCollection.Verify(p => p.Add(It.Is(p => p.ServiceType == typeof(IDatabaseMigrationManagerForPlugIns) && p.ImplementationType == typeof(MongoDbTypes.MigrationManager))), Times.Once()); + serviceCollection.Verify(p => p.Add(It.Is(p => p.ServiceType == typeof(IRemoteAppExecutionRepository) && p.ImplementationType == typeof(MongoDbTypes.RemoteAppExecutionRepository))), Times.Once()); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/MigrationManagerTest.cs b/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/MigrationManagerTest.cs new file mode 100644 index 000000000..6c7a95a71 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/MigrationManagerTest.cs @@ -0,0 +1,57 @@ +/* + * 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 Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.Database.EntityFramework +{ + public class MigrationManagerTest + { + private readonly Mock _host; + private readonly RemoteAppExecutionDbContext _dbContext; + private readonly IServiceProvider _serviceProvider; + + public MigrationManagerTest() + { + var options = new DbContextOptionsBuilder() + .UseSqlite("DataSource=file:memdbmigration?mode=memory&cache=shared") + .Options; + + _host = new Mock(); + _dbContext = new RemoteAppExecutionDbContext(options); + + var services = new ServiceCollection(); + services.AddScoped(p => _dbContext); + + _serviceProvider = services.BuildServiceProvider(); + _host.Setup(p => p.Services).Returns(_serviceProvider); + } + + [Fact] + public void GivenARemoteAppExecutionDbContext_OnMigration_MigratesSuccessfully() + { + var mgr = new MigrationManager(); + var result = mgr.Migrate(_host.Object); + + Assert.Same(_host.Object, result); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/RemoteAppExecutionRepositoryTest.cs b/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/RemoteAppExecutionRepositoryTest.cs new file mode 100644 index 000000000..1ba57f920 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/RemoteAppExecutionRepositoryTest.cs @@ -0,0 +1,169 @@ +/* + * 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 Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.Database.EntityFramework +{ + [Collection("SqliteDatabase")] + public class RemoteAppExecutionRepositoryTest + { + private readonly SqliteDatabaseFixture _databaseFixture; + + private readonly Mock _serviceScopeFactory; + private readonly Mock> _logger; + private readonly IOptions _options; + + private readonly Mock _serviceScope; + private readonly IServiceProvider _serviceProvider; + + public RemoteAppExecutionRepositoryTest(SqliteDatabaseFixture databaseFixture) + { + _databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture)); + _databaseFixture.InitDatabaseWithRemoteAppExecutions(); + + _serviceScopeFactory = new Mock(); + _logger = new Mock>(); + _options = Options.Create(new InformaticsGatewayConfiguration()); + + _serviceScope = new Mock(); + var services = new ServiceCollection(); + services.AddScoped(p => _logger.Object); + services.AddScoped(p => databaseFixture.DatabaseContext); + + _serviceProvider = services.BuildServiceProvider(); + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + + _options.Value.Database.Retries.DelaysMilliseconds = new[] { 1, 1, 1 }; + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenAddingToDatabase_ExpectItToBeSaved() + { + var record = new RemoteAppExecution + { + CorrelationId = Guid.NewGuid().ToString(), + ExportTaskId = Guid.NewGuid().ToString(), + Id = Guid.NewGuid(), + RequestTime = DateTimeOffset.UtcNow, + StudyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SeriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + }; + + record.OriginalValues.Add(DicomTag.StudyInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SeriesInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SOPInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.PatientID.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.AccessionNumber.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.StudyDescription.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options); + await store.AddAsync(record).ConfigureAwait(false); + var actual = await _databaseFixture.DatabaseContext.Set().FirstOrDefaultAsync(p => p.Id.Equals(record.Id)).ConfigureAwait(false); + + Assert.NotNull(actual); + Assert.Equal(record.CorrelationId, actual!.CorrelationId); + Assert.Equal(record.ExportTaskId, actual!.ExportTaskId); + Assert.Equal(record.Id, actual!.Id); + Assert.Equal(record.RequestTime, actual!.RequestTime); + Assert.Equal(record.OriginalValues, actual.OriginalValues); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenRemoveIsCalled_ExpectItToDeleted() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options); + + var record = _databaseFixture.RemoteAppExecutions.First(); + var expected = await store.GetAsync(record.SopInstanceUid).ConfigureAwait(false); + Assert.NotNull(expected); + + var actual = await store.RemoveAsync(expected!).ConfigureAwait(false); + Assert.Same(expected, actual); + + var dbResult = await _databaseFixture.DatabaseContext.Set().FirstOrDefaultAsync(p => p.Id == record.Id).ConfigureAwait(false); + Assert.Null(dbResult); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenGetAsyncIsCalledWithSopInstanceUid_ExpectItToBeReturned() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options); + + var expected = _databaseFixture.RemoteAppExecutions.First(); + var actual = await store.GetAsync(expected.SopInstanceUid).ConfigureAwait(false); + Assert.NotNull(actual); + Assert.Equal(expected.SopInstanceUid, actual.SopInstanceUid); + Assert.Equal(expected.StudyInstanceUid, actual.StudyInstanceUid); + Assert.Equal(expected.SeriesInstanceUid, actual.SeriesInstanceUid); + Assert.Equal(expected.WorkflowInstanceId, actual.WorkflowInstanceId); + Assert.Equal(expected.ExportTaskId, actual.ExportTaskId); + Assert.Equal(expected.RequestTime, actual.RequestTime); + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.CorrelationId, actual.CorrelationId); + Assert.Equal(expected.OriginalValues, actual.OriginalValues); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenGetAsyncIsCalledWithStudyAndSeriesUids_ExpectItToBeReturned() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options); + + var expected = _databaseFixture.RemoteAppExecutions.First(); + var actual = await store.GetAsync(expected.WorkflowInstanceId, expected.ExportTaskId, expected.StudyInstanceUid, expected.SeriesInstanceUid).ConfigureAwait(false); + Assert.NotNull(actual); + Assert.Equal(expected.SopInstanceUid, actual.SopInstanceUid); + Assert.Equal(expected.StudyInstanceUid, actual.StudyInstanceUid); + Assert.Equal(expected.SeriesInstanceUid, actual.SeriesInstanceUid); + Assert.Equal(expected.WorkflowInstanceId, actual.WorkflowInstanceId); + Assert.Equal(expected.ExportTaskId, actual.ExportTaskId); + Assert.Equal(expected.RequestTime, actual.RequestTime); + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.CorrelationId, actual.CorrelationId); + Assert.Equal(expected.OriginalValues, actual.OriginalValues); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenGetAsyncIsCalledWithRandomSeries_ExpectItToBeReturned() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options); + + var expected = _databaseFixture.RemoteAppExecutions.First(); + var actual = await store.GetAsync(expected.WorkflowInstanceId, expected.ExportTaskId, expected.StudyInstanceUid, DicomUIDGenerator.GenerateDerivedFromUUID().UID).ConfigureAwait(false); + Assert.NotNull(actual); + Assert.Equal(expected.SopInstanceUid, actual.SopInstanceUid); + Assert.Equal(expected.StudyInstanceUid, actual.StudyInstanceUid); + Assert.Equal(expected.SeriesInstanceUid, actual.SeriesInstanceUid); + Assert.Equal(expected.WorkflowInstanceId, actual.WorkflowInstanceId); + Assert.Equal(expected.ExportTaskId, actual.ExportTaskId); + Assert.Equal(expected.RequestTime, actual.RequestTime); + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.CorrelationId, actual.CorrelationId); + Assert.Equal(expected.OriginalValues, actual.OriginalValues); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/SqliteDatabaseFixture.cs b/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/SqliteDatabaseFixture.cs new file mode 100644 index 000000000..697916608 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/SqliteDatabaseFixture.cs @@ -0,0 +1,87 @@ +/* + * 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 Microsoft.EntityFrameworkCore; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.Database.EntityFramework +{ + [CollectionDefinition("SqliteDatabase")] + public class SqliteDatabaseCollection : ICollectionFixture + { + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + } + + public class SqliteDatabaseFixture + { + public RemoteAppExecutionDbContext DatabaseContext { get; set; } + public IList RemoteAppExecutions { get; init; } + + public SqliteDatabaseFixture() + { + var options = new DbContextOptionsBuilder() + .UseSqlite("DataSource=file::memory:?cache=shared") + .Options; + DatabaseContext = new RemoteAppExecutionDbContext(options); + DatabaseContext.Database.EnsureCreated(); + + RemoteAppExecutions = new List(); + } + + public void Dispose() + { + DatabaseContext.Dispose(); + } + + internal void InitDatabaseWithRemoteAppExecutions() + { + var set = DatabaseContext.Set(); + set.RemoveRange(set.ToList()); + RemoteAppExecutions.Clear(); + for (var i = 0; i < 5; i++) + { + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var record = new RemoteAppExecution + { + WorkflowInstanceId = Guid.NewGuid().ToString(), + CorrelationId = Guid.NewGuid().ToString(), + ExportTaskId = Guid.NewGuid().ToString(), + Id = Guid.NewGuid(), + RequestTime = DateTimeOffset.UtcNow, + StudyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SeriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + }; + + record.OriginalValues.Add(DicomTag.StudyInstanceUID.ToString(), studyInstanceUid); + record.OriginalValues.Add(DicomTag.SeriesInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SOPInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.PatientID.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.AccessionNumber.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.StudyDescription.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + + set.Add(record); + RemoteAppExecutions.Add(record); + } + + DatabaseContext.SaveChanges(); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/MongoDatabaseFixture.cs b/src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/MongoDatabaseFixture.cs new file mode 100644 index 000000000..119cf18bd --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/MongoDatabaseFixture.cs @@ -0,0 +1,90 @@ +/* + * 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 Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Database.Api; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.MongoDb; +using MongoDB.Driver; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.Database.MongoDb +{ + [CollectionDefinition("MongoDatabase")] + public class MongoDatabaseCollection : ICollectionFixture + { + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + } + + public class MongoDatabaseFixture + { + public IMongoClient Client { get; set; } + public IMongoDatabase Database { get; set; } + public IOptions Options { get; set; } + public IList RemoteAppExecutions { get; init; } + + public MongoDatabaseFixture() + { + Client = new MongoClient("mongodb://root:rootpassword@localhost:27017"); + Options = Microsoft.Extensions.Options.Options.Create(new DatabaseOptions { DatabaseName = $"IGTest" }); + Database = Client.GetDatabase(Options.Value.DatabaseName); + + var migration = new MigrationManager(); + migration.Migrate(null!); + + RemoteAppExecutions = new List(); + } + + internal void InitDatabaseWithRemoteAppExecutions() + { + var collection = Database.GetCollection(nameof(RemoteAppExecution)); + Clear(collection); + RemoteAppExecutions.Clear(); + for (var i = 0; i < 5; i++) + { + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var record = new RemoteAppExecution + { + WorkflowInstanceId = Guid.NewGuid().ToString(), + CorrelationId = Guid.NewGuid().ToString(), + ExportTaskId = Guid.NewGuid().ToString(), + Id = Guid.NewGuid(), + RequestTime = DateTimeOffset.UtcNow, + StudyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SeriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + }; + + record.OriginalValues.Add(DicomTag.StudyInstanceUID.ToString(), studyInstanceUid); + record.OriginalValues.Add(DicomTag.SeriesInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SOPInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.PatientID.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.AccessionNumber.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.StudyDescription.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + + collection.InsertOne(record); + RemoteAppExecutions.Add(record); + } + } + + public static void Clear(IMongoCollection collection) where T : class + { + collection.DeleteMany(Builders.Filter.Empty); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/RemoteAppExecutionRepositoryTest.cs b/src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/RemoteAppExecutionRepositoryTest.cs new file mode 100644 index 000000000..34ef85388 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/RemoteAppExecutionRepositoryTest.cs @@ -0,0 +1,173 @@ +/* + * 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 Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.MongoDb; +using MongoDB.Driver; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.Database.MongoDb +{ + [Collection("MongoDatabase")] + public class RemoteAppExecutionRepositoryTest + { + private readonly MongoDatabaseFixture _databaseFixture; + + private readonly Mock _serviceScopeFactory; + private readonly Mock> _logger; + private readonly IOptions _options; + + private readonly Mock _serviceScope; + private readonly IServiceProvider _serviceProvider; + + public RemoteAppExecutionRepositoryTest(MongoDatabaseFixture databaseFixture) + { + _databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture)); + _databaseFixture.InitDatabaseWithRemoteAppExecutions(); + + _serviceScopeFactory = new Mock(); + _logger = new Mock>(); + _options = Options.Create(new InformaticsGatewayConfiguration()); + + _serviceScope = new Mock(); + var services = new ServiceCollection(); + services.AddScoped(p => _logger.Object); + services.AddScoped(p => databaseFixture.Client); + + _serviceProvider = services.BuildServiceProvider(); + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + + _options.Value.Database.Retries.DelaysMilliseconds = new[] { 1, 1, 1 }; + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenAddingToDatabase_ExpectItToBeSaved() + { + var record = new RemoteAppExecution + { + CorrelationId = Guid.NewGuid().ToString(), + ExportTaskId = Guid.NewGuid().ToString(), + Id = Guid.NewGuid(), + RequestTime = DateTimeOffset.UtcNow, + StudyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SeriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + }; + + record.OriginalValues.Add(DicomTag.StudyInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SeriesInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SOPInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.PatientID.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.AccessionNumber.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.StudyDescription.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + await store.AddAsync(record).ConfigureAwait(false); + + var collection = _databaseFixture.Database.GetCollection(nameof(RemoteAppExecution)); + var actual = await collection.Find(p => p.Id == record.Id).FirstOrDefaultAsync().ConfigureAwait(false); + + Assert.NotNull(actual); + Assert.Equal(record.CorrelationId, actual!.CorrelationId); + Assert.Equal(record.ExportTaskId, actual!.ExportTaskId); + Assert.Equal(record.Id, actual!.Id); + Assert.Equal(record.RequestTime, actual!.RequestTime); + Assert.Equal(record.OriginalValues, actual.OriginalValues); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenRemoveIsCalled_ExpectItToDeleted() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + + var record = _databaseFixture.RemoteAppExecutions.First(); + var expected = await store.GetAsync(record.SopInstanceUid).ConfigureAwait(false); + Assert.NotNull(expected); + + var actual = await store.RemoveAsync(expected!).ConfigureAwait(false); + Assert.Same(expected, actual); + + var collection = _databaseFixture.Database.GetCollection(nameof(RemoteAppExecution)); + var dbResult = await collection.Find(p => p.Id == record.Id).FirstOrDefaultAsync().ConfigureAwait(false); + Assert.Null(dbResult); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenGetAsyncIsCalledWithSopInstanceUid_ExpectItToBeReturned() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + + var expected = _databaseFixture.RemoteAppExecutions.First(); + var actual = await store.GetAsync(expected.SopInstanceUid).ConfigureAwait(false); + Assert.NotNull(actual); + Assert.Equal(expected.SopInstanceUid, actual.SopInstanceUid); + Assert.Equal(expected.StudyInstanceUid, actual.StudyInstanceUid); + Assert.Equal(expected.SeriesInstanceUid, actual.SeriesInstanceUid); + Assert.Equal(expected.WorkflowInstanceId, actual.WorkflowInstanceId); + Assert.Equal(expected.ExportTaskId, actual.ExportTaskId); + Assert.Equal(expected.RequestTime, actual.RequestTime); + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.CorrelationId, actual.CorrelationId); + Assert.Equal(expected.OriginalValues, actual.OriginalValues); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenGetAsyncIsCalledWithStudyAndSeriesUids_ExpectItToBeReturned() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + + var expected = _databaseFixture.RemoteAppExecutions.First(); + var actual = await store.GetAsync(expected.WorkflowInstanceId, expected.ExportTaskId, expected.StudyInstanceUid, expected.SeriesInstanceUid).ConfigureAwait(false); + Assert.NotNull(actual); + Assert.Equal(expected.SopInstanceUid, actual.SopInstanceUid); + Assert.Equal(expected.StudyInstanceUid, actual.StudyInstanceUid); + Assert.Equal(expected.SeriesInstanceUid, actual.SeriesInstanceUid); + Assert.Equal(expected.WorkflowInstanceId, actual.WorkflowInstanceId); + Assert.Equal(expected.ExportTaskId, actual.ExportTaskId); + Assert.Equal(expected.RequestTime, actual.RequestTime); + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.CorrelationId, actual.CorrelationId); + Assert.Equal(expected.OriginalValues, actual.OriginalValues); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenGetAsyncIsCalledWithRandomSeries_ExpectItToBeReturned() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + + var expected = _databaseFixture.RemoteAppExecutions.First(); + var actual = await store.GetAsync(expected.WorkflowInstanceId, expected.ExportTaskId, expected.StudyInstanceUid, DicomUIDGenerator.GenerateDerivedFromUUID().UID).ConfigureAwait(false); + Assert.NotNull(actual); + Assert.Equal(expected.SopInstanceUid, actual.SopInstanceUid); + Assert.Equal(expected.StudyInstanceUid, actual.StudyInstanceUid); + Assert.Equal(expected.SeriesInstanceUid, actual.SeriesInstanceUid); + Assert.Equal(expected.WorkflowInstanceId, actual.WorkflowInstanceId); + Assert.Equal(expected.ExportTaskId, actual.ExportTaskId); + Assert.Equal(expected.RequestTime, actual.RequestTime); + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.CorrelationId, actual.CorrelationId); + Assert.Equal(expected.OriginalValues, actual.OriginalValues); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/ExternalAppIncomingTest.cs b/src/Plug-ins/RemoteAppExecution/Test/ExternalAppIncomingTest.cs new file mode 100644 index 000000000..23318a4a1 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/ExternalAppIncomingTest.cs @@ -0,0 +1,133 @@ +/* + * 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.Reflection; +using FellowOakDicom; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database; +using Monai.Deploy.InformaticsGateway.SharedTest; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test +{ + public class ExternalAppIncomingTest + { + private readonly Mock> _logger; + private readonly Mock _serviceScopeFactory; + private readonly ServiceCollection _serviceCollection; + private readonly Mock _repository; + private readonly Mock _serviceScope; + private readonly ServiceProvider _serviceProvider; + + public ExternalAppIncomingTest() + { + _logger = new Mock>(); + _serviceScopeFactory = new Mock(); + _repository = new Mock(); + _serviceScope = new Mock(); + + _serviceCollection = new ServiceCollection(); + _serviceCollection.AddScoped(p => _logger.Object); + _serviceCollection.AddScoped(p => _repository.Object); + + _serviceProvider = _serviceCollection.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 GivenExternalAppOutgoing_TestConstructors() + { + Assert.Throws(() => new ExternalAppIncoming(null, null)); + Assert.Throws(() => new ExternalAppIncoming(_logger.Object, null)); + + var app = new ExternalAppIncoming(_logger.Object, _serviceScopeFactory.Object); + + Assert.Equal(app.Name, app.GetType().GetCustomAttribute()!.Name); + } + + [Fact] + public async Task GivenIncomingInstance_WhenExecuteIsCalledWithMissingRecord_ExpectErrorToBeLogged() + { + var app = new ExternalAppIncoming(_logger.Object, _serviceScopeFactory.Object); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var seriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var dicom = InstanceGenerator.GenerateDicomFile(studyInstanceUid, seriesInstanceUid, sopInstanceUid); + var metadata = new DicomFileStorageMetadata(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), studyInstanceUid, seriesInstanceUid, sopInstanceUid); + + _repository.Setup(p => p.GetAsync(It.IsAny(), It.IsAny())).ReturnsAsync(default(RemoteAppExecution)); + + _ = await app.ExecuteAsync(dicom, metadata).ConfigureAwait(false); + + _repository.Verify(p => p.GetAsync(It.IsAny(), It.IsAny()), Times.Once()); + + _logger.VerifyLogging($"Cannot find entry for incoming instance {sopInstanceUid}.", LogLevel.Error, Times.Once()); + } + + [Fact] + public async Task GivenIncomingInstance_WhenExecuteIsCalledWithRecord_ExpectDataToBeFilled() + { + var app = new ExternalAppIncoming(_logger.Object, _serviceScopeFactory.Object); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var seriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var dicom = InstanceGenerator.GenerateDicomFile(studyInstanceUid, seriesInstanceUid, sopInstanceUid); + var metadata = new DicomFileStorageMetadata(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), studyInstanceUid, seriesInstanceUid, sopInstanceUid); + var record = new RemoteAppExecution + { + CorrelationId = Guid.NewGuid().ToString(), + ExportTaskId = Guid.NewGuid().ToString(), + Id = Guid.NewGuid(), + RequestTime = DateTimeOffset.UtcNow, + }; + + record.OriginalValues.Add(DicomTag.StudyInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SeriesInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SOPInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.PatientID.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.AccessionNumber.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.StudyDescription.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + + _repository.Setup(p => p.GetAsync(It.IsAny(), It.IsAny())).ReturnsAsync(record); + + _ = await app.ExecuteAsync(dicom, metadata).ConfigureAwait(false); + + _repository.Verify(p => p.GetAsync(It.IsAny(), It.IsAny()), Times.Once()); + + _logger.VerifyLogging($"Cannot find entry for incoming instance {sopInstanceUid}.", LogLevel.Error, Times.Never()); + + Assert.Equal(record.OriginalValues[DicomTag.StudyInstanceUID.ToString()], dicom.Dataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty)); + Assert.Equal(record.OriginalValues[DicomTag.SeriesInstanceUID.ToString()], dicom.Dataset.GetSingleValueOrDefault(DicomTag.SeriesInstanceUID, string.Empty)); + Assert.Equal(record.OriginalValues[DicomTag.PatientID.ToString()], dicom.Dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty)); + Assert.Equal(record.OriginalValues[DicomTag.AccessionNumber.ToString()], dicom.Dataset.GetSingleValueOrDefault(DicomTag.AccessionNumber, string.Empty)); + Assert.Equal(record.OriginalValues[DicomTag.StudyDescription.ToString()], dicom.Dataset.GetSingleValueOrDefault(DicomTag.StudyDescription, string.Empty)); + + Assert.Equal(record.CorrelationId, metadata.CorrelationId); + Assert.Equal(record.ExportTaskId, metadata.TaskId); + Assert.Equal(record.WorkflowInstanceId, metadata.WorkflowInstanceId); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/ExternalAppOutgoingTest.cs b/src/Plug-ins/RemoteAppExecution/Test/ExternalAppOutgoingTest.cs new file mode 100644 index 000000000..ab57a82be --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/ExternalAppOutgoingTest.cs @@ -0,0 +1,262 @@ +/* + * 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.Reflection; +using FellowOakDicom; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database; +using Monai.Deploy.InformaticsGateway.SharedTest; +using Monai.Deploy.Messaging.Events; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test +{ + public class ExternalAppOutgoingTest + { + private readonly Mock> _logger; + private readonly Mock _serviceScopeFactory; + private readonly ServiceCollection _serviceCollection; + private readonly Mock _repository; + private readonly IOptions _options; + private readonly Mock _serviceScope; + private readonly ServiceProvider _serviceProvider; + + public ExternalAppOutgoingTest() + { + _logger = new Mock>(); + _serviceScopeFactory = new Mock(); + _repository = new Mock(); + _serviceScope = new Mock(); + _options = Options.Create(new PlugInConfiguration()); + + _serviceCollection = new ServiceCollection(); + _serviceCollection.AddScoped(p => _logger.Object); + _serviceCollection.AddScoped(p => _repository.Object); + + _serviceProvider = _serviceCollection.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 GivenExternalAppOutgoing_TestConstructors() + { + Assert.Throws(() => new ExternalAppOutgoing(null, null, null)); + Assert.Throws(() => new ExternalAppOutgoing(_logger.Object, null, null)); + Assert.Throws(() => new ExternalAppOutgoing(_logger.Object, _serviceScopeFactory.Object, null)); + Assert.Throws(() => new ExternalAppOutgoing(_logger.Object, _serviceScopeFactory.Object, _options)); + + _options.Value.RemoteAppConfigurations.Add(SR.ConfigKey_ReplaceTags, "tag1, tag2"); + var app = new ExternalAppOutgoing(_logger.Object, _serviceScopeFactory.Object, _options); + + Assert.Equal(app.Name, app.GetType().GetCustomAttribute()!.Name); + } + + [Fact] + public async Task GivenEmptyReplaceTags_WhenExecuteIsCalledWithoutExistingRecords_ExpectAsync() + { + _options.Value.RemoteAppConfigurations.Add(SR.ConfigKey_ReplaceTags, string.Empty); + var app = new ExternalAppOutgoing(_logger.Object, _serviceScopeFactory.Object, _options); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var seriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var exportRequest = GenerateExportRequest(); + var message = new ExportRequestDataMessage(exportRequest, "file.dcm"); + var dicom = InstanceGenerator.GenerateDicomFile(studyInstanceUid, seriesInstanceUid, sopInstanceUid); + + _ = await app.ExecuteAsync(dicom, message).ConfigureAwait(false); + + _repository.Verify(p => p.GetAsync( + It.Is(p => p == exportRequest.WorkflowInstanceId), + It.Is(p => p == exportRequest.ExportTaskId), + It.Is(p => p == studyInstanceUid), + It.Is(p => p == seriesInstanceUid), + It.IsAny()), Times.Once()); + + _repository.Verify(p => p.AddAsync( + It.Is(p => AssertRecord(p, dicom, exportRequest, studyInstanceUid, seriesInstanceUid, sopInstanceUid)), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GivenReplaceTags_WhenExecuteIsCalledWithoutExistingRecords_ExpectAsync() + { + _options.Value.RemoteAppConfigurations.Add(SR.ConfigKey_ReplaceTags, "StudyInstanceUID,AccessionNumber,PatientID,PatientName"); + + var app = new ExternalAppOutgoing(_logger.Object, _serviceScopeFactory.Object, _options); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var seriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var accessionNumber = Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16); + var patientId = Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16); + var patientName = Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16); + var exportRequest = GenerateExportRequest(); + var message = new ExportRequestDataMessage(exportRequest, "file.dcm"); + var dicom = InstanceGenerator.GenerateDicomFile(studyInstanceUid, seriesInstanceUid, sopInstanceUid); + dicom.Dataset.AddOrUpdate(DicomTag.AccessionNumber, accessionNumber); + dicom.Dataset.AddOrUpdate(DicomTag.PatientID, patientId); + dicom.Dataset.AddOrUpdate(DicomTag.PatientName, patientName); + + _ = await app.ExecuteAsync(dicom, message).ConfigureAwait(false); + + _repository.Verify(p => p.GetAsync( + It.Is(p => p == exportRequest.WorkflowInstanceId), + It.Is(p => p == exportRequest.ExportTaskId), + It.Is(p => p == studyInstanceUid), + It.Is(p => p == seriesInstanceUid), + It.IsAny()), Times.Once()); + + _repository.Verify(p => p.AddAsync( + It.Is(p => AssertRecordWithAdditionalTags(p, dicom, exportRequest, studyInstanceUid, seriesInstanceUid, sopInstanceUid, accessionNumber, patientId, patientName)), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GivenExistingRecordWithSameStudy_WhenExecuteIsCalled_ExpectAsync() + { + _options.Value.RemoteAppConfigurations.Add(SR.ConfigKey_ReplaceTags, string.Empty); + var app = new ExternalAppOutgoing(_logger.Object, _serviceScopeFactory.Object, _options); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var seriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var exportRequest = GenerateExportRequest(); + var message = new ExportRequestDataMessage(exportRequest, "file.dcm"); + var dicom = InstanceGenerator.GenerateDicomFile(studyInstanceUid, seriesInstanceUid, sopInstanceUid); + + _repository.Setup(p => p.GetAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new RemoteAppExecution + { + WorkflowInstanceId = exportRequest.WorkflowInstanceId, + ExportTaskId = exportRequest.ExportTaskId, + StudyInstanceUid = studyInstanceUid, + SeriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID + }); + + _ = await app.ExecuteAsync(dicom, message).ConfigureAwait(false); + + _repository.Verify(p => p.GetAsync( + It.Is(p => p == exportRequest.WorkflowInstanceId), + It.Is(p => p == exportRequest.ExportTaskId), + It.Is(p => p == studyInstanceUid), + It.Is(p => p == seriesInstanceUid), + It.IsAny()), Times.Once()); + + _repository.Verify(p => p.AddAsync( + It.Is(p => AssertRecord(p, dicom, exportRequest, studyInstanceUid, seriesInstanceUid, sopInstanceUid)), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GivenExistingRecordWithSameSeries_WhenExecuteIsCalled_ExpectAsync() + { + _options.Value.RemoteAppConfigurations.Add(SR.ConfigKey_ReplaceTags, string.Empty); + var app = new ExternalAppOutgoing(_logger.Object, _serviceScopeFactory.Object, _options); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var seriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var exportRequest = GenerateExportRequest(); + var message = new ExportRequestDataMessage(exportRequest, "file.dcm"); + var dicom = InstanceGenerator.GenerateDicomFile(studyInstanceUid, seriesInstanceUid, sopInstanceUid); + + _repository.Setup(p => p.GetAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new RemoteAppExecution + { + WorkflowInstanceId = exportRequest.WorkflowInstanceId, + ExportTaskId = exportRequest.ExportTaskId, + StudyInstanceUid = studyInstanceUid, + SeriesInstanceUid = seriesInstanceUid + }); + + _ = await app.ExecuteAsync(dicom, message).ConfigureAwait(false); + + _repository.Verify(p => p.GetAsync( + It.Is(p => p == exportRequest.WorkflowInstanceId), + It.Is(p => p == exportRequest.ExportTaskId), + It.Is(p => p == studyInstanceUid), + It.Is(p => p == seriesInstanceUid), + It.IsAny()), Times.Once()); + + _repository.Verify(p => p.AddAsync( + It.Is(p => AssertRecord(p, dicom, exportRequest, studyInstanceUid, seriesInstanceUid, sopInstanceUid)), + It.IsAny()), Times.Once()); + } + + private bool AssertRecord( + RemoteAppExecution record, + DicomFile dicom, + ExportRequestEvent exportRequest, + string studyInstanceUid, + string seriesInstanceUid, + string sopInstanceUid) + { + return record.WorkflowInstanceId == exportRequest.WorkflowInstanceId && + record.ExportTaskId == exportRequest.ExportTaskId && + record.OriginalValues[DicomTag.StudyInstanceUID.ToString()] == studyInstanceUid && + record.OriginalValues[DicomTag.SeriesInstanceUID.ToString()] == seriesInstanceUid && + record.OriginalValues[DicomTag.SOPInstanceUID.ToString()] == sopInstanceUid && + + record.StudyInstanceUid == dicom.Dataset.GetSingleValue(DicomTag.StudyInstanceUID) && + record.SeriesInstanceUid == dicom.Dataset.GetSingleValue(DicomTag.SeriesInstanceUID) && + record.SopInstanceUid == dicom.Dataset.GetSingleValue(DicomTag.SOPInstanceUID); + } + + private bool AssertRecordWithAdditionalTags( + RemoteAppExecution record, + DicomFile dicom, + ExportRequestEvent exportRequest, + string studyInstanceUid, + string seriesInstanceUid, + string sopInstanceUid, + string accessionNumber, + string patientId, + string patientName) + { + return record.WorkflowInstanceId == exportRequest.WorkflowInstanceId && + record.ExportTaskId == exportRequest.ExportTaskId && + record.OriginalValues[DicomTag.StudyInstanceUID.ToString()] == studyInstanceUid && + record.OriginalValues[DicomTag.SeriesInstanceUID.ToString()] == seriesInstanceUid && + record.OriginalValues[DicomTag.SOPInstanceUID.ToString()] == sopInstanceUid && + record.OriginalValues[DicomTag.AccessionNumber.ToString()] == accessionNumber && + record.OriginalValues[DicomTag.PatientID.ToString()] == patientId && + record.OriginalValues[DicomTag.PatientName.ToString()] == patientName && + + record.StudyInstanceUid == dicom.Dataset.GetSingleValue(DicomTag.StudyInstanceUID) && + record.SeriesInstanceUid == dicom.Dataset.GetSingleValue(DicomTag.SeriesInstanceUID) && + record.SopInstanceUid == dicom.Dataset.GetSingleValue(DicomTag.SOPInstanceUID); + } + + private ExportRequestEvent GenerateExportRequest() => + new() + { + CorrelationId = Guid.NewGuid().ToString(), + ExportTaskId = Guid.NewGuid().ToString(), + WorkflowInstanceId = Guid.NewGuid().ToString(), + }; + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.csproj b/src/Plug-ins/RemoteAppExecution/Test/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.csproj new file mode 100644 index 000000000..5f7833ded --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.csproj @@ -0,0 +1,64 @@ + + + + + + net6.0 + enable + enable + Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test + false + Apache-2.0 + true + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Plug-ins/RemoteAppExecution/Test/packages.lock.json b/src/Plug-ins/RemoteAppExecution/Test/packages.lock.json new file mode 100644 index 000000000..d74179741 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/packages.lock.json @@ -0,0 +1,1601 @@ +{ + "version": 1, + "dependencies": { + "net6.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "tW3lsNS+dAEII6YGUX/VMoJjBS1QvsxqJeqLaJXub08y1FSjasFPtQ4UBUsudE9PNrzLjooClMsPtY2cZLdXpQ==" + }, + "Microsoft.EntityFrameworkCore.InMemory": { + "type": "Direct", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "NJq3pURTBBHWkHgYkZJlCesZ6udyQIlnS2gU8SdR0xZ5VhW3c90tCCkZel38CgPmq29vWfxLurJLEwroIUokzg==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "6.0.21" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Direct", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "iAs1F5gxEQRRGNHDKJ6ZtoSbOAWcjdk+mABEIy2vRLeACp7xBPdQRQdJnENmxykkBgOVef73RpU3xVdDcn8Omg==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.21", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Direct", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "2If1Lt04gD+KrKPFbMUeUzB8Av/EGJJFxNLGfC/CKLgy8+jAYsamyQ/Hux+93XCajJxFLnJimqSg+bBBvXX+2g==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "6.0.21", + "Microsoft.EntityFrameworkCore.Relational": "6.0.21", + "Microsoft.Extensions.DependencyModel": "6.0.0" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.6.3, )", + "resolved": "17.6.3", + "contentHash": "MglaNTl646dC2xpHKotSk1xscmHO5uV3x3NK057IUA9BM3Wgl16WMEb9ptGczk518JfLd1+Th5OAYwnoWgHQQQ==", + "dependencies": { + "Microsoft.CodeCoverage": "17.6.3", + "Microsoft.TestPlatform.TestHost": "17.6.3" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.20.69, )", + "resolved": "4.20.69", + "contentHash": "8P/oAUOL8ZVyXnzBBcgdhTsOD1kQbAWfOcMI7KDQO3HqQtzB/0WYLdnMa4Jefv8nu/MQYiiG0IuoJdvG0v0Nig==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "System.IO.Abstractions.TestingHelpers": { + "type": "Direct", + "requested": "[17.2.3, )", + "resolved": "17.2.3", + "contentHash": "tkXvQbsfOIfeoGso+WptCuouFLiWt3EU8s0D8poqIVz1BJOOszkPuFbFgP2HUTJ9bp5n1HH89eFHILo6Oz5XUw==", + "dependencies": { + "System.IO.Abstractions": "17.2.3" + } + }, + "xRetry": { + "type": "Direct", + "requested": "[1.9.0, )", + "resolved": "1.9.0", + "contentHash": "NeIbJrwpc5EUPagx/mdd/7KzpR36BO8IWrsbgtvOVjxD2xtmNfUHieZ24PeZ4oCYiLBcTviCy+og/bE/OvPchw==", + "dependencies": { + "xunit.core": "[2.4.0, 3.0.0)" + } + }, + "xunit": { + "type": "Direct", + "requested": "[2.5.0, )", + "resolved": "2.5.0", + "contentHash": "f2V5wuAdoaq0mRTt9UBmPbVex9HcwFYn+y7WaKUz5Xpakcrv7lhtQWBJUWNY4N3Z+o+atDBLyAALM1QWx04C6Q==", + "dependencies": { + "xunit.analyzers": "1.2.0", + "xunit.assert": "2.5.0", + "xunit.core": "[2.5.0]" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[2.5.0, )", + "resolved": "2.5.0", + "contentHash": "+Gp9vuC2431yPyKB15YrOTxCuEAErBQUTIs6CquumX1F073UaPHGW0VE/XVJLMh9W4sXdz3TBkcHdFWZrRn2Hw==" + }, + "Ardalis.GuardClauses": { + "type": "Transitive", + "resolved": "4.1.1", + "contentHash": "+UcJ2s+gf2wMNrwadCaHZV2DMcGgBU1t22A+jm40P4MHQRLy9hcleGy5xdVWd4dXZPa5Vlp4TG5xU2rhoDYrBA==" + }, + "AWSSDK.Core": { + "type": "Transitive", + "resolved": "3.7.200.13", + "contentHash": "yiUuhTI8w183euRqhXym1DyhnD/1ccxceRoruCfkIoqY3PAaFgFL8pE4iDLDZa7SUW4M4qZnQ5PMlFr1rrl6zw==" + }, + "AWSSDK.SecurityToken": { + "type": "Transitive", + "resolved": "3.7.201.9", + "contentHash": "yKlTPrvNeDdzkOX82Ydf7MD09Gk3dK74JWZPRWJ3QIxskWVoNTAyLvfVBzbi+/fGnjf8/qKsSzxT7GHLqds37A==", + "dependencies": { + "AWSSDK.Core": "[3.7.200.13, 4.0.0)" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "CommunityToolkit.HighPerformance": { + "type": "Transitive", + "resolved": "8.2.0", + "contentHash": "iKzsPiSnXoQUN5AoApYmdfnLn9osNb+YCLWRr5PFmrDEQVIu7OeOyf4DPvNBvbqbYLZCfvHozPkulyv6zBQsFw==" + }, + "DnsClient": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "4H/f2uYJOZ+YObZjpY9ABrKZI+JNw3uizp6oMzTXwDw6F+2qIPhpRl/1t68O/6e98+vqNiYGu+lswmwdYUy3gg==", + "dependencies": { + "Microsoft.Win32.Registry": "5.0.0" + } + }, + "fo-dicom": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "YraR81u9XuTN7l+pt6HzT0KvuhgWVZ9LCuHMH3zgFfAtv4peT1y+nYMSGwF9YqNP+oZnzh0s0PJ+vJMsTDpGIw==", + "dependencies": { + "CommunityToolkit.HighPerformance": "8.2.0", + "Microsoft.Bcl.AsyncInterfaces": "6.0.0", + "Microsoft.Bcl.HashCode": "1.1.1", + "Microsoft.Extensions.DependencyInjection": "6.0.1", + "Microsoft.Extensions.Logging": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Buffers": "4.5.1", + "System.Text.Encoding.CodePages": "6.0.0", + "System.Text.Encodings.Web": "6.0.0", + "System.Text.Json": "6.0.7", + "System.Threading.Channels": "6.0.0" + } + }, + "Macross.Json.Extensions": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "AkNshs6dopj8FXsmkkJxvLivN2SyDJQDbjcds5lo9+Y6L4zpcoXdmzXQ3VVN+AIWQr0CTD5A7vkuHGAr2aypZg==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.Bcl.HashCode": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "MalY0Y/uM/LjXtHfX/26l2VtN4LDNZ2OE3aumNOHDLsT4fNYy2hiHXI4CXCqKpNUNm7iJ2brrc4J89UdaL56FA==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.6.3", + "contentHash": "Gorg6F1dOxlI28yHYKhbQ3pOOfHeW6sUfsmwFQFaIV+xttUAZ+l8KarHIfsR+rBAnjY9VH71BXvPXBuObCkXsw==" + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "9S+kvYcPyGBqH5KX7sL0d7xYADTUrUVaBz+GZsSx4N4jKh/0mka6IFdeuFYzs3T6wdtHTvzdltcRwucwuTFpdw==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.2" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "XUPcDrn/Vrv9yF4M3b9FYEZvqW1gyS3hfJhFiP0pttuRYnGRB+y3/6g/9k0GIoU62+XkxGa78l1JUccq1uvAXQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.21", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.21", + "Microsoft.Extensions.Caching.Memory": "6.0.1", + "Microsoft.Extensions.DependencyInjection": "6.0.1", + "Microsoft.Extensions.Logging": "6.0.0", + "System.Collections.Immutable": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.1" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "GlNsy7qoFnCxgZlPpb8H/Srq1juOiV6W5XaijSA0+h8V0yn1VJ0owjb01If3di3Covs/8682A+ByTFjmEUxePA==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "gS8tH419vOY2kEyqEZBX8VnXWmtHaor7gVx6zVaXCsEyQurGR/aVB++IZ62vzeQFS9R46LbNY6D6bqEA6j3iCg==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "Ev5FM2KpXJu7+Dm9qvLf1FhSJMytrhSXho92Vompmgeiz3p4InldluidmKKmv8nZQAjs9dTCUUyvk1pxQjysaQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "6.0.21", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "bcz5sSFJbganH0+YrfvIjJDIcKNW7TL07C4d1eTmXy/wOt52iz4LVogJb6pazs7W0+74j0YpXFErvp++Aq5Bsw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "B4y+Cev05eMcjf1na0v9gza6GUtahXbtY1JCypIgx3B4Ea/KAgsWyXEmW4q6zMbmTMtKzmPVk09rvFJirvMwTg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "BUyFU9t+HzlSE7ri4B+AQN2BgTgHv/uM82s5ZkgU1BApyzWzIl48nDsG5wR1t0pniNuuyTBzG3qCW8152/NtSw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "qWzV9o+ZRWq+pGm+1dF+R7qTgTYoXvbyowRoBxQJGfqTpqDun2eteerjRQhq5PQ/14S+lqto3Ft4gYaRyl4rdQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "V4Dth2cYMZpw3HhGw9XUDIijpI6gN+22LDt0AhufIgOppCUfpWX4483OmN+dFXRJkJLc8Tv0Q8QK+1ingT2+KQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "6.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Physical": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "GJGery6QytCzS/BxJ96klgG9in3uH26KcUBbiVG/coNDXCRq6LGVVlUT4vXq34KPuM+R2av+LeYdX9h4IZOCUg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "6.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.Configuration.FileExtensions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "System.Text.Json": "6.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "vWXPg3HJQIpZkENn1KWq8SfbqVujVD7S7vIAyFXXqK5xkf1Vho+vG0bLBCHxU36lD1cLLtmGpfYf0B3MYFi9tQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "TD5QHg98m3+QhgEV1YVoNMl5KtBw/4rjfxLHO0e/YV9bPUBDKntApP4xdrVtGgCeQZHVfC2EXIGsdpRNrr87Pg==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.4", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "6.0.0", + "System.Text.Json": "6.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "1Qf/tEg6IlzbvCxrc+pZE+ZGrajBdB/+V2+abeAu6lg8wXGHmO8JtnrNqwc5svSbcz3udxinUPyH3vw6ZujKbg==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.21", + "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.4", + "Microsoft.Extensions.Options": "6.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "5FSA1euCRtbRqVgTn2ahgCG9Cy29UQXAZMCJLUlrQQaC5rko0+d/aq9SiFGIDP7cvoWUsatrlNdfc6UyOMV5aA==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "0pd4/fho0gC12rQswaGQxbU34jOS1TPS8lZPpkFCH68ppQjHNHYle9iRuHeev1LhrJ94YPvzcRd8UmIuFk23Qw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "QvkL7l0nM8udt3gfyu0Vw8bbCXblxaKOl7c2oBfgGy4LCURRaL9XWZX1FWJrQc43oMokVneVxH38iz+bY1sbhg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "Microsoft.Extensions.FileSystemGlobbing": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "ip8jnL1aPiaPeKINCqaTEbvBFDmVx9dXQEBZ2HOBRXPD1eabGNqP/bKlsIcp7U2lGxiXd5xIhoFcmY8nM4Hdiw==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "GcT5l2CYXL6Sa27KCSh0TixsRfADUgth+ojQSD5EkzisZxmGFh7CwzkcYuGwvmXLjr27uWRNrJ2vuuEjMhU05Q==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "6.0.4", + "contentHash": "K14wYgwOfKVELrUh5eBqlC8Wvo9vvhS3ZhIvcswV2uS/ubkTRPSQsN557EZiYUSSoZNxizG+alN4wjtdyLdcyw==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.6.3", + "contentHash": "gSqtX3RvcFisaLPs6sKXdZkSwUix83NQ9nOU/w6pYrHTl+d8GsVHSL9rvDNxMgoV5BNOdyU7zK7JOfbSaVMDWQ==", + "dependencies": { + "NuGet.Frameworks": "6.5.0", + "System.Reflection.Metadata": "1.6.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.6.3", + "contentHash": "lrgRXKFfIZSPlhuoQGLtciO/osL+4oADYEYb0d5or7n7YyJATIWespq3lRgz2IQpRh6N7cm0DnCOWeZiCRGzxA==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.6.3", + "Newtonsoft.Json": "13.0.1" + } + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "Monai.Deploy.Messaging": { + "type": "Transitive", + "resolved": "0.1.25", + "contentHash": "CllF1ANCwDV0ACbTU63SGxPPmgsivWP8dxgstAHvwo29w5TUs6PDCc8GcyVDTUO5Yl7/vsifdwcs3P/cYBe69w==", + "dependencies": { + "Ardalis.GuardClauses": "4.1.1", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", + "Newtonsoft.Json": "13.0.3", + "System.IO.Abstractions": "17.2.3" + } + }, + "Monai.Deploy.Storage": { + "type": "Transitive", + "resolved": "0.2.18", + "contentHash": "+1JX7QDgVEMqYA0/M1QMr1gtXRC6lEuhBtLfJXWi6cEgh9kOPE0KiHd1AWI7PxBgBbsEBZaNQSvWqShlwcu6bA==", + "dependencies": { + "AWSSDK.SecurityToken": "3.7.201.9", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", + "Monai.Deploy.Storage.S3Policy": "0.2.18", + "System.IO.Abstractions": "17.2.3" + } + }, + "Monai.Deploy.Storage.S3Policy": { + "type": "Transitive", + "resolved": "0.2.18", + "contentHash": "+b0nDnf4OoajdH2hB02elRC6G+GjlYnxJC+F3dGbUUXGMtPApzs8c8s/EG4fKzihxsVovJtqnJl7atcaPyl12Q==", + "dependencies": { + "Ardalis.GuardClauses": "4.1.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "MongoDB.Bson": { + "type": "Transitive", + "resolved": "2.21.0", + "contentHash": "QT+D1I3Jz6r6S6kCgJD1L9dRCLVJCKlkGRkA+tJ7uLpHRmjDNcNKy4D1T+L9gQrjl95lDN9PHdwEytdvCW/jzA==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "5.0.0" + } + }, + "MongoDB.Driver": { + "type": "Transitive", + "resolved": "2.21.0", + "contentHash": "VxKj1wuhadiXhaXkykCWRgsYOysdaOYJ202hJFz25UjkrqC/tHA8RS4hdS5HYfGWoI//fypBXnxZCkEjXLXdfw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "2.0.0", + "MongoDB.Bson": "2.21.0", + "MongoDB.Driver.Core": "2.21.0", + "MongoDB.Libmongocrypt": "1.8.0" + } + }, + "MongoDB.Driver.Core": { + "type": "Transitive", + "resolved": "2.21.0", + "contentHash": "Ac44U3bQfinmdH5KNFjTidJe9LKW87SxkXJ3YuIUJQMITEc4083YF1yvjJxaSeYF9er0YgHSmwhHpsZv0Fwplg==", + "dependencies": { + "AWSSDK.SecurityToken": "3.7.100.14", + "DnsClient": "1.6.1", + "Microsoft.Extensions.Logging.Abstractions": "2.0.0", + "MongoDB.Bson": "2.21.0", + "MongoDB.Libmongocrypt": "1.8.0", + "SharpCompress": "0.30.1", + "Snappier": "1.0.0", + "System.Buffers": "4.5.1", + "ZstdSharp.Port": "0.6.2" + } + }, + "MongoDB.Libmongocrypt": { + "type": "Transitive", + "resolved": "1.8.0", + "contentHash": "fgNw8Dxpkq7mpoaAYes8cfnPRzvFIoB8oL9GPXwi3op/rONftl0WAeg4akRLcxfoVuUvuUO2wGoVBr3JzJ7Svw==" + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "NuGet.Frameworks": { + "type": "Transitive", + "resolved": "6.5.0", + "contentHash": "QWINE2x3MbTODsWT1Gh71GaGb5icBz4chS8VYvTgsBnsi8esgN6wtHhydd7fvToWECYGq7T4cgBBDiKD/363fg==" + }, + "Polly": { + "type": "Transitive", + "resolved": "7.2.4", + "contentHash": "bw00Ck5sh6ekduDE3mnCo1ohzuad946uslCDEENu3091+6UKnBuKLo4e+yaNcCzXxOZCXWY2gV4a35+K1d4LDA==" + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "SharpCompress": { + "type": "Transitive", + "resolved": "0.30.1", + "contentHash": "XqD4TpfyYGa7QTPzaGlMVbcecKnXy4YmYLDWrU+JIj7IuRNl7DH2END+Ll7ekWIY8o3dAMWLFDE1xdhfIWD1nw==" + }, + "Snappier": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "rFtK2KEI9hIe8gtx3a0YDXdHOpedIf9wYCEYtBEmtlyiWVX3XlCNV03JrmmAi/Cdfn7dxK+k0sjjcLv4fpHnqA==" + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "ilkvNhrTersLmIVAcDwwPqfhUFCg19Z1GVMvCSi3xk6Akq94f4qadLORQCq/T8+9JgMiPs+F/NECw5uauviaNw==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.2", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.2" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "A8EBepVqY2lnAp3a8jnhbgzF2tlj2S3HcJQGANTYg/TbYbKa8Z5cM1h74An/vy0svhfzT7tVY0sFmUglLgv+2g==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "zibGtku8M4Eea1R3ZCAxc86QbNvyEN17mAcQkvWKBuHvRpMiK2g5anG4R5Be7cWKSd1i6baYz8y4dMMAKcXKPg==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "lxCZarZdvAsMl2zw9bXHrXK6RxVhB4b23iTFhCOdHFhxfbsxLxWf+ocvswJwR/9Wh/E//ddMi+wJGqUKV7VwoA==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.2" + } + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Abstractions": { + "type": "Transitive", + "resolved": "17.2.3", + "contentHash": "VcozGeE4SxIo0cnXrDHhbrh/Gb8KQnZ3BvMelvh+iw0PrIKtuuA46U2Xm4e4pgnaWFgT4RdZfTpWl/WPRdw0WQ==" + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", + "dependencies": { + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "1.6.0", + "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "6.0.7", + "contentHash": "/Tf/9XjprpHolbcDOrxsKVYy/mUG/FS7aGd9YUgBVEiHeQH4kAE0T1sMbde7q6B5xcrNUsJ5iW7D1RvHudQNqA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "6.0.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "TY8/9+tI0mNaUMgntOxxaq2ndTkdXqLSxvPmas7XEqOlv9lQtB7wLjYGd756lOaO7Dvb5r/WXhluM+0Xe87v5Q==" + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "xunit.abstractions": { + "type": "Transitive", + "resolved": "2.0.3", + "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "d3dehV/DASLRlR8stWQmbPPjfYC2tct50Evav+OlsJMkfFqkhYvzO1k0s81lk0px8O0knZU/FqC8SqbXOtn+hw==" + }, + "xunit.assert": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "wN84pKX5jzfpgJ0bB6arrCA/oelBeYLCpnQ9Wj5xGEVPydKzVSDY5tEatFLHE/rO0+0RC+I4H5igGE118jRh1w==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, + "xunit.core": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "dnV0Mn2s1C0y2m33AylQyMkEyhBQsL4R0302kwSGiEGuY3JwzEmhTa9pnghyMRPliYSs4fXfkEAP+5bKXryGFg==", + "dependencies": { + "xunit.extensibility.core": "[2.5.0]", + "xunit.extensibility.execution": "[2.5.0]" + } + }, + "xunit.extensibility.core": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "xRm6NIV3i7I+LkjsAJ91Xz2fxJm/oMEi2CYq1G5HlGTgcK1Zo2wNbLO6nKX1VG5FZzXibSdoLwr/MofVvh3mFA==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "xunit.abstractions": "2.0.3" + } + }, + "xunit.extensibility.execution": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "7+v2Bvp+1ew1iMGQVb1glICi8jcNdHbRUX6Ru0dmJBViGdjiS7kyqcX2VxleQhFbKNi+WF0an7/TeTXD283RlQ==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "xunit.extensibility.core": "[2.5.0]" + } + }, + "ZstdSharp.Port": { + "type": "Transitive", + "resolved": "0.6.2", + "contentHash": "jPao/LdUNLUz8rn3H1D8W7wQbZsRZM0iayvWI4xGejJg3XJHT56gcmYdgmCGPdJF1UEBqUjucCRrFB+4HbJsbw==" + }, + "monai.deploy.informaticsgateway.api": { + "type": "Project", + "dependencies": { + "Macross.Json.Extensions": "[3.0.0, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.21, )", + "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", + "Monai.Deploy.Messaging": "[0.1.25, )", + "Monai.Deploy.Storage": "[0.2.18, )", + "fo-dicom": "[5.1.1, )" + } + }, + "monai.deploy.informaticsgateway.common": { + "type": "Project", + "dependencies": { + "Ardalis.GuardClauses": "[4.1.1, )", + "System.IO.Abstractions": "[17.2.3, )" + } + }, + "monai.deploy.informaticsgateway.configuration": { + "type": "Project", + "dependencies": { + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )" + } + }, + "monai.deploy.informaticsgateway.database.api": { + "type": "Project", + "dependencies": { + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )" + } + }, + "monai.deploy.informaticsgateway.plugins.remoteappexecution": { + "type": "Project", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[6.0.21, )", + "Microsoft.EntityFrameworkCore.Relational": "[6.0.21, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.21, )", + "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, )", + "MongoDB.Driver": "[2.21.0, )", + "Polly": "[7.2.4, )" + } + } + } + } +} \ No newline at end of file diff --git a/src/Plug-ins/RemoteAppExecution/Utilities.cs b/src/Plug-ins/RemoteAppExecution/Utilities.cs new file mode 100644 index 000000000..8beb9cc94 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Utilities.cs @@ -0,0 +1,59 @@ +/* + * 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.Text.RegularExpressions; +using FellowOakDicom; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution +{ + internal static class Utilities + { + private static DicomTag GetDicomTagByName(string tag) => DicomDictionary.Default[tag] ?? DicomDictionary.Default[Regex.Replace(tag, @"\s+", "", RegexOptions.None, TimeSpan.FromSeconds(1))]; + + public static DicomTag[] GetTagArrayFromStringArray(string values) + { + var names = values.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return names.Select(n => GetDicomTagByName(n)).ToArray(); + } + + public static T? GetTagProxyValue(DicomTag tag) where T : class + { + // partial implementation for now see + // https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html + // for full list + switch (tag.DictionaryEntry.ValueRepresentations[0].Code) + { + case "UI": + case "LO": + case "LT": + { + return DicomUIDGenerator.GenerateDerivedFromUUID().UID as T; + } + case "SH": + case "AE": + case "CS": + case "PN": + case "ST": + case "UT": + { + return "no Value" as T; + } + default: + return default; + } + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/appsettings.json b/src/Plug-ins/RemoteAppExecution/appsettings.json new file mode 100644 index 000000000..57c93015b --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "InformaticsGatewayDatabase": "Data Source=mig.db" + } +} \ No newline at end of file diff --git a/src/Plug-ins/RemoteAppExecution/packages.lock.json b/src/Plug-ins/RemoteAppExecution/packages.lock.json new file mode 100644 index 000000000..3dc27f058 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/packages.lock.json @@ -0,0 +1,577 @@ +{ + "version": 1, + "dependencies": { + "net6.0": { + "Microsoft.EntityFrameworkCore": { + "type": "Direct", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "XUPcDrn/Vrv9yF4M3b9FYEZvqW1gyS3hfJhFiP0pttuRYnGRB+y3/6g/9k0GIoU62+XkxGa78l1JUccq1uvAXQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.21", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.21", + "Microsoft.Extensions.Caching.Memory": "6.0.1", + "Microsoft.Extensions.DependencyInjection": "6.0.1", + "Microsoft.Extensions.Logging": "6.0.0", + "System.Collections.Immutable": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.1" + } + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "Direct", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "G+e0jPI1nD2DHszHXGqO57ogAVIKRy4930jCb7W/v2JfYKVcEbupzdYxEOQAGZws98MXllHNSqeb6fE1EW131A==", + "dependencies": { + "Humanizer.Core": "2.8.26", + "Microsoft.EntityFrameworkCore.Relational": "6.0.21" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Direct", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "Ev5FM2KpXJu7+Dm9qvLf1FhSJMytrhSXho92Vompmgeiz3p4InldluidmKKmv8nZQAjs9dTCUUyvk1pxQjysaQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "6.0.21", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Direct", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "iAs1F5gxEQRRGNHDKJ6ZtoSbOAWcjdk+mABEIy2vRLeACp7xBPdQRQdJnENmxykkBgOVef73RpU3xVdDcn8Omg==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.21", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Direct", + "requested": "[6.0.1, )", + "resolved": "6.0.1", + "contentHash": "BUyFU9t+HzlSE7ri4B+AQN2BgTgHv/uM82s5ZkgU1BApyzWzIl48nDsG5wR1t0pniNuuyTBzG3qCW8152/NtSw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "V4Dth2cYMZpw3HhGw9XUDIijpI6gN+22LDt0AhufIgOppCUfpWX4483OmN+dFXRJkJLc8Tv0Q8QK+1ingT2+KQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "6.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Physical": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "GJGery6QytCzS/BxJ96klgG9in3uH26KcUBbiVG/coNDXCRq6LGVVlUT4vXq34KPuM+R2av+LeYdX9h4IZOCUg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "6.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.Configuration.FileExtensions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "System.Text.Json": "6.0.0" + } + }, + "MongoDB.Driver": { + "type": "Direct", + "requested": "[2.21.0, )", + "resolved": "2.21.0", + "contentHash": "VxKj1wuhadiXhaXkykCWRgsYOysdaOYJ202hJFz25UjkrqC/tHA8RS4hdS5HYfGWoI//fypBXnxZCkEjXLXdfw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "2.0.0", + "MongoDB.Bson": "2.21.0", + "MongoDB.Driver.Core": "2.21.0", + "MongoDB.Libmongocrypt": "1.8.0" + } + }, + "Polly": { + "type": "Direct", + "requested": "[7.2.4, )", + "resolved": "7.2.4", + "contentHash": "bw00Ck5sh6ekduDE3mnCo1ohzuad946uslCDEENu3091+6UKnBuKLo4e+yaNcCzXxOZCXWY2gV4a35+K1d4LDA==" + }, + "Ardalis.GuardClauses": { + "type": "Transitive", + "resolved": "4.1.1", + "contentHash": "+UcJ2s+gf2wMNrwadCaHZV2DMcGgBU1t22A+jm40P4MHQRLy9hcleGy5xdVWd4dXZPa5Vlp4TG5xU2rhoDYrBA==" + }, + "AWSSDK.Core": { + "type": "Transitive", + "resolved": "3.7.200.13", + "contentHash": "yiUuhTI8w183euRqhXym1DyhnD/1ccxceRoruCfkIoqY3PAaFgFL8pE4iDLDZa7SUW4M4qZnQ5PMlFr1rrl6zw==" + }, + "AWSSDK.SecurityToken": { + "type": "Transitive", + "resolved": "3.7.201.9", + "contentHash": "yKlTPrvNeDdzkOX82Ydf7MD09Gk3dK74JWZPRWJ3QIxskWVoNTAyLvfVBzbi+/fGnjf8/qKsSzxT7GHLqds37A==", + "dependencies": { + "AWSSDK.Core": "[3.7.200.13, 4.0.0)" + } + }, + "CommunityToolkit.HighPerformance": { + "type": "Transitive", + "resolved": "8.2.0", + "contentHash": "iKzsPiSnXoQUN5AoApYmdfnLn9osNb+YCLWRr5PFmrDEQVIu7OeOyf4DPvNBvbqbYLZCfvHozPkulyv6zBQsFw==" + }, + "DnsClient": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "4H/f2uYJOZ+YObZjpY9ABrKZI+JNw3uizp6oMzTXwDw6F+2qIPhpRl/1t68O/6e98+vqNiYGu+lswmwdYUy3gg==", + "dependencies": { + "Microsoft.Win32.Registry": "5.0.0" + } + }, + "fo-dicom": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "YraR81u9XuTN7l+pt6HzT0KvuhgWVZ9LCuHMH3zgFfAtv4peT1y+nYMSGwF9YqNP+oZnzh0s0PJ+vJMsTDpGIw==", + "dependencies": { + "CommunityToolkit.HighPerformance": "8.2.0", + "Microsoft.Bcl.AsyncInterfaces": "6.0.0", + "Microsoft.Bcl.HashCode": "1.1.1", + "Microsoft.Extensions.DependencyInjection": "6.0.1", + "Microsoft.Extensions.Logging": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Buffers": "4.5.1", + "System.Text.Encoding.CodePages": "6.0.0", + "System.Text.Encodings.Web": "6.0.0", + "System.Text.Json": "6.0.7", + "System.Threading.Channels": "6.0.0" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.8.26", + "contentHash": "OiKusGL20vby4uDEswj2IgkdchC1yQ6rwbIkZDVBPIR6al2b7n3pC91elBul9q33KaBgRKhbZH3+2Ur4fnWx2A==" + }, + "Macross.Json.Extensions": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "AkNshs6dopj8FXsmkkJxvLivN2SyDJQDbjcds5lo9+Y6L4zpcoXdmzXQ3VVN+AIWQr0CTD5A7vkuHGAr2aypZg==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.Bcl.HashCode": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "MalY0Y/uM/LjXtHfX/26l2VtN4LDNZ2OE3aumNOHDLsT4fNYy2hiHXI4CXCqKpNUNm7iJ2brrc4J89UdaL56FA==" + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "9S+kvYcPyGBqH5KX7sL0d7xYADTUrUVaBz+GZsSx4N4jKh/0mka6IFdeuFYzs3T6wdtHTvzdltcRwucwuTFpdw==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.2" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "GlNsy7qoFnCxgZlPpb8H/Srq1juOiV6W5XaijSA0+h8V0yn1VJ0owjb01If3di3Covs/8682A+ByTFjmEUxePA==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "gS8tH419vOY2kEyqEZBX8VnXWmtHaor7gVx6zVaXCsEyQurGR/aVB++IZ62vzeQFS9R46LbNY6D6bqEA6j3iCg==" + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "2If1Lt04gD+KrKPFbMUeUzB8Av/EGJJFxNLGfC/CKLgy8+jAYsamyQ/Hux+93XCajJxFLnJimqSg+bBBvXX+2g==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "6.0.21", + "Microsoft.EntityFrameworkCore.Relational": "6.0.21", + "Microsoft.Extensions.DependencyModel": "6.0.0" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "bcz5sSFJbganH0+YrfvIjJDIcKNW7TL07C4d1eTmXy/wOt52iz4LVogJb6pazs7W0+74j0YpXFErvp++Aq5Bsw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "B4y+Cev05eMcjf1na0v9gza6GUtahXbtY1JCypIgx3B4Ea/KAgsWyXEmW4q6zMbmTMtKzmPVk09rvFJirvMwTg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "qWzV9o+ZRWq+pGm+1dF+R7qTgTYoXvbyowRoBxQJGfqTpqDun2eteerjRQhq5PQ/14S+lqto3Ft4gYaRyl4rdQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "vWXPg3HJQIpZkENn1KWq8SfbqVujVD7S7vIAyFXXqK5xkf1Vho+vG0bLBCHxU36lD1cLLtmGpfYf0B3MYFi9tQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "TD5QHg98m3+QhgEV1YVoNMl5KtBw/4rjfxLHO0e/YV9bPUBDKntApP4xdrVtGgCeQZHVfC2EXIGsdpRNrr87Pg==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.4", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "6.0.0", + "System.Text.Json": "6.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "1Qf/tEg6IlzbvCxrc+pZE+ZGrajBdB/+V2+abeAu6lg8wXGHmO8JtnrNqwc5svSbcz3udxinUPyH3vw6ZujKbg==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.21", + "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.4", + "Microsoft.Extensions.Options": "6.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "5FSA1euCRtbRqVgTn2ahgCG9Cy29UQXAZMCJLUlrQQaC5rko0+d/aq9SiFGIDP7cvoWUsatrlNdfc6UyOMV5aA==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "0pd4/fho0gC12rQswaGQxbU34jOS1TPS8lZPpkFCH68ppQjHNHYle9iRuHeev1LhrJ94YPvzcRd8UmIuFk23Qw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "QvkL7l0nM8udt3gfyu0Vw8bbCXblxaKOl7c2oBfgGy4LCURRaL9XWZX1FWJrQc43oMokVneVxH38iz+bY1sbhg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "Microsoft.Extensions.FileSystemGlobbing": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "ip8jnL1aPiaPeKINCqaTEbvBFDmVx9dXQEBZ2HOBRXPD1eabGNqP/bKlsIcp7U2lGxiXd5xIhoFcmY8nM4Hdiw==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "GcT5l2CYXL6Sa27KCSh0TixsRfADUgth+ojQSD5EkzisZxmGFh7CwzkcYuGwvmXLjr27uWRNrJ2vuuEjMhU05Q==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "6.0.4", + "contentHash": "K14wYgwOfKVELrUh5eBqlC8Wvo9vvhS3ZhIvcswV2uS/ubkTRPSQsN557EZiYUSSoZNxizG+alN4wjtdyLdcyw==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "Monai.Deploy.Messaging": { + "type": "Transitive", + "resolved": "0.1.25", + "contentHash": "CllF1ANCwDV0ACbTU63SGxPPmgsivWP8dxgstAHvwo29w5TUs6PDCc8GcyVDTUO5Yl7/vsifdwcs3P/cYBe69w==", + "dependencies": { + "Ardalis.GuardClauses": "4.1.1", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", + "Newtonsoft.Json": "13.0.3", + "System.IO.Abstractions": "17.2.3" + } + }, + "Monai.Deploy.Storage": { + "type": "Transitive", + "resolved": "0.2.18", + "contentHash": "+1JX7QDgVEMqYA0/M1QMr1gtXRC6lEuhBtLfJXWi6cEgh9kOPE0KiHd1AWI7PxBgBbsEBZaNQSvWqShlwcu6bA==", + "dependencies": { + "AWSSDK.SecurityToken": "3.7.201.9", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", + "Monai.Deploy.Storage.S3Policy": "0.2.18", + "System.IO.Abstractions": "17.2.3" + } + }, + "Monai.Deploy.Storage.S3Policy": { + "type": "Transitive", + "resolved": "0.2.18", + "contentHash": "+b0nDnf4OoajdH2hB02elRC6G+GjlYnxJC+F3dGbUUXGMtPApzs8c8s/EG4fKzihxsVovJtqnJl7atcaPyl12Q==", + "dependencies": { + "Ardalis.GuardClauses": "4.1.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "MongoDB.Bson": { + "type": "Transitive", + "resolved": "2.21.0", + "contentHash": "QT+D1I3Jz6r6S6kCgJD1L9dRCLVJCKlkGRkA+tJ7uLpHRmjDNcNKy4D1T+L9gQrjl95lDN9PHdwEytdvCW/jzA==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "5.0.0" + } + }, + "MongoDB.Driver.Core": { + "type": "Transitive", + "resolved": "2.21.0", + "contentHash": "Ac44U3bQfinmdH5KNFjTidJe9LKW87SxkXJ3YuIUJQMITEc4083YF1yvjJxaSeYF9er0YgHSmwhHpsZv0Fwplg==", + "dependencies": { + "AWSSDK.SecurityToken": "3.7.100.14", + "DnsClient": "1.6.1", + "Microsoft.Extensions.Logging.Abstractions": "2.0.0", + "MongoDB.Bson": "2.21.0", + "MongoDB.Libmongocrypt": "1.8.0", + "SharpCompress": "0.30.1", + "Snappier": "1.0.0", + "System.Buffers": "4.5.1", + "ZstdSharp.Port": "0.6.2" + } + }, + "MongoDB.Libmongocrypt": { + "type": "Transitive", + "resolved": "1.8.0", + "contentHash": "fgNw8Dxpkq7mpoaAYes8cfnPRzvFIoB8oL9GPXwi3op/rONftl0WAeg4akRLcxfoVuUvuUO2wGoVBr3JzJ7Svw==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "SharpCompress": { + "type": "Transitive", + "resolved": "0.30.1", + "contentHash": "XqD4TpfyYGa7QTPzaGlMVbcecKnXy4YmYLDWrU+JIj7IuRNl7DH2END+Ll7ekWIY8o3dAMWLFDE1xdhfIWD1nw==" + }, + "Snappier": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "rFtK2KEI9hIe8gtx3a0YDXdHOpedIf9wYCEYtBEmtlyiWVX3XlCNV03JrmmAi/Cdfn7dxK+k0sjjcLv4fpHnqA==" + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "ilkvNhrTersLmIVAcDwwPqfhUFCg19Z1GVMvCSi3xk6Akq94f4qadLORQCq/T8+9JgMiPs+F/NECw5uauviaNw==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.2", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.2" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "A8EBepVqY2lnAp3a8jnhbgzF2tlj2S3HcJQGANTYg/TbYbKa8Z5cM1h74An/vy0svhfzT7tVY0sFmUglLgv+2g==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "zibGtku8M4Eea1R3ZCAxc86QbNvyEN17mAcQkvWKBuHvRpMiK2g5anG4R5Be7cWKSd1i6baYz8y4dMMAKcXKPg==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "lxCZarZdvAsMl2zw9bXHrXK6RxVhB4b23iTFhCOdHFhxfbsxLxWf+ocvswJwR/9Wh/E//ddMi+wJGqUKV7VwoA==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.2" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.IO.Abstractions": { + "type": "Transitive", + "resolved": "17.2.3", + "contentHash": "VcozGeE4SxIo0cnXrDHhbrh/Gb8KQnZ3BvMelvh+iw0PrIKtuuA46U2Xm4e4pgnaWFgT4RdZfTpWl/WPRdw0WQ==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "6.0.7", + "contentHash": "/Tf/9XjprpHolbcDOrxsKVYy/mUG/FS7aGd9YUgBVEiHeQH4kAE0T1sMbde7q6B5xcrNUsJ5iW7D1RvHudQNqA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "6.0.0" + } + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "TY8/9+tI0mNaUMgntOxxaq2ndTkdXqLSxvPmas7XEqOlv9lQtB7wLjYGd756lOaO7Dvb5r/WXhluM+0Xe87v5Q==" + }, + "ZstdSharp.Port": { + "type": "Transitive", + "resolved": "0.6.2", + "contentHash": "jPao/LdUNLUz8rn3H1D8W7wQbZsRZM0iayvWI4xGejJg3XJHT56gcmYdgmCGPdJF1UEBqUjucCRrFB+4HbJsbw==" + }, + "monai.deploy.informaticsgateway.api": { + "type": "Project", + "dependencies": { + "Macross.Json.Extensions": "[3.0.0, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.21, )", + "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", + "Monai.Deploy.Messaging": "[0.1.25, )", + "Monai.Deploy.Storage": "[0.2.18, )", + "fo-dicom": "[5.1.1, )" + } + }, + "monai.deploy.informaticsgateway.common": { + "type": "Project", + "dependencies": { + "Ardalis.GuardClauses": "[4.1.1, )", + "System.IO.Abstractions": "[17.2.3, )" + } + }, + "monai.deploy.informaticsgateway.configuration": { + "type": "Project", + "dependencies": { + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )" + } + }, + "monai.deploy.informaticsgateway.database.api": { + "type": "Project", + "dependencies": { + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )" + } + } + } + } +} \ No newline at end of file diff --git a/src/Shared/Test/InstanceGenerator.cs b/src/Shared/Test/InstanceGenerator.cs index 2287c18d6..954d738b7 100644 --- a/src/Shared/Test/InstanceGenerator.cs +++ b/src/Shared/Test/InstanceGenerator.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. @@ -14,7 +14,6 @@ * limitations under the License. */ -using System.IO; using System.IO.Abstractions; using FellowOakDicom; using FellowOakDicom.Network; @@ -65,7 +64,7 @@ public static byte[] GenerateDicomData( var dataset = GenerateDicomDataset(studyInstanceUid, seriesInstanceUid, ref sopInstanceUid); var dicomfile = new DicomFile(dataset); - using var ms = new MemoryStream(); + using var ms = new System.IO.MemoryStream(); dicomfile.Save(ms); return ms.ToArray(); } diff --git a/tests/Integration.Test/Common/Assertions.cs b/tests/Integration.Test/Common/Assertions.cs index 09012a6bc..1f039fde1 100644 --- a/tests/Integration.Test/Common/Assertions.cs +++ b/tests/Integration.Test/Common/Assertions.cs @@ -302,5 +302,52 @@ internal void ShoulddHaveCorrectNumberOfAckMessages(Dictionary r segment.Fields(9).Value.Should().Be("ACK"); } } + + internal async Task ShouldRestoreAllDicomMetaata(IReadOnlyList messages, Dictionary originalDicomFiles, params DicomTag[] dicomTags) + { + Guard.Against.Null(messages, nameof(messages)); + Guard.Against.NullOrEmpty(originalDicomFiles, nameof(originalDicomFiles)); + + var minioClient = GetMinioClient(); + + foreach (var message in messages) + { + var request = message.ConvertTo(); + foreach (var file in request.Payload) + { + await _retryPolicy.ExecuteAsync(async () => + { + var dicomValidationKey = string.Empty; + + _outputHelper.WriteLine($"Reading file from {request.Bucket} => {request.PayloadId}/{file.Path}."); + var getObjectArgs = new GetObjectArgs() + .WithBucket(request.Bucket) + .WithObject($"{request.PayloadId}/{file.Path}") + .WithCallbackStream((stream) => + { + using var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + memoryStream.Position = 0; + var dicomFile = DicomFile.Open(memoryStream); + dicomValidationKey = dicomFile.GenerateFileName(); + originalDicomFiles.Should().ContainKey(dicomValidationKey); + CompareDicomFiles(originalDicomFiles[dicomValidationKey], dicomFile, dicomTags); + }); + await minioClient.GetObjectAsync(getObjectArgs); + }); + } + } + } + + private void CompareDicomFiles(DicomFile left, DicomFile right, DicomTag[] dicomTags) + { + left.Should().NotBeNull(); + right.Should().NotBeNull(); + + foreach (var tag in dicomTags) + { + left.Dataset.GetString(tag).Should().Be(right.Dataset.GetString(tag)); + } + } } } diff --git a/tests/Integration.Test/Common/DataProvider.cs b/tests/Integration.Test/Common/DataProvider.cs index 9d2488e24..b4fac9bcf 100644 --- a/tests/Integration.Test/Common/DataProvider.cs +++ b/tests/Integration.Test/Common/DataProvider.cs @@ -25,6 +25,128 @@ namespace Monai.Deploy.InformaticsGateway.Integration.Test.Common { + internal static class DicomRandomDataProvider + { + private static readonly Random Random = new(); + private static readonly string AlphaNumeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + private static readonly string Numeric = "0123456789"; + + public static void InjectRandomData(this DicomDataset dataset, DicomTag tag) + { + var data = string.Empty; + switch (tag.DictionaryEntry.ValueRepresentations[0].Code) + { + case "IS": + data = RandomString(Numeric, 12); + dataset.AddOrUpdate(tag, Convert.ToInt32(data)); + return; + + case "UI": + data = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + break; + + case "LO": + case "LT": + data = RandomString(AlphaNumeric, 64); + break; + + case "AE": + data = RandomString(AlphaNumeric, 16); + break; + + case "CS": + data = RandomString(Numeric, 16); + break; + + case "FL": + var bufferFl = new byte[4]; + Random.NextBytes(bufferFl); + dataset.AddOrUpdate(tag, BitConverter.ToSingle(bufferFl, 0).ToString()); + return; + + case "FD": + var bufferFd = new byte[8]; + Random.NextBytes(bufferFd); + dataset.AddOrUpdate(tag, BitConverter.ToSingle(bufferFd, 0).ToString()); + return; + + case "OD": + var bufferOd = new byte[8]; + Random.NextBytes(bufferOd); + dataset.AddOrUpdate(tag, BitConverter.ToSingle(bufferOd, 0)); + return; + + case "OF": + var bufferOf = new byte[4]; + Random.NextBytes(bufferOf); + dataset.AddOrUpdate(tag, BitConverter.ToSingle(bufferOf, 0)); + return; + + case "PN": + data = RandomString(AlphaNumeric, 64); + break; + + case "DA": + data = "20000101"; + break; + + case "DT": + data = "20000101000000"; + break; + + case "TM": + data = "000000"; + break; + + case "SH": + data = RandomString(Numeric, 16); + break; + + case "DS": + case "SL": + case "UL": + data = RandomString(Numeric, 4); + break; + + case "SS": + case "US": + data = RandomString(Numeric, 2); + break; + + case "OB": + case "OW": + var bufferBytes = new byte[4]; + Random.NextBytes(bufferBytes); + dataset.AddOrUpdate(tag, bufferBytes); + break; + + case "ST": + case "UN": + case "UT": + data = RandomString(AlphaNumeric, 1024); + break; + + case "AS": + data = $"{RandomString(Numeric, 3).PadLeft(3, '0')}Y"; + break; + } + dataset.AddOrUpdate(tag, data); + } + + public static string RandomString(string characterSet, int maxLength) + { + var length = Random.Next(1, maxLength); + var output = new char[length]; + + for (int i = 0; i < length; i++) + { + output[i] = characterSet[Random.Next(characterSet.Length)]; + } + + return new string(output); + } + } + internal class DataProvider { private readonly Configurations _configurations; @@ -71,10 +193,21 @@ internal void GenerateDicomData(string modality, int studyCount, int? seriesPerS _outputHelper.WriteLine($"File specs: {DicomSpecs.StudyCount}, {DicomSpecs.SeriesPerStudyCount}, {DicomSpecs.InstancePerSeries}, {DicomSpecs.FileCount}"); } + internal void InjectRandomData(params DicomTag[] tags) + { + foreach (var dicomFile in DicomSpecs.Files.Values) + { + foreach (var tag in tags) + { + dicomFile.Dataset.InjectRandomData(tag); + } + } + } + internal void ReplaceGeneratedDicomDataWithHashes() { var dicomFileSize = new Dictionary(); - foreach (var dicomFile in DicomSpecs.Files) + foreach (var dicomFile in DicomSpecs.Files.Values) { var key = dicomFile.GenerateFileName(); dicomFileSize[key] = dicomFile.CalculateHash(); @@ -105,12 +238,12 @@ internal void GenerateAcrRequest(string requestType) case "Patient": inferenceRequest.InputMetadata.Details.Type = InferenceRequestType.DicomPatientId; - inferenceRequest.InputMetadata.Details.PatientId = DicomSpecs.Files[0].Dataset.GetSingleValue(DicomTag.PatientID); + inferenceRequest.InputMetadata.Details.PatientId = DicomSpecs.Files.Values.First().Dataset.GetSingleValue(DicomTag.PatientID); break; case "AccessionNumber": inferenceRequest.InputMetadata.Details.Type = InferenceRequestType.AccessionNumber; - inferenceRequest.InputMetadata.Details.AccessionNumber = new List() { DicomSpecs.Files[0].Dataset.GetSingleValue(DicomTag.AccessionNumber) }; + inferenceRequest.InputMetadata.Details.AccessionNumber = new List() { DicomSpecs.Files.Values.First().Dataset.GetSingleValue(DicomTag.AccessionNumber) }; break; default: diff --git a/tests/Integration.Test/Common/DicomCStoreDataClient.cs b/tests/Integration.Test/Common/DicomCStoreDataClient.cs index d84b75765..bd010a8d0 100644 --- a/tests/Integration.Test/Common/DicomCStoreDataClient.cs +++ b/tests/Integration.Test/Common/DicomCStoreDataClient.cs @@ -62,10 +62,10 @@ public async Task SendAsync(DataProvider dataProvider, params object[] args) var failureStatus = new List(); for (int i = 0; i < associations; i++) { - var files = dataProvider.DicomSpecs.Files.Skip(i * filesPerAssociations).Take(filesPerAssociations).ToList(); // .Take(20) + var files = dataProvider.DicomSpecs.Files.Values.Skip(i * filesPerAssociations).Take(filesPerAssociations).ToList(); // .Take(20) if (i + 1 == associations && dataProvider.DicomSpecs.Files.Count > (i + 1) * filesPerAssociations) { - files.AddRange(dataProvider.DicomSpecs.Files.Skip(i * filesPerAssociations)); + files.AddRange(dataProvider.DicomSpecs.Files.Values.Skip(i * filesPerAssociations)); } try diff --git a/tests/Integration.Test/Common/DicomDataSpecs.cs b/tests/Integration.Test/Common/DicomDataSpecs.cs index 9fa614187..b5db556e6 100644 --- a/tests/Integration.Test/Common/DicomDataSpecs.cs +++ b/tests/Integration.Test/Common/DicomDataSpecs.cs @@ -25,7 +25,7 @@ internal class DicomDataSpecs public int SeriesPerStudyCount { get; set; } public int InstancePerSeries { get; set; } public int FileCount { get; set; } - public List Files { get; set; } + public Dictionary Files { get; set; } = new Dictionary(); public Dictionary FileHashes { get; set; } = new Dictionary(); public int NumberOfExpectedRequests(string grouping) => grouping switch diff --git a/tests/Integration.Test/Common/DicomScp.cs b/tests/Integration.Test/Common/DicomScp.cs index 8b5bb65e5..ea56624f3 100644 --- a/tests/Integration.Test/Common/DicomScp.cs +++ b/tests/Integration.Test/Common/DicomScp.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. @@ -18,12 +18,11 @@ using FellowOakDicom; using FellowOakDicom.Network; using Microsoft.Extensions.Logging; -using Monai.Deploy.InformaticsGateway.Test.Plugins; +using Monai.Deploy.InformaticsGateway.Test.PlugIns; using TechTalk.SpecFlow.Infrastructure; namespace Monai.Deploy.InformaticsGateway.Integration.Test.Common { - public class DicomScp : IDisposable { public readonly string FeatureScpAeTitle = "TEST-SCP"; @@ -33,7 +32,11 @@ public class DicomScp : IDisposable private bool _disposedValue; public Dictionary> Instances { get; set; } = new Dictionary>(); + public Dictionary DicomFiles { get; set; } = new Dictionary(); public ISpecFlowOutputHelper OutputHelper { get; set; } + + public bool ClearFilesAndUseHashes { get; set; } = true; + public DicomScp(ISpecFlowOutputHelper outputHelper) { OutputHelper = outputHelper ?? throw new ArgumentNullException(nameof(outputHelper)); @@ -54,7 +57,6 @@ protected virtual void Dispose(bool disposing) } } - public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method @@ -63,8 +65,6 @@ public void Dispose() } } - - internal class CStoreScp : DicomService, IDicomServiceProvider, IDicomCStoreProvider { private static readonly object SyncLock = new object(); @@ -99,10 +99,17 @@ public Task OnCStoreRequestAsync(DicomCStoreRequest request var key = request.File.GenerateFileName(); lock (SyncLock) { - var values = new List(); - data.Instances.Add(key, values); - values.Add(request.File.CalculateHash()); - values.Add(request.File.Dataset.GetSingleValueOrDefault(TestOutputDataPluginModifyDicomFile.ExpectedTag, string.Empty)); + if (data.ClearFilesAndUseHashes) + { + var values = new List(); + data.Instances.Add(key, values); + values.Add(request.File.CalculateHash()); + values.Add(request.File.Dataset.GetSingleValueOrDefault(TestOutputDataPlugInModifyDicomFile.ExpectedTag, string.Empty)); + } + else + { + data.DicomFiles.Add(key, request.File); + } } data.OutputHelper.WriteLine("Instance received {0}", key); @@ -144,6 +151,7 @@ public Task OnReceiveAssociationRequestAsync(DicomAssociation association) return SendAssociationAcceptAsync(association); } - public void OnConnectionClosed(Exception exception) { } + public void OnConnectionClosed(Exception exception) + { } } } diff --git a/tests/Integration.Test/Common/MinioDataSink.cs b/tests/Integration.Test/Common/MinioDataSink.cs index 7d90f2ebd..dde07d4a1 100644 --- a/tests/Integration.Test/Common/MinioDataSink.cs +++ b/tests/Integration.Test/Common/MinioDataSink.cs @@ -50,7 +50,7 @@ await _retryPolicy.ExecuteAsync(async () => _outputHelper.WriteLine($"Uploading {dataProvider.DicomSpecs.FileCount} files to MinIO..."); - foreach (var file in dataProvider.DicomSpecs.Files) + foreach (var file in dataProvider.DicomSpecs.Files.Values) { var filename = file.GenerateFileName(); var stream = new MemoryStream(); diff --git a/tests/Integration.Test/Drivers/DicomInstanceGenerator.cs b/tests/Integration.Test/Drivers/DicomInstanceGenerator.cs index 727c19cf3..539d95476 100644 --- a/tests/Integration.Test/Drivers/DicomInstanceGenerator.cs +++ b/tests/Integration.Test/Drivers/DicomInstanceGenerator.cs @@ -103,7 +103,7 @@ public DicomDataSpecs Generate(string patientId, int studiesPerPatient, int seri studySpec.InstanceMax.Should().BeGreaterThan(0); var instancesPerSeries = _random.Next(studySpec.InstanceMin, studySpec.InstanceMax); - var files = new List(); + var files = new Dictionary(); var studyInstanceUids = new List(); DicomFile dicomFile = null; @@ -120,7 +120,7 @@ public DicomDataSpecs Generate(string patientId, int studiesPerPatient, int seri { var size = _random.NextLong(studySpec.SizeMinBytes, studySpec.SizeMaxBytes); dicomFile = generator.GenerateNewInstance(size); - files.Add(dicomFile); + files.Add(dicomFile.GenerateFileName(), dicomFile); } } _outputHelper.WriteLine("DICOM Instance: PID={0}, STUDY={1}", diff --git a/tests/Integration.Test/Features/RemoteAppExecutionPlugIn.feature b/tests/Integration.Test/Features/RemoteAppExecutionPlugIn.feature new file mode 100644 index 000000000..bfb40fd59 --- /dev/null +++ b/tests/Integration.Test/Features/RemoteAppExecutionPlugIn.feature @@ -0,0 +1,27 @@ +# Copyright 2022 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. + +# @ignored + +Feature: Remote App Execution Plug-in + +This feature tests the Remote App Execution plug-ins for de-identifying and +re-identifying data sent and received by the MIG respectively. + + + @messaging_workflow_request @messaging + Scenario: End-to-end test of plug-ins + Given a study that is exported to the test host + When the study is received and sent back to Informatics Gateway + Then ensure the original study and the received study are the same diff --git a/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj b/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj index f9779a509..83c57f973 100644 --- a/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj +++ b/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj @@ -24,6 +24,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -66,7 +70,8 @@ - + + @@ -90,7 +95,7 @@ - + diff --git a/tests/Integration.Test/StepDefinitions/DicomDimseScpServicesStepDefinitions.cs b/tests/Integration.Test/StepDefinitions/DicomDimseScpServicesStepDefinitions.cs index 5f2e3a325..ef7d97020 100644 --- a/tests/Integration.Test/StepDefinitions/DicomDimseScpServicesStepDefinitions.cs +++ b/tests/Integration.Test/StepDefinitions/DicomDimseScpServicesStepDefinitions.cs @@ -109,7 +109,7 @@ await _informaticsGatewayClient.MonaiScpAeTitle.Create(new MonaiApplicationEntit Grouping = grouping, Timeout = groupingTimeout, Workflows = new List(DummyWorkflows), - PluginAssemblies = new List() { typeof(Monai.Deploy.InformaticsGateway.Test.Plugins.TestInputDataPluginModifyDicomFile).AssemblyQualifiedName } + PlugInAssemblies = new List() { typeof(Monai.Deploy.InformaticsGateway.Test.PlugIns.TestInputDataPlugInModifyDicomFile).AssemblyQualifiedName } }, CancellationToken.None); _dataProvider.Workflows = DummyWorkflows; diff --git a/tests/Integration.Test/StepDefinitions/DicomWebStowServiceStepDefinitions.cs b/tests/Integration.Test/StepDefinitions/DicomWebStowServiceStepDefinitions.cs index 3c6c40e17..92970b5af 100644 --- a/tests/Integration.Test/StepDefinitions/DicomWebStowServiceStepDefinitions.cs +++ b/tests/Integration.Test/StepDefinitions/DicomWebStowServiceStepDefinitions.cs @@ -24,7 +24,7 @@ using Monai.Deploy.InformaticsGateway.DicomWeb.Client; using Monai.Deploy.InformaticsGateway.Integration.Test.Common; using Monai.Deploy.InformaticsGateway.Integration.Test.Drivers; -using Monai.Deploy.InformaticsGateway.Test.Plugins; +using Monai.Deploy.InformaticsGateway.Test.PlugIns; namespace Monai.Deploy.InformaticsGateway.Integration.Test.StepDefinitions { @@ -65,7 +65,7 @@ await _informaticsGatewayClient.VirtualAeTitle.Create(new VirtualApplicationEnti Name = virtualAe, VirtualAeTitle = virtualAe, Workflows = new List(DummyWorkflows), - PluginAssemblies = new List() { typeof(Monai.Deploy.InformaticsGateway.Test.Plugins.TestInputDataPluginVirtualAE).AssemblyQualifiedName } + PlugInAssemblies = new List() { typeof(Monai.Deploy.InformaticsGateway.Test.PlugIns.TestInputDataPlugInVirtualAE).AssemblyQualifiedName } }, CancellationToken.None); _dataProvider.Workflows = DummyWorkflows; @@ -110,7 +110,7 @@ public async Task WhenStudiesAreUploadedToTheDicomWebStowRSServiceWithoutStudyIn await _dataSink.SendAsync(_dataProvider, $"{_configurations.InformaticsGatewayOptions.ApiEndpoint}{endpoint}", _dataProvider.Workflows, async (DicomWebClient dicomWebClient, DicomDataSpecs specs) => { - return await dicomWebClient.Stow.Store(specs.Files); + return await dicomWebClient.Stow.Store(specs.Files.Values); }); _dataProvider.ReplaceGeneratedDicomDataWithHashes(); } @@ -122,7 +122,7 @@ public async Task WhenStudiesAreUploadedToTheDicomWebStowRSServiceWithoutOverrid await _dataSink.SendAsync(_dataProvider, $"{_configurations.InformaticsGatewayOptions.ApiEndpoint}{endpoint}", null, async (DicomWebClient dicomWebClient, DicomDataSpecs specs) => { - return await dicomWebClient.Stow.Store(specs.Files); + return await dicomWebClient.Stow.Store(specs.Files.Values); }); _dataProvider.ReplaceGeneratedDicomDataWithHashes(); } @@ -135,21 +135,21 @@ public async Task WhenStudiesAreUploadedToTheDicomWebStowRSServiceWithStudyInsta await _dataSink.SendAsync(_dataProvider, $"{_configurations.InformaticsGatewayOptions.ApiEndpoint}{endpoint}", _dataProvider.Workflows, async (DicomWebClient dicomWebClient, DicomDataSpecs specs) => { // Note: the MIG DICOMweb client ignores instances without matching StudyInstanceUID. - return await dicomWebClient.Stow.Store(specs.StudyInstanceUids.First(), specs.Files); + return await dicomWebClient.Stow.Store(specs.StudyInstanceUids.First(), specs.Files.Values); }); _dataProvider.ReplaceGeneratedDicomDataWithHashes(); } [Then(@"studies are uploaded to storage service with data input VAE plugin")] - public async Task ThenXXFilesUploadedToStorageServiceWithDataInputPlugins() + public async Task ThenXXFilesUploadedToStorageServiceWithDataInputPlugIns() { await _assertions.ShouldHaveUploadedDicomDataToMinio( _receivedMessages.Messages, _dataProvider.DicomSpecs.FileHashes, (dicomFile) => { - dicomFile.Dataset.GetString(TestInputDataPluginVirtualAE.ExpectedTag) - .Should().Be(TestInputDataPluginVirtualAE.ExpectedValue); + dicomFile.Dataset.GetString(TestInputDataPlugInVirtualAE.ExpectedTag) + .Should().Be(TestInputDataPlugInVirtualAE.ExpectedValue); }); } } diff --git a/tests/Integration.Test/StepDefinitions/ExportServicesStepDefinitions.cs b/tests/Integration.Test/StepDefinitions/ExportServicesStepDefinitions.cs index d36acc16e..8ca5811f9 100644 --- a/tests/Integration.Test/StepDefinitions/ExportServicesStepDefinitions.cs +++ b/tests/Integration.Test/StepDefinitions/ExportServicesStepDefinitions.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. @@ -26,7 +26,7 @@ using Monai.Deploy.InformaticsGateway.Integration.Test.Common; using Monai.Deploy.InformaticsGateway.Integration.Test.Drivers; using Monai.Deploy.InformaticsGateway.Integration.Test.Hooks; -using Monai.Deploy.InformaticsGateway.Test.Plugins; +using Monai.Deploy.InformaticsGateway.Test.PlugIns; using Monai.Deploy.Messaging.Events; using Monai.Deploy.Messaging.Messages; using Monai.Deploy.Messaging.RabbitMQ; @@ -127,7 +127,7 @@ public void WhenAExportRequestIsReceivedDesignatedFor(string routingKey) WorkflowInstanceId = Guid.NewGuid().ToString(), }; - exportRequestEvent.PluginAssemblies.Add(typeof(TestOutputDataPluginModifyDicomFile).AssemblyQualifiedName); + exportRequestEvent.PluginAssemblies.Add(typeof(TestOutputDataPlugInModifyDicomFile).AssemblyQualifiedName); var message = new JsonMessage( exportRequestEvent, @@ -149,7 +149,7 @@ public async Task ThenExportTheInstancesToTheDicomScp() (await Extensions.WaitUntil(() => _dicomServer.Instances.ContainsKey(key), DicomScpWaitTimeSpan)).Should().BeTrue("{0} should be received", key); _dicomServer.Instances.Should().ContainKey(key).WhoseValue.Count.Equals(2); _dicomServer.Instances.Should().ContainKey(key).WhoseValue[0].Equals(_dataProvider.DicomSpecs.FileHashes[key]); - _dicomServer.Instances.Should().ContainKey(key).WhoseValue[1].Equals(TestOutputDataPluginModifyDicomFile.ExpectedValue); + _dicomServer.Instances.Should().ContainKey(key).WhoseValue[1].Equals(TestOutputDataPlugInModifyDicomFile.ExpectedValue); } } @@ -172,11 +172,10 @@ public async Task ThenExportTheInstancesToOrthanc() { try { - var key = dicomFile.GenerateFileName(); var hash = dicomFile.CalculateHash(); actualHashes.Add(key, hash); - dicomFile.Dataset.GetSingleValueOrDefault(TestOutputDataPluginModifyDicomFile.ExpectedTag, string.Empty).Should().Be(TestOutputDataPluginModifyDicomFile.ExpectedValue); + dicomFile.Dataset.GetSingleValueOrDefault(TestOutputDataPlugInModifyDicomFile.ExpectedTag, string.Empty).Should().Be(TestOutputDataPlugInModifyDicomFile.ExpectedValue); ++instanceFound; } catch (Exception ex) diff --git a/tests/Integration.Test/StepDefinitions/RemoteAppExecutionPlugInsStepDefinitions.cs b/tests/Integration.Test/StepDefinitions/RemoteAppExecutionPlugInsStepDefinitions.cs new file mode 100644 index 000000000..a0f134949 --- /dev/null +++ b/tests/Integration.Test/StepDefinitions/RemoteAppExecutionPlugInsStepDefinitions.cs @@ -0,0 +1,228 @@ +/* + * 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.Net; +using BoDi; +using FellowOakDicom; +using FellowOakDicom.Network; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Client; +using Monai.Deploy.InformaticsGateway.Client.Common; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Integration.Test.Common; +using Monai.Deploy.InformaticsGateway.Integration.Test.Drivers; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution; +using Monai.Deploy.Messaging.Events; +using Monai.Deploy.Messaging.Messages; +using Monai.Deploy.Messaging.RabbitMQ; + +namespace Monai.Deploy.InformaticsGateway.Integration.Test.StepDefinitions +{ + [Binding] + [CollectionDefinition("SpecFlowNonParallelizableFeatures", DisableParallelization = true)] + public class RemoteAppExecutionPlugInsStepDefinitions + { + private static readonly TimeSpan MessageWaitTimeSpan = TimeSpan.FromMinutes(3); + private static readonly TimeSpan DicomScpWaitTimeSpan = TimeSpan.FromMinutes(20); + private static readonly string MonaiAeTitle = "REMOTE-APPS"; + private static readonly string SourceAeTitle = "MIGTestHost"; + private static readonly DicomTag[] DicomTags = new[] { DicomTag.AccessionNumber, DicomTag.StudyDescription, DicomTag.SeriesDescription, DicomTag.PatientAddress, DicomTag.PatientAge, DicomTag.PatientName }; + private static readonly List DefaultDicomTags = new() { DicomTag.PatientID, DicomTag.StudyInstanceUID, DicomTag.SeriesInstanceUID, DicomTag.SOPInstanceUID }; + + private readonly ObjectContainer _objectContainer; + private readonly InformaticsGatewayClient _informaticsGatewayClient; + private readonly IDataClient _dataSinkMinio; + private readonly DicomScp _dicomServer; + private readonly Configurations _configuration; + private string _dicomDestination; + private readonly DataProvider _dataProvider; + private readonly RabbitMqConsumer _receivedExportCompletedMessages; + private readonly RabbitMqConsumer _receivedWorkflowRequestMessages; + private readonly RabbitMQMessagePublisherService _messagePublisher; + private readonly InformaticsGatewayConfiguration _informaticsGatewayConfiguration; + private Dictionary _originalDicomFiles; + private readonly Assertions _assertions; + + public RemoteAppExecutionPlugInsStepDefinitions( + ObjectContainer objectContainer, + Configurations configuration) + { + _objectContainer = objectContainer ?? throw new ArgumentNullException(nameof(objectContainer)); + _informaticsGatewayClient = objectContainer.Resolve("InformaticsGatewayClient"); + _dataSinkMinio = objectContainer.Resolve("MinioClient"); + _dicomServer = objectContainer.Resolve("DicomScp"); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _dataProvider = objectContainer.Resolve("DataProvider"); + _receivedExportCompletedMessages = objectContainer.Resolve("ExportCompleteSubscriber"); + _receivedWorkflowRequestMessages = objectContainer.Resolve("WorkflowRequestSubscriber"); + _messagePublisher = objectContainer.Resolve("MessagingPublisher"); + _informaticsGatewayConfiguration = objectContainer.Resolve("InformaticsGatewayConfiguration"); + _assertions = objectContainer.Resolve("Assertions"); + + DefaultDicomTags.AddRange(DicomTags); + _dicomServer.ClearFilesAndUseHashes = false; //we need to store actual files to send the data back to MIG + } + + [Given(@"a study that is exported to the test host")] + public async Task AStudyThatIsExportedToTheTestHost() + { + // Register a new DICOM destination + DestinationApplicationEntity destination; + try + { + destination = await _informaticsGatewayClient.DicomDestinations.Create(new DestinationApplicationEntity + { + Name = _dicomServer.FeatureScpAeTitle, + AeTitle = _dicomServer.FeatureScpAeTitle, + HostIp = _configuration.InformaticsGatewayOptions.Host, + Port = _dicomServer.FeatureScpPort + }, CancellationToken.None); + } + catch (ProblemException ex) + { + if (ex.ProblemDetails.Status == (int)HttpStatusCode.Conflict && ex.ProblemDetails.Detail.Contains("already exists")) + { + destination = await _informaticsGatewayClient.DicomDestinations.GetAeTitle(_dicomServer.FeatureScpAeTitle, CancellationToken.None); + } + else + { + throw; + } + } + _dicomDestination = destination.Name; + + // Generate a study with multiple series + //_dataProvider.GenerateDicomData("MG", 1, 1); + _dataProvider.GenerateDicomData("CT", 1); + _dataProvider.InjectRandomData(DicomTags); + _originalDicomFiles = new Dictionary(_dataProvider.DicomSpecs.Files); + + await _dataSinkMinio.SendAsync(_dataProvider); + + // Emit a export request event + var exportRequestEvent = new ExportRequestEvent + { + CorrelationId = Guid.NewGuid().ToString(), + Destinations = new[] { _dicomDestination }, + ExportTaskId = Guid.NewGuid().ToString(), + Files = _dataProvider.DicomSpecs.Files.Keys.ToList(), + MessageId = Guid.NewGuid().ToString(), + WorkflowInstanceId = Guid.NewGuid().ToString(), + }; + + exportRequestEvent.PluginAssemblies.Add(typeof(ExternalAppOutgoing).AssemblyQualifiedName); + + var message = new JsonMessage( + exportRequestEvent, + MessageBrokerConfiguration.InformaticsGatewayApplicationId, + exportRequestEvent.CorrelationId, + string.Empty); + + _receivedExportCompletedMessages.ClearMessages(); + await _messagePublisher.Publish("md.export.request.monaiscu", message.ToMessage()); + } + + [When(@"the study is received and sent back to Informatics Gateway")] + public async Task TheStudyIsReceivedAndSentBackToInformaticsGateway() + { + // setup DICOM Source + try + { + await _informaticsGatewayClient.DicomSources.Create(new SourceApplicationEntity + { + Name = SourceAeTitle, + AeTitle = SourceAeTitle, + HostIp = _configuration.InformaticsGatewayOptions.Host, + }, CancellationToken.None); + } + catch (ProblemException ex) + { + if (ex.ProblemDetails.Status == (int)HttpStatusCode.Conflict && + ex.ProblemDetails.Detail.Contains("already exists")) + { + await _informaticsGatewayClient.DicomSources.GetAeTitle(SourceAeTitle, CancellationToken.None); + } + else + { + throw; + } + } + + // setup MONAI Deploy AET + _dataProvider.StudyGrouping = "0020,000D"; + try + { + await _informaticsGatewayClient.MonaiScpAeTitle.Create(new MonaiApplicationEntity + { + AeTitle = MonaiAeTitle, + Name = MonaiAeTitle, + Grouping = _dataProvider.StudyGrouping, + Timeout = 3, + PlugInAssemblies = new List() { typeof(ExternalAppIncoming).AssemblyQualifiedName } + }, CancellationToken.None); + } + catch (ProblemException ex) + { + if (ex.ProblemDetails.Status == (int)HttpStatusCode.Conflict && + ex.ProblemDetails.Detail.Contains("already exists")) + { + await _informaticsGatewayClient.MonaiScpAeTitle.GetAeTitle(MonaiAeTitle, CancellationToken.None); + } + else + { + throw; + } + } + + // Wait for export completed event + (await _receivedExportCompletedMessages.WaitforAsync(1, DicomScpWaitTimeSpan)).Should().BeTrue(); + + foreach (var key in _dataProvider.DicomSpecs.FileHashes.Keys) + { + (await Extensions.WaitUntil(() => _dicomServer.Instances.ContainsKey(key), DicomScpWaitTimeSpan)).Should().BeTrue("{0} should be received", key); + } + + // Send data received back to MIG + var storeScu = _objectContainer.Resolve("StoreSCU"); + + var host = _configuration.InformaticsGatewayOptions.Host; + var port = _informaticsGatewayConfiguration.Dicom.Scp.Port; + + _dataProvider.DicomSpecs.Files.Clear(); + _dataProvider.DicomSpecs.Files = new Dictionary(_dicomServer.DicomFiles); + _dataProvider.DicomSpecs.Files.Should().NotBeNull(); + + await storeScu.SendAsync( + _dataProvider, + SourceAeTitle, + host, + port, + MonaiAeTitle); + + _dataProvider.DimseRsponse.Should().Be(DicomStatus.Success); + + // Wait for workflow request events + (await _receivedWorkflowRequestMessages.WaitforAsync(1, MessageWaitTimeSpan)).Should().BeTrue(); + _assertions.ShouldHaveCorrectNumberOfWorkflowRequestMessages(_dataProvider, _receivedWorkflowRequestMessages.Messages, 1); + } + + [Then(@"ensure the original study and the received study are the same")] + public async Task EnsureTheOriginalStudyAndTheReceivedStudyAreTheSameAsync() + { + await _assertions.ShouldRestoreAllDicomMetaata(_receivedWorkflowRequestMessages.Messages, _originalDicomFiles, DefaultDicomTags.ToArray()).ConfigureAwait(false); + } + } +} diff --git a/tests/Integration.Test/StepDefinitions/SharedDefinitions.cs b/tests/Integration.Test/StepDefinitions/SharedDefinitions.cs index 25be1b7a6..f7700ced5 100644 --- a/tests/Integration.Test/StepDefinitions/SharedDefinitions.cs +++ b/tests/Integration.Test/StepDefinitions/SharedDefinitions.cs @@ -19,7 +19,7 @@ using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Integration.Test.Common; using Monai.Deploy.InformaticsGateway.Integration.Test.Drivers; -using Monai.Deploy.InformaticsGateway.Test.Plugins; +using Monai.Deploy.InformaticsGateway.Test.PlugIns; namespace Monai.Deploy.InformaticsGateway.Integration.Test.StepDefinitions { @@ -73,15 +73,15 @@ public async Task ThenXXFilesUploadedToStorageService() } [Then(@"studies are uploaded to storage service with data input plugins")] - public async Task ThenXXFilesUploadedToStorageServiceWithDataInputPlugins() + public async Task ThenXXFilesUploadedToStorageServiceWithDataInputPlugIns() { await _assertions.ShouldHaveUploadedDicomDataToMinio( _receivedMessages.Messages, _dataProvider.DicomSpecs.FileHashes, (dicomFile) => { - dicomFile.Dataset.GetString(TestInputDataPluginModifyDicomFile.ExpectedTag) - .Should().Be(TestInputDataPluginModifyDicomFile.ExpectedValue); + dicomFile.Dataset.GetString(TestInputDataPlugInModifyDicomFile.ExpectedTag) + .Should().Be(TestInputDataPlugInModifyDicomFile.ExpectedValue); }); } } diff --git a/tests/Integration.Test/appsettings.json b/tests/Integration.Test/appsettings.json index 4178e0db8..20a508424 100644 --- a/tests/Integration.Test/appsettings.json +++ b/tests/Integration.Test/appsettings.json @@ -21,7 +21,7 @@ } }, "dicomWeb": { - "plugins": [ "Monai.Deploy.InformaticsGateway.Test.Plugins.TestInputDataPluginModifyDicomFile, Monai.Deploy.InformaticsGateway.Test.Plugins" ], + "plugins": [ "Monai.Deploy.InformaticsGateway.Test.PlugIns.TestInputDataPlugInModifyDicomFile, Monai.Deploy.InformaticsGateway.Test.PlugIns" ], "timeout": 10 }, "messaging": { @@ -68,6 +68,11 @@ "maximumNumberOfConnections": 10, "clientTimeout": 60000, "sendAck": true + }, + "plugins": { + "remoteApp": { + "ReplaceTags": "AccessionNumber, StudyDescription, SeriesDescription, PatientAddress, PatientAge, PatientName" + } } }, "Kestrel": { diff --git a/tests/Integration.Test/packages.lock.json b/tests/Integration.Test/packages.lock.json index 3aff32daf..7f9116962 100644 --- a/tests/Integration.Test/packages.lock.json +++ b/tests/Integration.Test/packages.lock.json @@ -2,6 +2,12 @@ "version": 1, "dependencies": { "net6.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "tW3lsNS+dAEII6YGUX/VMoJjBS1QvsxqJeqLaJXub08y1FSjasFPtQ4UBUsudE9PNrzLjooClMsPtY2cZLdXpQ==" + }, "FluentAssertions": { "type": "Direct", "requested": "[6.11.0, )", @@ -2054,6 +2060,22 @@ "fo-dicom": "[5.1.1, )" } }, + "monai.deploy.informaticsgateway.plugins.remoteappexecution": { + "type": "Project", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[6.0.21, )", + "Microsoft.EntityFrameworkCore.Relational": "[6.0.21, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.21, )", + "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, )", + "MongoDB.Driver": "[2.21.0, )", + "Polly": "[7.2.4, )" + } + }, "monai.deploy.informaticsgateway.test.plugins": { "type": "Project", "dependencies": {