changeset 4940:304514ce84ee more-tags

tools/find + C-Find + list-resources now all using the same code (ExpandResource) to build 'computed tags'
author Alain Mazy <am@osimis.io>
date Tue, 15 Mar 2022 15:57:21 +0100
parents e8a2e145c80e
children 96a3e81eba90
files NEWS OrthancFramework/Sources/DicomFormat/DicomArray.cpp OrthancFramework/Sources/DicomFormat/DicomArray.h OrthancFramework/Sources/DicomFormat/DicomMap.cpp OrthancFramework/Sources/DicomFormat/DicomMap.h OrthancFramework/UnitTestsSources/DicomMapTests.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancFindRequestHandler.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h
diffstat 11 files changed, 566 insertions(+), 596 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Mar 15 09:09:52 2022 +0100
+++ b/NEWS	Tue Mar 15 15:57:21 2022 +0100
@@ -1,10 +1,6 @@
 Pending changes in the mainline
 ===============================
 
-* BREAKING_CHANGE: (TODO: when CFind uses ExpandResource)
-  If trying to get SOPClassesInStudy in a study that was saved in an Orthanc version
-  prior to 1.2.0, you will now get an error.  Orthanc logs will show an error message
-  asking you to POST to /studies/.../reconstruct to rebuild the missing study data.
 
 
 Documentation
--- a/OrthancFramework/Sources/DicomFormat/DicomArray.cpp	Tue Mar 15 09:09:52 2022 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomArray.cpp	Tue Mar 15 15:57:21 2022 +0100
@@ -69,6 +69,16 @@
     }
   }
 
+  void DicomArray::GetTags(std::set<DicomTag>& tags) const
+  {
+    tags.clear();
+
+    for (size_t i = 0; i < elements_.size(); i++)
+    {
+      tags.insert(elements_[i]->GetTag());
+    }
+   
+  }
 
   void DicomArray::Print(FILE* fp) const
   {
--- a/OrthancFramework/Sources/DicomFormat/DicomArray.h	Tue Mar 15 09:09:52 2022 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomArray.h	Tue Mar 15 15:57:21 2022 +0100
@@ -46,6 +46,8 @@
 
     const DicomElement& GetElement(size_t i) const;
 
+    void GetTags(std::set<DicomTag>& tags) const;
+
     void Print(FILE* fp) const;  // For debugging only
   };
 }
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Tue Mar 15 09:09:52 2022 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Tue Mar 15 15:57:21 2022 +0100
@@ -634,6 +634,85 @@
             IsMainDicomTag(tag, ResourceType_Instance));
   }
 
+  bool DicomMap::IsComputedTag(const DicomTag& tag)
+  {
+    return (IsComputedTag(tag, ResourceType_Patient) ||
+            IsComputedTag(tag, ResourceType_Study) ||
+            IsComputedTag(tag, ResourceType_Series) ||
+            IsComputedTag(tag, ResourceType_Instance));
+  }
+
+  bool DicomMap::IsComputedTag(const DicomTag& tag, ResourceType level)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return (
+          tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES ||
+          tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES ||
+          tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES
+        );
+      case ResourceType_Study:
+        return (
+          tag == DICOM_TAG_MODALITIES_IN_STUDY ||
+          tag == DICOM_TAG_SOP_CLASSES_IN_STUDY ||
+          tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES ||
+          tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES
+        );
+      case ResourceType_Series:
+        return (
+          tag == DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES
+        );
+      case ResourceType_Instance:
+        return false;
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  bool DicomMap::HasOnlyComputedTags(const std::set<DicomTag>& tags)
+  {
+    if (tags.size() == 0)
+    {
+      return false;
+    }
+
+    for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      if (!IsComputedTag(*it))
+      {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  bool DicomMap::HasComputedTags(const std::set<DicomTag>& tags)
+  {
+    for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      if (IsComputedTag(*it))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  bool DicomMap::HasComputedTags(const std::set<DicomTag>& tags, ResourceType level)
+  {
+    for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      if (IsComputedTag(*it, level))
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+
   const std::set<DicomTag>& DicomMap::GetMainDicomTags(ResourceType level)
   {
     return DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(level);
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h	Tue Mar 15 09:09:52 2022 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h	Tue Mar 15 15:57:21 2022 +0100
@@ -137,6 +137,16 @@
 
     static bool IsMainDicomTag(const DicomTag& tag);
 
+    static bool IsComputedTag(const DicomTag& tag, ResourceType level);
+
+    static bool IsComputedTag(const DicomTag& tag);
+
+    static bool HasOnlyComputedTags(const std::set<DicomTag>& tags);
+
+    static bool HasComputedTags(const std::set<DicomTag>& tags, ResourceType level);
+
+    static bool HasComputedTags(const std::set<DicomTag>& tags);
+
     static const std::set<DicomTag>& GetMainDicomTags(ResourceType level);
 
     // returns a string uniquely identifying the list of main dicom tags for a level
--- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Tue Mar 15 09:09:52 2022 +0100
+++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp	Tue Mar 15 15:57:21 2022 +0100
@@ -556,6 +556,54 @@
 }
 
 
+TEST(DicomMap, ComputedTags)
+{
+  {
+    std::set<DicomTag> tags;
+
+    ASSERT_FALSE(DicomMap::HasOnlyComputedTags(tags));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Study));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient));
+  }
+
+  {
+    std::set<DicomTag> tags;
+    tags.insert(DICOM_TAG_ACCESSION_NUMBER);
+
+    ASSERT_FALSE(DicomMap::HasOnlyComputedTags(tags));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Study));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient));
+  }
+
+  {
+    std::set<DicomTag> tags;
+    tags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
+
+    ASSERT_TRUE(DicomMap::HasOnlyComputedTags(tags));
+    ASSERT_TRUE(DicomMap::HasComputedTags(tags, ResourceType_Study));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance));
+  }
+
+  {
+    std::set<DicomTag> tags;
+    tags.insert(DICOM_TAG_ACCESSION_NUMBER);
+    tags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
+
+    ASSERT_FALSE(DicomMap::HasOnlyComputedTags(tags));
+    ASSERT_TRUE(DicomMap::HasComputedTags(tags, ResourceType_Study));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series));
+    ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance));
+  }
+
+}
+
 TEST(DicomMap, RemoveBinary)
 {
   DicomMap b;
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Tue Mar 15 09:09:52 2022 +0100
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Tue Mar 15 15:57:21 2022 +0100
@@ -713,10 +713,11 @@
   bool StatelessDatabaseOperations::ExpandResource(ExpandedResource& target,
                                                    const std::string& publicId,
                                                    ResourceType level,
-                                                   const std::set<DicomTag>& requestedTags)
+                                                   const std::set<DicomTag>& requestedTags,
+                                                   ExpandResourceDbFlags expandFlags)
   {    
-    class Operations : public ReadOnlyOperationsT5<
-      bool&, ExpandedResource&, const std::string&, ResourceType, const std::set<DicomTag>&>
+    class Operations : public ReadOnlyOperationsT6<
+      bool&, ExpandedResource&, const std::string&, ResourceType, const std::set<DicomTag>&, ExpandResourceDbFlags>
     {
     private:
   
@@ -759,163 +760,6 @@
         }
       }
 
