changeset 5960:e3945472797d get-scu

merged default -> get-scu
author Alain Mazy <am@orthanc.team>
date Thu, 16 Jan 2025 16:40:08 +0100
parents ae6de7d857c5 (diff) e068fbad9a36 (current diff)
children 40138c65907c
files NEWS OrthancFramework/Resources/CodeGeneration/ErrorCodes.json OrthancFramework/Sources/DicomFormat/DicomMap.cpp OrthancFramework/Sources/DicomFormat/DicomMap.h OrthancFramework/Sources/Enumerations.cpp OrthancFramework/Sources/Enumerations.h OrthancServer/CMakeLists.txt OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Resources/Configuration.json OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerJobs/ArchiveJob.cpp OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp OrthancServer/Sources/main.cpp OrthancServer/UnitTestsSources/ServerConfigTests.cpp OrthancServer/UnitTestsSources/ServerJobsTests.cpp TODO
diffstat 73 files changed, 2077 insertions(+), 529 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Thu Jan 16 15:49:50 2025 +0100
+++ b/NEWS	Thu Jan 16 16:40:08 2025 +0100
@@ -4,6 +4,14 @@
 General
 -------
 
+* DICOM:
+  - Added support for C-GET SCU.
+  - Added a configuration "AcceptedSopClasses" and "RejectedSopClasses" to limit 
+    the SOP classes accepted by Orthanc when acting as C-STORE SCP.
+  - New config option "DicomDefaultRetrieveMethod" to define wheter Orthanc uses C-MOVE or C-GET 
+    to retrieve a resource after a C-Find (when calling /queries/.../retrieve).
+    This configuration can be overriden for each modality in "DicomModalities->..->RetrieveMethod".
+    Default value: "C-MOVE" to keep the backward compatibility.
 * New configuration options:
   - "MaximumConcurrentDcmtkTranscoders" to reduce CPU and memory usage by limiting
     the number of concurrent DCMTK transcoders that are simultaneously running
@@ -90,12 +98,36 @@
   https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html
   This has no impact on the Stone Web viewer and OHIF:
   https://discourse.orthanc-server.org/t/dicomwebplugin-does-not-return-series-metadata-properly/5195
-
+* /queries/../retrieve now accepts a new field in the payload: "RetrieveMethod" to define wheter 
+  Orthanc uses C-MOVE or C-GET to retrieve the resource.
+* improved progress reporting of DicomMoveScu jobs.
 
 Maintenance
 -----------
 
 * DICOM TLS: "DicomTlsTrustedCertificates" is not required anymore when issuing
+<<<<<<< working copy
+  an outgoing SCU connexion when "DicomTlsRemoteCertificateRequired" is set to false.
+* DICOM negotiation:
+  - When opening a DICOM SCU connection, Orthanc now only proposes the contexts that it is
+    going to use in the connection and not all contexts as in previous versions.  E.g, when
+    performing a C-ECHO, Orthanc will not propose C-MOVE or C-FIND.
+* DICOM Get-SCP: Orthanc won't refuse anymore to send e.g. a LittleEndianExplicit file when
+  the accepted transfer syntax is a compressed one.
+* Introduced a new thread to update the statistics at regular interval for the
+  DB plugins that are implementing the UpdateAndGetStatistics function (currently only
+  PostgreSQL).  This avoids very long update times in case you don't call /statistics
+  for a long period.
+* Fix C-Find queries not returning computed tags like ModalitiesInStudy, NumberOfStudyRelatedSeries, ...
+  in very specific use-cases.
+* Fix C-Find queries not returning private tags in the modality worklist plugin.
+* Fix extremely rare error when 2 threads are trying to create the same folder in the File Storage 
+  at the same time.
+* Fix crashes if handling very large images.
+* Fix deadlock when parsing specific invalid DICOM files.
+* Loading plugins:
+  - Orthanc will now fail to start when provided with a plugin path that can not be found.
+=======
   an outgoing SCU connection if "DicomTlsRemoteCertificateRequired" is set to "false"
 * Fix C-Find queries not returning computed tags such as ModalitiesInStudy,
   NumberOfStudyRelatedSeries,... in very specific use cases
@@ -106,6 +138,7 @@
 * Fix deadlock when parsing specific invalid DICOM files
 * Loading plugins: Orthanc will now fail to start when provided with a plugin path
   that can not be found
+>>>>>>> merge rev
 * Metrics:
   - Fix a few metrics that were not published
   - Added 2 metrics: "orthanc_storage_cache_miss_count" and "orthanc_storage_cache_hit_count"
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Thu Jan 16 16:40:08 2025 +0100
@@ -607,6 +607,11 @@
     "Name": "NoCGetHandler", 
     "Description": "No request handler factory for DICOM C-GET SCP"
   },
+  {
+    "Code": 2045, 
+    "Name": "DicomGetUnavailable", 
+    "Description": "DicomUserConnection: The C-GET command is not supported by the remote SCP"
+  },
 
 
 
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -1333,7 +1333,22 @@
       return value->CopyToString(result, allowBinary);
     }
   }
-    
+
+  bool DicomMap::LookupStringValues(std::set<std::string>& results,
+                                    const DicomTag& tag,
+                                    bool allowBinary) const
+  {
+    std::string tmp;
+    if (LookupStringValue(tmp, tag, allowBinary))
+    {
+      Toolbox::SplitString(results, tmp, '\\');
+      return true;
+    }
+
+    return false;
+  }
+
+
   bool DicomMap::ParseInteger32(int32_t& result,
                                 const DicomTag& tag) const
   {
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h	Thu Jan 16 16:40:08 2025 +0100
@@ -181,7 +181,11 @@
     bool LookupStringValue(std::string& result,
                            const DicomTag& tag,
                            bool allowBinary) const;
-    
+
+    bool LookupStringValues(std::set<std::string>& results,
+                           const DicomTag& tag,
+                           bool allowBinary) const;
+
     bool ParseInteger32(int32_t& result,
                         const DicomTag& tag) const;
 
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -173,7 +173,6 @@
 
   DicomAssociation::DicomAssociation()
   {
-    role_ = DicomAssociationRole_Default;
     isOpen_ = false;
     net_ = NULL; 
     params_ = NULL;
@@ -198,16 +197,6 @@
   }
 
 
-  void DicomAssociation::SetRole(DicomAssociationRole role)
-  {
-    if (role_ != role)
-    {
-      Close();
-      role_ = role;
-    }
-  }
-
-  
   void DicomAssociation::ClearPresentationContexts()
   {
     Close();
@@ -215,7 +204,26 @@
     proposed_.reserve(MAX_PROPOSED_PRESENTATIONS);
   }
 
-  
+
+  static T_ASC_SC_ROLE GetDcmtkRole(DicomAssociationRole role)
+  {
+    switch (role)
+    {
+      case DicomAssociationRole_Default:
+        return ASC_SC_ROLE_DEFAULT;
+
+      case DicomAssociationRole_Scu:
+        return ASC_SC_ROLE_SCU;
+
+      case DicomAssociationRole_Scp:
+        return ASC_SC_ROLE_SCP;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
   void DicomAssociation::Open(const DicomAssociationParameters& parameters)
   {
     if (isOpen_)
@@ -240,24 +248,6 @@
       dcmConnectionTimeout.set(acseTimeout);
     }
       
-    T_ASC_SC_ROLE dcmtkRole;
-    switch (role_)
-    {
-      case DicomAssociationRole_Default:
-        dcmtkRole = ASC_SC_ROLE_DEFAULT;
-        break;
-
-      case DicomAssociationRole_Scu:
-        dcmtkRole = ASC_SC_ROLE_SCU;
-        break;
-
-      case DicomAssociationRole_Scp:
-        dcmtkRole = ASC_SC_ROLE_SCP;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
 
     assert(net_ == NULL &&
            params_ == NULL &&
@@ -291,7 +281,12 @@
                                   "no timeout") << ")";
 
     CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_));
+#if DCMTK_VERSION_NUMBER >= 368
+    CheckConnecting(parameters, ASC_createAssociationParameters(&params_, parameters.GetMaximumPduLength(), acseTimeout));
+#else
+    // from 3.6.8, this version is obsolete
     CheckConnecting(parameters, ASC_createAssociationParameters(&params_, parameters.GetMaximumPduLength()));
+#endif
 
 #if ORTHANC_ENABLE_SSL == 1
     if (parameters.GetRemoteModality().IsDicomTlsEnabled())
@@ -351,12 +346,12 @@
       assert(presentationContextId <= 255);
       const char* abstractSyntax = proposed_[i].abstractSyntax_.c_str();
 
-      const std::set<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_;
+      const std::list<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_;
           
       std::vector<const char*> transferSyntaxes;
       transferSyntaxes.reserve(source.size());
           
-      for (std::set<DicomTransferSyntax>::const_iterator
+      for (std::list<DicomTransferSyntax>::const_iterator
              it = source.begin(); it != source.end(); ++it)
       {
         transferSyntaxes.push_back(GetTransferSyntaxUid(*it));
@@ -365,7 +360,7 @@
       assert(!transferSyntaxes.empty());
       CheckConnecting(parameters, ASC_addPresentationContext(
                         params_, presentationContextId, abstractSyntax,
-                        &transferSyntaxes[0], transferSyntaxes.size(), dcmtkRole));
+                        &transferSyntaxes[0], transferSyntaxes.size(), GetDcmtkRole(proposed_[i].role_)));
 
       presentationContextId += 2;
     }
@@ -456,36 +451,56 @@
     }
   }
 
+  void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax,
+                                                           DicomAssociationRole role)
+  {
+    std::list<DicomTransferSyntax> ts;
+    ts.push_back(DicomTransferSyntax_LittleEndianExplicit); // the most standard one first !
+    ts.push_back(DicomTransferSyntax_LittleEndianImplicit);
+    ts.push_back(DicomTransferSyntax_BigEndianExplicit);  // Retired but was historicaly proposed by Orthanc
+    ProposePresentationContext(abstractSyntax, ts, role);
+  }
     
   void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax)
   {
-    std::set<DicomTransferSyntax> ts;
-    ts.insert(DicomTransferSyntax_LittleEndianImplicit);
-    ts.insert(DicomTransferSyntax_LittleEndianExplicit);
-    ts.insert(DicomTransferSyntax_BigEndianExplicit);  // Retired
-    ProposePresentationContext(abstractSyntax, ts);
+    ProposeGenericPresentationContext(abstractSyntax, DicomAssociationRole_Default);
   }
 
     
   void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax,
                                                     DicomTransferSyntax transferSyntax)
   {
-    std::set<DicomTransferSyntax> ts;
-    ts.insert(transferSyntax);
-    ProposePresentationContext(abstractSyntax, ts);
+    ProposePresentationContext(abstractSyntax, transferSyntax, DicomAssociationRole_Default);
   }
 
-    
+
+  void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax,
+                                                    DicomTransferSyntax transferSyntax,
+                                                    DicomAssociationRole role)
+  {
+    std::list<DicomTransferSyntax> ts;
+    ts.push_back(transferSyntax);
+    ProposePresentationContext(abstractSyntax, ts, role);
+  }
+
   size_t DicomAssociation::GetRemainingPropositions() const
   {
     assert(proposed_.size() <= MAX_PROPOSED_PRESENTATIONS);
     return MAX_PROPOSED_PRESENTATIONS - proposed_.size();
   }
     
+  void DicomAssociation::ProposePresentationContext(
+    const std::string& abstractSyntax,
+    const std::list<DicomTransferSyntax>& transferSyntaxes)
+  {
+    ProposePresentationContext(abstractSyntax, transferSyntaxes, DicomAssociationRole_Default);
+  }
+
 
   void DicomAssociation::ProposePresentationContext(
     const std::string& abstractSyntax,
-    const std::set<DicomTransferSyntax>& transferSyntaxes)
+    const std::list<DicomTransferSyntax>& transferSyntaxes,
+    DicomAssociationRole role)
   {
     if (transferSyntaxes.empty())
     {
@@ -507,6 +522,7 @@
     ProposedPresentationContext context;
     context.abstractSyntax_ = abstractSyntax;
     context.transferSyntaxes_ = transferSyntaxes;
+    context.role_ = role;
 
     proposed_.push_back(context);
   }
@@ -526,6 +542,35 @@
     }
   }
 
+  bool DicomAssociation::GetAssociationParameters(std::string& remoteAet,
+                                                  std::string& remoteIp,
+                                                  std::string& calledAet) const
+  {
+    T_ASC_Association& dcmtkAssoc = GetDcmtkAssociation();
+
+    DIC_AE remoteAet_C;
+    DIC_AE calledAet_C;
+    DIC_AE remoteIp_C;
+    DIC_AE calledIP_C;
+
+    if (
+#if DCMTK_VERSION_NUMBER >= 364
+      ASC_getAPTitles(dcmtkAssoc.params, remoteAet_C, sizeof(remoteAet_C), calledAet_C, sizeof(calledAet_C), NULL, 0).good() &&
+      ASC_getPresentationAddresses(dcmtkAssoc.params, remoteIp_C, sizeof(remoteIp_C), calledIP_C, sizeof(calledIP_C)).good()
+#else
+      ASC_getAPTitles(dcmtkAssoc.params, remoteAet_C, calledAet_C, NULL).good() &&
+      ASC_getPresentationAddresses(dcmtkAssoc.params, remoteIp_C, calledIP_C).good()
+#endif
+      )
+    {
+      remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C));
+      remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C));
+      calledAet = (/*OFSTRING_GUARD*/(calledAet_C));
+      return true;
+    }
+
+    return false;
+  }
     
   T_ASC_Network& DicomAssociation::GetDcmtkNetwork() const
   {
@@ -649,13 +694,12 @@
     DicomAssociation association;
 
     {
-      std::set<DicomTransferSyntax> transferSyntaxes;
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
+      std::list<DicomTransferSyntax> transferSyntaxes;
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit);
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit);
 
-      association.SetRole(DicomAssociationRole_Scp);
       association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
-                                             transferSyntaxes);
+                                             transferSyntaxes, DicomAssociationRole_Scp);
     }
       
     association.Open(parameters);
@@ -828,13 +872,13 @@
     DicomAssociation association;
 
     {
-      std::set<DicomTransferSyntax> transferSyntaxes;
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
+      std::list<DicomTransferSyntax> transferSyntaxes;
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit);
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit);
       
-      association.SetRole(DicomAssociationRole_Default);
+      // association.SetRole(DicomAssociationRole_Default);
       association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
-                                             transferSyntaxes);
+                                             transferSyntaxes, DicomAssociationRole_Default);
     }
       
     association.Open(parameters);
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h	Thu Jan 16 16:40:08 2025 +0100
@@ -44,6 +44,7 @@
 #include <stdint.h>   // For uint8_t
 #include <boost/noncopyable.hpp>
 #include <set>
+#include <list>
 
 namespace Orthanc
 {
@@ -58,13 +59,13 @@
     struct ProposedPresentationContext
     {
       std::string                    abstractSyntax_;
-      std::set<DicomTransferSyntax>  transferSyntaxes_;
+      std::list<DicomTransferSyntax> transferSyntaxes_;
+      DicomAssociationRole           role_;
     };
 
     typedef std::map<std::string, std::map<DicomTransferSyntax, uint8_t> >
     AcceptedPresentationContexts;
 
-    DicomAssociationRole                      role_;
     bool                                      isOpen_;
     std::vector<ProposedPresentationContext>  proposed_;
     AcceptedPresentationContexts              accepted_;
@@ -95,8 +96,6 @@
       return isOpen_;
     }
 
-    void SetRole(DicomAssociationRole role);
-
     void ClearPresentationContexts();
 
     void Open(const DicomAssociationParameters& parameters);
@@ -109,6 +108,13 @@
 
     void ProposeGenericPresentationContext(const std::string& abstractSyntax);
 
+    void ProposeGenericPresentationContext(const std::string& abstractSyntax,
+                                           DicomAssociationRole role);
+
+    void ProposePresentationContext(const std::string& abstractSyntax,
+                                    DicomTransferSyntax transferSyntax,
+                                    DicomAssociationRole role);
+
     void ProposePresentationContext(const std::string& abstractSyntax,
                                     DicomTransferSyntax transferSyntax);
 
