diff --git a/src/Entity/LocalDataAwareInterface.php b/src/Entity/LocalDataAwareInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..9aa8daf27965142ed64409445a137e5779f48a32
--- /dev/null
+++ b/src/Entity/LocalDataAwareInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Dbp\Relay\CoreBundle\Entity;
+
+interface LocalDataAwareInterface
+{
+    /**
+     * Returns the unique name (shortName of the ApiResource) of this entity.
+     */
+    public static function getUniqueEntityName(): string;
+
+    /**
+     * Sets the value of a local data attribute.
+     *
+     * @param mixed|null $value
+     */
+    public function setLocalDataValue(string $key, $value): void;
+
+    /**
+     * Returns the value of local data value attribute or null if the attribute is not found.
+     *
+     * @return ?mixed
+     */
+    public function getLocalDataValue(string $key);
+}
diff --git a/src/Entity/LocalDataAwareTrait.php b/src/Entity/LocalDataAwareTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..e2cd56b43635232da4741e6cef04a655a2e6a2e4
--- /dev/null
+++ b/src/Entity/LocalDataAwareTrait.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Dbp\Relay\CoreBundle\Entity;
+
+use ApiPlatform\Core\Annotation\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Serializer\Annotation\Ignore;
+
+trait LocalDataAwareTrait
+{
+    /**
+     * @ApiProperty(iri="https://schema.org/additionalProperty")
+     * @Groups({"LocalData:output"})
+     *
+     * @var array
+     */
+    private $localData;
+
+    public function getLocalData(): array
+    {
+        return $this->localData;
+    }
+
+    /**
+     * Adds a local data entry.
+     *
+     * @param mixed|null $value
+     */
+    public function setLocalDataValue(string $key, $value): void
+    {
+        if (!$this->localData) {
+            $this->localData = [];
+        }
+        $this->localData[$key] = $value;
+    }
+
+    /**
+     * @Ignore
+     * Returns the local data value for the given key or null if the key is not found.
+     *
+     * @return ?mixed
+     */
+    public function getLocalDataValue(string $key)
+    {
+        return $this->localData ? ($this->localData[$key] ?? null) : null;
+    }
+}
diff --git a/src/Event/LocalDataAwarePostEvent.php b/src/Event/LocalDataAwarePostEvent.php
new file mode 100644
index 0000000000000000000000000000000000000000..5ba46740f6e17d53a5a385137330de8c53ff03b4
--- /dev/null
+++ b/src/Event/LocalDataAwarePostEvent.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Dbp\Relay\CoreBundle\Event;
+
+use Dbp\Relay\CoreBundle\Entity\LocalDataAwareInterface;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+use Symfony\Contracts\EventDispatcher\Event;
+
+class LocalDataAwarePostEvent extends Event
+{
+    /** @var LocalDataAwareInterface */
+    private $entity;
+
+    /** @var array */
+    private $requestedAttributes;
+
+    protected function __construct(LocalDataAwareInterface $entity)
+    {
+        $this->entity = $entity;
+    }
+
+    public function setRequestedAttributes(array $requestedAttributes)
+    {
+        $this->requestedAttributes = $requestedAttributes;
+    }
+
+    public function getRemainingRequestedAttributes(): array
+    {
+        return $this->requestedAttributes;
+    }
+
+    public function getEntity(): LocalDataAwareInterface
+    {
+        return $this->entity;
+    }
+
+    public function setLocalDataAttribute(string $key, $value)
+    {
+        $arrayKey = array_search($key, $this->requestedAttributes, true);
+        if ($arrayKey === false) {
+            throw new HttpException(500, sprintf("local data attribute '%s' not requested for entity '%s'", $key, $this->entity->getUniqueEntityName()));
+        }
+
+        // once set, remove the attribute from the list of requested attributes
+        array_splice($this->requestedAttributes, $arrayKey, 1);
+        $this->entity->setLocalDataValue($key, $value);
+    }
+
+    public function isLocalDataAttributeRequested(string $key): bool
+    {
+        return in_array($key, $this->requestedAttributes, true);
+    }
+}
diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml
index 7162883223a82552f9f6451ad16aa2210724c9d2..7b77597f427ac324128cea97a038b2e6be2f5054 100644
--- a/src/Resources/config/services.yaml
+++ b/src/Resources/config/services.yaml
@@ -56,4 +56,8 @@ services:
 
   Dbp\Relay\CoreBundle\Auth\ProxyAuthenticator:
     autowire: true