-      static void ComputeSeriesTags(DicomMap& result,
-                                    const std::list<std::string>& children,
-                                    const std::set<DicomTag>& requestedTags)
-      {
-        if (requestedTags.count(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES) > 0)
-        {
-          result.SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES,
-                          boost::lexical_cast<std::string>(children.size()), false);
-        }
-      }
-
-      static void ComputeStudyTags(DicomMap& result,
-                                   ReadOnlyTransaction& transaction,
-                                   const std::string& studyPublicId,
-                                   int64_t studyInternalId,
-                                   const std::set<DicomTag>& requestedTags)
-      {
-        std::list<int64_t> seriesInternalIds;
-        std::list<int64_t> instancesInternalIds;
-
-        transaction.GetChildrenInternalId(seriesInternalIds, studyInternalId);
-
-        if (requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) > 0)
-        {
-          result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES,
-                          boost::lexical_cast<std::string>(seriesInternalIds.size()), false);
-        }
-
-        if (requestedTags.count(DICOM_TAG_MODALITIES_IN_STUDY) > 0)
-        {
-          std::set<std::string> values;
-
-          for (std::list<int64_t>::const_iterator
-                it = seriesInternalIds.begin(); it != seriesInternalIds.end(); ++it)
-          {
-            if (requestedTags.count(DICOM_TAG_MODALITIES_IN_STUDY) > 0)
-            {
-              DicomMap tags;
-              transaction.GetMainDicomTags(tags, *it);
-
-              const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
-
-              if (value != NULL &&
-                  !value->IsNull() &&
-                  !value->IsBinary())
-              {
-                values.insert(value->GetContent());
-              }
-            }
-          }
-
-          std::string modalities;
-          Toolbox::JoinStrings(modalities, values, "\\");
-          result.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, modalities, false);
-        }
-
-        if (requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) > 0
-          || requestedTags.count(DICOM_TAG_SOP_CLASSES_IN_STUDY) > 0)
-        {
-          for (std::list<int64_t>::const_iterator
-                it = seriesInternalIds.begin(); it != seriesInternalIds.end(); ++it)
-          {
-            std::list<int64_t> seriesInstancesIds;
-            transaction.GetChildrenInternalId(seriesInstancesIds, *it);
-
-            instancesInternalIds.splice(instancesInternalIds.end(), seriesInstancesIds);
-          }
-
-          if (requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) > 0)
-          {
-            result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES,
-                      boost::lexical_cast<std::string>(instancesInternalIds.size()), false);      
-          }
-
-          if (requestedTags.count(DICOM_TAG_SOP_CLASSES_IN_STUDY) > 0)
-          {
-            std::set<std::string> values;
-
-            for (std::list<int64_t>::const_iterator
-                  it = instancesInternalIds.begin(); it != instancesInternalIds.end(); ++it)
-            {
-              std::map<MetadataType, std::string> instanceMetadata;
-              // Extract the metadata
-              transaction.GetAllMetadata(instanceMetadata, *it);
-
-              std::string value;
-              if (!LookupStringMetadata(value, instanceMetadata, MetadataType_Instance_SopClassUid))
-              {
-                throw OrthancException(ErrorCode_InternalError, "Unable to get the SOP Class Uid from an instance of the study " + studyPublicId + " because the instance has been saved with an old version of Orthanc (< 1.2.0).  You should POST to /studies/" + studyPublicId + "/reconstruct to avoid this error");
-              }
-
-              values.insert(value);
-            }
-
-            std::string sopClassUids;
-            Toolbox::JoinStrings(sopClassUids, values, "\\");
-            result.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, sopClassUids, false);
-          }
-        }
-      }
-
-    static void ComputePatientTags(DicomMap& result,
-                                   ReadOnlyTransaction& transaction,
-                                   const std::string& patientPublicId,
-                                   int64_t patientInternalId,
-                                   const std::set<DicomTag>& requestedTags)
-    {
-      std::list<int64_t> studiesInternalIds;
-      std::list<int64_t> seriesInternalIds;
-      std::list<int64_t> instancesInternalIds;
-
-      bool hasNbRelatedStudies = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) > 0;
-      bool hasNbRelatedSeries = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) > 0;
-      bool hasNbRelatedInstances = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES) > 0;
-
-      transaction.GetChildrenInternalId(studiesInternalIds, patientInternalId);
-
-      if (hasNbRelatedStudies)
-      {
-        result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES,
-                        boost::lexical_cast<std::string>(studiesInternalIds.size()), false);
-      }
-
-      if (hasNbRelatedSeries || hasNbRelatedInstances)
-      {
-        for (std::list<int64_t>::const_iterator
-              it = studiesInternalIds.begin(); it != studiesInternalIds.end(); ++it)
-        {
-          std::list<int64_t> thisSeriesIds;
-          transaction.GetChildrenInternalId(thisSeriesIds, *it);
-          seriesInternalIds.splice(seriesInternalIds.end(), thisSeriesIds);
-
-          if (hasNbRelatedInstances)
-          {
-            for (std::list<int64_t>::const_iterator
-                  it2 = seriesInternalIds.begin(); it2 != seriesInternalIds.end(); ++it2)
-            {
-              std::list<int64_t> thisInstancesIds;
-              transaction.GetChildrenInternalId(thisInstancesIds, *it2);
-              instancesInternalIds.splice(instancesInternalIds.end(), thisInstancesIds);
-            }
-          }
-        }
-
-        if (hasNbRelatedSeries)
-        {
-          result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES,
-                          boost::lexical_cast<std::string>(seriesInternalIds.size()), false);
-        }
-
-        if (hasNbRelatedInstances)
-        {
-          result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES,
-                          boost::lexical_cast<std::string>(instancesInternalIds.size()), false);
-        }
-      }
-    }
 
     public:
       virtual void ApplyTuple(ReadOnlyTransaction& transaction,
@@ -933,6 +777,7 @@
         else
         {
           ExpandedResource& target = tuple.get<1>();
+          ExpandResourceDbFlags expandFlags = tuple.get<5>();
 
           // Set information about the parent resource (if it exists)
           if (type == ResourceType_Patient)
@@ -952,156 +797,134 @@
             target.parentId_ = parent;
           }
 
-          // List the children resources
-          transaction.GetChildrenPublicId(target.childrenIds_, internalId);
-
-          // Extract the metadata
-          transaction.GetAllMetadata(target.metadata_, internalId);
-
-          // Set the resource type
           target.type_ = type;
-
-          switch (type)
+          target.id_ = tuple.get<2>();
+
+          if (expandFlags & ExpandResourceDbFlags_IncludeChildren)
           {
-            case ResourceType_Patient:
-            case ResourceType_Study:
-              break;
-
-            case ResourceType_Series:
+            // List the children resources
+            transaction.GetChildrenPublicId(target.childrenIds_, internalId);
+          }
+
+          if (expandFlags & ExpandResourceDbFlags_IncludeMetadata)
+          {
+            // Extract the metadata
+            transaction.GetAllMetadata(target.metadata_, internalId);
+
+            switch (type)
             {
-              int64_t i;
-              if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Series_ExpectedNumberOfInstances))
-              {
-                target.expectedNumberOfInstances_ = static_cast<int>(i);
-                target.status_ = EnumerationToString(transaction.GetSeriesStatus(internalId, i));
-              }
-              else
-              {
-                target.expectedNumberOfInstances_ = -1;
-                target.status_ = EnumerationToString(SeriesStatus_Unknown);
-              }
-
-              break;
-            }
-
-            case ResourceType_Instance:
-            {
-              FileInfo attachment;
-              int64_t revision;  // ignored
-              if (!transaction.LookupAttachment(attachment, revision, internalId, FileContentType_Dicom))
-              {
-                throw OrthancException(ErrorCode_InternalError);
-              }
-
-              target.fileSize_ = static_cast<unsigned int>(attachment.GetUncompressedSize());
-              target.fileUuid_ = attachment.GetUuid();
-
-              int64_t i;
-              if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Instance_IndexInSeries))
+              case ResourceType_Patient:
+              case ResourceType_Study:
+                break;
+
+              case ResourceType_Series:
               {
-                target.indexInSeries_ = static_cast<int>(i);
-              }
-              else
-              {
-                target.indexInSeries_ = -1;
+                int64_t i;
+                if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Series_ExpectedNumberOfInstances))
+                {
+                  target.expectedNumberOfInstances_ = static_cast<int>(i);
+                  target.status_ = EnumerationToString(transaction.GetSeriesStatus(internalId, i));
+                }
+                else
+                {
+                  target.expectedNumberOfInstances_ = -1;
+                  target.status_ = EnumerationToString(SeriesStatus_Unknown);
+                }
+
+                break;
               }
 