@@ -116,12 +122,21 @@
 
     void ProposePresentationContext(
       const std::string& abstractSyntax,
-      const std::set<DicomTransferSyntax>& transferSyntaxes);
-    
+      const std::list<DicomTransferSyntax>& transferSyntaxes);
+
+    void ProposePresentationContext(
+      const std::string& abstractSyntax,
+      const std::list<DicomTransferSyntax>& transferSyntaxes,
+      DicomAssociationRole role);
+
     T_ASC_Association& GetDcmtkAssociation() const;
 
     T_ASC_Network& GetDcmtkNetwork() const;
 
+    bool GetAssociationParameters(std::string& remoteAet,
+                                  std::string& remoteIp,
+                                  std::string& calledAet) const;
+
     static void CheckCondition(const OFCondition& cond,
                                const DicomAssociationParameters& parameters,
                                const std::string& command);
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -234,15 +234,58 @@
 
 
 
-  void DicomControlUserConnection::SetupPresentationContexts()
+  void DicomControlUserConnection::SetupPresentationContexts(
+    ScuOperationFlags scuOperation,
+    const std::set<std::string>& acceptedStorageSopClasses,
+    const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes)
   {
     assert(association_.get() != NULL);
-    association_->ProposeGenericPresentationContext(UID_VerificationSOPClass);
-    association_->ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_MOVEPatientRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
+
+    if ((scuOperation & ScuOperationFlags_Echo) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_VerificationSOPClass);
+    }
+
+    if ((scuOperation & ScuOperationFlags_FindPatient) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_FindStudy) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_FindWorklist) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_MovePatient) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_MOVEPatientRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_MoveStudy) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_Get) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_GETStudyRootQueryRetrieveInformationModel);
+      association_->ProposeGenericPresentationContext(UID_GETPatientRootQueryRetrieveInformationModel);
+
+      if (acceptedStorageSopClasses.size() == 0)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls); // the acceptedStorageSopClassUids should always be defined for a C-Get
+      }
+
+      for (std::set<std::string>::const_iterator it = acceptedStorageSopClasses.begin(); it != acceptedStorageSopClasses.end(); ++it)
+      {
+        association_->ProposePresentationContext(*it, proposedStorageTransferSyntaxes, DicomAssociationRole_Scp);
+      }
+    }
   }
     
 
@@ -350,6 +393,21 @@
     }
   }
 
+  void MoveProgressCallback(void *callbackData,
+                            T_DIMSE_C_MoveRQ *request,
+                            int responseCount, 
+                            T_DIMSE_C_MoveRSP *response)
+  {
+    DicomControlUserConnection::IProgressListener* listener = reinterpret_cast<DicomControlUserConnection::IProgressListener*>(callbackData);
+    if (listener)
+    {
+      listener->OnProgressUpdated(response->NumberOfRemainingSubOperations,
+                                  response->NumberOfCompletedSubOperations,
+                                  response->NumberOfFailedSubOperations,
+                                  response->NumberOfWarningSubOperations);
+    }
+  }
+
     
   void DicomControlUserConnection::MoveInternal(const std::string& targetAet,
                                                 ResourceType level,
@@ -391,7 +449,8 @@
     DcmDataset* statusDetail = NULL;
     DcmDataset* responseIdentifiers = NULL;
     OFCondition cond = DIMSE_moveUser(
-      &association_->GetDcmtkAssociation(), presID, &request, dataset, /*moveCallback*/ NULL, NULL,
+      &association_->GetDcmtkAssociation(), presID, &request, dataset, 
+      (progressListener_ != NULL ? MoveProgressCallback : NULL), progressListener_,
       /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
       /*opt_dimse_timeout*/ parameters_.GetTimeout(),
       &association_->GetDcmtkNetwork(), /*subOpCallback*/ NULL, NULL,
@@ -413,6 +472,14 @@
       OFString str;
       CLOG(TRACE, DICOM) << "Received Final Move Response:" << std::endl
                          << DIMSE_dumpMessage(str, response, DIMSE_INCOMING);
+
+      if (progressListener_ != NULL)
+      {
+        progressListener_->OnProgressUpdated(response.NumberOfRemainingSubOperations,
+                                             response.NumberOfCompletedSubOperations,
+                                             response.NumberOfFailedSubOperations,
+                                             response.NumberOfWarningSubOperations);
+      }
     }
     
     /**
@@ -445,11 +512,240 @@
   }
     
 
-  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params) :
+  void DicomControlUserConnection::Get(const DicomMap& findResult,
+                                       CGetInstanceReceivedCallback instanceReceivedCallback,
+                                       void* callbackContext)
+  {
+    assert(association_.get() != NULL);
+    association_->Open(parameters_);
+
+    std::unique_ptr<ParsedDicomFile> query(
+      ConvertQueryFields(findResult, parameters_.GetRemoteModality().GetManufacturer()));
+    DcmDataset* queryDataset = query->GetDcmtkObject().getDataset();
+
+    std::string remoteAet;
+    std::string remoteIp;
+    std::string calledAet;
+
+    association_->GetAssociationParameters(remoteAet, remoteIp, calledAet);
+
+    const char* sopClass = NULL;
+    const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
+    ResourceType level = StringToResourceType(tmp.c_str());
+    switch (level)
+    {
+      case ResourceType_Patient:
+        sopClass = UID_GETPatientRootQueryRetrieveInformationModel;
+        break;
+      case ResourceType_Study:
+      case ResourceType_Series:
+      case ResourceType_Instance:
+        sopClass = UID_GETStudyRootQueryRetrieveInformationModel;
+        break;
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // Figure out which of the accepted presentation contexts should be used
+    int cgetPresID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass);
+    if (cgetPresID == 0)
+    {
+      throw OrthancException(ErrorCode_DicomGetUnavailable,
+                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
+    }
+
+    T_DIMSE_Message msgGetRequest;
+    memset((char*)&msgGetRequest, 0, sizeof(msgGetRequest));
+    msgGetRequest.CommandField = DIMSE_C_GET_RQ;
+
+    T_DIMSE_C_GetRQ* request = &(msgGetRequest.msg.CGetRQ);
+    request->MessageID = association_->GetDcmtkAssociation().nextMsgID++;
+    strncpy(request->AffectedSOPClassUID, sopClass, DIC_UI_LEN);
+    request->Priority = DIMSE_PRIORITY_MEDIUM;
+    request->DataSetType = DIMSE_DATASET_PRESENT;
+
+    {
+      OFString str;
+      CLOG(TRACE, DICOM) << "Sending Get Request:" << std::endl
+                         << DIMSE_dumpMessage(str, *request, DIMSE_OUTGOING, NULL, cgetPresID);
+    }
+    
+    OFCondition cond = DIMSE_sendMessageUsingMemoryData(
+          &(association_->GetDcmtkAssociation()), cgetPresID, &msgGetRequest, NULL /* statusDetail */, queryDataset,
+          NULL, NULL, NULL /* commandSet */);
+      
+    if (cond.bad())
+    {
+        OFString tempStr;
+        CLOG(TRACE, DICOM) << "Failed sending C-GET request: " << DimseCondition::dump(tempStr, cond);
+        // return cond;
+    }
+
+    // equivalent to handleCGETSession in DCMTK
+    bool continueSession = true;
+
+    // As long we want to continue (usually, as long as we receive more objects,
+    // i.e. the final C-GET response has not arrived yet)
+    while (continueSession)
+    {
+        T_DIMSE_Message rsp;
+        // Make sure everything is zeroed (especially options)
+        memset((char*)&rsp, 0, sizeof(rsp));
+
+        // DcmDataset* statusDetail = NULL;
+        T_ASC_PresentationContextID cmdPresId = 0;
+
+        OFCondition result = DIMSE_receiveCommand(&(association_->GetDcmtkAssociation()),
+                                                  (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                                  parameters_.GetTimeout(),
+                                                  &cmdPresId,
+                                                  &rsp,
+                                                  NULL /* statusDetail */,
+                                                  NULL /* not interested in the command set */);
+
+        if (result.bad())
+        {
+          OFString tempStr;
+          CLOG(TRACE, DICOM) << "Failed receiving DIMSE command: " << DimseCondition::dump(tempStr, result);
+          // delete statusDetail;
+          break;  // TODO: return value
+        }
+        // Handle C-GET Response
+        if (rsp.CommandField == DIMSE_C_GET_RSP)
+        {
+          {
+            OFString tempStr;
+            CLOG(TRACE, DICOM) << "Received C-GET Response: " << std::endl
+              << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId);
+          }
+
+          if (progressListener_ != NULL)
+          {
+            progressListener_->OnProgressUpdated(rsp.msg.CGetRSP.NumberOfRemainingSubOperations,
+                                                  rsp.msg.CGetRSP.NumberOfCompletedSubOperations,
+                                                  rsp.msg.CGetRSP.NumberOfFailedSubOperations,
+                                                  rsp.msg.CGetRSP.NumberOfWarningSubOperations);
+          }
+
+          if (rsp.msg.CGetRSP.DimseStatus == 0x0000)  // final success message
+          {
+            continueSession = false;
+          }
+        }
+        // Handle C-STORE Request
+        else if (rsp.CommandField == DIMSE_C_STORE_RQ)
+        {
+          {
+            OFString tempStr;
+            CLOG(TRACE, DICOM) << "Received C-STORE Request: " << std::endl
+              << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId);
+          }
+
+          T_DIMSE_C_StoreRQ* storeRequest = &(rsp.msg.CStoreRQ);
+
+          // Check if dataset is announced correctly
+          if (rsp.msg.CStoreRQ.DataSetType == DIMSE_DATASET_NULL)
+          {
+            CLOG(WARNING, DICOM) << "C-GET SCU handler: Incoming C-STORE with no dataset";
+          }
+
+          Uint16 desiredCStoreReturnStatus = 0;
+          DcmDataset* dataObject = NULL;
+
+          // Receive dataset
+          result = DIMSE_receiveDataSetInMemory(&(association_->GetDcmtkAssociation()),
+                                                  (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                                  parameters_.GetTimeout(),
+                                                  &cmdPresId,
+                                                  &dataObject,
+                                                  NULL, NULL);
+
+          if (result.bad())
+          {
+            LOG(WARNING) << "C-GET SCU handler: Failed to receive dataset: " << result.text();
+            desiredCStoreReturnStatus = STATUS_STORE_Error_CannotUnderstand;
+          }
+          else
+          {
+            // callback the OrthancServer with the received data
+            if (instanceReceivedCallback != NULL)
+            {
+              desiredCStoreReturnStatus = instanceReceivedCallback(callbackContext, *dataObject, remoteAet, remoteIp, calledAet);
+            }
+
+            // send the Store response
+            T_DIMSE_Message storeResponse;
+            memset((char*)&storeResponse, 0, sizeof(storeResponse));
+            storeResponse.CommandField         = DIMSE_C_STORE_RSP;
+
+            T_DIMSE_C_StoreRSP& storeRsp       = storeResponse.msg.CStoreRSP;
+            storeRsp.MessageIDBeingRespondedTo = storeRequest->MessageID;
+            storeRsp.DimseStatus               = desiredCStoreReturnStatus;
+            storeRsp.DataSetType               = DIMSE_DATASET_NULL;
+
+            OFStandard::strlcpy(
+                storeRsp.AffectedSOPClassUID, storeRequest->AffectedSOPClassUID, sizeof(storeRsp.AffectedSOPClassUID));
+            OFStandard::strlcpy(
+                storeRsp.AffectedSOPInstanceUID, storeRequest->AffectedSOPInstanceUID, sizeof(storeRsp.AffectedSOPInstanceUID));
+            storeRsp.opts = O_STORE_AFFECTEDSOPCLASSUID | O_STORE_AFFECTEDSOPINSTANCEUID;
+
+            result = DIMSE_sendMessageUsingMemoryData(&(association_->GetDcmtkAssociation()), 
+                                                      cmdPresId, 
+                                                      &storeResponse, NULL /* statusDetail */, NULL /* dataObject */,
+                                                      NULL, NULL, NULL /* commandSet */);
+            if (result.bad())
+            {
+              continueSession = false;
+            }
+            else
+            {
+              OFString tempStr;
+              CLOG(TRACE, DICOM) << "Sent C-STORE Response: " << std::endl
+                << DIMSE_dumpMessage(tempStr, storeResponse, DIMSE_OUTGOING, NULL, cmdPresId);
+            }
+          }
+        }
+        // Handle other DIMSE command (error since other command than GET/STORE not expected)
+        else
+        {
+          CLOG(WARNING, DICOM) << "Expected C-GET response or C-STORE request but received DIMSE command 0x"
+                               << std::hex << std::setfill('0') << std::setw(4)
+                               << static_cast<unsigned int>(rsp.CommandField);
+          
+          result          = DIMSE_BADCOMMANDTYPE;
+          continueSession = false;
+        }
+
+        // delete statusDetail; // should be NULL if not existing or added to response list
+        // statusDetail = NULL;
+    }
+    /* All responses received or break signal occurred */
+
+    // return result;
+}
+
+
+  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params, ScuOperationFlags scuOperation) :
     parameters_(params),