-    autoconfigure: true
\ No newline at end of file
+    autoconfigure: true
+
+  Dbp\Relay\CoreBundle\Service\LocalDataAwareEventDispatcher:
+    autowire: true
+    autoconfigure: true
diff --git a/src/Service/LocalDataAwareEventDispatcher.php b/src/Service/LocalDataAwareEventDispatcher.php
new file mode 100644
index 0000000000000000000000000000000000000000..2d08fe2ed553b2217a2cd80d08de7ddc6c9644f2
--- /dev/null
+++ b/src/Service/LocalDataAwareEventDispatcher.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Dbp\Relay\CoreBundle\Service;
+
+use Dbp\Relay\CoreBundle\Event\LocalDataAwarePostEvent;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+class LocalDataAwareEventDispatcher
+{
+    private $requestedAttributes;
+
+    /** @var string */
+    private $unqiueEntityName;
+
+    /** @var EventDispatcherInterface */
+    private $eventDispatcher;
+
+    /** @var string */
+    private $eventName;
+
+    public function __construct(string $unqiueEntityName, EventDispatcherInterface $eventDispatcher, string $eventName)
+    {
+        $this->unqiueEntityName = $unqiueEntityName;
+        $this->eventDispatcher = $eventDispatcher;
+        $this->eventName = $eventName;
+    }
+
+    public function initRequestedLocalDataAttributes(array $options)
+    {
+        $this->requestedAttributes = [];
+        if ($include = $options['include'] ?? null) {
+            $requestedLocalDataAttributes = explode(',', $include);
+
+            foreach ($requestedLocalDataAttributes as $requestedLocalDataAttribute) {
+                $requestedLocalDataAttribute = trim($requestedLocalDataAttribute);
+                if (!empty($requestedLocalDataAttribute)) {
+                    $requestedUniqueEntityName = null;
+                    $requestedAttributeName = null;
+                    if (!self::parseLocalDataAttribute($requestedLocalDataAttribute, $requestedUniqueEntityName, $requestedAttributeName)) {
+                        throw new HttpException(400, sprintf("value of 'include' parameter has invalid format: '%s' (Example: 'ResourceName.attr,ResourceName.attr2')", $requestedLocalDataAttribute));
+                    }
+
+                    if ($this->unqiueEntityName === $requestedUniqueEntityName) {
+                        $this->requestedAttributes[] = $requestedAttributeName;
+                    }
+                }
+            }
+            $this->requestedAttributes = array_unique($this->requestedAttributes);
+        }
+    }
+
+    public function dispatch(LocalDataAwarePostEvent $event)
+    {
+        $event->setRequestedAttributes($this->requestedAttributes);
+
+        $this->eventDispatcher->dispatch($event, $this->eventName);
+
+        $remainingLocalDataAttributes = $event->getRemainingRequestedAttributes();
+        if (!empty($remainingLocalDataAttributes)) {
+            throw new HttpException(500, sprintf("the following local data attributes were not provided for resource '%s': %s", $this->unqiueEntityName, implode(', ', $remainingLocalDataAttributes)));
+        }
+    }
+
+    private static function parseLocalDataAttribute(string $localDataAttribute, ?string &$entityUniqueName, ?string &$attributeName): bool
+    {
+        $parts = explode('.', $localDataAttribute);
+        if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) {
+            return false;
+        }
+        $entityUniqueName = $parts[0];
+        $attributeName = $parts[1];
+
+        return true;
+    }
+}