-              break;
-            }
-
-            default:
-              throw OrthancException(ErrorCode_InternalError);
-          }
-
-          // check the main dicom tags list has not changed since the resource was stored
-          target.mainDicomTagsSignature_ = DicomMap::GetDefaultMainDicomTagsSignature(type);
-          LookupStringMetadata(target.mainDicomTagsSignature_, target.metadata_, MetadataType_MainDicomTagsSignature);
-
-          // Record the remaining information
-          target.id_ = tuple.get<2>();
-
-          // read all tags from DB
-          transaction.GetMainDicomTags(target.tags_, internalId);
-
-          // check if we have access to all requestedTags or if we must get tags from parents
-          const std::set<DicomTag>& requestedTags = tuple.get<4>();
-
-          if (requestedTags.size() > 0)
-          {
-            std::set<DicomTag> savedMainDicomTags;
-            
-            FromDcmtkBridge::ParseListOfTags(savedMainDicomTags, target.mainDicomTagsSignature_);
-
-            // read parent main dicom tags as long as we don't have gathered all requested tags
-            ResourceType currentLevel = target.type_;
-            int64_t currentInternalId = internalId;
-            Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags);
-
-            while ((target.missingRequestedTags_.size() > 0)
-                   && currentLevel != ResourceType_Patient)
-            {
-              currentLevel = GetParentResourceType(currentLevel);
-
-              int64_t currentParentId;
-              if (!transaction.LookupParent(currentParentId, currentInternalId))
+              case ResourceType_Instance:
               {
+                FileInfo attachment;
+                int64_t revision;  // ignored
+                if (!transaction.LookupAttachment(attachment, revision, internalId, FileContentType_Dicom))
+                {
+                  throw OrthancException(ErrorCode_InternalError);
+                }
+
+                target.fileSize_ = static_cast<unsigned int>(attachment.GetUncompressedSize());
+                target.fileUuid_ = attachment.GetUuid();
+
+                int64_t i;
+                if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Instance_IndexInSeries))
+                {
+                  target.indexInSeries_ = static_cast<int>(i);
+                }
+                else
+                {
+                  target.indexInSeries_ = -1;
+                }
+
                 break;
               }
 