-    association_(new DicomAssociation)
+    association_(new DicomAssociation),
+    progressListener_(NULL)
   {
-    SetupPresentationContexts();
+    assert((scuOperation & ScuOperationFlags_Get) == 0);  // you must provide acceptedStorageSopClassUids for Get SCU
+    std::set<std::string> emptyStorageSopClasses;
+    std::list<DicomTransferSyntax> emptyStorageTransferSyntaxes;
+
+    SetupPresentationContexts(scuOperation, emptyStorageSopClasses, emptyStorageTransferSyntaxes);
+  }
+    
+  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params, 
+                                                         ScuOperationFlags scuOperation,
+                                                         const std::set<std::string>& acceptedStorageSopClasses,
+                                                         const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes) :
+    parameters_(params),
+    association_(new DicomAssociation),
+    progressListener_(NULL)
+  {
+    SetupPresentationContexts(scuOperation, acceptedStorageSopClasses, proposedStorageTransferSyntaxes);
   }
     
 
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h	Thu Jan 16 16:40:08 2025 +0100
@@ -37,13 +37,50 @@
 {
   class DicomAssociation;  // Forward declaration for PImpl design pattern
   
+  typedef uint16_t (*CGetInstanceReceivedCallback)(void *callbackContext,
+                                                   DcmDataset& dataset,
+                                                   const std::string& remoteAet,
+                                                   const std::string& remoteIp,
+                                                   const std::string& calledAet
+                                                   );
+
+
+  enum ScuOperationFlags
+  {
+    ScuOperationFlags_Echo = 1 << 0,
+    ScuOperationFlags_FindPatient = 1 << 1,
+    ScuOperationFlags_FindStudy = 1 << 2,
+    ScuOperationFlags_FindWorklist = 1 << 3,
+    ScuOperationFlags_MoveStudy = 1 << 4,
+    ScuOperationFlags_MovePatient = 1 << 5,
+    // C-Store is not using DicomControlUserConnection but DicomStoreUserConnection
+    ScuOperationFlags_Get = 1 << 6,
+
+    ScuOperationFlags_Find = ScuOperationFlags_FindPatient | ScuOperationFlags_FindStudy | ScuOperationFlags_FindWorklist,
+    ScuOperationFlags_Move = ScuOperationFlags_MoveStudy | ScuOperationFlags_MovePatient,
+    ScuOperationFlags_All = ScuOperationFlags_Echo | ScuOperationFlags_Find | ScuOperationFlags_Move | ScuOperationFlags_Get
+  };
+
   class DicomControlUserConnection : public boost::noncopyable
   {
+  public:
+    class IProgressListener
+    {
+    public:
+      virtual void OnProgressUpdated(uint16_t nbRemainingSubOperations,
+                                     uint16_t nbCompletedSubOperations,
+                                     uint16_t nbFailedSubOperations,
+                                     uint16_t nbWarningSubOperations) = 0;
+    };
+
   private:
     DicomAssociationParameters           parameters_;
     boost::shared_ptr<DicomAssociation>  association_;
+    IProgressListener*                   progressListener_;
 
-    void SetupPresentationContexts();
+    void SetupPresentationContexts(ScuOperationFlags scuOperation,
+                                   const std::set<std::string>& acceptedStorageSopClasses,
+                                   const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes);
 
     void FindInternal(DicomFindAnswers& answers,
                       DcmDataset* dataset,
@@ -56,8 +93,14 @@
                       const DicomMap& fields);
     
   public:
-    explicit DicomControlUserConnection(const DicomAssociationParameters& params);
-    
+    explicit DicomControlUserConnection(const DicomAssociationParameters& params, ScuOperationFlags scuOperation);
+
+    // specific constructor for CGet SCU
+    explicit DicomControlUserConnection(const DicomAssociationParameters& params, 
+                                        ScuOperationFlags scuOperation,
+                                        const std::set<std::string>& acceptedStorageSopClasses,
+                                        const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes);
+
     const DicomAssociationParameters& GetParameters() const
     {
       return parameters_;
@@ -67,11 +110,20 @@
 
     bool Echo();
 
+    void SetProgressListener(IProgressListener* progressListener)
+    {
+      progressListener_ = progressListener;
+    }
+
     void Find(DicomFindAnswers& result,
               ResourceType level,
               const DicomMap& originalFields,
               bool normalize);
 
+    void Get(const DicomMap& getQuery,
+             CGetInstanceReceivedCallback instanceReceivedCallback,
+             void* callbackContext);
+
     void Move(const std::string& targetAet,
               ResourceType level,
               const DicomMap& findResult);
--- a/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -57,7 +57,7 @@
                                                      bool hasPreferred,
                                                      DicomTransferSyntax preferred)
   {
-    typedef std::list< std::set<DicomTransferSyntax> >  GroupsOfSyntaxes;
+    typedef std::list< std::list<DicomTransferSyntax> >  GroupsOfSyntaxes;
 
     GroupsOfSyntaxes  groups;
 
@@ -65,8 +65,8 @@
     for (std::set<DicomTransferSyntax>::const_iterator
            it = sourceSyntaxes.begin(); it != sourceSyntaxes.end(); ++it)
     {
-      std::set<DicomTransferSyntax> group;
-      group.insert(*it);
+      std::list<DicomTransferSyntax> group;
+      group.push_back(*it);
       groups.push_back(group);
     }
 
@@ -74,8 +74,8 @@
     if (hasPreferred &&
         sourceSyntaxes.find(preferred) == sourceSyntaxes.end())
     {
-      std::set<DicomTransferSyntax> group;
-      group.insert(preferred);
+      std::list<DicomTransferSyntax> group;
+      group.push_back(preferred);
       groups.push_back(group);
     }
 
@@ -89,7 +89,7 @@
         DicomTransferSyntax_BigEndianExplicit
       };
 
-      std::set<DicomTransferSyntax> group;
+      std::list<DicomTransferSyntax> group;
 
       for (size_t i = 0; i < N; i++)
       {
@@ -97,7 +97,7 @@
         if (sourceSyntaxes.find(syntax) == sourceSyntaxes.end() &&
             (!hasPreferred || preferred != syntax))
         {
-          group.insert(syntax);
+          group.push_back(syntax);
         }
       }
 
--- a/OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h	Thu Jan 16 16:40:08 2025 +0100
@@ -28,6 +28,7 @@
 
 #include <boost/noncopyable.hpp>
 #include <string>
+#include <list>
 
 namespace Orthanc
 {
@@ -47,13 +48,23 @@
                                   const std::string& calledAet,
                                   DicomRequestType type) = 0;
 
+    // Get the set of TransferSyntaxes that are accepted when negotiation a C-Store association, acting as SCP when it has been initiated by the C-Store SCU.
     virtual void GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& target,
                                              const std::string& remoteIp,
                                              const std::string& remoteAet,
                                              const std::string& calledAet) = 0;
-    
+
+    // Get the list of TransferSyntaxes that are proposed when initiating a C-Store SCP which actually only happens in a C-Get SCU
+    virtual void GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& target,
+                                                    const std::string& remoteIp,
+                                                    const std::string& remoteAet,
+                                                    const std::string& calledAet) = 0;
+
     virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
                                            const std::string& remoteAet,
                                            const std::string& calledAet) = 0;
+
+    virtual void GetAcceptedSopClasses(std::set<std::string>& sopClasses,
+                                       size_t maxCount) = 0;
   };
 }
--- a/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -502,13 +502,21 @@
           }
           else                                         // see dcmqrsrv.cc lines 839 - 876
           {
+            std::set<std::string> acceptedStorageClasses;
+
+            if (server.HasApplicationEntityFilter())
+            {
+              server.GetApplicationEntityFilter().GetAcceptedSopClasses(acceptedStorageClasses, 0);
+            }
+
             /* accept storage syntaxes with proposed role */
             int npc = ASC_countPresentationContexts(assoc->params);
             for (int i = 0; i < npc; i++)
             {
               T_ASC_PresentationContext pc;
               ASC_getPresentationContext(assoc->params, i, &pc);
-              if (dcmIsaStorageSOPClassUID(pc.abstractSyntax))
+              if (acceptedStorageClasses.find(pc.abstractSyntax) != acceptedStorageClasses.end()
+                 || (!server.HasApplicationEntityFilter() && dcmIsaStorageSOPClassUID(pc.abstractSyntax)))  // previous behavior kept for compatibility in case the server does not have an ApplicationEntityFilter
               {
                 /**
                  * We are prepared to accept whatever role the caller
--- a/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -50,6 +50,7 @@
 static const char* KEY_USE_DICOM_TLS = "UseDicomTls";
 static const char* KEY_LOCAL_AET = "LocalAet";
 static const char* KEY_TIMEOUT = "Timeout";
+static const char* KEY_RETRIEVE_METHOD = "RetrieveMethod";
 
 
 namespace Orthanc
@@ -72,6 +73,7 @@
     useDicomTls_ = false;
     localAet_.clear();
     timeout_ = 0;
+    retrieveMethod_ = RetrieveMethod_SystemDefault;
   }
 
 
@@ -308,6 +310,17 @@
     {
       timeout_ = SerializationToolbox::ReadUnsignedInteger(serialized, KEY_TIMEOUT);
     }
+
+    if (serialized.isMember(KEY_RETRIEVE_METHOD))
+    {
+      retrieveMethod_ = StringToRetrieveMethod
+        (SerializationToolbox::ReadString(serialized, KEY_RETRIEVE_METHOD));
+    }   
+    else
+    {
+      retrieveMethod_ = RetrieveMethod_SystemDefault;
+    }
+
   }
 
 
@@ -427,6 +440,7 @@
       target[KEY_USE_DICOM_TLS] = useDicomTls_;
       target[KEY_LOCAL_AET] = localAet_;
       target[KEY_TIMEOUT] = timeout_;
+      target[KEY_RETRIEVE_METHOD] = EnumerationToString(retrieveMethod_);
     }
     else
     {
@@ -521,4 +535,14 @@
   {
     return timeout_ != 0;
   }
+
+  RetrieveMethod RemoteModalityParameters::GetRetrieveMethod() const
+  {
+    return retrieveMethod_;
+  }
+
+  void RemoteModalityParameters::SetRetrieveMethod(RetrieveMethod retrieveMethod)
+  {
+    retrieveMethod_ = retrieveMethod;
+  }
 }
--- a/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h	Thu Jan 16 16:40:08 2025 +0100
@@ -51,6 +51,7 @@
     bool                  useDicomTls_;
     std::string           localAet_;
     uint32_t              timeout_;
+    RetrieveMethod        retrieveMethod_;   // New in Orthanc 1.12.6
     
     void Clear();
 
@@ -118,5 +119,10 @@
     uint32_t GetTimeout() const;
 
     bool HasTimeout() const;    
+
+    RetrieveMethod GetRetrieveMethod() const;
+
+    void SetRetrieveMethod(RetrieveMethod retrieveMethod);
+
   };
 }
--- a/OrthancFramework/Sources/Enumerations.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/Enumerations.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -372,6 +372,9 @@
       case ErrorCode_NoCGetHandler:
         return "No request handler factory for DICOM C-GET SCP";
 
+      case ErrorCode_DicomGetUnavailable:
+        return "DicomUserConnection: The C-GET command is not supported by the remote SCP";
+
       case ErrorCode_UnsupportedMediaType:
         return "Unsupported media type";
 
@@ -2493,6 +2496,44 @@
     }
   }
 
+  RetrieveMethod StringToRetrieveMethod(const std::string& str)
+  {
+    if (str == "C-MOVE")
+    {
+      return RetrieveMethod_Move;
+    }
+    else if (str == "C-GET")
+    {
+      return RetrieveMethod_Get;
+    }
+    else if (str == "SystemDefault")
+    {
+      return RetrieveMethod_SystemDefault;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "RetrieveMethod can be \"C-MOVE\", \"C-GET\" or \"SystemDefault\": " + str);
+    }    
+  }
+
+  const char* EnumerationToString(RetrieveMethod method)
+  {
+    switch (method)
+    {
+      case RetrieveMethod_Get:
+        return "C-GET";
+
+      case RetrieveMethod_Move:
+        return "C-MOVE";
+
+      case RetrieveMethod_SystemDefault:
+        return "SystemDefault";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
 }
 
 
--- a/OrthancFramework/Sources/Enumerations.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/Enumerations.h	Thu Jan 16 16:40:08 2025 +0100
@@ -234,6 +234,7 @@
     ErrorCode_AlreadyExistingTag = 2042    /*!< Cannot override the value of a tag that already exists */,
     ErrorCode_NoStorageCommitmentHandler = 2043    /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */,
     ErrorCode_NoCGetHandler = 2044    /*!< No request handler factory for DICOM C-GET SCP */,
+    ErrorCode_DicomGetUnavailable = 2045    /*!< DicomUserConnection: The C-GET command is not supported by the remote SCP */,
     ErrorCode_UnsupportedMediaType = 3000    /*!< Unsupported media type */,
     ErrorCode_START_PLUGINS = 1000000
   };
@@ -793,6 +794,14 @@
     ResourceType_Instance = 4
   };
 
+  enum RetrieveMethod                         // new in Orthanc 1.12.6
+  {
+    RetrieveMethod_Move = 1,
+    RetrieveMethod_Get = 2,
+
+    RetrieveMethod_SystemDefault = 65535
+  };
+
 
   ORTHANC_PUBLIC
   const char* EnumerationToString(ErrorCode code);
@@ -849,6 +858,9 @@
   const char* EnumerationToString(DicomToJsonFormat format);
 
   ORTHANC_PUBLIC
+  const char* EnumerationToString(RetrieveMethod method);
+
+  ORTHANC_PUBLIC
   Encoding StringToEncoding(const char* encoding);
 
   ORTHANC_PUBLIC
@@ -947,4 +959,7 @@
 
   ORTHANC_PUBLIC
   void GetAllDicomTransferSyntaxes(std::set<DicomTransferSyntax>& target);
+
+  ORTHANC_PUBLIC 
+  RetrieveMethod StringToRetrieveMethod(const std::string& str);
 }
--- a/OrthancFramework/Sources/JobsEngine/IJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/JobsEngine/IJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -49,13 +49,18 @@
     // For pausing/canceling/ending jobs: This method must release allocated resources
     virtual void Stop(JobStopReason reason) = 0;
 
-    virtual float GetProgress() = 0;
+    virtual float GetProgress() const = 0;
 
-    virtual void GetJobType(std::string& target) = 0;
+    virtual bool NeedsProgressUpdateBetweenSteps() const // only for jobs whose progress is updated by outside events (like C-Move and C-Get)
+    {
+      return false;
+    }
+
+    virtual void GetJobType(std::string& target) const = 0;
     
-    virtual void GetPublicContent(Json::Value& value) = 0;
+    virtual void GetPublicContent(Json::Value& value) const = 0;
 
-    virtual bool Serialize(Json::Value& value) = 0;
+    virtual bool Serialize(Json::Value& value) const = 0;
 
     // This function can only be called if the job has reached its
     // "success" state
--- a/OrthancFramework/Sources/JobsEngine/JobInfo.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/JobsEngine/JobInfo.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -54,7 +54,8 @@
                    const JobStatus& status,
                    const boost::posix_time::ptime& creationTime,
                    const boost::posix_time::ptime& lastStateChangeTime,
-                   const boost::posix_time::time_duration& runtime) :
+                   const boost::posix_time::time_duration& runtime,
+                   const IJob& job) :
     id_(id),
     priority_(priority),
     state_(state),
@@ -68,11 +69,16 @@
     if (state_ == JobState_Running)
     {
       float ms = static_cast<float>(runtime_.total_milliseconds());
+      if (job.NeedsProgressUpdateBetweenSteps())
+      {
+        status_.UpdateProgress(job);
+      }
 
-      if (status_.GetProgress() > 0.01f &&
+      float progress = status_.GetProgress();
+
+      if (progress > 0.01f &&
           ms > 0.01f)
       {
-        float progress = status_.GetProgress();
         long long remaining = boost::math::llround(ms / progress * (1.0f - progress));
         eta_ = timestamp_ + boost::posix_time::milliseconds(remaining);
         hasEta_ = true;
--- a/OrthancFramework/Sources/JobsEngine/JobInfo.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/JobsEngine/JobInfo.h	Thu Jan 16 16:40:08 2025 +0100
@@ -51,7 +51,8 @@
             const JobStatus& status,
             const boost::posix_time::ptime& creationTime,
             const boost::posix_time::ptime& lastStateChangeTime,
-            const boost::posix_time::time_duration& runtime) ORTHANC_LOCAL;
+            const boost::posix_time::time_duration& runtime,
+            const IJob& job) ORTHANC_LOCAL;
 
     JobInfo();
 
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/JobsEngine/JobStatus.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -41,7 +41,7 @@
   
   JobStatus::JobStatus(ErrorCode code,
                        const std::string& details,
-                       IJob& job) :
+                       const IJob& job) :
     errorCode_(code),
     progress_(job.GetProgress()),
     publicContent_(Json::objectValue),
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/JobsEngine/JobStatus.h	Thu Jan 16 16:40:08 2025 +0100
@@ -44,7 +44,7 @@
 
     JobStatus(ErrorCode code,
               const std::string& details,
-              IJob& job);
+              const IJob& job);
 
     ErrorCode GetErrorCode() const
     {
@@ -61,6 +61,11 @@
       return progress_;
     }
 
+    void UpdateProgress(const IJob& job)
+    {
+      progress_ = job.GetProgress();
+    }
+
     const std::string& GetJobType() const
     {
       return jobType_;
--- a/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -132,7 +132,10 @@
 
       if (running.IsValid())
       {
-        CLOG(INFO, JOBS) << "Executing job with priority " << running.GetPriority()
+        std::string jobType;
+        running.GetJob().GetJobType(jobType);
+
+        CLOG(INFO, JOBS) << "Executing " << jobType << " job with priority " << running.GetPriority()
                          << " in worker thread " << workerIndex << ": " << running.GetId();
 
         while (engine->IsRunning())
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -223,8 +223,14 @@
       lastStateChangeTime_ = time;
     }
 
-    const boost::posix_time::time_duration& GetRuntime() const
+    boost::posix_time::time_duration GetRuntime() const
     {
+      if (state_ == JobState_Running)
+      {
+        const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();
+        return now - lastStateChangeTime_;
+      }
+
       return runtime_;
     }
 
@@ -644,7 +650,8 @@
                        handler.GetLastStatus(),
                        handler.GetCreationTime(),
                        handler.GetLastStateChangeTime(),
-                       handler.GetRuntime());
+                       handler.GetRuntime(),
+                       handler.GetJob());
       return true;
     }
   }
@@ -792,7 +799,10 @@
         }
       }
 
-      LOG(INFO) << "New job submitted with priority " << priority << ": " << id;
+      std::string jobType;
+      handler->GetJob().GetJobType(jobType);
+
+      LOG(INFO) << "New " << jobType << " job submitted with priority " << priority << ": " << id;
 
       if (observer_ != NULL)
       {
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -398,7 +398,7 @@
   }
 
 
