diff --git a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/common/interfaces/IMetaDataUpdateHolder.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/common/interfaces/IMetaDataUpdateHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..d170a6d946648e6d38f1deb5a2cbc6674cb313db
--- /dev/null
+++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/common/interfaces/IMetaDataUpdateHolder.java
@@ -0,0 +1,28 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.IUpdate;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.ListUpdateMapValues;
+import ch.systemsx.cisd.base.annotation.JsonObject;
+
+@JsonObject("as.dto.common.interfaces.IMetaDataUpdateHolder")
+public interface IMetaDataUpdateHolder extends IUpdate
+{
+    ListUpdateMapValues getMetaData();
+}
diff --git a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/dataset/update/DataSetTypeUpdate.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/dataset/update/DataSetTypeUpdate.java
index 0f616c8c65e0a5e5386ebc1955bd426e911a585d..389505f8d9182b15366cf394645fc5a40a9d6600 100644
--- a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/dataset/update/DataSetTypeUpdate.java
+++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/dataset/update/DataSetTypeUpdate.java
@@ -17,6 +17,7 @@ package ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.update;
 
 import java.util.List;
 
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IMetaDataUpdateHolder;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.ListUpdateMapValues;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
@@ -34,7 +35,7 @@ import ch.systemsx.cisd.base.annotation.JsonObject;
  * @author Franz-Josef Elmer
  */
 @JsonObject("as.dto.dataset.update.DataSetTypeUpdate")
-public class DataSetTypeUpdate implements IEntityTypeUpdate
+public class DataSetTypeUpdate implements IEntityTypeUpdate, IMetaDataUpdateHolder
 {
     private static final long serialVersionUID = 1L;
 
@@ -57,7 +58,8 @@ public class DataSetTypeUpdate implements IEntityTypeUpdate
     private FieldUpdateValue<IPluginId> validationPluginId = new FieldUpdateValue<IPluginId>();
 
     @JsonProperty
-    private PropertyAssignmentListUpdateValue propertyAssignments = new PropertyAssignmentListUpdateValue();
+    private PropertyAssignmentListUpdateValue propertyAssignments =
+            new PropertyAssignmentListUpdateValue();
 
     @JsonProperty
     private ListUpdateMapValues metaData = new ListUpdateMapValues();
@@ -161,6 +163,7 @@ public class DataSetTypeUpdate implements IEntityTypeUpdate
         propertyAssignments.setActions(actions);
     }
 