-              std::map<MetadataType, std::string> parentMetadata;
-              transaction.GetAllMetadata(parentMetadata, currentParentId);
-
-              std::string parentMainDicomTagsSignature = DicomMap::GetDefaultMainDicomTagsSignature(currentLevel);
-              LookupStringMetadata(parentMainDicomTagsSignature, parentMetadata, MetadataType_MainDicomTagsSignature);
-
-              std::set<DicomTag> parentSavedMainDicomTags;
-              FromDcmtkBridge::ParseListOfTags(parentSavedMainDicomTags, parentMainDicomTagsSignature);
-              
-              size_t previousMissingCount = target.missingRequestedTags_.size();
-              Toolbox::AppendSets(savedMainDicomTags, parentSavedMainDicomTags);
-              Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags);
-
-              // read the parent tags from DB only if it reduces the number of missing tags
-              if (target.missingRequestedTags_.size() < previousMissingCount)
-              { 
-                Toolbox::AppendSets(savedMainDicomTags, parentSavedMainDicomTags);
-
-                DicomMap parentTags;
-                transaction.GetMainDicomTags(parentTags, currentParentId);
-
-                target.tags_.Merge(parentTags);
-              }
-
-              currentInternalId = currentParentId;
+              default:
+                throw OrthancException(ErrorCode_InternalError);
             }
 
-            { // handle the tags that must be rebuilt because they are not saved in DB
-              if (target.type_ == ResourceType_Study && (
-                target.missingRequestedTags_.count(DICOM_TAG_MODALITIES_IN_STUDY) > 0 
-                || target.missingRequestedTags_.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) > 0 
-                || target.missingRequestedTags_.count(DICOM_TAG_SOP_CLASSES_IN_STUDY) > 0 
-                || target.missingRequestedTags_.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) > 0 
-              ))
-              {
-                ComputeStudyTags(target.tags_, transaction, target.id_, internalId, requestedTags);
-              }
-
-              if (target.type_ == ResourceType_Series 
-                && target.missingRequestedTags_.count(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES) > 0)
+            // check the main dicom tags list has not changed since the resource was stored
+            target.mainDicomTagsSignature_ = DicomMap::GetDefaultMainDicomTagsSignature(type);
+            LookupStringMetadata(target.mainDicomTagsSignature_, target.metadata_, MetadataType_MainDicomTagsSignature);
+          }
+
+          if (expandFlags & ExpandResourceDbFlags_IncludeMainDicomTags)
+          {
+            // read all tags from DB
+            transaction.GetMainDicomTags(target.tags_, internalId);
+
+            // check if we have access to all requestedTags or if we must get tags from parents
+            const std::set<DicomTag>& requestedTags = tuple.get<4>();
+
+            if (requestedTags.size() > 0)
+            {
+              std::set<DicomTag> savedMainDicomTags;
+              
+              FromDcmtkBridge::ParseListOfTags(savedMainDicomTags, target.mainDicomTagsSignature_);
+
+              // read parent main dicom tags as long as we don't have gathered all requested tags
+              ResourceType currentLevel = target.type_;
+              int64_t currentInternalId = internalId;
+              Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags);
+
+              while ((target.missingRequestedTags_.size() > 0)
+                    && currentLevel != ResourceType_Patient)
               {
-                ComputeSeriesTags(target.tags_, target.childrenIds_, requestedTags);
+                currentLevel = GetParentResourceType(currentLevel);
+
+                int64_t currentParentId;
+                if (!transaction.LookupParent(currentParentId, currentInternalId))
+                {
+                  break;
+                }
+
+                std::map<MetadataType, std::string> parentMetadata;
+                transaction.GetAllMetadata(parentMetadata, currentParentId);
+
+                std::string parentMainDicomTagsSignature = DicomMap::GetDefaultMainDicomTagsSignature(currentLevel);
+                LookupStringMetadata(parentMainDicomTagsSignature, parentMetadata, MetadataType_MainDicomTagsSignature);
+
+                std::set<DicomTag> parentSavedMainDicomTags;
+                FromDcmtkBridge::ParseListOfTags(parentSavedMainDicomTags, parentMainDicomTagsSignature);
+                
+                size_t previousMissingCount = target.missingRequestedTags_.size();
+                Toolbox::AppendSets(savedMainDicomTags, parentSavedMainDicomTags);
+                Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags);
+
+                // read the parent tags from DB only if it reduces the number of missing tags
+                if (target.missingRequestedTags_.size() < previousMissingCount)
+                { 
+                  Toolbox::AppendSets(savedMainDicomTags, parentSavedMainDicomTags);
+
+                  DicomMap parentTags;
+                  transaction.GetMainDicomTags(parentTags, currentParentId);
+
+                  target.tags_.Merge(parentTags);
+                }
+
+                currentInternalId = currentParentId;
               }
-
-              if (target.type_ == ResourceType_Patient && (
-                target.missingRequestedTags_.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) > 0 
-                || target.missingRequestedTags_.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) > 0 
-                || target.missingRequestedTags_.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES) > 0 
-              ))
-              {
-                ComputePatientTags(target.tags_, transaction, target.id_, internalId, requestedTags);
-              }
-            }            
-
+            }
           }
 
           std::string tmp;