-  float SequenceOfOperationsJob::GetProgress()
+  float SequenceOfOperationsJob::GetProgress() const
   {
     boost::mutex::scoped_lock lock(mutex_);
       
@@ -406,13 +406,13 @@
             static_cast<float>(operations_.size() + 1));
   }
 
-  void SequenceOfOperationsJob::GetJobType(std::string& target)
+  void SequenceOfOperationsJob::GetJobType(std::string& target) const
   {
     target = "SequenceOfOperations";
   }
 
 
-  void SequenceOfOperationsJob::GetPublicContent(Json::Value& value)
+  void SequenceOfOperationsJob::GetPublicContent(Json::Value& value) const
   {
     boost::mutex::scoped_lock lock(mutex_);
 
@@ -421,7 +421,7 @@
   }
 
 
-  bool SequenceOfOperationsJob::Serialize(Json::Value& value)
+  bool SequenceOfOperationsJob::Serialize(Json::Value& value) const
   {
     boost::mutex::scoped_lock lock(mutex_);
 
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -54,7 +54,7 @@
 
     std::string                       description_;
     bool                              done_;
-    boost::mutex                      mutex_;
+    mutable boost::mutex              mutex_;
     std::vector<Operation*>           operations_;
     size_t                            current_;
     boost::condition_variable         operationAdded_;
@@ -117,13 +117,13 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE;
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE;
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -137,7 +137,7 @@
   }
 
 
-  float SetOfCommandsJob::GetProgress()
+  float SetOfCommandsJob::GetProgress() const
   {
     if (commands_.empty())
     {
@@ -237,13 +237,13 @@
   static const char* KEY_COMMANDS = "Commands";
 
   
-  void SetOfCommandsJob::GetPublicContent(Json::Value& value)
+  void SetOfCommandsJob::GetPublicContent(Json::Value& value) const
   {
     value[KEY_DESCRIPTION] = GetDescription();
   }    
 
 
-  bool SetOfCommandsJob::Serialize(Json::Value& target)
+  bool SetOfCommandsJob::Serialize(Json::Value& target) const
   {
     target = Json::objectValue;
 
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -92,7 +92,7 @@
     
     virtual void Start() ORTHANC_OVERRIDE;
     
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
     bool IsStarted() const;
 
@@ -100,9 +100,9 @@
       
     virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
     
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
     
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -209,7 +209,7 @@
   static const char* KEY_FAILED_INSTANCES = "FailedInstances";
   static const char* KEY_PARENT_RESOURCES = "ParentResources";
 
-  void SetOfInstancesJob::GetPublicContent(Json::Value& target)
+  void SetOfInstancesJob::GetPublicContent(Json::Value& target) const
   {
     SetOfCommandsJob::GetPublicContent(target);
     target["InstancesCount"] = static_cast<uint32_t>(GetInstancesCount());
@@ -222,7 +222,7 @@
   }
 
 
-  bool SetOfInstancesJob::Serialize(Json::Value& target) 
+  bool SetOfInstancesJob::Serialize(Json::Value& target) const 
   {
     if (SetOfCommandsJob::Serialize(target))
     {
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -77,8 +77,8 @@
 
     virtual void Reset() ORTHANC_OVERRIDE;
 
-    virtual void GetPublicContent(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& target) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancFramework/Sources/Toolbox.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/Sources/Toolbox.h	Thu Jan 16 16:40:08 2025 +0100
@@ -262,7 +262,7 @@
       }
     }
 
-    // returns true if all element of 'needles' are found in 'haystack'
+    // returns the elements that are both in a and b
     template <typename T> static void GetIntersection(std::set<T>& target, const std::set<T>& a, const std::set<T>& b)
     {
       target.clear();
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -103,24 +103,24 @@
     {
     }
 
-    virtual float GetProgress() ORTHANC_OVERRIDE
+    virtual float GetProgress() const ORTHANC_OVERRIDE
     {
       return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
     }
 
-    virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& type) const ORTHANC_OVERRIDE
     {
       type = "DummyJob";
     }
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE
     {
       value = Json::objectValue;
       value["Type"] = "DummyJob";
       return true;
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE
     {
       value["hello"] = "world";
     }
@@ -199,7 +199,7 @@
     {
     }
 
-    virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& s) const ORTHANC_OVERRIDE
     {
       s = "DummyInstancesJob";
     }
--- a/OrthancServer/CMakeLists.txt	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/CMakeLists.txt	Thu Jan 16 16:40:08 2025 +0100
@@ -138,6 +138,8 @@
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ArchiveJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/CleaningInstancesJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomModalityStoreJob.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomRetrieveScuBaseJob.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomGetScuJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomMoveScuJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/LuaJobManager.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/MergeStudyJob.cpp
@@ -184,6 +186,7 @@
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/DatabaseLookupTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/LuaServerTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp
+  ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerConfigTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerIndexTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerJobsTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/SizeOfTests.cpp
--- a/OrthancServer/OrthancExplorer/query-retrieve.js	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/OrthancExplorer/query-retrieve.js	Thu Jan 16 16:40:08 2025 +0100
@@ -341,9 +341,9 @@
       success: function(system) {
         $('#retrieve-target').val(system['DicomAet']);
 
+        $('#retrieve-form').unbind('submit');
         $('#retrieve-form').submit(function(event) {
           var aet;
-
           event.preventDefault();
 
           aet = $('#retrieve-target').val();
--- a/OrthancServer/Plugins/Engine/PluginsJob.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Plugins/Engine/PluginsJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -147,7 +147,7 @@
     }
   }
 
-  float PluginsJob::GetProgress()
+  float PluginsJob::GetProgress() const
   {
     return parameters_.getProgress(parameters_.job);
   }
@@ -194,7 +194,7 @@
     };
   }
   
-  void PluginsJob::GetPublicContent(Json::Value& value)
+  void PluginsJob::GetPublicContent(Json::Value& value) const
   {
     if (parameters_.getContent != NULL)
     {
@@ -232,7 +232,7 @@
     }
   }
 
-  bool PluginsJob::Serialize(Json::Value& value)
+  bool PluginsJob::Serialize(Json::Value& value) const
   {
     if (parameters_.getSerialized != NULL)
     {
--- a/OrthancServer/Plugins/Engine/PluginsJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Plugins/Engine/PluginsJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -58,16 +58,16 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = type_;
     }
     
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Jan 16 16:40:08 2025 +0100
@@ -324,6 +324,7 @@
     OrthancPluginErrorCode_AlreadyExistingTag = 2042    /*!< Cannot override the value of a tag that already exists */,
     OrthancPluginErrorCode_NoStorageCommitmentHandler = 2043    /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */,
     OrthancPluginErrorCode_NoCGetHandler = 2044    /*!< No request handler factory for DICOM C-GET SCP */,
+    OrthancPluginErrorCode_DicomGetUnavailable = 2045    /*!< DicomUserConnection: The C-GET command is not supported by the remote SCP */,
     OrthancPluginErrorCode_UnsupportedMediaType = 3000    /*!< Unsupported media type */,
 
     _OrthancPluginErrorCode_INTERNAL = 0x7fffffff
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -29,10 +29,11 @@
 #include "../../../../OrthancFramework/Sources/OrthancException.h"
 
 #include "../Common/OrthancPluginCppWrapper.h"
-
+#include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
 
 DicomFilter::DicomFilter() :
-  hasAcceptedTransferSyntaxes_(false)
+  hasAcceptedTransferSyntaxes_(false),
+  hasAcceptedStorageClasses_(false)
 {
   {
     OrthancPlugins::OrthancConfiguration config;
@@ -200,6 +201,18 @@
 }
 
 
+void DicomFilter::GetProposedStorageTransferSyntaxes(std::list<Orthanc::DicomTransferSyntax>& target,
+                                                     const std::string& remoteIp,
+                                                     const std::string& remoteAet,
+                                                     const std::string& calledAet)
+{
+  // default TS
+  target.push_back(Orthanc::DicomTransferSyntax_LittleEndianExplicit);
+  target.push_back(Orthanc::DicomTransferSyntax_LittleEndianImplicit);
+  target.push_back(Orthanc::DicomTransferSyntax_BigEndianExplicit);
+}
+
+
 bool DicomFilter::IsUnknownSopClassAccepted(const std::string& remoteIp,
                                             const std::string& remoteAet,
                                             const std::string& calledAet)
@@ -207,3 +220,47 @@
   boost::shared_lock<boost::shared_mutex>  lock(mutex_);
   return unknownSopClassAccepted_;
 }
+
+
+void DicomFilter::GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount)
+{
+  boost::unique_lock<boost::shared_mutex>  lock(mutex_);
+
+  if (!hasAcceptedStorageClasses_)
+  {
+    Json::Value jsonSopClasses;
+
+    if (!OrthancPlugins::RestApiGet(jsonSopClasses, "/tools/accepted-sop-classes", false) ||
+        jsonSopClasses.type() != Json::arrayValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      for (Json::Value::ArrayIndex i = 0; i < jsonSopClasses.size(); i++)
+      {
+        if (jsonSopClasses[i].type() != Json::stringValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+        else
+        {
+          acceptedStorageClasses_.insert(jsonSopClasses[i].asString());
+        }
+      }
+    }
+
+    hasAcceptedStorageClasses_ = true;
+  }
+
+  std::set<std::string>::const_iterator it = acceptedStorageClasses_.begin();
+    size_t count = 0;
+
+  while (it != acceptedStorageClasses_.end() && (maxCount == 0 || count < maxCount))
+  {
+    sopClasses.insert(*it);
+    count++;
+    it++;
+  }
+
+}
\ No newline at end of file
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h	Thu Jan 16 16:40:08 2025 +0100
@@ -44,6 +44,8 @@
 
   bool hasAcceptedTransferSyntaxes_;
   std::set<Orthanc::DicomTransferSyntax>  acceptedTransferSyntaxes_;
+  bool hasAcceptedStorageClasses_;
+  std::set<std::string>                   acceptedStorageClasses_;
 
 public:
   DicomFilter();
@@ -62,7 +64,15 @@
                                            const std::string& remoteAet,
                                            const std::string& calledAet) ORTHANC_OVERRIDE;
 
+  virtual void GetProposedStorageTransferSyntaxes(std::list<Orthanc::DicomTransferSyntax>& target,
+                                                  const std::string& remoteIp,
+                                                  const std::string& remoteAet,
+                                                  const std::string& calledAet) ORTHANC_OVERRIDE;
+
+
   virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
                                          const std::string& remoteAet,
                                          const std::string& calledAet) ORTHANC_OVERRIDE;
+  
+  virtual void GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) ORTHANC_OVERRIDE;
 };
--- a/OrthancServer/Resources/Configuration.json	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Resources/Configuration.json	Thu Jan 16 16:40:08 2025 +0100
@@ -202,6 +202,32 @@
   // SOP classes (aka. "promiscuous mode")
   "UnknownSopClassAccepted" : false,
 
+  // The list of accepted Storage SOP classes.
+  // If empty or not defined, this list defaults
+  // to all storage classes defined in DCMTK in case of 
+  // C-STORE SCP and to a reduced list of 120 common storage
+  // classes in case of C-GET SCU.
+  // Each entry can contain wildcards ("?" or "*") to add
+  // subsets of SOP classes that are defined in DCMTK.
+  // If you want to add a a SOP class that is not defined in
+  // DCMTK, you must add it explicitely.
+  // (new in Orthanc 1.12.6)
+  // "AcceptedSopClasses" : [
+  //   "1.2.840.10008.5.1.4.1.1.2",
+  //   "1.2.840.10008.5.1.4.1.1.4"
+  // ]
+
+  // The list of rejected Storage SOP classes.
+  // This configuration is only meaningful if
+  // "AcceptedSopClasses" is using regular expressions
+  // or if it is not defined.
+  // Each entry can contain wildcards ("?" or "*").
+  // (new in Orthanc 1.12.6)
+  // "RejectedSopClasses" : [
+  //   "1.2.840.10008.5.1.4.1.1.2",
+  //   "1.2.840.10008.5.1.4.1.1.4"
+  // ]
+
   // Set the timeout (in seconds) after which the DICOM associations
   // are closed by the Orthanc SCP (server) if no further DIMSE
   // command is received from the SCU (client).
@@ -459,6 +485,10 @@
      * for Orthanc when initiating an SCU to this very specific
      * modality. Similarly, "Timeout" allows one to overwrite the
      * global value "DicomScuTimeout" on a per-modality basis.
+     *
+     * The "RetrieveMethod" option allows one to overwrite the global
+     * "DicomDefaultRetrieveMethod" configuration option for this
+     * specific modality. (Allowed values: "C-MOVE" or "C-GET").
      **/
     //"untrusted" : {
     //  "AET" : "ORTHANC",
@@ -475,7 +505,8 @@
     //  "AllowTranscoding" : true,         // new in 1.7.0
     //  "UseDicomTls" : false,             // new in 1.9.0
     //  "LocalAet" : "HELLO",              // new in 1.9.0
-    //  "Timeout" : 60                     // new in 1.9.1
+    //  "Timeout" : 60,                    // new in 1.9.1
+    //  "RetrieveMethod": "C-MOVE"         // new in 1.12.6
     //}
   },
 
@@ -489,6 +520,13 @@
   // accept C-FIND requests from Orthanc (new in Orthanc 1.8.1).
   "DicomEchoChecksFind" : false,
 
+  // Wheter Orthanc uses C-MOVE or C-GET to retrieve a resource after
+  // a C-Find (when calling /queries/.../retrieve).
+  // This configuration can be overriden for each modality by providing
+  // "RetrieveMethod" in the "DicomModalities" entry.
+  // (new in Orthanc 1.12.6)
+  "DicomDefaultRetrieveMethod" : "C-MOVE",
+
   // The timeout (in seconds) after which the DICOM associations are
   // considered as closed by the Orthanc SCU (client) if the remote
   // DICOM SCP (server) does not answer.
--- a/OrthancServer/Resources/DicomConformanceStatement.txt	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Resources/DicomConformanceStatement.txt	Thu Jan 16 16:40:08 2025 +0100
@@ -209,6 +209,16 @@
   MOVEStudyRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.2.2
 
 
+-------------------
+Get SCU Conformance
+-------------------
+
+Orthanc supports the following SOP Classes as an SCU for C-Get:
+
+  GETPatientRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.1.3
+  GETStudyRootQueryRetrieveInformationModel      | 1.2.840.10008.5.1.4.1.2.2.3
+
+
 -----------------
 Transfer Syntaxes
 -----------------
--- a/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -165,6 +165,7 @@
      * 2. Select the preferred transfer syntaxes, which corresponds to
      * the source transfer syntax, plus all the uncompressed transfer
      * syntaxes if transcoding is enabled.
+     * This way, we minimize the transcoding on our side.
      **/
     
     std::list<DicomTransferSyntax> preferred;
@@ -208,7 +209,16 @@
       }
     }
 
-    // No preferred syntax was accepted
+    // No preferred syntax was accepted but, if a PC has been accepted, it means that we have accepted a TS.
+    // This maybe means that we need to transcode twice on our side (from a compressed format to another compressed format).
+    if (allowTranscoding && accepted.size() >  0)
+    {
+      Accepted::const_iterator it = accepted.begin();
+      selectedPresentationId = it->second;
+      selectedSyntax = it->first;
+      return true;
+    }
+
     return false;
   }                                                           
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -36,6 +36,7 @@
 #include "../ServerContext.h"
 #include "../ServerJobs/DicomModalityStoreJob.h"
 #include "../ServerJobs/DicomMoveScuJob.h"
+#include "../ServerJobs/DicomGetScuJob.h"
 #include "../ServerJobs/OrthancPeerStoreJob.h"
 #include "../ServerToolbox.h"
 #include "../StorageCommitmentReports.h"
@@ -56,6 +57,7 @@
   static const char* const KEY_CHECK_FIND = "CheckFind";
   static const char* const SOP_CLASS_UID = "SOPClassUID";
   static const char* const SOP_INSTANCE_UID = "SOPInstanceUID";