+    @Override
     @JsonIgnore
     public ListUpdateMapValues getMetaData()
     {
diff --git a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/dataset/update/DataSetUpdate.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/dataset/update/DataSetUpdate.java
index 6a9fa9351c7bab3b0a6acaf97674059b5463a6c4..c003dfcf7d36fa5c1388cf67abea41dc9f430eb5 100644
--- a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/dataset/update/DataSetUpdate.java
+++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/dataset/update/DataSetUpdate.java
@@ -19,6 +19,7 @@ import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
 
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IMetaDataUpdateHolder;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.*;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SamplePermId;
 import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -37,7 +38,8 @@ import ch.systemsx.cisd.base.annotation.JsonObject;
  * @author pkupczyk
  */
 @JsonObject("as.dto.dataset.update.DataSetUpdate")
-public class DataSetUpdate implements IUpdate, IObjectUpdate<IDataSetId>, IPropertiesHolder
+public class DataSetUpdate implements IUpdate, IObjectUpdate<IDataSetId>, IPropertiesHolder,
+        IMetaDataUpdateHolder
 {
     private static final long serialVersionUID = 1L;
 
@@ -501,6 +503,7 @@ public class DataSetUpdate implements IUpdate, IObjectUpdate<IDataSetId>, IPrope
         setProperty(propertyName, propertyValue);
     }
 
+    @Override
     @JsonIgnore
     public ListUpdateMapValues getMetaData()
     {
diff --git a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/experiment/update/ExperimentTypeUpdate.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/experiment/update/ExperimentTypeUpdate.java
index 926f86f5e833feeed59ec451cc9d7babed20efbd..5a5472fe1ad930dfd0daa79f3eef4e5bb94c02a5 100644
--- a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/experiment/update/ExperimentTypeUpdate.java
+++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/experiment/update/ExperimentTypeUpdate.java
@@ -17,6 +17,7 @@ package ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.update;
 
 import java.util.List;
 
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IMetaDataUpdateHolder;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.ListUpdateMapValues;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
@@ -34,7 +35,7 @@ import ch.systemsx.cisd.base.annotation.JsonObject;
  * @author Franz-Josef Elmer
  */
 @JsonObject("as.dto.experiment.update.ExperimentTypeUpdate")
-public class ExperimentTypeUpdate implements IEntityTypeUpdate
+public class ExperimentTypeUpdate implements IEntityTypeUpdate, IMetaDataUpdateHolder
 {
     private static final long serialVersionUID = 1L;
 
@@ -116,6 +117,7 @@ public class ExperimentTypeUpdate implements IEntityTypeUpdate
         propertyAssignments.setActions(actions);
     }
 
+    @Override
     @JsonIgnore
     public ListUpdateMapValues getMetaData()
     {
diff --git a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/experiment/update/ExperimentUpdate.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/experiment/update/ExperimentUpdate.java
index 65dfee91b4d817dbba59b9d4b8ba3f98df91670a..675f6bfa58c7bba83c8536adfa0e04d6d1a3b768 100644
--- a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/experiment/update/ExperimentUpdate.java
+++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/experiment/update/ExperimentUpdate.java
@@ -19,6 +19,7 @@ import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
 
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IMetaDataUpdateHolder;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.*;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SamplePermId;
 import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -37,7 +38,8 @@ import ch.systemsx.cisd.base.annotation.JsonObject;
  * @author pkupczyk
  */
 @JsonObject("as.dto.experiment.update.ExperimentUpdate")
-public class ExperimentUpdate implements IUpdate, IObjectUpdate<IExperimentId>, IPropertiesHolder
+public class ExperimentUpdate implements IUpdate, IObjectUpdate<IExperimentId>, IPropertiesHolder,
+        IMetaDataUpdateHolder
 {
 
     private static final long serialVersionUID = 1L;
@@ -185,7 +187,9 @@ public class ExperimentUpdate implements IUpdate, IObjectUpdate<IExperimentId>,
     public Long getIntegerProperty(String propertyName)
     {
         String propertyValue = getProperty(propertyName);
-        return (propertyValue == null || propertyValue.isBlank()) ? null : Long.parseLong(propertyValue);
+        return (propertyValue == null || propertyValue.isBlank()) ?
+                null :
+                Long.parseLong(propertyValue);
     }
 
     @Override
@@ -222,7 +226,9 @@ public class ExperimentUpdate implements IUpdate, IObjectUpdate<IExperimentId>,
     public Double getRealProperty(String propertyName)
     {
         String propertyValue = getProperty(propertyName);
-        return (propertyValue == null || propertyValue.isBlank()) ? null : Double.parseDouble(propertyValue);
+        return (propertyValue == null || propertyValue.isBlank()) ?
+                null :
+                Double.parseDouble(propertyValue);
     }
 
     @Override
@@ -241,7 +247,9 @@ public class ExperimentUpdate implements IUpdate, IObjectUpdate<IExperimentId>,
     @Override
     public void setTimestampProperty(String propertyName, ZonedDateTime propertyValue)
     {
-        String value = (propertyValue == null) ? null : propertyValue.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX"));
+        String value = (propertyValue == null) ?
+                null :
+                propertyValue.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX"));
         setProperty(propertyName, value);
     }
 
@@ -249,7 +257,9 @@ public class ExperimentUpdate implements IUpdate, IObjectUpdate<IExperimentId>,
     public Boolean getBooleanProperty(String propertyName)
     {
         String propertyValue = getProperty(propertyName);
-        return (propertyValue == null || propertyValue.isBlank()) ? null : Boolean.parseBoolean(propertyValue);
+        return (propertyValue == null || propertyValue.isBlank()) ?
+                null :
+                Boolean.parseBoolean(propertyValue);
     }
 
     @Override
@@ -298,7 +308,9 @@ public class ExperimentUpdate implements IUpdate, IObjectUpdate<IExperimentId>,
     public SamplePermId getSampleProperty(String propertyName)
     {
         String propertyValue = getProperty(propertyName);
-        return (propertyValue == null || propertyValue.isBlank()) ? null : new SamplePermId(propertyValue);
+        return (propertyValue == null || propertyValue.isBlank()) ?
+                null :
+                new SamplePermId(propertyValue);
     }
 
     @Override
@@ -311,39 +323,55 @@ public class ExperimentUpdate implements IUpdate, IObjectUpdate<IExperimentId>,
     public Long[] getIntegerArrayProperty(String propertyName)
     {
         String propertyValue = getProperty(propertyName);
-        return (propertyValue == null || propertyValue.isBlank()) ? null : Arrays.stream(propertyValue.split(",")).map(String::trim).map(Long::parseLong).toArray(Long[]::new);
+        return (propertyValue == null || propertyValue.isBlank()) ?
+                null :
+                Arrays.stream(propertyValue.split(",")).map(String::trim).map(Long::parseLong)
+                        .toArray(Long[]::new);
     }
 
     @Override
     public void setIntegerArrayProperty(String propertyName, Long[] propertyValue)
     {
-        setProperty(propertyName, propertyValue == null ? null : Arrays.stream(propertyValue).map(Object::toString).reduce((a,b) -> a + ", " + b).get());
+        setProperty(propertyName, propertyValue == null ?
+                null :
+                Arrays.stream(propertyValue).map(Object::toString).reduce((a, b) -> a + ", " + b)
+                        .get());
     }
 
     @Override
     public Double[] getRealArrayProperty(String propertyName)
     {
         String propertyValue = getProperty(propertyName);
-        return (propertyValue == null || propertyValue.isBlank()) ? null : Arrays.stream(propertyValue.split(",")).map(String::trim).map(Double::parseDouble).toArray(Double[]::new);
+        return (propertyValue == null || propertyValue.isBlank()) ?
+                null :
+                Arrays.stream(propertyValue.split(",")).map(String::trim).map(Double::parseDouble)
+                        .toArray(Double[]::new);
     }
 
     @Override
     public void setRealArrayProperty(String propertyName, Double[] propertyValue)
     {
-        setProperty(propertyName, propertyValue == null ? null : Arrays.stream(propertyValue).map(Object::toString).reduce((a,b) -> a + ", " + b).get());
+        setProperty(propertyName, propertyValue == null ?
+                null :
+                Arrays.stream(propertyValue).map(Object::toString).reduce((a, b) -> a + ", " + b)
+                        .get());
     }
 
     @Override
     public String[] getStringArrayProperty(String propertyName)
     {
         String propertyValue = getProperty(propertyName);
-        return (propertyValue == null || propertyValue.isBlank()) ? null : Arrays.stream(propertyValue.split(",")).map(String::trim).toArray(String[]::new);
+        return (propertyValue == null || propertyValue.isBlank()) ?
+                null :
+                Arrays.stream(propertyValue.split(",")).map(String::trim).toArray(String[]::new);
     }
 
     @Override
     public void setStringArrayProperty(String propertyName, String[] propertyValue)
     {
-        setProperty(propertyName, propertyValue == null ? null : Arrays.stream(propertyValue).reduce((a,b) -> a + ", " + b).get());
+        setProperty(propertyName, propertyValue == null ?
+                null :
+                Arrays.stream(propertyValue).reduce((a, b) -> a + ", " + b).get());
     }
 
     @Override
@@ -352,7 +380,8 @@ public class ExperimentUpdate implements IUpdate, IObjectUpdate<IExperimentId>,
         String propertyValue = getProperty(propertyName);
         return propertyValue == null ? null : Arrays.stream(propertyValue.split(","))
                 .map(String::trim)
-                .map(dateTime -> ZonedDateTime.parse(dateTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss X")))
+                .map(dateTime -> ZonedDateTime.parse(dateTime,
+                        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss X")))
                 .toArray(ZonedDateTime[]::new);
     }
 
@@ -360,8 +389,9 @@ public class ExperimentUpdate implements IUpdate, IObjectUpdate<IExperimentId>,
     public void setTimestampArrayProperty(String propertyName, ZonedDateTime[] propertyValue)
     {
         String value = (propertyValue == null) ? null : Arrays.stream(propertyValue)
-                .map(dateTime -> dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX")))
-                .reduce((a,b) -> a + ", " + b)
+                .map(dateTime -> dateTime.format(
+                        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX")))
+                .reduce((a, b) -> a + ", " + b)
                 .get();
         setProperty(propertyName, value);
     }
@@ -378,6 +408,7 @@ public class ExperimentUpdate implements IUpdate, IObjectUpdate<IExperimentId>,
         setProperty(propertyName, propertyValue);
     }
 
+    @Override
     @JsonIgnore
     public ListUpdateMapValues getMetaData()
     {
@@ -390,7 +421,6 @@ public class ExperimentUpdate implements IUpdate, IObjectUpdate<IExperimentId>,
         metaData.setActions(actions);
     }
 
-
     @Override
     public String toString()
     {
diff --git a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/sample/update/SampleTypeUpdate.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/sample/update/SampleTypeUpdate.java
index cff30959d23ec4e610e3aefe81333666fd6a9128..28b14828ae2efc6c76797e570911725da123e6d0 100644
--- a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/sample/update/SampleTypeUpdate.java
+++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/sample/update/SampleTypeUpdate.java
@@ -18,6 +18,7 @@ package ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.update;
 import java.util.List;
 import java.util.Map;
 
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IMetaDataUpdateHolder;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.ListUpdateMapValues;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
@@ -35,7 +36,7 @@ import ch.systemsx.cisd.base.annotation.JsonObject;
  * @author Franz-Josef Elmer
  */
 @JsonObject("as.dto.sample.update.SampleTypeUpdate")
-public class SampleTypeUpdate implements IEntityTypeUpdate
+public class SampleTypeUpdate implements IEntityTypeUpdate, IMetaDataUpdateHolder
 {
     private static final long serialVersionUID = 1L;
 
@@ -222,6 +223,7 @@ public class SampleTypeUpdate implements IEntityTypeUpdate
         propertyAssignments.setActions(actions);
     }
 
+    @Override
     @JsonIgnore
     public ListUpdateMapValues getMetaData()
     {
diff --git a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/sample/update/SampleUpdate.java b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/sample/update/SampleUpdate.java
index 063192849d106c023b001a8534ed478694aca3db..4e35da2d9be964bed32587e3ba2a61338da3249b 100644
--- a/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/sample/update/SampleUpdate.java
+++ b/api-openbis-java/source/java/ch/ethz/sis/openbis/generic/asapi/v3/dto/sample/update/SampleUpdate.java
@@ -19,6 +19,7 @@ import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
 
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IMetaDataUpdateHolder;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.*;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SamplePermId;
 import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -41,7 +42,8 @@ import ch.systemsx.cisd.base.annotation.JsonObject;
  * @author pkupczyk
  */
 @JsonObject("as.dto.sample.update.SampleUpdate")
-public class SampleUpdate implements IUpdate, IPropertiesHolder, IObjectUpdate<ISampleId>
+public class SampleUpdate implements IUpdate, IPropertiesHolder, IObjectUpdate<ISampleId>,
+        IMetaDataUpdateHolder
 {
     private static final long serialVersionUID = 1L;
 
@@ -336,11 +338,14 @@ public class SampleUpdate implements IUpdate, IPropertiesHolder, IObjectUpdate<I
     {
         attachments.setActions(actions);
     }
+
     @Override
     public Long getIntegerProperty(String propertyName)
     {
         String propertyValue = getProperty(propertyName);
-        return (propertyValue == null || propertyValue.isBlank()) ? null : Long.parseLong(propertyValue);
+        return (propertyValue == null || propertyValue.isBlank()) ?
+                null :
+                Long.parseLong(propertyValue);
     }
 
     @Override
@@ -377,7 +382,9 @@ public class SampleUpdate implements IUpdate, IPropertiesHolder, IObjectUpdate<I
     public Double getRealProperty(String propertyName)
     {
         String propertyValue = getProperty(propertyName);
-        return (propertyValue == null || propertyValue.isBlank()) ? null : Double.parseDouble(propertyValue);
+        return (propertyValue == null || propertyValue.isBlank()) ?
+                null :
+                Double.parseDouble(propertyValue);
     }
 
     @Override
@@ -396,7 +403,9 @@ public class SampleUpdate implements IUpdate, IPropertiesHolder, IObjectUpdate<I
     @Override
     public void setTimestampProperty(String propertyName, ZonedDateTime propertyValue)
     {
-        String value = (propertyValue == null) ? null : propertyValue.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX"));
+        String value = (propertyValue == null) ?
+                null :
+                propertyValue.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX"));
         setProperty(propertyName, value);
     }
 
@@ -404,7 +413,9 @@ public class SampleUpdate implements IUpdate, IPropertiesHolder, IObjectUpdate<I
     public Boolean getBooleanProperty(String propertyName)
     {
         String propertyValue = getProperty(propertyName);
-        return (propertyValue == null || propertyValue.isBlank()) ? null : Boolean.parseBoolean(propertyValue);
+        return (propertyValue == null || propertyValue.isBlank()) ?
+                null :
+                Boolean.parseBoolean(propertyValue);
     }
 
     @Override
@@ -453,7 +464,9 @@ public class SampleUpdate implements IUpdate, IPropertiesHolder, IObjectUpdate<I
     public SamplePermId getSampleProperty(String propertyName)
     {
         String propertyValue = getProperty(propertyName);
-        return (propertyValue == null || propertyValue.isBlank()) ? null : new SamplePermId(propertyValue);
+        return (propertyValue == null || propertyValue.isBlank()) ?
+                null :
+                new SamplePermId(propertyValue);
     }
 
     @Override
@@ -466,39 +479,55 @@ public class SampleUpdate implements IUpdate, IPropertiesHolder, IObjectUpdate<I
     public Long[] getIntegerArrayProperty(String propertyName)
     {
         String propertyValue = getProperty(propertyName);
-        return (propertyValue == null || propertyValue.isBlank()) ? null : Arrays.stream(propertyValue.split(",")).map(String::trim).map(Long::parseLong).toArray(Long[]::new);
+        return (propertyValue == null || propertyValue.isBlank()) ?
+                null :
+                Arrays.stream(propertyValue.split(",")).map(String::trim).map(Long::parseLong)
+                        .toArray(Long[]::new);
     }
 
     @Override
     public void setIntegerArrayProperty(String propertyName, Long[] propertyValue)
     {
-        setProperty(propertyName, propertyValue == null ? null : Arrays.stream(propertyValue).map(Object::toString).reduce((a,b) -> a + ", " + b).get());
+        setProperty(propertyName, propertyValue == null ?
+                null :
+                Arrays.stream(propertyValue).map(Object::toString).reduce((a, b) -> a + ", " + b)
+                        .get());
     }
 
     @Override
     public Double[] getRealArrayProperty(String propertyName)
     {
         String propertyValue = getProperty(propertyName);
-        return (propertyValue == null || propertyValue.isBlank()) ? null : Arrays.stream(propertyValue.split(",")).map(String::trim).map(Double::parseDouble).toArray(Double[]::new);
+        return (propertyValue == null || propertyValue.isBlank()) ?
+                null :
+                Arrays.stream(propertyValue.split(",")).map(String::trim).map(Double::parseDouble)
+                        .toArray(Double[]::new);
     }
 
     @Override
     public void setRealArrayProperty(String propertyName, Double[] propertyValue)
     {
-        setProperty(propertyName, propertyValue == null ? null : Arrays.stream(propertyValue).map(Object::toString).reduce((a,b) -> a + ", " + b).get());
+        setProperty(propertyName, propertyValue == null ?
+                null :
+                Arrays.stream(propertyValue).map(Object::toString).reduce((a, b) -> a + ", " + b)
+                        .get());
     }
 
     @Override
     public String[] getStringArrayProperty(String propertyName)
     {
         String propertyValue = getProperty(propertyName);
-        return (propertyValue == null || propertyValue.isBlank()) ? null : Arrays.stream(propertyValue.split(",")).map(String::trim).toArray(String[]::new);
+        return (propertyValue == null || propertyValue.isBlank()) ?
+                null :
+                Arrays.stream(propertyValue.split(",")).map(String::trim).toArray(String[]::new);
     }
 
     @Override
     public void setStringArrayProperty(String propertyName, String[] propertyValue)
     {
-        setProperty(propertyName, propertyValue == null ? null : Arrays.stream(propertyValue).reduce((a,b) -> a + ", " + b).get());
+        setProperty(propertyName, propertyValue == null ?
+                null :
+                Arrays.stream(propertyValue).reduce((a, b) -> a + ", " + b).get());
     }
 
     @Override
@@ -507,7 +536,8 @@ public class SampleUpdate implements IUpdate, IPropertiesHolder, IObjectUpdate<I
         String propertyValue = getProperty(propertyName);
         return propertyValue == null ? null : Arrays.stream(propertyValue.split(","))
                 .map(String::trim)
-                .map(dateTime -> ZonedDateTime.parse(dateTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss X")))
+                .map(dateTime -> ZonedDateTime.parse(dateTime,
+                        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss X")))
                 .toArray(ZonedDateTime[]::new);
     }
 
@@ -515,8 +545,9 @@ public class SampleUpdate implements IUpdate, IPropertiesHolder, IObjectUpdate<I
     public void setTimestampArrayProperty(String propertyName, ZonedDateTime[] propertyValue)
     {
         String value = (propertyValue == null) ? null : Arrays.stream(propertyValue)
-                .map(dateTime -> dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX")))
-                .reduce((a,b) -> a + ", " + b)
+                .map(dateTime -> dateTime.format(
+                        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX")))
+                .reduce((a, b) -> a + ", " + b)
                 .get();
         setProperty(propertyName, value);
     }
@@ -534,6 +565,7 @@ public class SampleUpdate implements IUpdate, IPropertiesHolder, IObjectUpdate<I
     }
 
     @JsonIgnore
+    @Override
     public ListUpdateMapValues getMetaData()
     {
         return metaData;
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/dataset/UpdateDataSetExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/dataset/UpdateDataSetExecutor.java
index 85f3fce4bb7774c11617c9657918c3de086c21aa..4c140459b549ee3bb46b5b963631ba002133f685 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/dataset/UpdateDataSetExecutor.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/dataset/UpdateDataSetExecutor.java
@@ -17,9 +17,8 @@ package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.dataset;
 
 import java.util.*;
 import java.util.Map.Entry;
-import java.util.concurrent.atomic.AtomicBoolean;
 
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.ListUpdateValue;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.metadata.IUpdateMetaDataForEntityExecutor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.dao.DataAccessException;
 import org.springframework.stereotype.Component;
@@ -87,6 +86,9 @@ public class UpdateDataSetExecutor extends AbstractUpdateEntityExecutor<DataSetU
     @Autowired
     private IEventExecutor eventExecutor;
 
+    @Autowired
+    private IUpdateMetaDataForEntityExecutor<DataSetUpdate, DataPE> updateMetaDataForEntityExecutor;
+
     @Override
     protected IDataSetId getId(DataSetUpdate update)
     {
@@ -123,7 +125,7 @@ public class UpdateDataSetExecutor extends AbstractUpdateEntityExecutor<DataSetU
         updateDataSetSampleExecutor.update(context, batch);
         updateDataSetPropertyExecutor.update(context, batch);
         updateTags(context, batch);
-        updateMetaData(context, batch);
+        updateMetaDataForEntityExecutor.update(context, batch);
 
         PersonPE person = context.getSession().tryGetPerson();
         Date timeStamp = daoFactory.getTransactionTimestamp();
@@ -200,69 +202,6 @@ public class UpdateDataSetExecutor extends AbstractUpdateEntityExecutor<DataSetU
             };
     }
 
-    private void updateMetaData(final IOperationContext context, final MapBatch<DataSetUpdate, DataPE> batch)
-    {
-        new MapBatchProcessor<DataSetUpdate, DataPE>(context, batch)
-        {
-            @Override
-            public void process(DataSetUpdate update, DataPE entity)
-            {
-                Map<String, String> metaData = new HashMap<>();
-                if(entity.getMetaData() != null) {
-                    metaData.putAll(entity.getMetaData());
-                }
-                ListUpdateValue.ListUpdateActionSet<?> lastSetAction = null;
-                AtomicBoolean metaDataChanged = new AtomicBoolean(false);
-                for (ListUpdateValue.ListUpdateAction<Object> action : update.getMetaData().getActions())
-                {
-                    if (action instanceof ListUpdateValue.ListUpdateActionAdd<?>)
-                    {
-                        addTo(metaData, action, metaDataChanged);
-                    } else if (action instanceof ListUpdateValue.ListUpdateActionRemove<?>)
-                    {
-                        for (String key : (Collection<String>) action.getItems())
-                        {
-                            metaDataChanged.set(true);
-                            metaData.remove(key);
-                        }
-                    } else if (action instanceof ListUpdateValue.ListUpdateActionSet<?>)
-                    {
-                        lastSetAction = (ListUpdateValue.ListUpdateActionSet<?>) action;
-                    }
-                }
-                if (lastSetAction != null)
-                {
-                    metaData.clear();
-                    addTo(metaData, lastSetAction, metaDataChanged);
-                }
-                if (metaDataChanged.get())
-                {
-                    entity.setMetaData(metaData.isEmpty() ? null : metaData);
-                }
-            }
-
-            @Override
-            public IProgress createProgress(DataSetUpdate key, DataPE value, int objectIndex, int totalObjectCount)
-            {
-                return new UpdateRelationProgress(key, value, "dataset-metadata", objectIndex, totalObjectCount);
-            }
-
-            @SuppressWarnings("unchecked")
-            private void addTo(Map<String, String> metaData, ListUpdateValue.ListUpdateAction<?> lastSetAction, AtomicBoolean metaDataChanged)
-            {
-                Collection<Map<String, String>> maps = (Collection<Map<String, String>>) lastSetAction.getItems();
-                for (Map<String, String> map : maps)
-                {
-                    if (!map.isEmpty())
-                    {
-                        metaDataChanged.set(true);
-                        metaData.putAll(map);
-                    }
-                }
-            }
-        };
-    }
-
     @Override
     protected void updateAll(IOperationContext context, MapBatch<DataSetUpdate, DataPE> batch)
     {
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/dataset/UpdateDataSetTypeExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/dataset/UpdateDataSetTypeExecutor.java
index 1847ba7440a9970d08398e7a25c75ac4aac9f040..dcc28125eebf339089767141da396205956746a9 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/dataset/UpdateDataSetTypeExecutor.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/dataset/UpdateDataSetTypeExecutor.java
@@ -15,7 +15,7 @@
  */
 package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.dataset;
 
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.ListUpdateValue;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.metadata.IUpdateMetaDataForEntityExecutor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -27,10 +27,6 @@ import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.entity.IUpdateEntity
 import ch.systemsx.cisd.openbis.generic.shared.dto.DataSetTypePE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.properties.EntityKind;
 
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * @author Franz-Josef Elmer
@@ -42,10 +38,14 @@ public class UpdateDataSetTypeExecutor
 {
     @Autowired
     private IDataSetTypeAuthorizationExecutor authorizationExecutor;
-    
+
     @Autowired
     private IUpdateDataSetTypePropertyTypesExecutor updateDataSetTypePropertyTypesExecutor;
 
+    @Autowired
+    private IUpdateMetaDataForEntityExecutor<DataSetTypeUpdate, DataSetTypePE>
+            updateMetaDataForEntityExecutor;
+
     @Override
     protected EntityKind getDAOEntityKind()
     {
@@ -60,10 +60,13 @@ public class UpdateDataSetTypeExecutor
     @Override
     protected void updateSpecific(DataSetTypePE type, DataSetTypeUpdate update)
     {
-        type.setMainDataSetPattern(getNewValue(update.getMainDataSetPattern(), type.getMainDataSetPattern()));
-        type.setMainDataSetPath(getNewValue(update.getMainDataSetPath(), type.getMainDataSetPath()));
-        type.setDeletionDisallow(getNewValue(update.isDisallowDeletion(), type.isDeletionDisallow()));
-        updateMetaData(type, update);
+        type.setMainDataSetPattern(
+                getNewValue(update.getMainDataSetPattern(), type.getMainDataSetPattern()));
+        type.setMainDataSetPath(
+                getNewValue(update.getMainDataSetPath(), type.getMainDataSetPath()));
+        type.setDeletionDisallow(
+                getNewValue(update.isDisallowDeletion(), type.isDeletionDisallow()));
+        updateMetaDataForEntityExecutor.updateSpecific(update, type);
     }
 
     @Override
@@ -78,53 +81,4 @@ public class UpdateDataSetTypeExecutor
         authorizationExecutor.canUpdate(context);
     }
 
-    private void updateMetaData(DataSetTypePE type, DataSetTypeUpdate update)
-    {
-        Map<String, String> metaData = new HashMap<>();
-        if(type.getMetaData() != null) {
-            metaData.putAll(type.getMetaData());
-        }
-        ListUpdateValue.ListUpdateActionSet<?> lastSetAction = null;
-        AtomicBoolean metaDataChanged = new AtomicBoolean(false);
-        for (ListUpdateValue.ListUpdateAction<Object> action : update.getMetaData().getActions())
-        {
-            if (action instanceof ListUpdateValue.ListUpdateActionAdd<?>)
-            {
-                addTo(metaData, action, metaDataChanged);
-            } else if (action instanceof ListUpdateValue.ListUpdateActionRemove<?>)
-            {
-                for (String key : (Collection<String>) action.getItems())
-                {
-                    metaDataChanged.set(true);
-                    metaData.remove(key);
-                }
-            } else if (action instanceof ListUpdateValue.ListUpdateActionSet<?>)
-            {
-                lastSetAction = (ListUpdateValue.ListUpdateActionSet<?>) action;
-            }
-        }
-        if (lastSetAction != null)
-        {
-            metaData.clear();
-            addTo(metaData, lastSetAction, metaDataChanged);
-        }
-        if (metaDataChanged.get())
-        {
-            type.setMetaData(metaData.isEmpty() ? null : metaData);
-        }
-    }
-
-    private void addTo(Map<String, String> metaData, ListUpdateValue.ListUpdateAction<?> lastSetAction, AtomicBoolean metaDataChanged)
-    {
-        Collection<Map<String, String>> maps = (Collection<Map<String, String>>) lastSetAction.getItems();
-        for (Map<String, String> map : maps)
-        {
-            if (!map.isEmpty())
-            {
-                metaDataChanged.set(true);
-                metaData.putAll(map);
-            }
-        }
-    }
-
 }
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/experiment/UpdateExperimentExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/experiment/UpdateExperimentExecutor.java
index cbb46df8247974df8ba475c38234a048d8f6a42b..7cfe50395b9bd67df0f1bbf30e278a9a6b6b773e 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/experiment/UpdateExperimentExecutor.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/experiment/UpdateExperimentExecutor.java
@@ -17,9 +17,8 @@ package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.experiment;
 
 import java.util.*;
 import java.util.Map.Entry;
-import java.util.concurrent.atomic.AtomicBoolean;
 
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.ListUpdateValue;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.metadata.IUpdateMetaDataForEntityExecutor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.dao.DataAccessException;
 import org.springframework.stereotype.Component;
@@ -50,7 +49,9 @@ import ch.systemsx.cisd.openbis.generic.shared.util.RelationshipUtils;
  * @author pkupczyk
  */
 @Component
-public class UpdateExperimentExecutor extends AbstractUpdateEntityExecutor<ExperimentUpdate, ExperimentPE, IExperimentId, ExperimentPermId> implements
+public class UpdateExperimentExecutor extends
+        AbstractUpdateEntityExecutor<ExperimentUpdate, ExperimentPE, IExperimentId, ExperimentPermId>
+        implements
         IUpdateExperimentExecutor
 {
 
@@ -78,6 +79,10 @@ public class UpdateExperimentExecutor extends AbstractUpdateEntityExecutor<Exper
     @Autowired
     private IEventExecutor eventExecutor;
 
+    @Autowired
+    private IUpdateMetaDataForEntityExecutor<ExperimentUpdate, ExperimentPE>
+            updateMetaDataForEntityExecutor;
+
     @Override
     protected IExperimentId getId(ExperimentUpdate update)
     {
@@ -106,13 +111,14 @@ public class UpdateExperimentExecutor extends AbstractUpdateEntityExecutor<Exper
     }
 
     @Override
-    protected void updateBatch(final IOperationContext context, final MapBatch<ExperimentUpdate, ExperimentPE> batch)
+    protected void updateBatch(final IOperationContext context,
+            final MapBatch<ExperimentUpdate, ExperimentPE> batch)
     {
         updateExperimentProjectExecutor.update(context, batch);
         updateExperimentPropertyExecutor.update(context, batch);
         updateTags(context, batch);
         updateAttachments(context, batch);
-        updateMetaData(context, batch);
+        updateMetaDataForEntityExecutor.update(context, batch);
 
         PersonPE person = context.getSession().tryGetPerson();
         Date timeStamp = daoFactory.getTransactionTimestamp();
@@ -133,20 +139,24 @@ public class UpdateExperimentExecutor extends AbstractUpdateEntityExecutor<Exper
             if (update.shouldBeFrozenForDataSets())
             {
                 authorizationExecutor.canFreeze(context, experiment);
-                assertionOfNoDeletedEntityExecutor.assertExperimentHasNoDeletedDataSets(experiment.getPermId());
+                assertionOfNoDeletedEntityExecutor.assertExperimentHasNoDeletedDataSets(
+                        experiment.getPermId());
                 experiment.setFrozenForDataSet(true);
                 freezingFlags.freezeForDataSets();
             }
             if (update.shouldBeFrozenForSamples())
             {
                 authorizationExecutor.canFreeze(context, experiment);
-                assertionOfNoDeletedEntityExecutor.assertExperimentHasNoDeletedSamples(experiment.getPermId());
+                assertionOfNoDeletedEntityExecutor.assertExperimentHasNoDeletedSamples(
+                        experiment.getPermId());
                 experiment.setFrozenForSample(true);
                 freezingFlags.freezeForSamples();
             }
             if (freezingFlags.noFlags() == false)
             {
-                freezingEvents.add(new FreezingEvent(experiment.getIdentifier(), EventPE.EntityType.EXPERIMENT, freezingFlags));
+                freezingEvents.add(
+                        new FreezingEvent(experiment.getIdentifier(), EventPE.EntityType.EXPERIMENT,
+                                freezingFlags));
             }
         }
         if (freezingEvents.isEmpty() == false)
@@ -155,118 +165,64 @@ public class UpdateExperimentExecutor extends AbstractUpdateEntityExecutor<Exper
         }
     }
 
-    private void updateMetaData(final IOperationContext context, final MapBatch<ExperimentUpdate, ExperimentPE> batch)
+    private void updateTags(final IOperationContext context,
+            final MapBatch<ExperimentUpdate, ExperimentPE> batch)
     {
         new MapBatchProcessor<ExperimentUpdate, ExperimentPE>(context, batch)
         {
             @Override
             public void process(ExperimentUpdate update, ExperimentPE entity)
             {
-                Map<String, String> metaData = new HashMap<>();
-                if(entity.getMetaData() != null) {
-                    metaData.putAll(entity.getMetaData());
-                }
-                ListUpdateValue.ListUpdateActionSet<?> lastSetAction = null;
-                AtomicBoolean metaDataChanged = new AtomicBoolean(false);
-                for (ListUpdateValue.ListUpdateAction<Object> action : update.getMetaData().getActions())
-                {
-                    if (action instanceof ListUpdateValue.ListUpdateActionAdd<?>)
-                    {
-                        addTo(metaData, action, metaDataChanged);
-                    } else if (action instanceof ListUpdateValue.ListUpdateActionRemove<?>)
-                    {
-                        for (String key : (Collection<String>) action.getItems())
-                        {
-                            metaDataChanged.set(true);
-                            metaData.remove(key);
-                        }
-                    } else if (action instanceof ListUpdateValue.ListUpdateActionSet<?>)
-                    {
-                        lastSetAction = (ListUpdateValue.ListUpdateActionSet<?>) action;
-                    }
-                }
-                if (lastSetAction != null)
-                {
-                    metaData.clear();
-                    addTo(metaData, lastSetAction, metaDataChanged);
-                }
-                if (metaDataChanged.get())
+                if (update.getTagIds() != null && update.getTagIds().hasActions())
                 {
-                    entity.setMetaData(metaData.isEmpty() ? null : metaData);
+                    updateTagForEntityExecutor.update(context, entity, update.getTagIds());
                 }
             }
 
             @Override
-            public IProgress createProgress(ExperimentUpdate update, ExperimentPE entity, int objectIndex, int totalObjectCount)
+            public IProgress createProgress(ExperimentUpdate update, ExperimentPE entity,
+                    int objectIndex, int totalObjectCount)
             {
-                return new UpdateRelationProgress(update, entity, "experiment-metadata", objectIndex, totalObjectCount);
-            }
-
-            @SuppressWarnings("unchecked")
-            private void addTo(Map<String, String> metaData, ListUpdateValue.ListUpdateAction<?> lastSetAction, AtomicBoolean metaDataChanged)
-            {
-                Collection<Map<String, String>> maps = (Collection<Map<String, String>>) lastSetAction.getItems();
-                for (Map<String, String> map : maps)
-                {
-                    if (!map.isEmpty())
-                    {
-                        metaDataChanged.set(true);
-                        metaData.putAll(map);
-                    }
-                }
+                return new UpdateRelationProgress(update, entity, "experiment-tag", objectIndex,
+                        totalObjectCount);
             }
         };
     }
 
-    private void updateTags(final IOperationContext context, final MapBatch<ExperimentUpdate, ExperimentPE> batch)
+    private void updateAttachments(final IOperationContext context,
+            final MapBatch<ExperimentUpdate, ExperimentPE> batch)
     {
         new MapBatchProcessor<ExperimentUpdate, ExperimentPE>(context, batch)
+        {
+            @Override
+            public void process(ExperimentUpdate update, ExperimentPE entity)
             {
-                @Override
-                public void process(ExperimentUpdate update, ExperimentPE entity)
+                if (update.getAttachments() != null && update.getAttachments().hasActions())
                 {
-                    if (update.getTagIds() != null && update.getTagIds().hasActions())
-                    {
-                        updateTagForEntityExecutor.update(context, entity, update.getTagIds());
-                    }
+                    updateExperimentAttachmentExecutor.update(context, entity,
+                            update.getAttachments());
                 }
+            }
 
-                @Override
-                public IProgress createProgress(ExperimentUpdate update, ExperimentPE entity, int objectIndex, int totalObjectCount)
-                {
-                    return new UpdateRelationProgress(update, entity, "experiment-tag", objectIndex, totalObjectCount);
-                }
-            };
-    }
-
-    private void updateAttachments(final IOperationContext context, final MapBatch<ExperimentUpdate, ExperimentPE> batch)
-    {
-        new MapBatchProcessor<ExperimentUpdate, ExperimentPE>(context, batch)
+            @Override
+            public IProgress createProgress(ExperimentUpdate update, ExperimentPE entity,
+                    int objectIndex, int totalObjectCount)
             {
-                @Override
-                public void process(ExperimentUpdate update, ExperimentPE entity)
-                {
-                    if (update.getAttachments() != null && update.getAttachments().hasActions())
-                    {
-                        updateExperimentAttachmentExecutor.update(context, entity, update.getAttachments());
-                    }
-                }
-
-                @Override
-                public IProgress createProgress(ExperimentUpdate update, ExperimentPE entity, int objectIndex, int totalObjectCount)
-                {
-                    return new UpdateRelationProgress(update, entity, "experiment-attachment", objectIndex, totalObjectCount);
-                }
-            };
+                return new UpdateRelationProgress(update, entity, "experiment-attachment",
+                        objectIndex, totalObjectCount);
+            }
+        };
     }
 
     @Override
-    protected void updateAll(IOperationContext context, MapBatch<ExperimentUpdate, ExperimentPE> batch)
+    protected void updateAll(IOperationContext context,
+            MapBatch<ExperimentUpdate, ExperimentPE> batch)
     {
     }
 
     @Override
-    protected Map<IExperimentId, ExperimentPE> map(IOperationContext context, Collection<IExperimentId> ids)
+    protected Map<IExperimentId, ExperimentPE> map(IOperationContext context,
+            Collection<IExperimentId> ids)
     {
         return mapExperimentByIdExecutor.map(context, ids);
     }
@@ -280,13 +236,16 @@ public class UpdateExperimentExecutor extends AbstractUpdateEntityExecutor<Exper
     @Override
     protected void save(IOperationContext context, List<ExperimentPE> entities, boolean clearCache)
     {
-        daoFactory.getExperimentDAO().createOrUpdateExperiments(entities, context.getSession().tryGetPerson(), clearCache);
+        daoFactory.getExperimentDAO()
+                .createOrUpdateExperiments(entities, context.getSession().tryGetPerson(),
+                        clearCache);
     }
 
     @Override
     protected void handleException(DataAccessException e)
     {
-        DataAccessExceptionTranslator.throwException(e, EntityKind.EXPERIMENT.getLabel(), EntityKind.EXPERIMENT);
+        DataAccessExceptionTranslator.throwException(e, EntityKind.EXPERIMENT.getLabel(),
+                EntityKind.EXPERIMENT);
     }
 
 }
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/experiment/UpdateExperimentTypeExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/experiment/UpdateExperimentTypeExecutor.java
index 1f3df9a824de3e5002af6bf8aec44e39ecbb9e58..1318a8aa35afd807b6bf02e65c9f9e5656ab0467 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/experiment/UpdateExperimentTypeExecutor.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/experiment/UpdateExperimentTypeExecutor.java
@@ -15,7 +15,7 @@
  */
 package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.experiment;
 
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.ListUpdateValue;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.metadata.IUpdateMetaDataForEntityExecutor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -27,11 +27,6 @@ import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.entity.IUpdateEntity
 import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentTypePE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.properties.EntityKind;
 
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
-
 /**
  * @author Franz-Josef Elmer
  */
@@ -46,6 +41,10 @@ public class UpdateExperimentTypeExecutor
     @Autowired
     private IUpdateExperimentTypePropertyTypesExecutor updateExperimentTypePropertyTypesExecutor;
 
+    @Autowired
+    private IUpdateMetaDataForEntityExecutor<ExperimentTypeUpdate, ExperimentTypePE>
+            updateMetaDataForEntityExecutor;
+
     @Override
     protected EntityKind getDAOEntityKind()
     {
@@ -60,7 +59,7 @@ public class UpdateExperimentTypeExecutor
     @Override
     protected void updateSpecific(ExperimentTypePE type, ExperimentTypeUpdate update)
     {
-        updateMetaData(type, update);
+        updateMetaDataForEntityExecutor.updateSpecific(update, type);
     }
 
     @Override
@@ -75,53 +74,4 @@ public class UpdateExperimentTypeExecutor
         authorizationExecutor.canUpdate(context);
     }
 
-    private void updateMetaData(ExperimentTypePE type, ExperimentTypeUpdate update)
-    {
-        Map<String, String> metaData = new HashMap<>();
-        if(type.getMetaData() != null) {
-            metaData.putAll(type.getMetaData());
-        }
-        ListUpdateValue.ListUpdateActionSet<?> lastSetAction = null;
-        AtomicBoolean metaDataChanged = new AtomicBoolean(false);
-        for (ListUpdateValue.ListUpdateAction<Object> action : update.getMetaData().getActions())
-        {
-            if (action instanceof ListUpdateValue.ListUpdateActionAdd<?>)
-            {
-                addTo(metaData, action, metaDataChanged);
-            } else if (action instanceof ListUpdateValue.ListUpdateActionRemove<?>)
-            {
-                for (String key : (Collection<String>) action.getItems())
-                {
-                    metaDataChanged.set(true);
-                    metaData.remove(key);
-                }
-            } else if (action instanceof ListUpdateValue.ListUpdateActionSet<?>)
-            {
-                lastSetAction = (ListUpdateValue.ListUpdateActionSet<?>) action;
-            }
-        }
-        if (lastSetAction != null)
-        {
-            metaData.clear();
-            addTo(metaData, lastSetAction, metaDataChanged);
-        }
-        if (metaDataChanged.get())
-        {
-            type.setMetaData(metaData.isEmpty() ? null : metaData);
-        }
-    }
-
-    private void addTo(Map<String, String> metaData, ListUpdateValue.ListUpdateAction<?> lastSetAction, AtomicBoolean metaDataChanged)
-    {
-        Collection<Map<String, String>> maps = (Collection<Map<String, String>>) lastSetAction.getItems();
-        for (Map<String, String> map : maps)
-        {
-            if (!map.isEmpty())
-            {
-                metaDataChanged.set(true);
-                metaData.putAll(map);
-            }
-        }
-    }
-
 }
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/metadata/IUpdateMetaDataForEntityExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/metadata/IUpdateMetaDataForEntityExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..11265acab231ffbea081b55d6c0a8d430d67c6d5
--- /dev/null
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/metadata/IUpdateMetaDataForEntityExecutor.java
@@ -0,0 +1,29 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.metadata;
+
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.helper.common.batch.MapBatch;
+
+public interface IUpdateMetaDataForEntityExecutor<ENTITY_UPDATE, ENTITY_PE>
+{
+    void update(final IOperationContext context, final MapBatch<ENTITY_UPDATE, ENTITY_PE> batch);
+
+    void updateSpecific(ENTITY_UPDATE update, ENTITY_PE entity);
+
+}
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/metadata/UpdateMetaDataForEntityExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/metadata/UpdateMetaDataForEntityExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..e00007449376db23b9787796e32e0a9afc4542c2
--- /dev/null
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/metadata/UpdateMetaDataForEntityExecutor.java
@@ -0,0 +1,116 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.metadata;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.IMetaDataUpdateHolder;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.ListUpdateValue;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.context.IProgress;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.helper.common.batch.MapBatch;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.helper.common.batch.MapBatchProcessor;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.helper.entity.progress.UpdateRelationProgress;
+import ch.systemsx.cisd.openbis.generic.shared.dto.IEntityWithMetaData;
+import org.springframework.stereotype.Component;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@Component
+public class UpdateMetaDataForEntityExecutor<ENTITY_UPDATE extends IMetaDataUpdateHolder, ENTITY_PE extends IEntityWithMetaData>
+        implements IUpdateMetaDataForEntityExecutor<ENTITY_UPDATE, ENTITY_PE>
+{
+    @Override
+    public void update(final IOperationContext context,
+            final MapBatch<ENTITY_UPDATE, ENTITY_PE> batch)
+    {
+        new MapBatchProcessor<ENTITY_UPDATE, ENTITY_PE>(context, batch)
+        {
+            @Override
+            public void process(ENTITY_UPDATE update, ENTITY_PE entity)
+            {
+                updateSpecific(update, entity);
+            }
+
+            @Override
+            public IProgress createProgress(ENTITY_UPDATE update, ENTITY_PE entity, int objectIndex,
+                    int totalObjectCount)
+            {
+                return new UpdateRelationProgress(update, entity, "metadata", objectIndex,
+                        totalObjectCount);
+            }
+
+        };
+
+    }
+
+    @Override
+    public void updateSpecific(ENTITY_UPDATE update, ENTITY_PE entity)
+    {
+        Map<String, String> metaData = new HashMap<>();
+        if (entity.getMetaData() != null)
+        {
+            metaData.putAll(entity.getMetaData());
+        }
+        ListUpdateValue.ListUpdateActionSet<?> lastSetAction = null;
+        AtomicBoolean metaDataChanged = new AtomicBoolean(false);
+        for (ListUpdateValue.ListUpdateAction<Object> action : update.getMetaData().getActions())
+        {
+            if (action instanceof ListUpdateValue.ListUpdateActionAdd<?>)
+            {
+                addTo(metaData, action, metaDataChanged);
+            } else if (action instanceof ListUpdateValue.ListUpdateActionRemove<?>)
+            {
+                for (String key : (Collection<String>) action.getItems())
+                {
+                    metaDataChanged.set(true);
+                    metaData.remove(key);
+                }
+            } else if (action instanceof ListUpdateValue.ListUpdateActionSet<?>)
+            {
+                lastSetAction = (ListUpdateValue.ListUpdateActionSet<?>) action;
+            }
+        }
+        if (lastSetAction != null)
+        {
+            metaData.clear();
+            addTo(metaData, lastSetAction, metaDataChanged);
+        }
+        if (metaDataChanged.get())
+        {
+            entity.setMetaData(metaData.isEmpty() ? null : metaData);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void addTo(Map<String, String> metaData,
+            ListUpdateValue.ListUpdateAction<?> lastSetAction, AtomicBoolean metaDataChanged)
+    {
+        Collection<Map<String, String>> maps =
+                (Collection<Map<String, String>>) lastSetAction.getItems();
+        for (Map<String, String> map : maps)
+        {
+            if (map.isEmpty() == false)
+            {
+                metaDataChanged.set(true);
+                metaData.putAll(map);
+            }
+        }
+    }
+}
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/sample/UpdateSampleExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/sample/UpdateSampleExecutor.java
index 1d8bedbbed7bbc18d519e56ab1a48ce9711ee63d..fdb6d92808acddd9939aab22a005a360fbb0f4fc 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/sample/UpdateSampleExecutor.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/sample/UpdateSampleExecutor.java
@@ -17,9 +17,8 @@ package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.sample;
 
 import java.util.*;
 import java.util.Map.Entry;
-import java.util.concurrent.atomic.AtomicBoolean;
 
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.ListUpdateValue;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.metadata.IUpdateMetaDataForEntityExecutor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.dao.DataAccessException;
 import org.springframework.stereotype.Component;
@@ -51,7 +50,9 @@ import ch.systemsx.cisd.openbis.generic.shared.util.RelationshipUtils;
  * @author pkupczyk
  */
 @Component
-public class UpdateSampleExecutor extends AbstractUpdateEntityExecutor<SampleUpdate, SamplePE, ISampleId, SamplePermId> implements
+public class UpdateSampleExecutor
+        extends AbstractUpdateEntityExecutor<SampleUpdate, SamplePE, ISampleId, SamplePermId>
+        implements
         IUpdateSampleExecutor
 {
 
@@ -91,6 +92,9 @@ public class UpdateSampleExecutor extends AbstractUpdateEntityExecutor<SampleUpd
     @Autowired
     private IEventExecutor eventExecutor;
 
+    @Autowired
+    private IUpdateMetaDataForEntityExecutor<SampleUpdate, SamplePE> updateMetaDataExecutor;
+
     @Override
     protected ISampleId getId(SampleUpdate update)
     {
@@ -142,34 +146,40 @@ public class UpdateSampleExecutor extends AbstractUpdateEntityExecutor<SampleUpd
             if (update.shouldBeFrozenForComponents())
             {
                 authorizationExecutor.canFreeze(context, entity);
-                assertionOfNoDeletedEntityExecutor.assertSampleHasNoDeletedComponents(entity.getPermId());
+                assertionOfNoDeletedEntityExecutor.assertSampleHasNoDeletedComponents(
+                        entity.getPermId());
                 entity.setFrozenForComponent(true);
                 freezingFlags.freezeForComponents();
             }
             if (update.shouldBeFrozenForChildren())
             {
                 authorizationExecutor.canFreeze(context, entity);
-                assertionOfNoDeletedEntityExecutor.assertSampleHasNoDeletedChildren(entity.getPermId());
+                assertionOfNoDeletedEntityExecutor.assertSampleHasNoDeletedChildren(
+                        entity.getPermId());
                 entity.setFrozenForChildren(true);
                 freezingFlags.freezeForChildren();
             }
             if (update.shouldBeFrozenForParents())
             {
                 authorizationExecutor.canFreeze(context, entity);
-                assertionOfNoDeletedEntityExecutor.assertSampleHasNoDeletedParents(entity.getPermId());
+                assertionOfNoDeletedEntityExecutor.assertSampleHasNoDeletedParents(
+                        entity.getPermId());
                 entity.setFrozenForParents(true);
                 freezingFlags.freezeForParents();
             }
             if (update.shouldBeFrozenForDataSets())
             {
                 authorizationExecutor.canFreeze(context, entity);
-                assertionOfNoDeletedEntityExecutor.assertSampleHasNoDeletedDataSets(entity.getPermId());
+                assertionOfNoDeletedEntityExecutor.assertSampleHasNoDeletedDataSets(
+                        entity.getPermId());
                 entity.setFrozenForDataSet(true);
                 freezingFlags.freezeForDataSets();
             }
             if (freezingFlags.noFlags() == false)
             {
-                freezingEvents.add(new FreezingEvent(entity.getIdentifier(), EventPE.EntityType.SAMPLE, freezingFlags));
+                freezingEvents.add(
+                        new FreezingEvent(entity.getIdentifier(), EventPE.EntityType.SAMPLE,
+                                freezingFlags));
             }
         }
         if (freezingEvents.isEmpty() == false)
@@ -181,15 +191,16 @@ public class UpdateSampleExecutor extends AbstractUpdateEntityExecutor<SampleUpd
         updateSampleExperimentExecutor.update(context, batch);
         updateSampleProjectExecutor.update(context, batch);
         updateSamplePropertyExecutor.update(context, batch);
+        updateMetaDataExecutor.update(context, batch);
         updateTags(context, batch);
         updateAttachments(context, batch);
-        updateMetaData(context, batch);
 
         for (SamplePE entity : experimentOrProjectSamples)
         {
             if (entity.getExperiment() == null && entity.getProject() == null)
             {
-                relationshipService.assignSampleToSpace(context.getSession(), entity, entity.getSpace());
+                relationshipService.assignSampleToSpace(context.getSession(), entity,
+                        entity.getSpace());
             }
         }
 
@@ -202,69 +213,8 @@ public class UpdateSampleExecutor extends AbstractUpdateEntityExecutor<SampleUpd
         }
     }
 
-    private void updateMetaData(final IOperationContext context, final MapBatch<SampleUpdate, SamplePE> batch) {
-        new MapBatchProcessor<SampleUpdate, SamplePE>(context, batch)
-        {
-            @Override
-            public void process(SampleUpdate update, SamplePE entity)
-            {
-                Map<String, String> metaData = new HashMap<>();
-                if(entity.getMetaData() != null) {
-                    metaData.putAll(entity.getMetaData());
-                }
-                ListUpdateValue.ListUpdateActionSet<?> lastSetAction = null;
-                AtomicBoolean metaDataChanged = new AtomicBoolean(false);
-                for (ListUpdateValue.ListUpdateAction<Object> action : update.getMetaData().getActions())
-                {
-                    if (action instanceof ListUpdateValue.ListUpdateActionAdd<?>)
-                    {
-                        addTo(metaData, action, metaDataChanged);
-                    } else if (action instanceof ListUpdateValue.ListUpdateActionRemove<?>)
-                    {
-                        for (String key : (Collection<String>) action.getItems())
-                        {
-                            metaDataChanged.set(true);
-                            metaData.remove(key);
-                        }
-                    } else if (action instanceof ListUpdateValue.ListUpdateActionSet<?>)
-                    {
-                        lastSetAction = (ListUpdateValue.ListUpdateActionSet<?>) action;
-                    }
-                }
-                if (lastSetAction != null)
-                {
-                    metaData.clear();
-                    addTo(metaData, lastSetAction, metaDataChanged);
-                }
-                if (metaDataChanged.get())
-                {
-                    entity.setMetaData(metaData.isEmpty() ? null : metaData);
-                }
-            }
-
-            @Override
-            public IProgress createProgress(SampleUpdate update, SamplePE entity, int objectIndex, int totalObjectCount)
-            {
-                return new UpdateRelationProgress(update, entity, "sample-metadata", objectIndex, totalObjectCount);
-            }
-
-            @SuppressWarnings("unchecked")
-            private void addTo(Map<String, String> metaData, ListUpdateValue.ListUpdateAction<?> lastSetAction, AtomicBoolean metaDataChanged)
-            {
-                Collection<Map<String, String>> maps = (Collection<Map<String, String>>) lastSetAction.getItems();
-                for (Map<String, String> map : maps)
-                {
-                    if (map.isEmpty() == false)
-                    {
-                        metaDataChanged.set(true);
-                        metaData.putAll(map);
-                    }
-                }
-            }
-        };
-    }
-
-    private void updateTags(final IOperationContext context, final MapBatch<SampleUpdate, SamplePE> batch)
+    private void updateTags(final IOperationContext context,
+            final MapBatch<SampleUpdate, SamplePE> batch)
     {
         new MapBatchProcessor<SampleUpdate, SamplePE>(context, batch)
         {
@@ -278,14 +228,17 @@ public class UpdateSampleExecutor extends AbstractUpdateEntityExecutor<SampleUpd
             }
 
             @Override
-            public IProgress createProgress(SampleUpdate update, SamplePE entity, int objectIndex, int totalObjectCount)
+            public IProgress createProgress(SampleUpdate update, SamplePE entity, int objectIndex,
+                    int totalObjectCount)
             {
-                return new UpdateRelationProgress(update, entity, "sample-tag", objectIndex, totalObjectCount);
+                return new UpdateRelationProgress(update, entity, "sample-tag", objectIndex,
+                        totalObjectCount);
             }
         };
     }
 
-    private void updateAttachments(final IOperationContext context, final MapBatch<SampleUpdate, SamplePE> batch)
+    private void updateAttachments(final IOperationContext context,
+            final MapBatch<SampleUpdate, SamplePE> batch)
     {
         new MapBatchProcessor<SampleUpdate, SamplePE>(context, batch)
         {
@@ -299,9 +252,11 @@ public class UpdateSampleExecutor extends AbstractUpdateEntityExecutor<SampleUpd
             }
 
             @Override
-            public IProgress createProgress(SampleUpdate update, SamplePE entity, int objectIndex, int totalObjectCount)
+            public IProgress createProgress(SampleUpdate update, SamplePE entity, int objectIndex,
+                    int totalObjectCount)
             {
-                return new UpdateRelationProgress(update, entity, "sample-attachment", objectIndex, totalObjectCount);
+                return new UpdateRelationProgress(update, entity, "sample-attachment", objectIndex,
+                        totalObjectCount);
             }
         };
     }
@@ -327,13 +282,15 @@ public class UpdateSampleExecutor extends AbstractUpdateEntityExecutor<SampleUpd
     @Override
     protected void save(IOperationContext context, List<SamplePE> entities, boolean clearCache)
     {
-        daoFactory.getSampleDAO().createOrUpdateSamples(entities, context.getSession().tryGetPerson(), clearCache);
+        daoFactory.getSampleDAO()
+                .createOrUpdateSamples(entities, context.getSession().tryGetPerson(), clearCache);
     }
 
     @Override
     protected void handleException(DataAccessException e)
     {
-        DataAccessExceptionTranslator.throwException(e, EntityKind.SAMPLE.getLabel(), EntityKind.SAMPLE);
+        DataAccessExceptionTranslator.throwException(e, EntityKind.SAMPLE.getLabel(),
+                EntityKind.SAMPLE);
     }
 
 }
diff --git a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/sample/UpdateSampleTypeExecutor.java b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/sample/UpdateSampleTypeExecutor.java
index 3a9e1ca6afe7ba44819517ec380658bd4c7a614f..ac987d6e97bb0c12b3d1a7f55cfed259338b9c23 100644
--- a/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/sample/UpdateSampleTypeExecutor.java
+++ b/server-application-server/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/sample/UpdateSampleTypeExecutor.java
@@ -15,7 +15,7 @@
  */
 package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.sample;
 
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.update.ListUpdateValue;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.metadata.IUpdateMetaDataForEntityExecutor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -27,14 +27,7 @@ import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.entity.IUpdateEntity
 import ch.systemsx.cisd.openbis.generic.shared.dto.SampleTypePE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.properties.EntityKind;
 
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
-
 /**
- * 
- *
  * @author Franz-Josef Elmer
  */
 @Component
@@ -44,10 +37,13 @@ public class UpdateSampleTypeExecutor
 {
     @Autowired
     private ISampleTypeAuthorizationExecutor authorizationExecutor;
-    
+
     @Autowired
     private IUpdateSampleTypePropertyTypesExecutor updateSampleTypePropertyTypesExecutor;
 
+    @Autowired
+    private IUpdateMetaDataForEntityExecutor<SampleTypeUpdate, SampleTypePE> updateMetaDataExecutor;
+
     @Override
     protected EntityKind getDAOEntityKind()
     {
@@ -68,20 +64,25 @@ public class UpdateSampleTypeExecutor
     @Override
     protected void updateSpecific(SampleTypePE type, SampleTypeUpdate update)
     {
-        type.setGeneratedCodePrefix(getNewValue(update.getGeneratedCodePrefix(), type.getGeneratedCodePrefix()));
-        type.setAutoGeneratedCode(getNewValue(update.isAutoGeneratedCode(), type.isAutoGeneratedCode()));
+        type.setGeneratedCodePrefix(
+                getNewValue(update.getGeneratedCodePrefix(), type.getGeneratedCodePrefix()));
+        type.setAutoGeneratedCode(
+                getNewValue(update.isAutoGeneratedCode(), type.isAutoGeneratedCode()));
         type.setListable(getNewValue(update.isListable(), type.isListable()));
         type.setSubcodeUnique(getNewValue(update.isSubcodeUnique(), type.isSubcodeUnique()));
-        type.setShowParentMetadata(getNewValue(update.isShowParentMetadata(), type.isShowParentMetadata()));
+        type.setShowParentMetadata(
+                getNewValue(update.isShowParentMetadata(), type.isShowParentMetadata()));
         if (update.isShowContainer() != null && update.isShowContainer().isModified())
         {
-            type.setContainerHierarchyDepth(Boolean.TRUE.equals(update.isShowContainer().getValue()) ? 1 : 0);
+            type.setContainerHierarchyDepth(
+                    Boolean.TRUE.equals(update.isShowContainer().getValue()) ? 1 : 0);
         }
         if (update.isShowParents() != null && update.isShowParents().isModified())
         {
-            type.setGeneratedFromHierarchyDepth(Boolean.TRUE.equals(update.isShowParents().getValue()) ? 1 : 0);
+            type.setGeneratedFromHierarchyDepth(
+                    Boolean.TRUE.equals(update.isShowParents().getValue()) ? 1 : 0);
         }
-        updateMetaData(type, update);
+        updateMetaDataExecutor.updateSpecific(update, type);
     }
 
     @Override
@@ -90,53 +91,4 @@ public class UpdateSampleTypeExecutor
         return updateSampleTypePropertyTypesExecutor;
     }
 
-
-    private void updateMetaData(SampleTypePE type, SampleTypeUpdate update) {
-        Map<String, String> metaData = new HashMap<>();
-        if(type.getMetaData() != null) {
-            metaData.putAll(type.getMetaData());
-        }
-        ListUpdateValue.ListUpdateActionSet<?> lastSetAction = null;
-        AtomicBoolean metaDataChanged = new AtomicBoolean(false);
-        for (ListUpdateValue.ListUpdateAction<Object> action : update.getMetaData().getActions())
-        {
-            if (action instanceof ListUpdateValue.ListUpdateActionAdd<?>)
-            {
-                addTo(metaData, action, metaDataChanged);
-            } else if (action instanceof ListUpdateValue.ListUpdateActionRemove<?>)
-            {
-                for (String key : (Collection<String>) action.getItems())
-                {
-                    metaDataChanged.set(true);
-                    metaData.remove(key);
-                }
-            } else if (action instanceof ListUpdateValue.ListUpdateActionSet<?>)
-            {
-                lastSetAction = (ListUpdateValue.ListUpdateActionSet<?>) action;
-            }
-        }
-        if (lastSetAction != null)
-        {
-            metaData.clear();
-            addTo(metaData, lastSetAction, metaDataChanged);
-        }
-        if (metaDataChanged.get())
-        {
-            type.setMetaData(metaData.isEmpty() ? null : metaData);
-        }
-    }
-
-    private void addTo(Map<String, String> metaData, ListUpdateValue.ListUpdateAction<?> lastSetAction, AtomicBoolean metaDataChanged)
-    {
-        Collection<Map<String, String>> maps = (Collection<Map<String, String>>) lastSetAction.getItems();
-        for (Map<String, String> map : maps)
-        {
-            if (!map.isEmpty())
-            {
-                metaDataChanged.set(true);
-                metaData.putAll(map);
-            }
-        }
-    }
-
 }
diff --git a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/DataPE.java b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/DataPE.java
index 5ad52f1501e8c6060b3c8f73a3934a598829ed21..33d7fa71720a848731003c4e9fa280beb400fb59 100644
--- a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/DataPE.java
+++ b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/DataPE.java
@@ -63,7 +63,8 @@ import ch.systemsx.cisd.openbis.generic.shared.util.HibernateUtils;
 @Inheritance(strategy = InheritanceType.JOINED)
 public class DataPE extends AbstractIdAndCodeHolder<DataPE> implements
         IEntityInformationWithPropertiesHolder, IMatchingEntity, IIdentifierHolder, IDeletablePE,
-        IEntityWithMetaprojects, IModifierAndModificationDateBean, IIdentityHolder
+        IEntityWithMetaprojects, IModifierAndModificationDateBean, IIdentityHolder,
+        IEntityWithMetaData
 {
 
     private static final long serialVersionUID = IServer.VERSION;
@@ -1039,6 +1040,7 @@ public class DataPE extends AbstractIdAndCodeHolder<DataPE> implements
         this.postRegistration = postRegistration;
     }
 
+    @Override
     @Column(name = "meta_data")
     @Type(type = "JsonMap")
     public Map<String, String> getMetaData()
@@ -1046,6 +1048,7 @@ public class DataPE extends AbstractIdAndCodeHolder<DataPE> implements
         return metaData;
     }
 
+    @Override
     public void setMetaData(Map<String, String> metaData)
     {
         this.metaData = metaData;
diff --git a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/DataSetTypePE.java b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/DataSetTypePE.java
index 80ccb8de14b0efcc49884c22ec82c1598d937ca7..b52af250d7a3374605f501e300af8ad89967b2fc 100644
--- a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/DataSetTypePE.java
+++ b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/DataSetTypePE.java
@@ -45,13 +45,14 @@ import ch.systemsx.cisd.openbis.generic.shared.dto.properties.EntityKind;
 
 /**
  * Persistence Entity representing data set type.
- * 
+ *
  * @author Izabela Adamczyk
  */
 @Entity
-@Table(name = TableNames.DATA_SET_TYPES_TABLE, uniqueConstraints = { @UniqueConstraint(columnNames = { ColumnNames.CODE_COLUMN }) })
+@Table(name = TableNames.DATA_SET_TYPES_TABLE, uniqueConstraints = {
+        @UniqueConstraint(columnNames = { ColumnNames.CODE_COLUMN }) })
 @TypeDefs({ @TypeDef(name = "JsonMap", typeClass = JsonMapUserType.class) })
-public class DataSetTypePE extends EntityTypePE
+public class DataSetTypePE extends EntityTypePE implements IEntityWithMetaData
 {
     private static final long serialVersionUID = IServer.VERSION;
 
@@ -155,7 +156,8 @@ public class DataSetTypePE extends EntityTypePE
     }
 
     /**
-     * Returns <code>true</code> if data sets of this type require special user rights and an additional confirmation to be deleted.
+     * Returns <code>true</code> if data sets of this type require special user rights and an
+     * additional confirmation to be deleted.
      */
     @Column(name = ColumnNames.DELETION_DISALLOW)
     public boolean isDeletionDisallow()
@@ -164,13 +166,15 @@ public class DataSetTypePE extends EntityTypePE
     }
 
     /**
-     * Set to <code>true</code> if data sets of this type require special user rights and an additional confirmation to be deleted.
+     * Set to <code>true</code> if data sets of this type require special user rights and an
+     * additional confirmation to be deleted.
      */
     public void setDeletionDisallow(boolean deletionDisallow)
     {
         this.deletionDisallow = deletionDisallow;
     }
 
+    @Override
     @Column(name = "meta_data")
     @Type(type = "JsonMap")
     public Map<String, String> getMetaData()
@@ -178,10 +182,10 @@ public class DataSetTypePE extends EntityTypePE
         return metaData;
     }
 
+    @Override
     public void setMetaData(Map<String, String> metaData)
     {
         this.metaData = metaData;
     }
 
-
 }
diff --git a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ExperimentPE.java b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ExperimentPE.java
index e64c51d641d89e049f6cf7ca69b207b520984f55..e3b98e5452b0a233cb3624779936319d2b7bbd3c 100644
--- a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ExperimentPE.java
+++ b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ExperimentPE.java
@@ -62,7 +62,7 @@ import ch.systemsx.cisd.openbis.generic.shared.util.HibernateUtils;
 
 /**
  * Persistence Entity representing experiment.
- * 
+ *
  * @author Izabela Adamczyk
  */
 @Entity
@@ -72,8 +72,9 @@ import ch.systemsx.cisd.openbis.generic.shared.util.HibernateUtils;
 @TypeDefs({ @TypeDef(name = "JsonMap", typeClass = JsonMapUserType.class) })
 public class ExperimentPE extends AttachmentHolderPE implements
         IEntityInformationWithPropertiesHolder, IIdAndCodeHolder, Comparable<ExperimentPE>,
-        IModifierAndModificationDateBean, IMatchingEntity, IDeletablePE, IEntityWithMetaprojects, IIdentityHolder,
-        Serializable
+        IModifierAndModificationDateBean, IMatchingEntity, IDeletablePE, IEntityWithMetaprojects,
+        IIdentityHolder,
+        Serializable, IEntityWithMetaData
 {
     private static final long serialVersionUID = IServer.VERSION;
 
@@ -137,8 +138,8 @@ public class ExperimentPE extends AttachmentHolderPE implements
     private int version;
 
     /**
-     * If not null than this object has been originally trashed. (As oposed to the entities which were trashed as being dependent on other trashed
-     * entity)
+     * If not null than this object has been originally trashed. (As oposed to the entities which
+     * were trashed as being dependent on other trashed entity)
      */
     private Integer originalDeletion;
 
@@ -647,11 +648,14 @@ public class ExperimentPE extends AttachmentHolderPE implements
      * Non hibernate related methods to keep API compatibility inside the business logic
      */
 
-    private static List<SamplePE> getExperimentSamples(long experimentTechId) {
+    private static List<SamplePE> getExperimentSamples(long experimentTechId)
+    {
         ISampleDAO sampleDAO = CommonServiceProvider.getDAOFactory().getSampleDAO();
-        List<TechId> techIds = sampleDAO.listSampleIdsByExperimentIds(TechId.createList(experimentTechId));
+        List<TechId> techIds =
+                sampleDAO.listSampleIdsByExperimentIds(TechId.createList(experimentTechId));
         List<Long> techIdsAsLongs = new ArrayList<>();
-        for (TechId id : techIds) {
+        for (TechId id : techIds)
+        {
             Long idId = id.getId();
             techIdsAsLongs.add(idId);
         }
@@ -668,7 +672,8 @@ public class ExperimentPE extends AttachmentHolderPE implements
     public void setSamples(List<SamplePE> samples)
     {
         List<SamplePE> currentSamples = getExperimentSamples(id);
-        for (SamplePE samplePE:currentSamples) {
+        for (SamplePE samplePE : currentSamples)
+        {
             samplePE.setExperimentInternal(null);
         }
         for (SamplePE sample : samples)
@@ -703,6 +708,7 @@ public class ExperimentPE extends AttachmentHolderPE implements
         setSamples(samples);
     }
 
+    @Override
     @Column(name = "meta_data")
     @Type(type = "JsonMap")
     public Map<String, String> getMetaData()
@@ -710,6 +716,7 @@ public class ExperimentPE extends AttachmentHolderPE implements
         return metaData;
     }
 
+    @Override
     public void setMetaData(Map<String, String> metaData)
     {
         this.metaData = metaData;
diff --git a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ExperimentTypePE.java b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ExperimentTypePE.java
index 3fd5eac42632c3c7624c5c2d02612d4a8f467dbf..06efdfd2a7ae9438b8542b6d59194b235d8258f6 100644
--- a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ExperimentTypePE.java
+++ b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/ExperimentTypePE.java
@@ -39,7 +39,7 @@ import org.hibernate.annotations.TypeDefs;
 @Entity
 @Table(name = TableNames.EXPERIMENT_TYPES_TABLE, uniqueConstraints = { @UniqueConstraint(columnNames = { ColumnNames.CODE_COLUMN }) })
 @TypeDefs({ @TypeDef(name = "JsonMap", typeClass = JsonMapUserType.class) })
-public final class ExperimentTypePE extends EntityTypePE
+public final class ExperimentTypePE extends EntityTypePE implements IEntityWithMetaData
 {
     private static final long serialVersionUID = IServer.VERSION;
 
@@ -112,6 +112,7 @@ public final class ExperimentTypePE extends EntityTypePE
         return getExperimentTypePropertyTypes();
     }
 
+    @Override
     @Column(name = "meta_data")
     @Type(type = "JsonMap")
     public Map<String, String> getMetaData()
@@ -119,6 +120,7 @@ public final class ExperimentTypePE extends EntityTypePE
         return metaData;
     }
 
+    @Override
     public void setMetaData(Map<String, String> metaData)
     {
         this.metaData = metaData;
diff --git a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/IEntityWithMetaData.java b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/IEntityWithMetaData.java
new file mode 100644
index 0000000000000000000000000000000000000000..25759d8e19ad70bfd786300114a5d645cd86a333
--- /dev/null
+++ b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/IEntityWithMetaData.java
@@ -0,0 +1,29 @@
+/*
+ *  Copyright ETH 2023 Zürich, Scientific IT Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package ch.systemsx.cisd.openbis.generic.shared.dto;
+
+import ch.systemsx.cisd.openbis.generic.shared.basic.IIdentityHolder;
+
+import java.util.Map;
+
+public interface IEntityWithMetaData extends IIdentityHolder
+{
+    Map<String, String> getMetaData();
+
+    void setMetaData(Map<String, String> metaData);
+}
diff --git a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SamplePE.java b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SamplePE.java
index fa3c9d44b116e4dfc1050ca59466e6990348bff6..3a1449798799f2c911cb376da9e247ff73a224a9 100644
--- a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SamplePE.java
+++ b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SamplePE.java
@@ -77,7 +77,7 @@ import ch.systemsx.cisd.openbis.generic.shared.util.HibernateUtils;
 @Friend(toClasses = ProjectPE.class)
 @TypeDefs({ @TypeDef(name = "JsonMap", typeClass = JsonMapUserType.class) })
 public class SamplePE extends AttachmentHolderPE implements IIdAndCodeHolder, Comparable<SamplePE>,
-        IEntityInformationWithPropertiesHolder, IMatchingEntity, IDeletablePE,
+        IEntityInformationWithPropertiesHolder, IMatchingEntity, IDeletablePE, IEntityWithMetaData,
         IEntityWithMetaprojects, IModifierAndModificationDateBean, IIdentityHolder, Serializable
 {
     private static final long serialVersionUID = IServer.VERSION;
@@ -1104,11 +1104,13 @@ public class SamplePE extends AttachmentHolderPE implements IIdAndCodeHolder, Co
 
     @Column(name = "meta_data")
     @Type(type = "JsonMap")
+    @Override
     public Map<String, String> getMetaData()
     {
         return metaData;
     }
 
+    @Override
     public void setMetaData(Map<String, String> metaData)
     {
         this.metaData = metaData;
diff --git a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SampleTypePE.java b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SampleTypePE.java
index 3a7cf1dc127c4c80bb573c1236d70f9b4cd3737f..454c38b438a4bcbb58775ab3121b87c641c6b3bf 100644
--- a/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SampleTypePE.java
+++ b/server-application-server/source/java/ch/systemsx/cisd/openbis/generic/shared/dto/SampleTypePE.java
@@ -47,14 +47,15 @@ import ch.systemsx.cisd.openbis.generic.shared.dto.properties.EntityKind;
 
 /**
  * Persistence Entity representing 'sample type'.
- * 
+ *
  * @author Christian Ribeaud
  * @author Izabela Adamczyk
  */
 @Entity
-@Table(name = TableNames.SAMPLE_TYPES_TABLE, uniqueConstraints = { @UniqueConstraint(columnNames = { ColumnNames.CODE_COLUMN }) })
+@Table(name = TableNames.SAMPLE_TYPES_TABLE, uniqueConstraints = {
+        @UniqueConstraint(columnNames = { ColumnNames.CODE_COLUMN }) })
 @TypeDefs({ @TypeDef(name = "JsonMap", typeClass = JsonMapUserType.class) })
-public final class SampleTypePE extends EntityTypePE
+public final class SampleTypePE extends EntityTypePE implements IEntityWithMetaData
 {
     private static final long serialVersionUID = IServer.VERSION;
 
@@ -239,6 +240,7 @@ public final class SampleTypePE extends EntityTypePE
         return getSampleTypePropertyTypes();
     }
 
+    @Override
     @Column(name = "meta_data")
     @Type(type = "JsonMap")
     public Map<String, String> getMetaData()
@@ -246,6 +248,7 @@ public final class SampleTypePE extends EntityTypePE
         return metaData;
     }
 
+    @Override
     public void setMetaData(Map<String, String> metaData)
     {
         this.metaData = metaData;