@@ -1139,7 +962,7 @@
 
     bool found;
     Operations operations;
-    operations.Apply(*this, found, target, publicId, level, requestedTags);
+    operations.Apply(*this, found, target, publicId, level, requestedTags, expandFlags);
     return found;
   }
 
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Tue Mar 15 09:09:52 2022 +0100
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Tue Mar 15 15:57:21 2022 +0100
@@ -64,6 +64,17 @@
     int                                 indexInSeries_;
   };
 
+  enum ExpandResourceDbFlags
+  {
+    ExpandResourceDbFlags_None                    = 0,
+    ExpandResourceDbFlags_IncludeMetadata         = (1 << 0),
+    ExpandResourceDbFlags_IncludeChildren         = (1 << 1),
+    ExpandResourceDbFlags_IncludeMainDicomTags    = (1 << 2),
+
+    ExpandResourceDbFlags_Default = (ExpandResourceDbFlags_IncludeMetadata |
+                                     ExpandResourceDbFlags_IncludeChildren |
+                                     ExpandResourceDbFlags_IncludeMainDicomTags)
+  };
 
   class StatelessDatabaseOperations : public boost::noncopyable
   {
@@ -479,7 +490,8 @@
     bool ExpandResource(ExpandedResource& target,
                         const std::string& publicId,
                         ResourceType level,
-                        const std::set<DicomTag>& requestedTags);
+                        const std::set<DicomTag>& requestedTags,
+                        ExpandResourceDbFlags expandFlags);
 
     void GetAllMetadata(std::map<MetadataType, std::string>& target,
                         const std::string& publicId,
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Tue Mar 15 09:09:52 2022 +0100
+++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Tue Mar 15 15:57:21 2022 +0100
@@ -38,275 +38,27 @@
 
 namespace Orthanc
 {
-  static void GetChildren(std::list<std::string>& target,
-                          ServerIndex& index,
-                          const std::list<std::string>& source)
-  {
-    target.clear();
-
-    for (std::list<std::string>::const_iterator
-           it = source.begin(); it != source.end(); ++it)
-    {
-      std::list<std::string> tmp;
-      index.GetChildren(tmp, *it);
-      target.splice(target.end(), tmp);
-    }
-  }
-
-
-  static void StoreSetOfStrings(DicomMap& result,
-                                const DicomTag& tag,
-                                const std::set<std::string>& values)
-  {
-    bool isFirst = true;
-
-    std::string s;
-    for (std::set<std::string>::const_iterator
-           it = values.begin(); it != values.end(); ++it)
-    {
-      if (isFirst)
-      {
-        isFirst = false;
-      }
-      else
-      {
-        s += "\\";
-      }
-
-      s += *it;
-    }
-
-    result.SetValue(tag, s, false);
-  }
-
-
-  static void ComputePatientCounters(DicomMap& result,
-                                     ServerIndex& index,
-                                     const std::string& patient,
-                                     const DicomMap& query)
-  {
-    std::list<std::string> studies;
-    index.GetChildren(studies, patient);
-
-    if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES))
-    {
-      result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES,
-                      boost::lexical_cast<std::string>(studies.size()), false);
-    }
-
-    if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) &&
-        !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
-    {
-      return;
-    }
-
-    std::list<std::string> series;
-    GetChildren(series, index, studies);
-    studies.clear();  // This information is useless below
-    
-    if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES))
-    {
-      result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES,
-                      boost::lexical_cast<std::string>(series.size()), false);
-    }
-
-    if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
-    {
-      return;
-    }
-
-    std::list<std::string> instances;
-    GetChildren(instances, index, series);
-
-    if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
-    {
-      result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES,
-                      boost::lexical_cast<std::string>(instances.size()), false);
-    }
-  }
-
-
-  static void ComputeStudyCounters(DicomMap& result,
-                                   ServerContext& context,
-                                   const std::string& study,
-                                   const DicomMap& query)
-  {
-    ServerIndex& index = context.GetIndex();
-
-    std::list<std::string> series;
-    index.GetChildren(series, study);
-    
-    if (query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES))
-    {
-      result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES,
-                      boost::lexical_cast<std::string>(series.size()), false);
-    }
-
-    if (query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
-    {
-      std::set<std::string> values;
-
-      for (std::list<std::string>::const_iterator
-             it = series.begin(); it != series.end(); ++it)
-      {
-        DicomMap tags;
-        if (index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series))
-        {
-          const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
-
-          if (value != NULL &&
-              !value->IsNull() &&
-              !value->IsBinary())
-          {
-            values.insert(value->GetContent());
-          }
-        }
-      }
-
-      StoreSetOfStrings(result, DICOM_TAG_MODALITIES_IN_STUDY, values);
-    }
-
-    if (!query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) &&
-        !query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY))
-    {
-      return;
-    }
-
-    std::list<std::string> instances;
-    GetChildren(instances, index, series);
-
-    if (query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES))
-    {
-      result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES,
-                      boost::lexical_cast<std::string>(instances.size()), false);
-    }
-
-    if (query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY))
-    {
-      std::set<std::string> values;
-
-      for (std::list<std::string>::const_iterator
-             it = instances.begin(); it != instances.end(); ++it)
-      {
-        std::string value;
-        if (context.LookupOrReconstructMetadata(value, *it, ResourceType_Instance, MetadataType_Instance_SopClassUid))
-        {
-          values.insert(value);
-        }
-      }
-
-      StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
-    }
-  }
-
-
-  static void ComputeSeriesCounters(DicomMap& result,
-                                    ServerIndex& index,
-                                    const std::string& series,
-                                    const DicomMap& query)
-  {
-    std::list<std::string> instances;
-    index.GetChildren(instances, series);
-
-    if (query.HasTag(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES))
-    {
-      result.SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES,
-                      boost::lexical_cast<std::string>(instances.size()), false);
-    }
-  }
-
-
-  static DicomMap* ComputeCounters(ServerContext& context,
-                                   const std::string& instanceId,
-                                   ResourceType level,
-                                   const DicomMap& query)
-  {
-    switch (level)
-    {
-      case ResourceType_Patient:
-        if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) &&
-            !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) &&
-            !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES))
-        {
-          return NULL;
-        }
-
-        break;
-
-      case ResourceType_Study:
-        if (!query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) &&
-            !query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) &&
-            !query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY) &&
-            !query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
-        {
-          return NULL;
-        }
-
-        break;
-
-      case ResourceType_Series:
-        if (!query.HasTag(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES))
-        {
-          return NULL;
-        }
-
-        break;
-
-      default:
-        return NULL;
-    }
-
-    std::string parent;
-    if (!context.GetIndex().LookupParent(parent, instanceId, level))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);  // The resource was deleted in between
-    }
-
-    std::unique_ptr<DicomMap> result(new DicomMap);
-
-    switch (level)
-    {
-      case ResourceType_Patient:
-        ComputePatientCounters(*result, context.GetIndex(), parent, query);
-        break;
-
-      case ResourceType_Study:
-        ComputeStudyCounters(*result, context, parent, query);
-        break;
-
-      case ResourceType_Series:
-        ComputeSeriesCounters(*result, context.GetIndex(), parent, query);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    return result.release();
-  }
-
-
   static void AddAnswer(DicomFindAnswers& answers,
+                        ServerContext& context,
+                        const std::string& publicId,
+                        const std::string& instanceId,
                         const DicomMap& mainDicomTags,
                         const Json::Value* dicomAsJson,
+                        ResourceType level,
                         const DicomArray& query,
                         const std::list<DicomTag>& sequencesToReturn,
-                        const DicomMap* counters,
                         const std::string& defaultPrivateCreator,
                         const std::map<uint16_t, std::string>& privateCreators,
                         const std::string& retrieveAet)
   {
-    DicomMap match;
+    ExpandedResource resource;
+    std::set<DicomTag> requestedTags;
+    
+    query.GetTags(requestedTags);
 
-    if (dicomAsJson != NULL)
-    {
-      match.FromDicomAsJson(*dicomAsJson);
-    }
-    else
-    {
-      match.Assign(mainDicomTags);
-    }
-    
+    // reuse ExpandResource to get missing tags and computed tags (ModalitiesInStudy ...).  This code is therefore shared between C-Find, tools/find, list-resources and QIDO-RS
+    context.ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceDbFlags_IncludeMainDicomTags);
+
     DicomMap result;
 
     /**
@@ -330,7 +82,7 @@
       else
       {
         const DicomTag& tag = query.GetElement(i).GetTag();
-        const DicomValue* value = match.TestAndGetValue(tag);
+        const DicomValue* value = resource.tags_.TestAndGetValue(tag);
 
         if (value != NULL &&
             !value->IsNull() &&
@@ -345,15 +97,6 @@
       }
     }
 
-    if (counters != NULL)
-    {
-      DicomArray tmp(*counters);
-      for (size_t i = 0; i < tmp.GetSize(); i++)
-      {
-        result.SetValue(tmp.GetElement(i).GetTag(), tmp.GetElement(i).GetValue().GetContent(), false);
-      }
-    }
-
     if (result.GetSize() == 0 &&
         sequencesToReturn.empty())
     {
@@ -563,10 +306,8 @@
                        const DicomMap& mainDicomTags,
                        const Json::Value* dicomAsJson) ORTHANC_OVERRIDE
     {
-      std::unique_ptr<DicomMap> counters(ComputeCounters(context_, instanceId, level_, query_));
-
-      AddAnswer(answers_, mainDicomTags, dicomAsJson, queryAsArray_, sequencesToReturn_,
-                counters.get(), defaultPrivateCreator_, privateCreators_, retrieveAet_);
+      AddAnswer(answers_, context_, publicId, instanceId, mainDicomTags, dicomAsJson, level_, queryAsArray_, sequencesToReturn_,
+                defaultPrivateCreator_, privateCreators_, retrieveAet_);
     }
   };
 
--- a/OrthancServer/Sources/ServerContext.cpp	Tue Mar 15 09:09:52 2022 +0100
+++ b/OrthancServer/Sources/ServerContext.cpp	Tue Mar 15 15:57:21 2022 +0100
@@ -2256,6 +2256,203 @@
   }
 
 
+  static void ComputeSeriesTags(ExpandedResource& resource,
+                                ServerContext& context,
+                                const std::string& seriesPublicId,
+                                const std::set<DicomTag>& requestedTags)
+  {
+    if (requestedTags.count(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES) > 0)
+    {
+      ServerIndex& index = context.GetIndex();
+      std::list<std::string> instances;
+
+      index.GetChildren(instances, seriesPublicId);
+
+      resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES,
+                              boost::lexical_cast<std::string>(instances.size()), false);
+      resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
+    }
+  }
+
+  static void ComputeStudyTags(ExpandedResource& resource,
+                               ServerContext& context,
+                               const std::string& studyPublicId,
+                               const std::set<DicomTag>& requestedTags)
+  {
+    ServerIndex& index = context.GetIndex();
+    std::list<std::string> series;
+    std::list<std::string> instances;
+
+    bool hasNbRelatedSeries = requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) > 0;
+    bool hasNbRelatedInstances = requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) > 0;
+    bool hasModalitiesInStudy = requestedTags.count(DICOM_TAG_MODALITIES_IN_STUDY) > 0;
+    bool hasSopClassesInStudy = requestedTags.count(DICOM_TAG_SOP_CLASSES_IN_STUDY) > 0;
+
+    index.GetChildren(series, studyPublicId);
+
+    if (hasModalitiesInStudy)
+    {
+      std::set<std::string> values;
+
+      for (std::list<std::string>::const_iterator
+            it = series.begin(); it != series.end(); ++it)
+      {
+        DicomMap tags;
+        index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series);
+
+        const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
+
+        if (value != NULL &&
+            !value->IsNull() &&
+            !value->IsBinary())
+        {
+          values.insert(value->GetContent());
+        }
+      }
+
+      std::string modalities;
+      Toolbox::JoinStrings(modalities, values, "\\");
+
+      resource.tags_.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, modalities, false);
+      resource.missingRequestedTags_.erase(DICOM_TAG_MODALITIES_IN_STUDY);
+    }
+
+    if (hasNbRelatedSeries)
+    {
+      resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES,
+                              boost::lexical_cast<std::string>(series.size()), false);
+      resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
+    }
+
+    if (hasNbRelatedInstances || hasSopClassesInStudy)
+    {
+      for (std::list<std::string>::const_iterator
+            it = series.begin(); it != series.end(); ++it)
+      {
+        std::list<std::string> seriesInstancesIds;
+        index.GetChildren(seriesInstancesIds, *it);
+
+        instances.splice(instances.end(), seriesInstancesIds);
+      }
+
+      if (hasNbRelatedInstances)
+      {
+        resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES,
+                                boost::lexical_cast<std::string>(instances.size()), false);      
+        resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
+      }
+
+      if (hasSopClassesInStudy)
+      {
+        std::set<std::string> values;
+
+        for (std::list<std::string>::const_iterator
+              it = instances.begin(); it != instances.end(); ++it)
+        {
+          std::string value;
+
+          if (context.LookupOrReconstructMetadata(value, *it, ResourceType_Instance, MetadataType_Instance_SopClassUid))
+          {
+            values.insert(value);
+          }
+        }
+
+        if (values.size() > 0)
+        {
+          std::string sopClassUids;
+          Toolbox::JoinStrings(sopClassUids, values, "\\");
+          resource.tags_.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, sopClassUids, false);
+        }
+
+        resource.missingRequestedTags_.erase(DICOM_TAG_SOP_CLASSES_IN_STUDY);
+      }
+    }
+  }
+
+  static void ComputePatientTags(ExpandedResource& resource,
+                                 ServerContext& context,
+                                 const std::string& patientPublicId,
+                                 const std::set<DicomTag>& requestedTags)
+  {
+    ServerIndex& index = context.GetIndex();
+
+    std::list<std::string> studies;
+    std::list<std::string> series;
+    std::list<std::string> instances;
+
+    bool hasNbRelatedStudies = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) > 0;
+    bool hasNbRelatedSeries = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) > 0;
+    bool hasNbRelatedInstances = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES) > 0;
+
+    index.GetChildren(studies, patientPublicId);
+
+    if (hasNbRelatedStudies)
+    {
+      resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES,
+                              boost::lexical_cast<std::string>(studies.size()), false);
+      resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
+    }
+
+    if (hasNbRelatedSeries || hasNbRelatedInstances)
+    {
+      for (std::list<std::string>::const_iterator
+            it = studies.begin(); it != studies.end(); ++it)
+      {
+        std::list<std::string> thisSeriesIds;
+        index.GetChildren(thisSeriesIds, *it);
+        series.splice(series.end(), thisSeriesIds);
+      }
+
+      if (hasNbRelatedSeries)
+      {
+        resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES,
+                                boost::lexical_cast<std::string>(series.size()), false);
+        resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
+      }
+    }
+
+    if (hasNbRelatedInstances)
+    {
+      for (std::list<std::string>::const_iterator
+            it = series.begin(); it != series.end(); ++it)
+      {
+        std::list<std::string> thisInstancesIds;
+        index.GetChildren(thisInstancesIds, *it);
+        instances.splice(instances.end(), thisInstancesIds);
+      }
+
+      resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES,
+                              boost::lexical_cast<std::string>(instances.size()), false);
+      resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
+    }
+  }
+
+
+  static void ComputeTags(ExpandedResource& resource,
+                          ServerContext& context,
+                          const std::string& resourceId,
+                          ResourceType level,
+                          const std::set<DicomTag>& requestedTags)
+  {
+    if (level == ResourceType_Patient 
+        && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Patient))
+    {
+      ComputePatientTags(resource, context, resourceId, requestedTags);
+    }
+
+    if (level == ResourceType_Study 
+        && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Study))
+    {
+      ComputeStudyTags(resource, context, resourceId, requestedTags);
+    }
+
+    if (level == ResourceType_Series 
+        && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Series))
+    {
+      ComputeSeriesTags(resource, context, resourceId, requestedTags);
+    }
+  }
+
   bool ServerContext::ExpandResource(Json::Value& target,
                                      const std::string& publicId,
                                      ResourceType level,
@@ -2263,22 +2460,24 @@
                                      const std::set<DicomTag>& requestedTags)
   {
     std::string unusedInstanceId;
-    Json::Value unusedValue;
+    Json::Value* unusedDicomAsJson = NULL;
+    DicomMap unusedMainDicomTags;
 
-    return ExpandResource(target, publicId, unusedInstanceId, unusedValue, level, format, requestedTags);
+    return ExpandResource(target, publicId, unusedMainDicomTags, unusedInstanceId, unusedDicomAsJson, level, format, requestedTags);
   }
 
   bool ServerContext::ExpandResource(Json::Value& target,
                                      const std::string& publicId,
+                                     const DicomMap& mainDicomTags,    // optional: the main dicom tags for the resource (if already available)
                                      const std::string& instanceId,    // optional: the id of an instance for the resource (if already available)
-                                     const Json::Value& dicomAsJson,   // optional: the dicom-as-json for the resource (if already available)
+                                      const Json::Value* dicomAsJson,  // optional: the dicom-as-json for the resource (if already available)
                                      ResourceType level,
                                      DicomToJsonFormat format,
                                      const std::set<DicomTag>& requestedTags)
   {
     ExpandedResource resource;
 
-    if (ExpandResource(resource, publicId, instanceId, dicomAsJson, level, requestedTags))
+    if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceDbFlags_Default))
     {
       SerializeExpandedResource(target, resource, format, requestedTags);
       return true;
@@ -2289,12 +2488,50 @@
   
   bool ServerContext::ExpandResource(ExpandedResource& resource,
                                      const std::string& publicId,
+                                     const DicomMap& mainDicomTags,    // optional: the main dicom tags for the resource (if already available)
                                      const std::string& instanceId,    // optional: the id of an instance for the resource (if already available)
-                                     const Json::Value& dicomAsJson,   // optional: the dicom-as-json for the resource (if already available)
+                                     const Json::Value* dicomAsJson,   // optional: the dicom-as-json for the resource (if already available)
                                      ResourceType level,
-                                     const std::set<DicomTag>& requestedTags)
+                                     const std::set<DicomTag>& requestedTags,
+                                     ExpandResourceDbFlags expandFlags)
   {
-    if (GetIndex().ExpandResource(resource, publicId, level, requestedTags))
+    // first try to get the tags from what is already available
+    
+    if ((expandFlags & ExpandResourceDbFlags_IncludeMainDicomTags)
+      && (mainDicomTags.GetSize() > 0)
+      && (dicomAsJson != NULL))
+    {
+      
+      if (mainDicomTags.GetSize() > 0)
+      {
+        resource.tags_.Merge(mainDicomTags);
+      }
+
+      if (dicomAsJson != NULL && dicomAsJson->isObject())
+      {
+        resource.tags_.FromDicomAsJson(*dicomAsJson);
+      }
+
+      std::set<DicomTag> retrievedTags;
+      std::set<DicomTag> missingTags;
+      resource.tags_.GetTags(retrievedTags);
+
+      Toolbox::GetMissingsFromSet(missingTags, requestedTags, retrievedTags);
+
+      // if all possible tags have been read, no need to get them from DB anymore
+      if (missingTags.size() == 0 || DicomMap::HasOnlyComputedTags(missingTags))
+      {
+        expandFlags = static_cast<ExpandResourceDbFlags>(expandFlags & ~ExpandResourceDbFlags_IncludeMainDicomTags);
+      }
+
+      if (missingTags.size() == 0 && expandFlags == ExpandResourceDbFlags_None)  // we have already retrieved anything we need
+      {
+        return true;
+      }
+    }
+
+    if (expandFlags != ExpandResourceDbFlags_None
+        && GetIndex().ExpandResource(resource, publicId, level, requestedTags, expandFlags))
     {
       // check the main dicom tags list has not changed since the resource was stored
       if (resource.mainDicomTagsSignature_ != DicomMap::GetMainDicomTagsSignature(resource.type_))
@@ -2307,11 +2544,12 @@
       }
 
       // possibly merge missing requested tags from dicom-as-json
-      if (!resource.missingRequestedTags_.empty())
+      if (!resource.missingRequestedTags_.empty() && !DicomMap::HasOnlyComputedTags(resource.missingRequestedTags_))
       {
         std::string instanceId_ = instanceId;
-        Json::Value dicomAsJson_ = dicomAsJson;
-        if (dicomAsJson_.isNull())
+        DicomMap tagsFromJson;
+
+        if (dicomAsJson == NULL)
         {
           if (instanceId_.empty())
           {
@@ -2331,20 +2569,28 @@
             }
           }
   
-          // MORE_TAGS :TODO: log warning (add an option to disable them)
-          ReadDicomAsJson(dicomAsJson_, instanceId_);
+          // MORE_TAGS :TODO: log "performance" warning (add an option to disable them)
+          Json::Value tmpDicomAsJson;
+          ReadDicomAsJson(tmpDicomAsJson, instanceId_);
+          tagsFromJson.FromDicomAsJson(tmpDicomAsJson);
+        }
+        else
+        {
+          tagsFromJson.FromDicomAsJson(*dicomAsJson);
         }
 
-        DicomMap allTags;
-        allTags.FromDicomAsJson(dicomAsJson_);
-
-        resource.tags_.Merge(allTags);
+        resource.tags_.Merge(tagsFromJson);
       }
 
-      return true;
+      // compute the requested tags
+      ComputeTags(resource, *this, publicId, level, requestedTags);
+    }
+    else
+    {
+      return false;
     }
 
-    return false;
+    return true;
   }
 
 }
--- a/OrthancServer/Sources/ServerContext.h	Tue Mar 15 09:09:52 2022 +0100
+++ b/OrthancServer/Sources/ServerContext.h	Tue Mar 15 15:57:21 2022 +0100
@@ -548,18 +548,21 @@
 
     bool ExpandResource(Json::Value& target,
                         const std::string& publicId,
+                        const DicomMap& mainDicomTags,    // optional: the main dicom tags for the resource (if already available)
                         const std::string& instanceId,    // optional: the id of an instance for the resource
-                        const Json::Value& dicomAsJson,   // optional: the dicom-as-json for the resource
+                         const Json::Value* dicomAsJson,  // optional: the dicom-as-json for the resource
                         ResourceType level,
                         DicomToJsonFormat format,
                         const std::set<DicomTag>& requestedTags);
 
     bool ExpandResource(ExpandedResource& target,
                         const std::string& publicId,
+                        const DicomMap& mainDicomTags,    // optional: the main dicom tags for the resource (if already available)
                         const std::string& instanceId,    // optional: the id of an instance for the resource
-                        const Json::Value& dicomAsJson,   // optional: the dicom-as-json for the resource
+                         const Json::Value* dicomAsJson,  // optional: the dicom-as-json for the resource
                         ResourceType level,
-                        const std::set<DicomTag>& requestedTags);
+                        const std::set<DicomTag>& requestedTags,
+                        ExpandResourceDbFlags expandFlags);
 
   };
 }