+  static const char* const KEY_RETRIEVE_METHOD = "RetrieveMethod";
   
   static RemoteModalityParameters MyGetModalityUsingSymbolicName(const std::string& name)
   {
@@ -155,24 +157,31 @@
                           const DicomAssociationParameters& parameters,
                           const Json::Value& body)
   {
-    DicomControlUserConnection connection(parameters);
-
+    bool checkFind = false;
+    
+    if (body.type() == Json::objectValue &&
+        body.isMember(KEY_CHECK_FIND))
+    {
+      checkFind = SerializationToolbox::ReadBoolean(body, KEY_CHECK_FIND);
+    }
+    else
+    {
+      OrthancConfiguration::ReaderLock lock;
+      checkFind = lock.GetConfiguration().GetBooleanParameter("DicomEchoChecksFind", false);
+    }
+
+    ScuOperationFlags operations = ScuOperationFlags_Echo;
+    
+    if (checkFind)
+    {
+      operations = static_cast<ScuOperationFlags>(operations | ScuOperationFlags_Find);
+    }
+
+    DicomControlUserConnection connection(parameters, operations);
     if (connection.Echo())
     {
-      bool find = false;
-      
-      if (body.type() == Json::objectValue &&
-          body.isMember(KEY_CHECK_FIND))
-      {
-        find = SerializationToolbox::ReadBoolean(body, KEY_CHECK_FIND);
-      }
-      else
-      {
-        OrthancConfiguration::ReaderLock lock;
-        find = lock.GetConfiguration().GetBooleanParameter("DicomEchoChecksFind", false);
-      }
-
-      if (find)
+
+      if (checkFind)
       {
         // Issue a C-FIND request at the study level about a random Study Instance UID
         const std::string studyInstanceUid = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study);
@@ -385,7 +394,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
+      DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindPatient);
       FindPatient(answers, connection, fields);
     }
 
@@ -428,7 +437,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
+      DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy);
       FindStudy(answers, connection, fields);
     }
 
@@ -472,7 +481,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
+      DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy);
       FindSeries(answers, connection, fields);
     }
 
@@ -517,7 +526,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
+      DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy);
       FindInstance(answers, connection, fields);
     }
 
@@ -566,7 +575,7 @@
       return;
     }
  
-    DicomControlUserConnection connection(GetAssociationParameters(call));
+    DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_Find);
     
     DicomFindAnswers patients(false);
     FindPatient(patients, connection, m);
@@ -914,12 +923,25 @@
 
     std::string targetAet;
     int timeout = -1;
-    
+
+    QueryAccessor query(call);
+
+    RetrieveMethod retrieveMethod = query.GetHandler().GetRemoteModality().GetRetrieveMethod();
+
     Json::Value body;
     if (call.ParseJsonRequest(body))
     {
+      OrthancConfiguration::ReaderLock lock;
+
       targetAet = Toolbox::GetJsonStringField(body, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle());
       timeout = Toolbox::GetJsonIntegerField(body, KEY_TIMEOUT, -1);
+      
+      std::string strRetrieveMethod = SerializationToolbox::ReadString(body, KEY_RETRIEVE_METHOD, "");
+      
+      if (!strRetrieveMethod.empty())
+      {
+        retrieveMethod = StringToRetrieveMethod(strRetrieveMethod);
+      }
     }
     else
     {
@@ -934,45 +956,67 @@
       }
     }
     
-    std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
-    job->SetQueryFormat(OrthancRestApi::GetDicomFormat(body, DicomToJsonFormat_Short));
-    
+    if (retrieveMethod == RetrieveMethod_SystemDefault)
+    {
+      retrieveMethod = context.GetDefaultDicomRetrieveMethod();
+    }
+
+    std::unique_ptr<DicomRetrieveScuBaseJob> job;
+
+
+    switch (retrieveMethod)
     {
-      QueryAccessor query(call);
-      job->SetTargetAet(targetAet);
-      job->SetLocalAet(query.GetHandler().GetLocalAet());
-      job->SetRemoteModality(query.GetHandler().GetRemoteModality());
-
-      if (timeout >= 0)
+      case RetrieveMethod_Move:
       {
-        // New in Orthanc 1.7.0
-        job->SetTimeout(static_cast<uint32_t>(timeout));
-      }
-      else if (query.GetHandler().HasTimeout())
+        job.reset(new DicomMoveScuJob(context));
+        (dynamic_cast<DicomMoveScuJob*>(job.get()))->SetTargetAet(targetAet);
+
+        LOG(WARNING) << "Driving C-Move SCU on remote modality "
+                    << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle()
+                    << " to target modality " << targetAet;
+      }; break;
+      case RetrieveMethod_Get:
       {
-        // New in Orthanc 1.9.1
-        job->SetTimeout(query.GetHandler().GetTimeout());
-      }
-
-      LOG(WARNING) << "Driving C-Move SCU on remote modality "
-                   << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle()
-                   << " to target modality " << targetAet;
-
-      if (allAnswers)
+        job.reset(new DicomGetScuJob(context));
+
+        LOG(WARNING) << "Driving C-Get SCU on remote modality "
+                    << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle();
+      }; break;
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    job->SetQueryFormat(OrthancRestApi::GetDicomFormat(body, DicomToJsonFormat_Short));
+
+    job->SetLocalAet(query.GetHandler().GetLocalAet());
+    job->SetRemoteModality(query.GetHandler().GetRemoteModality());
+
+    if (timeout >= 0)
+    {
+      // New in Orthanc 1.7.0
+      job->SetTimeout(static_cast<uint32_t>(timeout));
+    }
+    else if (query.GetHandler().HasTimeout())
+    {
+      // New in Orthanc 1.9.1
+      job->SetTimeout(query.GetHandler().GetTimeout());
+    }
+
+    if (allAnswers)
+    {
+      for (size_t i = 0; i < query.GetHandler().GetAnswersCount(); i++)
       {
-        for (size_t i = 0; i < query.GetHandler().GetAnswersCount(); i++)
-        {
-          job->AddFindAnswer(query.GetHandler(), i);
-        }
+        job->AddFindAnswer(query.GetHandler(), i);
       }
-      else
-      {
-        job->AddFindAnswer(query.GetHandler(), index);
-      }
+    }
+    else
+    {
+      job->AddFindAnswer(query.GetHandler(), index);
     }
 
     OrthancRestApi::GetApi(call).SubmitCommandsJob
       (call, job.release(), true /* synchronous by default */, body);
+
   }
 
 
@@ -989,6 +1033,10 @@
                        "`DicomAet` configuration option.", false)
       .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
                        "Timeout for the C-MOVE command, in seconds", false)
+      .SetRequestField(KEY_RETRIEVE_METHOD, RestApiCallDocumentation::Type_String,
+                        "Force usage of C-MOVE or C-GET to retrieve the resource.  If note defined in the payload, "
+                        "the retrieve method is defined in the DicomDefaultRetrieveMethod configuration or in "
+                        "DicomModalities->..->RetrieveMethod", false)
       .AddRequestType(MimeType_PlainText, "AET of the target modality");
   }
   
@@ -999,8 +1047,8 @@
     {
       DocumentRetrieveShared(call);
       call.GetDocumentation()
-        .SetSummary("Retrieve one answer")
-        .SetDescription("Start a C-MOVE SCU command as a job, in order to retrieve one answer associated with the "
+        .SetSummary("Retrieve one answer with a C-MOVE or a C-GET SCU")
+        .SetDescription("Start a C-MOVE or a C-GET SCU command as a job, in order to retrieve one answer associated with the "
                         "query/retrieve operation whose identifiers are provided in the URL: "
                         "https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-move")
         .SetUriArgument("index", "Index of the answer");
@@ -1012,13 +1060,15 @@
   }
 
 
+
+
   static void RetrieveAllAnswers(RestApiPostCall& call)
   {
     if (call.IsDocumentation())
     {
       DocumentRetrieveShared(call);
       call.GetDocumentation()
-        .SetSummary("Retrieve all answers")
+        .SetSummary("Retrieve all answers with C-MOVE SCU")
         .SetDescription("Start a C-MOVE SCU command as a job, in order to retrieve all the answers associated with the "
                         "query/retrieve operation whose identifier is provided in the URL: "
                         "https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-move");
@@ -1536,6 +1586,48 @@
     call.GetOutput().AnswerJson(answer);
   }
 
+  void ParseMoveGetJob(DicomRetrieveScuBaseJob& job, Json::Value& request, RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    if (!call.ParseJsonRequest(request) ||
+        request.type() != Json::objectValue ||
+        !request.isMember(KEY_RESOURCES) ||
+        !request.isMember(KEY_LEVEL) ||
+        request[KEY_RESOURCES].type() != Json::arrayValue ||
+        request[KEY_LEVEL].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON body containing fields " +
+                             std::string(KEY_RESOURCES) + " and " + std::string(KEY_LEVEL));
+    }
+
+    ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
+    
+    std::string localAet = Toolbox::GetJsonStringField
+      (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle());
+
+    const RemoteModalityParameters source =
+      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+
+    job.SetQueryFormat(DicomToJsonFormat_Short);
+    job.SetLocalAet(localAet);
+    job.SetRemoteModality(source);
+
+    if (request.isMember(KEY_TIMEOUT))
+    {
+      job.SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT));
+    }
+
+    for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++)
+    {
+      DicomMap resource;
+      FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i], "Resources elements");
+
+      resource.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, std::string(ResourceTypeToDicomQueryRetrieveLevel(level)), false);
+
+      job.AddQuery(resource);      
+    }
+  }
 
   /***************************************************************************
    * DICOM C-Move SCU
@@ -1569,53 +1661,15 @@
     }
 
     ServerContext& context = OrthancRestApi::GetContext(call);
-
     Json::Value request;
 
-    if (!call.ParseJsonRequest(request) ||
-        request.type() != Json::objectValue ||
-        !request.isMember(KEY_RESOURCES) ||
-        !request.isMember(KEY_LEVEL) ||
-        request[KEY_RESOURCES].type() != Json::arrayValue ||
-        request[KEY_LEVEL].type() != Json::stringValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON body containing fields " +
-                             std::string(KEY_RESOURCES) + " and " + std::string(KEY_LEVEL));
-    }
-
-    ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
-    
-    std::string localAet = Toolbox::GetJsonStringField
-      (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle());
+    std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
+
+    ParseMoveGetJob(*job, request, call);
+
     std::string targetAet = Toolbox::GetJsonStringField
       (request, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle());
-
-    const RemoteModalityParameters source =
-      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-
-    std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
-
-    job->SetQueryFormat(DicomToJsonFormat_Short);
-    
-    // QueryAccessor query(call);
     job->SetTargetAet(targetAet);
-    job->SetLocalAet(localAet);
-    job->SetRemoteModality(source);
-
-    if (request.isMember(KEY_TIMEOUT))
-    {
-      job->SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT));
-    }
-
-    for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++)
-    {
-      DicomMap resource;
-      FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i], "Resources elements");
-
-      resource.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, std::string(ResourceTypeToDicomQueryRetrieveLevel(level)), false);
-
-      job->AddQuery(resource);      
-    }
 
     OrthancRestApi::GetApi(call).SubmitCommandsJob
       (call, job.release(), true /* synchronous by default */, request);
@@ -1623,6 +1677,51 @@
   }
 
 
+  /***************************************************************************
+   * DICOM C-Get SCU
+   ***************************************************************************/
+  
+  static void DicomGet(RestApiPostCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      OrthancRestApi::DocumentSubmitCommandsJob(call);
+      call.GetDocumentation()
+        .SetTag("Networking")
+        .SetSummary("Trigger C-GET SCU")
+        .SetDescription("Start a C-GET SCU command as a job, in order to retrieve DICOM resources "
+                        "from a remote DICOM modality whose identifier is provided in the URL: ")
+                        // "https://orthanc.uclouvain.be/book/users/rest.html#performing-c-move")   // TODO-GET
+        .SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfObjects,
+                         "List of queries identifying all the DICOM resources to be sent.  "
+                         "Usage of wildcards is prohibited and the query shall only contain DICOM ID tags.  "
+                         "Additionally, you may provide SOPClassesInStudy to limit the scope of the DICOM "
+                         "negotiation to certain SOPClassUID or to present uncommon SOPClassUID during "
+                         "the DICOM negotation.  By default, "
+                         "Orhanc will propose the most 120 common SOPClassUIDs.", true)
+        .SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject,
+                         "A query object identifying all the DICOM resources to be retrieved", true)
+        .SetRequestField(KEY_LOCAL_AET, RestApiCallDocumentation::Type_String,
+                         "Local AET that is used for this commands, defaults to `DicomAet` configuration option. "
+                         "Ignored if `DicomModalities` already sets `LocalAet` for this modality.", false)
+        .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
+                         "Timeout for the C-GET command, in seconds", false)
+        .SetUriArgument("id", "Identifier of the modality of interest");
+      return;
+    }
+
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    Json::Value request;
+
+    std::unique_ptr<DicomGetScuJob> job(new DicomGetScuJob(context));
+
+    ParseMoveGetJob(*job, request, call);
+
+    OrthancRestApi::GetApi(call).SubmitCommandsJob
+      (call, job.release(), true /* synchronous by default */, request);
+    return;
+  }
+
 
   /***************************************************************************
    * Orthanc Peers => Store client
@@ -2213,7 +2312,7 @@
       DicomFindAnswers answers(true);
 
       {
-        DicomControlUserConnection connection(GetAssociationParameters(call, json));
+        DicomControlUserConnection connection(GetAssociationParameters(call, json), ScuOperationFlags_FindWorklist);
         connection.FindWorklist(answers, *query);
       }
 
@@ -2544,6 +2643,7 @@
     Register("/modalities/{id}/store", DicomStore);
     Register("/modalities/{id}/store-straight", DicomStoreStraight);  // New in 1.6.1
     Register("/modalities/{id}/move", DicomMove);
+    Register("/modalities/{id}/get", DicomGet);
     Register("/modalities/{id}/configuration", GetModalityConfiguration);  // New in 1.8.1
 
     // For Query/Retrieve
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -75,6 +75,7 @@
     static const char* const DATABASE_BACKEND_PLUGIN = "DatabaseBackendPlugin";
     static const char* const DATABASE_VERSION = "DatabaseVersion";
     static const char* const DATABASE_SERVER_IDENTIFIER = "DatabaseServerIdentifier";
+    static const char* const DEFAULT_RETRIEVE_METHOD = "DefaultRetrieveMethod";
     static const char* const DICOM_AET = "DicomAet";
     static const char* const DICOM_PORT = "DicomPort";
     static const char* const HTTP_PORT = "HttpPort";
@@ -115,6 +116,7 @@
                         "Information about the installed storage area plugin (`null` if no such plugin is installed)")
         .SetAnswerField(DATABASE_BACKEND_PLUGIN, RestApiCallDocumentation::Type_String,
                         "Information about the installed database index plugin (`null` if no such plugin is installed)")
+        .SetAnswerField(DEFAULT_RETRIEVE_METHOD, RestApiCallDocumentation::Type_String, "The DefaultRetrieveMethod configuration")
         .SetAnswerField(DICOM_AET, RestApiCallDocumentation::Type_String, "The DICOM AET of Orthanc")
         .SetAnswerField(DICOM_PORT, RestApiCallDocumentation::Type_Number, "The port to the DICOM server of Orthanc")
         .SetAnswerField(HTTP_PORT, RestApiCallDocumentation::Type_Number, "The port to the HTTP server of Orthanc")
@@ -173,6 +175,7 @@
       result[MAXIMUM_STORAGE_SIZE] = lock.GetConfiguration().GetUnsignedIntegerParameter(MAXIMUM_STORAGE_SIZE, 0); // New in Orthanc 1.11.3
       result[MAXIMUM_PATIENT_COUNT] = lock.GetConfiguration().GetUnsignedIntegerParameter(MAXIMUM_PATIENT_COUNT, 0); // New in Orthanc 1.12.4
       result[MAXIMUM_STORAGE_MODE] = lock.GetConfiguration().GetStringParameter(MAXIMUM_STORAGE_MODE, "Recycle"); // New in Orthanc 1.11.3
+      result[DEFAULT_RETRIEVE_METHOD] = lock.GetConfiguration().GetStringParameter(DEFAULT_RETRIEVE_METHOD, "C-MOVE");
     }
 
     result[STORAGE_AREA_PLUGIN] = Json::nullValue;
@@ -478,6 +481,33 @@
     AnswerAcceptedTransferSyntaxes(call);
   }
 
+  static void GetAcceptedSopClasses(RestApiGetCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("System")
+        .SetSummary("Get accepted SOPClassUID")
+        .SetDescription("Get the list of SOP Class UIDs that are accepted "
+                        "by Orthanc C-STORE SCP. This corresponds to the configuration options "
+                        "`AcceptedSopClasses` and `RejectedSopClasses`.")
+        .AddAnswerType(MimeType_Json, "JSON array containing the SOP Class UIDs");
+      return;
+    }
+
+    std::set<std::string> sopClasses;
+    OrthancRestApi::GetContext(call).GetAcceptedSopClasses(sopClasses, 0);
+    
+    Json::Value json = Json::arrayValue;
+    for (std::set<std::string>::const_iterator
+           sop = sopClasses.begin(); sop != sopClasses.end(); ++sop)
+    {
+      json.append(*sop);
+    }
+    
+    call.GetOutput().AnswerJson(json);
+  }
+
 
   static void GetUnknownSopClassAccepted(RestApiGetCall& call)
   {
@@ -1200,5 +1230,8 @@
     Register("/tools/unknown-sop-class-accepted", SetUnknownSopClassAccepted);
 
     Register("/tools/labels", ListAllLabels);  // New in Orthanc 1.12.0
+
+    Register("/tools/accepted-sop-classes", GetAcceptedSopClasses);
+
   }
 }
--- a/OrthancServer/Sources/QueryRetrieveHandler.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/QueryRetrieveHandler.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -79,7 +79,7 @@
           params.SetTimeout(timeout_);
         }
         
-        DicomControlUserConnection connection(params);
+        DicomControlUserConnection connection(params, static_cast<ScuOperationFlags>(ScuOperationFlags_Find));
         connection.Find(answers_, level_, fixed, findNormalized_);
       }
 
--- a/OrthancServer/Sources/ServerContext.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerContext.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -50,6 +50,9 @@
 
 #include <dcmtk/dcmdata/dcfilefo.h>
 #include <dcmtk/dcmnet/dimse.h>
+#include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
+
+#include <boost/regex.hpp>
 
 #if HAVE_MALLOC_TRIM == 1
 #  include <malloc.h>
@@ -490,6 +493,15 @@
         lock.GetConfiguration().GetAcceptedTransferSyntaxes(acceptedTransferSyntaxes_);
 
         isUnknownSopClassAccepted_ = lock.GetConfiguration().GetBooleanParameter("UnknownSopClassAccepted", false);
+
+        // New options in Orthanc 1.12.6
+        std::list<std::string> acceptedSopClasses;
+        std::set<std::string> rejectedSopClasses;
+        lock.GetConfiguration().GetListOfStringsParameter(acceptedSopClasses, "AcceptedSopClasses");
+        lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, "RejectSopClasses");
+        SetAcceptedSopClasses(acceptedSopClasses, rejectedSopClasses);
+
+        defaultDicomRetrieveMethod_ = StringToRetrieveMethod(lock.GetConfiguration().GetStringParameter("DicomDefaultRetrieveMethod", "C-MOVE"));
       }
 
       jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
@@ -2009,8 +2021,118 @@
     }
   }
 
+  void ServerContext::SetAcceptedSopClasses(const std::list<std::string>& acceptedSopClasses,
+                                            const std::set<std::string>& rejectedSopClasses)
+  {
+    boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
+    acceptedSopClasses_.clear();
 
-  void ServerContext::GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes)
+    size_t count = 0;
+    std::set<std::string> allDcmtkSopClassUids;
+    std::set<std::string> shortDcmtkSopClassUids;
+
+    // we actually take a list of default 120 most common storage SOP classes defined in DCMTK
+    while (dcmLongSCUStorageSOPClassUIDs[count] != NULL)
+    {
+      shortDcmtkSopClassUids.insert(dcmLongSCUStorageSOPClassUIDs[count++]);
+    }
+
+    count = 0;
+    while (dcmAllStorageSOPClassUIDs[count] != NULL)
+    {
+      allDcmtkSopClassUids.insert(dcmAllStorageSOPClassUIDs[count++]);
+    }
+
+    if (acceptedSopClasses.size() == 0)
+    {
+      // by default, include the short list first and then all the others
+      for (std::set<std::string>::const_iterator it = shortDcmtkSopClassUids.begin(); it != shortDcmtkSopClassUids.end(); ++it)
+      {
+        acceptedSopClasses_.push_back(*it);
+      }
+
+      for (std::set<std::string>::const_iterator it = allDcmtkSopClassUids.begin(); it != allDcmtkSopClassUids.end(); ++it)
+      {
+        if (shortDcmtkSopClassUids.find(*it) == shortDcmtkSopClassUids.end()) // don't add the classes that we have already added
+        {
+          acceptedSopClasses_.push_back(*it);
+        }
+      }
+    }
+    else
+    {
+      std::set<std::string> addedSopClasses;
+
+      for (std::list<std::string>::const_iterator it = acceptedSopClasses.begin(); it != acceptedSopClasses.end(); ++it)
+      {
+        if (it->find('*') != std::string::npos || it->find('?') != std::string::npos)
+        {
+          // if it contains wildcard, add all the matching SOP classes known by DCMTK
+          boost::regex pattern(Toolbox::WildcardToRegularExpression(*it));
+
+          for (std::set<std::string>::const_iterator itall = allDcmtkSopClassUids.begin(); itall != allDcmtkSopClassUids.end(); ++itall)
+          {
+            if (regex_match(*itall, pattern) && addedSopClasses.find(*itall) == addedSopClasses.end())
+            {
+              acceptedSopClasses_.push_back(*itall);
+              addedSopClasses.insert(*itall);
+            }
+          }
+        }
+        else
+        {
+          // if it is a SOP Class UID, add it without checking if it is known by DCMTK
+          acceptedSopClasses_.push_back(*it);
+          addedSopClasses.insert(*it);
+        }
+      }
+    }
+    
+    // now remove all rejected syntaxes
+    if (rejectedSopClasses.size() > 0)
+    {
+      for (std::set<std::string>::const_iterator it = rejectedSopClasses.begin(); it != rejectedSopClasses.end(); ++it)
+      {
+        if (it->find('*') != std::string::npos || it->find('?') != std::string::npos)
+        {
+          // if it contains wildcard, get all the matching SOP classes known by DCMTK
+          boost::regex pattern(Toolbox::WildcardToRegularExpression(*it));
+
+          for (std::set<std::string>::const_iterator itall = allDcmtkSopClassUids.begin(); itall != allDcmtkSopClassUids.end(); ++itall)
+          {
+            if (regex_match(*itall, pattern))
+            {
+              acceptedSopClasses_.remove(*itall);
+            }
+          }
+        }
+        else
+        {
+          // if it is a SOP Class UID, remove it without checking if it is known by DCMTK
+          acceptedSopClasses_.remove(*it);
+        }
+      }
+    }
+  }
+
+  void ServerContext::GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) const
+  {
+    sopClasses.clear();
+
+    boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
+
+    size_t count = 0;
+    std::list<std::string>::const_iterator it = acceptedSopClasses_.begin();
+
+    while (it != acceptedSopClasses_.end() && (maxCount == 0 || count < maxCount))
+    {
+      sopClasses.insert(*it);
+      count++;
+      it++;
+    }
+  }
+
+  void ServerContext::GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes) const
   {
     boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
     syntaxes = acceptedTransferSyntaxes_;
@@ -2024,7 +2146,25 @@
   }
 
 
-  bool ServerContext::IsUnknownSopClassAccepted()
+  void ServerContext::GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& syntaxes) const
+  {
+    boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
+    
+    // // TODO: investigate: actually, neither Orthanc 1.12.4 nor DCM4CHEE will accept to send a LittleEndianExplicit file
+    // //                    while e.g., Jpeg-LS has been presented (and accepted) as the preferred TS for the C-Store SCP.
+    // // if we have defined IngestTranscoding, let's propose this TS first to avoid any unnecessary transcoding
+    // if (isIngestTranscoding_)
+    // {
+    //   syntaxes.push_back(ingestTransferSyntax_);
+    // }
+    
+    // then, propose the default ones
+    syntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit);
+    syntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit);
+  }
+  
+
+  bool ServerContext::IsUnknownSopClassAccepted() const
   {
     boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
     return isUnknownSopClassAccepted_;
--- a/OrthancServer/Sources/ServerContext.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerContext.h	Thu Jan 16 16:40:08 2025 +0100
@@ -233,6 +233,7 @@
         
     std::unique_ptr<SharedArchive>  queryRetrieveArchive_;
     std::string defaultLocalAet_;
+    RetrieveMethod defaultDicomRetrieveMethod_;
     OrthancHttpHandler  httpHandler_;
     bool saveJobs_;
     FindStorageAccessMode findStorageAccessMode_;
@@ -257,9 +258,10 @@
 
     // New in Orthanc 1.9.0
     DicomTransferSyntax preferredTransferSyntax_;
-    boost::mutex dynamicOptionsMutex_;
+    mutable boost::mutex dynamicOptionsMutex_;
     bool isUnknownSopClassAccepted_;
     std::set<DicomTransferSyntax>  acceptedTransferSyntaxes_;
+    std::list<std::string>         acceptedSopClasses_;  // ordered; the most 120 common ones first
     bool readOnly_;
 
     StoreResult StoreAfterTranscoding(std::string& resultPublicId,
@@ -436,6 +438,11 @@
       return defaultLocalAet_;
     }
 
+    RetrieveMethod GetDefaultDicomRetrieveMethod() const
+    {
+      return defaultDicomRetrieveMethod_;
+    }
+
     LuaScripting& GetLuaScripting()
     {
       return mainLua_;
@@ -583,11 +590,18 @@
 
     const std::string& GetDeidentifiedContent(const DicomElement& element) const;
 
-    void GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes);
+    void GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes) const;
 
     void SetAcceptedTransferSyntaxes(const std::set<DicomTransferSyntax>& syntaxes);
 
-    bool IsUnknownSopClassAccepted();
+    void GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& syntaxes) const;
+
+    void SetAcceptedSopClasses(const std::list<std::string>& acceptedSopClasses,
+                               const std::set<std::string>& rejectedSopClasses);
+
+    void GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) const;
+
+    bool IsUnknownSopClassAccepted() const;
 
     void SetUnknownSopClassAccepted(bool accepted);
 
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -1518,7 +1518,7 @@
   }
 
 
-  float ArchiveJob::GetProgress()
+  float ArchiveJob::GetProgress() const
   {
     if (writer_.get() == NULL ||
         writer_->GetStepsCount() == 0)
@@ -1533,7 +1533,7 @@
   }
 
     
-  void ArchiveJob::GetJobType(std::string& target)
+  void ArchiveJob::GetJobType(std::string& target) const
   {
     if (isMedia_)
     {
@@ -1546,7 +1546,7 @@
   }
 
 
-  void ArchiveJob::GetPublicContent(Json::Value& value)
+  void ArchiveJob::GetPublicContent(Json::Value& value) const
   {
     value = Json::objectValue;
     value[KEY_DESCRIPTION] = description_;
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -107,13 +107,13 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE;
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE;
     
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE
     {
       return false;  // Cannot serialize this kind of job
     }
--- a/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -83,7 +83,7 @@
   }
 
   
-  bool CleaningInstancesJob::Serialize(Json::Value& target)
+  bool CleaningInstancesJob::Serialize(Json::Value& target) const
   {
     if (!SetOfInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -62,7 +62,7 @@
     
     void SetKeepSource(bool keep);
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
 
     virtual void Start() ORTHANC_OVERRIDE;
   };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -0,0 +1,121 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomGetScuJob.h"
+
+#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../../OrthancFramework/Sources/SerializationToolbox.h"
+#include "../ServerContext.h"
+#include <dcmtk/dcmnet/dimse.h>
+#include <algorithm>
+
+static const char* const LOCAL_AET = "LocalAet";
+static const char* const QUERY = "Query";
+static const char* const QUERY_FORMAT = "QueryFormat";  // New in 1.9.5
+static const char* const REMOTE = "Remote";
+static const char* const TIMEOUT = "Timeout";
+
+namespace Orthanc
+{
+
+
+  static uint16_t InstanceReceivedHandler(void* callbackContext,
+                                          DcmDataset& dataset,
+                                          const std::string& remoteAet,
+                                          const std::string& remoteIp,
+                                          const std::string& calledAet)
+  {
+    // this code is equivalent to OrthancStoreRequestHandler
+    ServerContext* context = reinterpret_cast<ServerContext*>(callbackContext);
+
+    std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromDcmDataset(dataset));
+    
+    if (toStore->GetBufferSize() > 0)
+    {
+      toStore->SetOrigin(DicomInstanceOrigin::FromDicomProtocol
+                         (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str()));
+
+      std::string id;
+      ServerContext::StoreResult result = context->Store(id, *toStore, StoreInstanceMode_Default);
+      return result.GetCStoreStatusCode();
+    }
+
+    return STATUS_STORE_Error_CannotUnderstand;
+  }
+
+  void DicomGetScuJob::Retrieve(const DicomMap& findAnswer)
+  {
+    if (connection_.get() == NULL)
+    {
+      std::set<std::string> sopClassesToPropose;
+      std::set<std::string> acceptedSopClasses;
+      std::list<DicomTransferSyntax> proposedTransferSyntaxes;
+
+      if (sopClassesFromResourcesToRetrieve_.size() > 0)
+      {
+        context_.GetAcceptedSopClasses(acceptedSopClasses, 0); 
+
+        // keep the sop classes from the resources to retrieve only if they are accepted by Orthanc
+        Toolbox::GetIntersection(sopClassesToPropose, sopClassesFromResourcesToRetrieve_, acceptedSopClasses);
+      }
+      else
+      {
+        // when we don't know what SOP Classes to use, we include the 120 most common SOP Classes because 
+        // there are only 128 presentation contexts available
+        context_.GetAcceptedSopClasses(sopClassesToPropose, 120); 
+      }
+
+      if (sopClassesToPropose.size() == 0)
+      {
+        throw OrthancException(ErrorCode_NoPresentationContext, "Cannot perform C-Get, no SOPClassUID have been accepted by Orthanc.");        
+      }
+
+      context_.GetProposedStorageTransferSyntaxes(proposedTransferSyntaxes);
+
+      connection_.reset(new DicomControlUserConnection(parameters_, 
+                                                       ScuOperationFlags_Get, 
+                                                       sopClassesToPropose,
+                                                       proposedTransferSyntaxes));
+    }
+
+    connection_->SetProgressListener(this);
+    connection_->Get(findAnswer, InstanceReceivedHandler, &context_);
+  }
+
+  void DicomGetScuJob::AddFindAnswer(const DicomMap& answer)
+  {
+    DicomRetrieveScuBaseJob::AddFindAnswer(answer);
+
+    std::set<std::string> sopClassesInStudy;
+    if (answer.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY) 
+        && answer.LookupStringValues(sopClassesInStudy, DICOM_TAG_SOP_CLASSES_IN_STUDY, false))
+    {
+      for (std::set<std::string>::const_iterator it = sopClassesInStudy.begin(); it != sopClassesInStudy.end(); ++it)
+      {
+        sopClassesFromResourcesToRetrieve_.insert(*it);
+      }
+    }
+  }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -0,0 +1,60 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h"
+#include "DicomRetrieveScuBaseJob.h"
+#include "../QueryRetrieveHandler.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class DicomGetScuJob : public DicomRetrieveScuBaseJob
+  {
+  private:
+    std::set<std::string> sopClassesFromResourcesToRetrieve_;
+
+    virtual void Retrieve(const DicomMap& findAnswer) ORTHANC_OVERRIDE;
+    
+  public:
+    explicit DicomGetScuJob(ServerContext& context) :
+      DicomRetrieveScuBaseJob(context)
+    {
+    }
+
+    DicomGetScuJob(ServerContext& context,
+                   const Json::Value& serialized);
+
+
+    virtual void AddFindAnswer(const DicomMap &answer) ORTHANC_OVERRIDE;
+
+
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
+    {
+      target = "DicomGetScu";
+    }
+  };
+}
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -242,7 +242,7 @@
   }
   
 
-  void DicomModalityStoreJob::GetPublicContent(Json::Value& value)
+  void DicomModalityStoreJob::GetPublicContent(Json::Value& value) const
   {
     SetOfInstancesJob::GetPublicContent(value);
     
@@ -281,7 +281,7 @@
   }
 
 
-  bool DicomModalityStoreJob::Serialize(Json::Value& target)
+  bool DicomModalityStoreJob::Serialize(Json::Value& target) const
   {
     if (!SetOfInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -88,14 +88,14 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "DicomModalityStore";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
 
     virtual void Reset() ORTHANC_OVERRIDE;
 
--- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -36,123 +36,21 @@
 
 namespace Orthanc
 {
-  class DicomMoveScuJob::Command : public SetOfCommandsJob::ICommand
-  {
-  private:
-    DicomMoveScuJob&           that_;
-    std::unique_ptr<DicomMap>  findAnswer_;
 
-  public:
-    Command(DicomMoveScuJob& that,
-            const DicomMap&  findAnswer) :
-      that_(that),
-      findAnswer_(findAnswer.Clone())
-    {
-    }
-
-    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
-    {
-      that_.Retrieve(*findAnswer_);
-      return true;
-    }
-
-    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
-    {
-      findAnswer_->Serialize(target);
-    }
-  };
-
-
-  class DicomMoveScuJob::Unserializer :
-    public SetOfCommandsJob::ICommandUnserializer
-  {
-  private:
-    DicomMoveScuJob&   that_;
-
-  public:
-    explicit Unserializer(DicomMoveScuJob&  that) :
-      that_(that)
-    {
-    }
-
-    virtual ICommand* Unserialize(const Json::Value& source) const ORTHANC_OVERRIDE
-    {
-      DicomMap findAnswer;
-      findAnswer.Unserialize(source);
-      return new Command(that_, findAnswer);
-    }
-  };
 
 
   void DicomMoveScuJob::Retrieve(const DicomMap& findAnswer)
   {
     if (connection_.get() == NULL)
     {
-      connection_.reset(new DicomControlUserConnection(parameters_));
+      connection_.reset(new DicomControlUserConnection(parameters_, ScuOperationFlags_Move));
     }
     
+    connection_->SetProgressListener(this);
     connection_->Move(targetAet_, findAnswer);
   }
 
 
-  static void AddToQuery(DicomFindAnswers& query,
-                         const DicomMap& item)
-  {
-    query.Add(item);
-
-    /**
-     * Compatibility with Orthanc <= 1.9.4: Remove the
-     * "SpecificCharacterSet" (0008,0005) tag that is automatically
-     * added if creating a ParsedDicomFile object from a DicomMap.
-     **/
-    query.GetAnswer(query.GetSize() - 1).Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET);
-  }
-
-  // this method is used to implement the retrieve part of a Q&R 
-  // it keeps only the main dicom tags from the C-Find answer
-  void DicomMoveScuJob::AddFindAnswer(const DicomMap& answer)
-  {
-    DicomMap item;
-    item.CopyTagIfExists(answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL);
-    item.CopyTagIfExists(answer, DICOM_TAG_PATIENT_ID);
-    item.CopyTagIfExists(answer, DICOM_TAG_STUDY_INSTANCE_UID);
-    item.CopyTagIfExists(answer, DICOM_TAG_SERIES_INSTANCE_UID);
-    item.CopyTagIfExists(answer, DICOM_TAG_SOP_INSTANCE_UID);
-    item.CopyTagIfExists(answer, DICOM_TAG_ACCESSION_NUMBER);
-    AddToQuery(query_, item);
-    
-    AddCommand(new Command(*this, answer));
-  }
-
-  // this method is used to implement a C-Move
-  // it keeps all tags from the C-Move query
-  void DicomMoveScuJob::AddQuery(const DicomMap& query)
-  {
-    AddToQuery(query_, query);
-    AddCommand(new Command(*this, query));
-  }
-  
-  void DicomMoveScuJob::AddFindAnswer(QueryRetrieveHandler& query,
-                                      size_t i)
-  {
-    DicomMap answer;
-    query.GetAnswer(answer, i);
-    AddFindAnswer(answer);
-  }    
-
-
-  void DicomMoveScuJob::SetLocalAet(const std::string& aet)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      parameters_.SetLocalApplicationEntityTitle(aet);
-    }
-  }
-
   
   void DicomMoveScuJob::SetTargetAet(const std::string& aet)
   {
@@ -167,109 +65,33 @@
   }
 
   
-  void DicomMoveScuJob::SetRemoteModality(const RemoteModalityParameters& remote)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      parameters_.SetRemoteModality(remote);
-    }
-  }
 
 
-  void DicomMoveScuJob::SetTimeout(uint32_t seconds)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      parameters_.SetTimeout(seconds);
-    }
-  }
-
-  
-  void DicomMoveScuJob::Stop(JobStopReason reason)
-  {
-    connection_.reset();
-  }
-  
-
-  void DicomMoveScuJob::SetQueryFormat(DicomToJsonFormat format)
+  void DicomMoveScuJob::GetPublicContent(Json::Value& value) const
   {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      queryFormat_ = format;
-    }
-  }
+    DicomRetrieveScuBaseJob::GetPublicContent(value);
 
-
-  void DicomMoveScuJob::GetPublicContent(Json::Value& value)
-  {
-    SetOfCommandsJob::GetPublicContent(value);
-
-    value[LOCAL_AET] = parameters_.GetLocalApplicationEntityTitle();
-    value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle();
-
-    value[QUERY] = Json::objectValue;
-    query_.ToJson(value[QUERY], queryFormat_);
+    value[TARGET_AET] = targetAet_;
   }
 
 
   DicomMoveScuJob::DicomMoveScuJob(ServerContext& context,
                                    const Json::Value& serialized) :
-    SetOfCommandsJob(new Unserializer(*this), serialized),
-    context_(context),
-    parameters_(DicomAssociationParameters::UnserializeJob(serialized)),
-    targetAet_(SerializationToolbox::ReadString(serialized, TARGET_AET)),
-    query_(true),
-    queryFormat_(DicomToJsonFormat_Short)
+    DicomRetrieveScuBaseJob(context, serialized),
+    targetAet_(SerializationToolbox::ReadString(serialized, TARGET_AET))
   {
-    if (serialized.isMember(QUERY))
-    {
-      const Json::Value& query = serialized[QUERY];
-      if (query.type() == Json::arrayValue)
-      {
-        for (Json::Value::ArrayIndex i = 0; i < query.size(); i++)
-        {
-          DicomMap item;
-          FromDcmtkBridge::FromJson(item, query[i]);
-          AddToQuery(query_, item);
-        }
-      }
-    }
-
-    if (serialized.isMember(QUERY_FORMAT))
-    {
-      queryFormat_ = StringToDicomToJsonFormat(SerializationToolbox::ReadString(serialized, QUERY_FORMAT));
-    }
   }
 
   
-  bool DicomMoveScuJob::Serialize(Json::Value& target)
+  bool DicomMoveScuJob::Serialize(Json::Value& target) const
   {
-    if (!SetOfCommandsJob::Serialize(target))
+    if (!DicomRetrieveScuBaseJob::Serialize(target))
     {
       return false;
     }
     else
     {
-      parameters_.SerializeJob(target);
       target[TARGET_AET] = targetAet_;
-
-      // "Short" is for compatibility with Orthanc <= 1.9.4
-      target[QUERY] = Json::objectValue;
-      query_.ToJson(target[QUERY], DicomToJsonFormat_Short);
-
-      target[QUERY_FORMAT] = EnumerationToString(queryFormat_);
       
       return true;
     }
--- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -25,7 +25,8 @@
 
 #include "../../../OrthancFramework/Sources/Compatibility.h"
 #include "../../../OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h"
-#include "../../../OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h"
+// #include "../../../OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h"
+#include "DicomRetrieveScuBaseJob.h"
 
 #include "../QueryRetrieveHandler.h"
 
@@ -33,51 +34,22 @@
 {
   class ServerContext;
   
-  class DicomMoveScuJob : public SetOfCommandsJob
+  class DicomMoveScuJob : public DicomRetrieveScuBaseJob
   {
   private:
-    class Command;
-    class Unserializer;
+    std::string                 targetAet_;
     
-    ServerContext&              context_;
-    DicomAssociationParameters  parameters_;
-    std::string                 targetAet_;
-    DicomFindAnswers            query_;
-    DicomToJsonFormat           queryFormat_;  // New in 1.9.5
-
-    std::unique_ptr<DicomControlUserConnection>  connection_;
-    
-    void Retrieve(const DicomMap& findAnswer);
+    virtual void Retrieve(const DicomMap& findAnswer) ORTHANC_OVERRIDE;
     
   public:
     explicit DicomMoveScuJob(ServerContext& context) :
-      context_(context),
-      query_(false  /* this is not for worklists */),
-      queryFormat_(DicomToJsonFormat_Short)
+      DicomRetrieveScuBaseJob(context)
     {
     }
 
     DicomMoveScuJob(ServerContext& context,
                     const Json::Value& serialized);
 
-    void AddFindAnswer(const DicomMap& answer);
-    
-    void AddQuery(const DicomMap& query);
-
-    void AddFindAnswer(QueryRetrieveHandler& query,
-                       size_t i);
-
-    const DicomAssociationParameters& GetParameters() const
-    {
-      return parameters_;
-    }
-    
-    void SetLocalAet(const std::string& aet);
-
-    void SetRemoteModality(const RemoteModalityParameters& remote);
-
-    void SetTimeout(uint32_t timeout);
-
     const std::string& GetTargetAet() const
     {
       return targetAet_;
@@ -85,22 +57,13 @@
     
     void SetTargetAet(const std::string& aet);
 
-    void SetQueryFormat(DicomToJsonFormat format);
-
-    DicomToJsonFormat GetQueryFormat() const
-    {
-      return queryFormat_;
-    }
-
-    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
-
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "DicomMoveScu";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -0,0 +1,236 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomGetScuJob.h"
+
+#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../../OrthancFramework/Sources/SerializationToolbox.h"
+#include "../ServerContext.h"
+#include <dcmtk/dcmnet/dimse.h>
+#include <algorithm>
+#include "../../../OrthancFramework/Sources/Logging.h"
+
+static const char* const LOCAL_AET = "LocalAet";
+static const char* const QUERY = "Query";
+static const char* const QUERY_FORMAT = "QueryFormat";  // New in 1.9.5
+static const char* const REMOTE = "Remote";
+static const char* const TIMEOUT = "Timeout";
+
+namespace Orthanc
+{
+
+  static void AddToQuery(DicomFindAnswers& query,
+                         const DicomMap& item)
+  {
+    query.Add(item);
+
+    /**
+     * Compatibility with Orthanc <= 1.9.4: Remove the
+     * "SpecificCharacterSet" (0008,0005) tag that is automatically
+     * added if creating a ParsedDicomFile object from a DicomMap.
+     **/
+    query.GetAnswer(query.GetSize() - 1).Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET);
+  }
+
+  // this method is used to implement the retrieve part of a Q&R 
+  // it keeps only the main dicom tags from the C-Find answer
+  void DicomRetrieveScuBaseJob::AddFindAnswer(const DicomMap& answer)
+  {
+    DicomMap item;
+    item.CopyTagIfExists(answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+    item.CopyTagIfExists(answer, DICOM_TAG_PATIENT_ID);
+    item.CopyTagIfExists(answer, DICOM_TAG_STUDY_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_SERIES_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_SOP_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_ACCESSION_NUMBER);
+    AddToQuery(query_, item);
+    
+    AddCommand(new Command(*this, answer));
+  }
+
+  void DicomRetrieveScuBaseJob::AddFindAnswer(QueryRetrieveHandler& query,
+                                              size_t i)
+  {
+    DicomMap answer;
+    query.GetAnswer(answer, i);
+    AddFindAnswer(answer);
+  }    
+
+  // this method is used to implement a C-Move
+  // it keeps all tags from the C-Move query
+  void DicomRetrieveScuBaseJob::AddQuery(const DicomMap& query)
+  {
+    AddToQuery(query_, query);
+    AddCommand(new Command(*this, query));
+  }
+ 
+
+  void DicomRetrieveScuBaseJob::SetLocalAet(const std::string& aet)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parameters_.SetLocalApplicationEntityTitle(aet);
+    }
+  }
+
+  
+  void DicomRetrieveScuBaseJob::SetRemoteModality(const RemoteModalityParameters& remote)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parameters_.SetRemoteModality(remote);
+    }
+  }
+
+
+  void DicomRetrieveScuBaseJob::SetTimeout(uint32_t seconds)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parameters_.SetTimeout(seconds);
+    }
+  }
+
+  
+  void DicomRetrieveScuBaseJob::Stop(JobStopReason reason)
+  {
+    connection_.reset();
+  }
+  
+
+  void DicomRetrieveScuBaseJob::SetQueryFormat(DicomToJsonFormat format)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      queryFormat_ = format;
+    }
+  }
+
+
+  void DicomRetrieveScuBaseJob::GetPublicContent(Json::Value& value) const
+  {
+    SetOfCommandsJob::GetPublicContent(value);
+
+    value[LOCAL_AET] = parameters_.GetLocalApplicationEntityTitle();
+    value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle();
+
+    value[QUERY] = Json::objectValue;
+    query_.ToJson(value[QUERY], queryFormat_);
+  }
+
+
+  DicomRetrieveScuBaseJob::DicomRetrieveScuBaseJob(ServerContext& context,
+                                                   const Json::Value& serialized) :
+    SetOfCommandsJob(new Unserializer(*this), serialized),
+    context_(context),
+    parameters_(DicomAssociationParameters::UnserializeJob(serialized)),
+    query_(true),
+    queryFormat_(DicomToJsonFormat_Short),
+    nbRemainingSubOperations_(0),
+    nbCompletedSubOperations_(0),
+    nbFailedSubOperations_(0),
+    nbWarningSubOperations_(0)  
+  {
+    if (serialized.isMember(QUERY))
+    {
+      const Json::Value& query = serialized[QUERY];
+      if (query.type() == Json::arrayValue)
+      {
+        for (Json::Value::ArrayIndex i = 0; i < query.size(); i++)
+        {
+          DicomMap item;
+          FromDcmtkBridge::FromJson(item, query[i]);
+          AddToQuery(query_, item);
+        }
+      }
+    }
+
+    if (serialized.isMember(QUERY_FORMAT))
+    {
+      queryFormat_ = StringToDicomToJsonFormat(SerializationToolbox::ReadString(serialized, QUERY_FORMAT));
+    }
+  }
+
+  
+  bool DicomRetrieveScuBaseJob::Serialize(Json::Value& target) const
+  {
+    if (!SetOfCommandsJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      parameters_.SerializeJob(target);
+
+      // "Short" is for compatibility with Orthanc <= 1.9.4
+      target[QUERY] = Json::objectValue;
+      query_.ToJson(target[QUERY], DicomToJsonFormat_Short);
+
+      target[QUERY_FORMAT] = EnumerationToString(queryFormat_);
+      
+      return true;
+    }
+  }
+
+  void DicomRetrieveScuBaseJob::OnProgressUpdated(uint16_t nbRemainingSubOperations,
+                                                  uint16_t nbCompletedSubOperations,
+                                                  uint16_t nbFailedSubOperations,
+                                                  uint16_t nbWarningSubOperations)
+  {
+    boost::mutex::scoped_lock lock(progressMutex_);
+
+    nbRemainingSubOperations_ = nbRemainingSubOperations;
+    nbCompletedSubOperations_ = nbCompletedSubOperations;
+    nbFailedSubOperations_ = nbFailedSubOperations;
+    nbWarningSubOperations_ = nbWarningSubOperations;
+  }
+
+  float DicomRetrieveScuBaseJob::GetProgress() const
+  {
+    boost::mutex::scoped_lock lock(progressMutex_);
+    
+    uint32_t totalOperations = nbRemainingSubOperations_ + nbCompletedSubOperations_ + nbFailedSubOperations_ + nbWarningSubOperations_;
+    if (totalOperations == 0)
+    {
+      return 0.0f;
+    }
+
+    return float(nbCompletedSubOperations_ + nbFailedSubOperations_ + nbWarningSubOperations_) / float(totalOperations);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -0,0 +1,154 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h"
+#include "../../../OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h"
+#include <boost/thread/mutex.hpp>
+
+#include "../QueryRetrieveHandler.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+
+  class DicomRetrieveScuBaseJob : public SetOfCommandsJob, public DicomControlUserConnection::IProgressListener
+  {
+  protected:
+    class Command : public SetOfCommandsJob::ICommand
+    {
+    private:
+      DicomRetrieveScuBaseJob &that_;
+      std::unique_ptr<DicomMap> findAnswer_;
+
+    public:
+      Command(DicomRetrieveScuBaseJob &that,
+              const DicomMap &findAnswer) :
+      that_(that),
+      findAnswer_(findAnswer.Clone())
+      {
+      }
+
+      virtual bool Execute(const std::string &jobId) ORTHANC_OVERRIDE
+      {
+        that_.Retrieve(*findAnswer_);
+        return true;
+      }
+
+      virtual void Serialize(Json::Value &target) const ORTHANC_OVERRIDE
+      {
+        findAnswer_->Serialize(target);
+      }
+    };
+
+    class Unserializer : public SetOfCommandsJob::ICommandUnserializer
+    {
+    protected:
+      DicomRetrieveScuBaseJob &that_;
+
+    public:
+      explicit Unserializer(DicomRetrieveScuBaseJob &that) : 
+      that_(that)
+      {
+      }
+
+      virtual ICommand *Unserialize(const Json::Value &source) const ORTHANC_OVERRIDE
+      {
+        DicomMap findAnswer;
+        findAnswer.Unserialize(source);
+        return new Command(that_, findAnswer);
+      }
+    };
+
+    ServerContext &context_;
+    DicomAssociationParameters parameters_;
+    DicomFindAnswers query_;
+    DicomToJsonFormat queryFormat_; // New in 1.9.5
+
+    std::unique_ptr<DicomControlUserConnection> connection_;
+
+    mutable boost::mutex progressMutex_;
+    uint16_t nbRemainingSubOperations_;
+    uint16_t nbCompletedSubOperations_;
+    uint16_t nbFailedSubOperations_;
+    uint16_t nbWarningSubOperations_;
+
+    virtual void Retrieve(const DicomMap &findAnswer) = 0;
+
+    explicit DicomRetrieveScuBaseJob(ServerContext &context) : 
+    context_(context),
+    query_(false /* this is not for worklists */),
+    queryFormat_(DicomToJsonFormat_Short)
+    {
+    }
+
+    DicomRetrieveScuBaseJob(ServerContext &context,
+                            const Json::Value &serialized);
+
+  public:
+    virtual void AddFindAnswer(const DicomMap &answer);
+
+    void AddQuery(const DicomMap& query);
+
+    void AddFindAnswer(QueryRetrieveHandler &query,
+                       size_t i);
+
+    const DicomAssociationParameters &GetParameters() const
+    {
+      return parameters_;
+    }
+
+    void SetLocalAet(const std::string &aet);
+
+    void SetRemoteModality(const RemoteModalityParameters &remote);
+
+    void SetTimeout(uint32_t timeout);
+
+    void SetQueryFormat(DicomToJsonFormat format);
+
+    DicomToJsonFormat GetQueryFormat() const
+    {
+      return queryFormat_;
+    }
+
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
+
+    virtual void GetPublicContent(Json::Value &value) const ORTHANC_OVERRIDE;
+
+    virtual bool Serialize(Json::Value &target) const ORTHANC_OVERRIDE;
+
+    virtual void OnProgressUpdated(uint16_t nbRemainingSubOperations,
+                                    uint16_t nbCompletedSubOperations,
+                                    uint16_t nbFailedSubOperations,
+                                    uint16_t nbWarningSubOperations) ORTHANC_OVERRIDE;
+
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
+
+    virtual bool NeedsProgressUpdateBetweenSteps() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+  };
+}
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -353,7 +353,7 @@
   }
   
 
-  void MergeStudyJob::GetPublicContent(Json::Value& value)
+  void MergeStudyJob::GetPublicContent(Json::Value& value) const
   {
     CleaningInstancesJob::GetPublicContent(value);
     value["TargetStudy"] = targetStudy_;
@@ -386,7 +386,7 @@
   }
 
   
-  bool MergeStudyJob::Serialize(Json::Value& target)
+  bool MergeStudyJob::Serialize(Json::Value& target) const
   {
     if (!CleaningInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -89,13 +89,13 @@
     {
     }
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "MergeStudy";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -220,7 +220,7 @@
   }
 
 
-  void OrthancPeerStoreJob::GetPublicContent(Json::Value& value)
+  void OrthancPeerStoreJob::GetPublicContent(Json::Value& value) const
   {
     SetOfInstancesJob::GetPublicContent(value);
     
@@ -285,7 +285,7 @@
   }
 
 
-  bool OrthancPeerStoreJob::Serialize(Json::Value& target)
+  bool OrthancPeerStoreJob::Serialize(Json::Value& target) const
   {
     if (!SetOfInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -92,13 +92,13 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;   // For pausing jobs
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "OrthancPeerStore";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -542,7 +542,7 @@
   }
 
 
-  void ResourceModificationJob::GetPublicContent(Json::Value& value)
+  void ResourceModificationJob::GetPublicContent(Json::Value& value) const
   {
     boost::recursive_mutex::scoped_lock lock(outputMutex_);
 
@@ -636,7 +636,7 @@
     }
   }
   
-  bool ResourceModificationJob::Serialize(Json::Value& value)
+  bool ResourceModificationJob::Serialize(Json::Value& value) const
   {
     if (modification_.get() == NULL)
     {
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -116,14 +116,14 @@
     // Only possible if "IsSingleResourceModification()"
     ResourceType GetOutputLevel() const;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "ResourceModification";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
     
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE;
 
     virtual void Reset() ORTHANC_OVERRIDE;
 
--- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -306,7 +306,7 @@
   }
   
     
-  void SplitStudyJob::GetPublicContent(Json::Value& value)
+  void SplitStudyJob::GetPublicContent(Json::Value& value) const
   {
     CleaningInstancesJob::GetPublicContent(value);
 
@@ -351,7 +351,7 @@
   }
 
   
-  bool SplitStudyJob::Serialize(Json::Value& target)
+  bool SplitStudyJob::Serialize(Json::Value& target) const
   {
     if (!CleaningInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -113,13 +113,13 @@
     {
     }
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "SplitStudy";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -404,7 +404,7 @@
   }
 
 
-  void StorageCommitmentScpJob::GetPublicContent(Json::Value& value)
+  void StorageCommitmentScpJob::GetPublicContent(Json::Value& value) const
   {
     SetOfCommandsJob::GetPublicContent(value);
       
@@ -434,7 +434,7 @@
   }
   
 
-  bool StorageCommitmentScpJob::Serialize(Json::Value& target)
+  bool StorageCommitmentScpJob::Serialize(Json::Value& target) const
   {
     if (!SetOfCommandsJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -89,13 +89,13 @@
     {
     }
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "StorageCommitmentScp";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -376,7 +376,7 @@
   static const char* KEY_WORKERS_COUNT = "WorkersCount";
 
 
-  void ThreadedSetOfInstancesJob::GetPublicContent(Json::Value& target)
+  void ThreadedSetOfInstancesJob::GetPublicContent(Json::Value& target) const
   {
     boost::recursive_mutex::scoped_lock lock(mutex_);
 
@@ -391,7 +391,7 @@
   }
 
 
-  bool ThreadedSetOfInstancesJob::Serialize(Json::Value& target)
+  bool ThreadedSetOfInstancesJob::Serialize(Json::Value& target) const
   {
     boost::recursive_mutex::scoped_lock lock(mutex_);
 
@@ -478,7 +478,7 @@
     return keepSource_;
   }
 
-  float ThreadedSetOfInstancesJob::GetProgress()
+  float ThreadedSetOfInstancesJob::GetProgress() const
   {
     boost::recursive_mutex::scoped_lock lock(mutex_);
 
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h	Thu Jan 16 16:40:08 2025 +0100
@@ -140,15 +140,15 @@
     
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
     bool IsStarted() const;
 
     virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
     
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
     
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
--- a/OrthancServer/Sources/main.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/Sources/main.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -496,6 +496,13 @@
     context_.GetAcceptedTransferSyntaxes(target);
   }
 
+  virtual void GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& target,
+                                                  const std::string& remoteIp,
+                                                  const std::string& remoteAet,
+                                                  const std::string& calledAet) ORTHANC_OVERRIDE
+  {
+    context_.GetProposedStorageTransferSyntaxes(target);
+  }
   
   virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
                                          const std::string& remoteAet,
@@ -503,6 +510,12 @@
   {
     return context_.IsUnknownSopClassAccepted();
   }
+
+  virtual void GetAcceptedSopClasses(std::set<std::string>& sopClasses,
+                                     size_t maxCount) ORTHANC_OVERRIDE
+  {
+    context_.GetAcceptedSopClasses(sopClasses, maxCount);
+  }
 };
 
 
@@ -889,6 +902,7 @@
     PrintErrorCode(ErrorCode_AlreadyExistingTag, "Cannot override the value of a tag that already exists");
     PrintErrorCode(ErrorCode_NoStorageCommitmentHandler, "No request handler factory for DICOM N-ACTION SCP (storage commitment)");
     PrintErrorCode(ErrorCode_NoCGetHandler, "No request handler factory for DICOM C-GET SCP");
+    PrintErrorCode(ErrorCode_DicomGetUnavailable, "DicomUserConnection: The C-GET command is not supported by the remote SCP");
     PrintErrorCode(ErrorCode_UnsupportedMediaType, "Unsupported media type");
   }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -0,0 +1,150 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeadersUnitTests.h"
+#include <gtest/gtest.h>
+
+#include "../../OrthancFramework/Sources/Compatibility.h"
+#include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
+#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h"
+#include "../../OrthancFramework/Sources/Logging.h"
+#include "../../OrthancFramework/Sources/SerializationToolbox.h"
+
+#include "../Sources/Database/SQLiteDatabaseWrapper.h"
+#include "../Sources/ServerContext.h"
+
+using namespace Orthanc;
+
+TEST(ServerConfig, AcceptedSopClasses)
+{
+  const std::string path = "UnitTestsStorage";
+
+  MemoryStorageArea storage;
+  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
+  db.Open();
+  ServerContext context(db, storage, true /* running unit tests */, 10, false, 1);
+
+  { // default config -> all SOP Classes should be accepted
+    std::set<std::string> s;
+
+    context.GetAcceptedSopClasses(s, 0);
+    ASSERT_LE(100u, s.size());
+
+    ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+    ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") != s.end());
+
+    context.GetAcceptedSopClasses(s, 1);
+    ASSERT_EQ(1u, s.size());
+  }
+
+  {
+    std::list<std::string> acceptedStorageClasses;
+    std::set<std::string> rejectedStorageClasses;
+
+    std::set<std::string> s;
+    context.GetAcceptedSopClasses(s, 0);
+    size_t allSize = s.size();
+
+    { // default config but reject one class
+      acceptedStorageClasses.clear();
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.4");
+
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_EQ(allSize - 1, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") != s.end());
+
+      context.GetAcceptedSopClasses(s, 1);
+      ASSERT_EQ(1u, s.size());
+    }
+
+    { // default config but reject one regex
+      acceptedStorageClasses.clear();
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.*");
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+    }
+
+    { // accept a single - no rejection
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.1.4");
+      rejectedStorageClasses.clear();
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_EQ(1, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+    }
+
+    { // accept from regex - reject one
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.*");
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.12.1.1");
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_LE(10, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.2.1") != s.end());
+    }
+
+    { // accept from regex - reject from regex
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.*");
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.12.1.*");
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_LE(10, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.2.1") != s.end());
+    }
+
+    { // accept one that is unknown form DCMTK
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.3.4");
+      rejectedStorageClasses.clear();
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_EQ(1, s.size());
+      ASSERT_TRUE(s.find("1.2.3.4") != s.end());
+    }
+
+  }
+
+  context.Stop();
+  db.Close();
+}
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Thu Jan 16 15:49:50 2025 +0100
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Thu Jan 16 16:40:08 2025 +0100
@@ -106,24 +106,24 @@
     {
     }
 
-    virtual float GetProgress() ORTHANC_OVERRIDE
+    virtual float GetProgress() const ORTHANC_OVERRIDE
     {
       return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
     }
 
-    virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& type) const ORTHANC_OVERRIDE
     {
       type = "DummyJob";
     }
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE
     {
       value = Json::objectValue;
       value["Type"] = "DummyJob";
       return true;
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE
     {
       value["hello"] = "world";
     }
@@ -202,7 +202,7 @@
     {
     }
 
-    virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& s) const ORTHANC_OVERRIDE
     {
       s = "DummyInstancesJob";
     }
--- a/TODO	Thu Jan 16 15:49:50 2025 +0100
+++ b/TODO	Thu Jan 16 16:40:08 2025 +0100
@@ -1,3 +1,11 @@
+current work on C-Get SCU:
+- for the negotiation, limit SOPClassUID to the ones listed in a C-Find response or to a list provided in the Rest API ?
+- SetupPresentationContexts
+- handle progress
+- handle cancellation when the job is cancelled ?
+
+
+
 =======================
 === Orthanc Roadmap ===
 =======================
@@ -32,6 +40,11 @@
 * Add configurations to enable/disable warnings:
   - Modifying an instance while keeping its original SOPInstanceUID: This should be avoided!
   - Modifying a study while keeping its original StudyInstanceUID: This should be avoided!
+  In order to be able to disable/enable warnings in both the server and the framework, we should add a map of
+  enabled warnings in the logging classes directly and have something like:
+  LOG_WARNING_IF_ENABLED("Warning_ID") << ...
+  ENABLE_WARNING("Warning_ID", true)
+  Warnings from Framework should have a separate range like W999_ or WF001_ ...
 * Store the job registry in a dedicatd table in DB ?
   https://discourse.orthanc-server.org/t/performance-issue-when-adding-a-lot-of-jobs-in-the-queue/3915/2
   Note: that might also be the right time to have a central jobs registry when working
@@ -173,9 +186,12 @@
 Mid-term
 --------
 
-* Support C-GET SCU (note that C-GET SCP was introduced in Orthanc 1.7.0)
-* Configure the list of accepted SOP Classes
-  https://discourse.orthanc-server.org/t/can-you-limit-the-sop-classes-accepted-as-store-scp/4606
+* Check how Orthanc shall behave wrt to AcceptedSopClasses in these situations (consider Orthanc
+  accepts CT but not PT)
+  - What shall we log/warn if an external modality tries to send a PT/CT
+  - What shall we log/warn if we try to C-GET a PT/CT
+  Should the rejected files be logged as Failed, Warning, Refused, ...
+  Note: some tentative work has been initiated in the get-scu-test branch.
 * Support "Retrieve AE Title" (0008,0054) in C-FIND:
   - On SCP side: done by https://orthanc.uclouvain.be/hg/orthanc/rev/1ec3e1e18f50
   - On SCU side:
@@ -185,9 +201,6 @@
 * Strict hierarchical C-FIND:
   https://groups.google.com/d/msg/orthanc-users/VBHpeGVSNKM/tkaVvjWFBwAJ
 * report DIMSE error codes in Rest API and job status for /store /query /move /retrieve
-* report progress report of C-Move operation in jop progress.  There are 
-  progress callbacks available in DIMSE_moveUser
-  https://groups.google.com/g/orthanc-users/c/c8cGnA7FzsE/m/BSs66D8wBwAJ
 * Log outgoing C-Find queries
 * Support other Transfer Syntaxes in the Worklist plugin:
   https://discourse.orthanc-server.org/t/could-you-please-create-an-option-to-set-the-transfer-syntax-in-the-worklist-plugin-currently-little-endian-explicit-is-fixed/4871