diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php
index a25f72f..e35a533 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php
@@ -278,6 +278,7 @@ protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
    */
   public function create(array $values) {
     $class = $this->entityInfo['class'];
+    $class::preCreate($this, $values);
 
     // Set default language to site default if not provided.
     $values += array('langcode' => language_default()->langcode);
@@ -292,6 +293,7 @@ public function create(array $values) {
       $uuid = new Uuid();
       $entity->{$this->uuidKey} = $uuid->generate();
     }
+    $entity->postCreate($this);
 
     // Modules might need to add or change the data initially held by the new
     // entity object, for instance to fill-in default values.
@@ -314,7 +316,8 @@ public function delete(array $entities) {
       return;
     }
 
-    $this->preDelete($entities);
+    $entity_class = $this->entityInfo['class'];
+    $entity_class::preDelete($this, $entities);
     foreach ($entities as $id => $entity) {
       $this->invokeHook('predelete', $entity);
     }
@@ -324,7 +327,7 @@ public function delete(array $entities) {
       $config->delete();
     }
 
-    $this->postDelete($entities);
+    $entity_class::postDelete($this, $entities);
     foreach ($entities as $id => $entity) {
       $this->invokeHook('delete', $entity);
     }
@@ -367,7 +370,7 @@ public function save(EntityInterface $entity) {
       $this->configFactory->rename($prefix . $id, $prefix . $entity->id());
     }
 
-    $this->preSave($entity);
+    $entity->preSave($this);
     $this->invokeHook('presave', $entity);
 
     // Retrieve the desired properties and set them in config.
@@ -378,7 +381,7 @@ public function save(EntityInterface $entity) {
     if (!$is_new) {
       $return = SAVED_UPDATED;
       $config->save();
-      $this->postSave($entity, TRUE);
+      $entity->postSave($this, TRUE);
       $this->invokeHook('update', $entity);
 
       // Immediately update the original ID.
@@ -388,7 +391,7 @@ public function save(EntityInterface $entity) {
       $return = SAVED_NEW;
       $config->save();
       $entity->enforceIsNew(FALSE);
-      $this->postSave($entity, FALSE);
+      $entity->postSave($this, FALSE);
       $this->invokeHook('insert', $entity);
     }
 
@@ -398,45 +401,6 @@ public function save(EntityInterface $entity) {
   }
 
   /**
-   * Acts on an entity before the presave hook is invoked.
-   *
-   * Used before the entity is saved and before invoking the presave hook.
-   */
-  protected function preSave(EntityInterface $entity) {
-  }
-
-  /**
-   * Acts on a saved entity before the insert or update hook is invoked.
-   *
-   * Used after the entity is saved, but before invoking the insert or update
-   * hook.
-   *
-   * @param EntityInterface $entity
-   *   The entity to act on.
-   * @param $update
-   *   (bool) TRUE if the entity has been updated, or FALSE if it has been
-   *   inserted.
-   */
-  protected function postSave(EntityInterface $entity, $update) {
-  }
-
-  /**
-   * Acts on entities before they are deleted.
-   *
-   * Used before the entities are deleted and before invoking the delete hook.
-   */
-  protected function preDelete($entities) {
-  }
-
-  /**
-   * Acts on deleted entities before the delete hook is invoked.
-   *
-   * Used after the entities are deleted but before invoking the delete hook.
-   */
-  protected function postDelete($entities) {
-  }
-
-  /**
    * Implements Drupal\Core\Entity\EntityStorageControllerInterface::getFieldDefinitions().
    */
   public function getFieldDefinitions(array $constraints) {
diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php
index fe746f4..a3cd9df 100644
--- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php
+++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php
@@ -370,15 +370,17 @@ protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
    * Implements \Drupal\Core\Entity\EntityStorageControllerInterface::create().
    */
   public function create(array $values) {
-    $class = $this->entityInfo['class'];
+    $entity_class = $this->entityInfo['class'];
+    $entity_class::preCreate($this, $values);
 
-    $entity = new $class($values, $this->entityType);
+    $entity = new $entity_class($values, $this->entityType);
 
     // Assign a new UUID if there is none yet.
     if ($this->uuidKey && !isset($entity->{$this->uuidKey})) {
       $uuid = new Uuid();
       $entity->{$this->uuidKey} = $uuid->generate();
     }
+    $entity->postCreate($this);
 
     // Modules might need to add or change the data initially held by the new
     // entity object, for instance to fill-in default values.
@@ -398,7 +400,8 @@ public function delete(array $entities) {
     $transaction = $this->database->startTransaction();
 
     try {
-      $this->preDelete($entities);
+      $entity_class = $this->entityInfo['class'];
+      $entity_class::preDelete($this, $entities);
       foreach ($entities as $id => $entity) {
         $this->invokeHook('predelete', $entity);
       }
@@ -417,7 +420,7 @@ public function delete(array $entities) {
       // Reset the cache as soon as the changes have been applied.
       $this->resetCache($ids);
 
-      $this->postDelete($entities);
+      $entity_class::postDelete($this, $entities);
       foreach ($entities as $id => $entity) {
         $this->invokeHook('delete', $entity);
       }
@@ -442,7 +445,7 @@ public function save(EntityInterface $entity) {
         $entity->original = entity_load_unchanged($this->entityType, $entity->id());
       }
 
-      $this->preSave($entity);
+      $entity->preSave($this);
       $this->invokeHook('presave', $entity);
 
       if (!$entity->isNew()) {
@@ -458,7 +461,7 @@ public function save(EntityInterface $entity) {
           $this->saveRevision($entity);
         }
         $this->resetCache(array($entity->id()));
-        $this->postSave($entity, TRUE);
+        $entity->postSave($this, TRUE);
         $this->invokeHook('update', $entity);
       }
       else {
@@ -470,7 +473,7 @@ public function save(EntityInterface $entity) {
         $this->resetCache(array());
 
         $entity->enforceIsNew(FALSE);
-        $this->postSave($entity, FALSE);
+        $entity->postSave($this, FALSE);
         $this->invokeHook('insert', $entity);
       }
 
@@ -507,7 +510,7 @@ protected function saveRevision(EntityInterface $entity) {
     // Cast to object as preSaveRevision() expects one to be compatible with the
     // upcoming NG storage controller.
     $record = (object) $record;
-    $this->preSaveRevision($record, $entity);
+    $entity->preSaveRevision($this, $record);
     $record = (array) $record;
 
     if ($entity->isNewRevision()) {
@@ -528,49 +531,6 @@ protected function saveRevision(EntityInterface $entity) {
   }
 
   /**
-   * Acts on an entity before the presave hook is invoked.
-   *
-   * Used before the entity is saved and before invoking the presave hook.
-   */
-  protected function preSave(EntityInterface $entity) { }
-
-  /**
-   * Acts on a saved entity before the insert or update hook is invoked.
-   *
-   * Used after the entity is saved, but before invoking the insert or update
-   * hook.
-   *
-   * @param $update
-   *   (bool) TRUE if the entity has been updated, or FALSE if it has been
-   *   inserted.
-   */
-  protected function postSave(EntityInterface $entity, $update) { }
-
-  /**
-   * Acts on entities before they are deleted.
-   *
-   * Used before the entities are deleted and before invoking the delete hook.
-   */
-  protected function preDelete($entities) { }
-
-  /**
-   * Acts on deleted entities before the delete hook is invoked.
-   *
-   * Used after the entities are deleted but before invoking the delete hook.
-   */
-  protected function postDelete($entities) { }
-
-  /**
-   * Act on a revision before being saved.
-   *
-   * @param \stdClass $record
-   *   The revision object.
-   * @param Drupal\Core\Entity\EntityInterface $entity
-   *   The entity object.
-   */
-  protected function preSaveRevision(\stdClass $record, EntityInterface $entity) { }
-
-  /**
    * Invokes a hook on behalf of the entity.
    *
    * @param $hook
diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php
index 91ec62d..c3a76e2 100644
--- a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php
+++ b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php
@@ -98,6 +98,9 @@ public function __construct($entity_type, array $entity_info, Connection $databa
    *   A new entity object.
    */
   public function create(array $values) {
+    $entity_class = $this->entityClass;
+    $entity_class::preCreate($this, $values);
+
     // We have to determine the bundle first.
     $bundle = FALSE;
     if ($this->bundleKey) {
@@ -118,6 +121,7 @@ public function create(array $values) {
       $uuid = new Uuid();
       $entity->{$this->uuidKey} = $uuid->generate();
     }
+    $entity->postCreate($this);
 
     // Modules might need to add or change the data initially held by the new
     // entity object, for instance to fill-in default values.
@@ -354,7 +358,7 @@ public function save(EntityInterface $entity) {
         $entity->original = entity_load_unchanged($this->entityType, $entity->id());
       }
 
-      $this->preSave($entity);
+      $entity->preSave($this);
       $this->invokeHook('presave', $entity);
 
       // Create the storage record to be saved.
@@ -376,7 +380,7 @@ public function save(EntityInterface $entity) {
           $this->savePropertyData($entity);
         }
         $this->resetCache(array($entity->id()));
-        $this->postSave($entity, TRUE);
+        $entity->postSave($this, TRUE);
         $this->invokeHook('update', $entity);
       }
       else {
@@ -394,7 +398,7 @@ public function save(EntityInterface $entity) {
         $this->resetCache(array());
 
         $entity->enforceIsNew(FALSE);
-        $this->postSave($entity, FALSE);
+        $entity->postSave($this, FALSE);
         $this->invokeHook('insert', $entity);
       }
 
@@ -445,7 +449,7 @@ protected function saveRevision(EntityInterface $entity) {
         $record->{$this->revisionKey} = NULL;
       }
 
-      $this->preSaveRevision($record, $entity);
+      $entity->preSaveRevision($this, $record);
 
       if ($entity->isNewRevision()) {
         drupal_write_record($this->revisionTable, $record);
@@ -596,12 +600,14 @@ public function delete(array $entities) {
 
     $transaction = $this->database->startTransaction();
     try {
+      $entity_class = $this->entityClass;
+      $entity_class::preDelete($this, $entities);
+
       // Ensure we are dealing with the actual entities.
       foreach ($entities as $id => $entity) {
         $entities[$id] = $entity->getNGEntity();
       }
 
-      $this->preDelete($entities);
       foreach ($entities as $id => $entity) {
         $this->invokeHook('predelete', $entity);
       }
@@ -626,7 +632,7 @@ public function delete(array $entities) {
       // Reset the cache as soon as the changes have been applied.
       $this->resetCache($ids);
 
-      $this->postDelete($entities);
+      $entity_class::postDelete($this, $entities);
       foreach ($entities as $id => $entity) {
         $this->invokeHook('delete', $entity);
       }
diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 9e95a0b..f8f8eb1 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -531,4 +531,52 @@ public function isTranslatable() {
     return !empty($bundles[$this->bundle()]['translatable']);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageControllerInterface $storage_controller) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postCreate(EntityStorageControllerInterface $storage_controller) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postLoad(EntityStorageControllerInterface $storage_controller, array $entities) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php
index 475f7b3..93bafb4 100644
--- a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php
+++ b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php
@@ -533,4 +533,53 @@ public function onChange($property_name) {
   public function isTranslatable() {
     return $this->decorated->isTranslatable();
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageControllerInterface $storage_controller) {
+    $this->decorated->preSave($storage_controller);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
+    $this->decorated->preSave($storage_controller, $record);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    $this->decorated->postSave($storage_controller, $update);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
+  }
+
+  public function postCreate(EntityStorageControllerInterface $storage_controller) {
+    $this->decorated->postCreate($storage_controller);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postLoad(EntityStorageControllerInterface $storage_controller, array $entities) {
+  }
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityInterface.php b/core/lib/Drupal/Core/Entity/EntityInterface.php
index 692c329..b8fe82b 100644
--- a/core/lib/Drupal/Core/Entity/EntityInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityInterface.php
@@ -163,6 +163,94 @@ public function save();
   public function delete();
 
   /**
+   * Acts on an entity before the presave hook is invoked.
+   *
+   * Used before the entity is saved and before invoking the presave hook.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage_controller
+   *   The entity storage controller object.
+   */
+  public function preSave(EntityStorageControllerInterface $storage_controller);
+
+  /**
+   * Acts on a revision before it gets saved.
+   *
+   * @param EntityStorageControllerInterface $storage_controller
+   *   The entity storage controller object.
+   * @param \stdClass $record
+   *   The revision object.
+   */
+  public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record);
+
+  /**
+   * Acts on a saved entity before the insert or update hook is invoked.
+   *
+   * Used after the entity is saved, but before invoking the insert or update
+   * hook.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage_controller
+   *   The entity storage controller object.
+   * @param bool $update
+   *   TRUE if the entity has been updated, or FALSE if it has been inserted.
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE);
+
+  /**
+   * Changes the values of an entity before it is created.
+   *
+   * Load defaults for example.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage_controller
+   *   The entity storage controller object.
+   * @param array $values
+   *   An array of values to set, keyed by property name. If the entity type has
+   *   bundles the bundle key has to be specified.
+   */
+  public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values);
+
+  /**
+   * Acts on an entity after it is created but before hooks are invoked.
+   *
+   * @param EntityStorageControllerInterface $storage_controller
+   *   The entity storage controller object.
+   */
+  public function postCreate(EntityStorageControllerInterface $storage_controller);
+
+  /**
+   * Acts on entities before they are deleted and before hooks are invoked.
+   *
+   * Used before the entities are deleted and before invoking the delete hook.
+   *
+   * @param EntityStorageControllerInterface $storage_controller
+   *   The entity storage controller object.
+   * @param array $entities
+   *   An array of entities.
+   */
+  public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities);
+
+  /**
+   * Acts on deleted entities before the delete hook is invoked.
+   *
+   * Used after the entities are deleted but before invoking the delete hook.
+   *
+   * @param EntityStorageControllerInterface $storage_controller
+   *   The entity storage controller object.
+   * @param array $entities
+   *   An array of entities.
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities);
+
+  /**
+   * Acts on loaded entities before the load hook is invoked.
+   *
+   * @param EntityStorageControllerInterface $storage_controller
+   *   The entity storage controller object.
+   * @param array $entities
+   *   An array of entities.
+   */
+  public static function postLoad(EntityStorageControllerInterface $storage_controller, array $entities);
+
+  /**
    * Creates a duplicate of the entity.
    *
    * @return \Drupal\Core\Entity\EntityInterface
diff --git a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php
index 610cb4d..e533dd7 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php
@@ -33,7 +33,7 @@ public function resetCache(array $ids = NULL);
    * Loads one or more entities.
    *
    * @param $ids
-   *   An array of entity IDs, or FALSE to load all entities.
+   *   An array of entity IDs, or NULL to load all entities.
    *
    * @return
    *   An array of entity objects indexed by their ids.
diff --git a/core/modules/aggregator/lib/Drupal/aggregator/FeedStorageController.php b/core/modules/aggregator/lib/Drupal/aggregator/FeedStorageController.php
index 82a5643..36d87eb 100644
--- a/core/modules/aggregator/lib/Drupal/aggregator/FeedStorageController.php
+++ b/core/modules/aggregator/lib/Drupal/aggregator/FeedStorageController.php
@@ -8,6 +8,7 @@
 namespace Drupal\aggregator;
 
 use Drupal\Core\Entity\DatabaseStorageControllerNG;
+use Drupal\aggregator\Plugin\Core\Entity\Feed;
 use Drupal\Core\Entity\EntityInterface;
 
 /**
@@ -16,101 +17,14 @@
  * This extends the Drupal\Core\Entity\DatabaseStorageController class, adding
  * required special handling for feed entities.
  */
-class FeedStorageController extends DatabaseStorageControllerNG {
-
-  /**
-   * Overrides Drupal\Core\Entity\DataBaseStorageController::create().
-   */
-  public function create(array $values) {
-    $values += array(
-      'link' => '',
-      'description' => '',
-      'image' => '',
-    );
-    return parent::create($values);
-  }
+class FeedStorageController extends DatabaseStorageControllerNG implements FeedStorageControllerInterface {
 
   /**
    * Overrides Drupal\Core\Entity\DataBaseStorageController::attachLoad().
    */
   protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
     parent::attachLoad($queried_entities, $load_revision);
-    foreach ($queried_entities as $item) {
-      $item->categories = db_query('SELECT c.cid, c.title FROM {aggregator_category} c JOIN {aggregator_category_feed} f ON c.cid = f.cid AND f.fid = :fid ORDER BY title', array(':fid' => $item->id()))->fetchAllKeyed();
-    }
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DataBaseStorageController::preDelete().
-   */
-  protected function preDelete($entities) {
-    parent::preDelete($entities);
-
-    // Invalidate the block cache to update aggregator feed-based derivatives.
-    if (module_exists('block')) {
-      \Drupal::service('plugin.manager.block')->clearCachedDefinitions();
-    }
-    foreach ($entities as $entity) {
-      // Notify processors to remove stored items.
-      $manager = \Drupal::service('plugin.manager.aggregator.processor');
-      foreach ($manager->getDefinitions() as $id => $definition) {
-        $manager->createInstance($id)->remove($entity);
-      }
-    }
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DataBaseStorageController::postDelete().
-   */
-  protected function postDelete($entities) {
-    parent::postDelete($entities);
-
-    foreach ($entities as $entity) {
-      // Make sure there is no active block for this feed.
-      $block_configs = config_get_storage_names_with_prefix('plugin.core.block');
-      foreach ($block_configs as $config_id) {
-        $config = config($config_id);
-        if ($config->get('id') == 'aggregator_feed_block:' . $entity->id()) {
-          $config->delete();
-        }
-      }
-    }
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DataBaseStorageController::preSave().
-   */
-  protected function preSave(EntityInterface $entity) {
-    parent::preSave($entity);
-
-    // Invalidate the block cache to update aggregator feed-based derivatives.
-    if (module_exists('block')) {
-      drupal_container()->get('plugin.manager.block')->clearCachedDefinitions();
-    }
-    // An existing feed is being modified, delete the category listings.
-    db_delete('aggregator_category_feed')
-      ->condition('fid', $entity->id())
-      ->execute();
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DataBaseStorageController::postSave().
-   */
-  protected function postSave(EntityInterface $entity, $update) {
-    parent::postSave($entity, $update);
-
-    if (!empty($entity->categories)) {
-      foreach ($entity->categories as $cid => $value) {
-        if ($value) {
-          db_insert('aggregator_category_feed')
-            ->fields(array(
-              'fid' => $entity->id(),
-              'cid' => $cid,
-            ))
-            ->execute();
-        }
-      }
-    }
+    $this->loadCategories($queried_entities);
   }
 
   /**
@@ -191,4 +105,39 @@ public function baseFieldDefinitions() {
     return $fields;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function loadCategories(array $feeds) {
+    foreach ($feeds as $feed) {
+      $feed->categories = $this->database->query('SELECT c.cid, c.title FROM {aggregator_category} c JOIN {aggregator_category_feed} f ON c.cid = f.cid AND f.fid = :fid ORDER BY title', array(':fid' => $feed->id()))->fetchAllKeyed();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function saveCategories(Feed $feed, array $categories) {
+    foreach ($categories as $cid => $value) {
+      if ($value) {
+        $this->database->insert('aggregator_category_feed')
+          ->fields(array(
+            'fid' => $feed->id(),
+            'cid' => $cid,
+          ))
+          ->execute();
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteCategories(array $feeds) {
+    // An existing feed is being modified, delete the category listings.
+    $this->database->delete('aggregator_category_feed')
+      ->condition('fid', array_keys($feeds))
+      ->execute();
+  }
+
 }
diff --git a/core/modules/aggregator/lib/Drupal/aggregator/FeedStorageControllerInterface.php b/core/modules/aggregator/lib/Drupal/aggregator/FeedStorageControllerInterface.php
new file mode 100644
index 0000000..24545ca
--- /dev/null
+++ b/core/modules/aggregator/lib/Drupal/aggregator/FeedStorageControllerInterface.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\aggregator\FeedStorageControllerInterface.
+ */
+
+namespace Drupal\aggregator;
+
+use Drupal\aggregator\Plugin\Core\Entity\Feed;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+
+/**
+ * Defines a common interface for aggregator feed entity controller classes.
+ */
+interface FeedStorageControllerInterface extends EntityStorageControllerInterface {
+
+  /**
+   * Loads the categories of a feed.
+   *
+   * @param array $entities
+   *   A list of feed entities keyed by feed id. Each entity will get a
+   *   categories property added.
+   */
+  public function loadCategories(array $feeds);
+
+  /**
+   * Saves the categories of a feed.
+   *
+   * @param Feed $feed
+   *   The feed entity.
+   * @param array $categories
+   *   The array of categories.
+   */
+  public function saveCategories(Feed $feed, array $categories);
+
+  /**
+   * Deletes the categories of a feed.
+   *
+   * @param array $feeds
+   *   A list of feed entities keyed by feed id.
+   */
+  public function deleteCategories(array $feeds);
+
+}
diff --git a/core/modules/aggregator/lib/Drupal/aggregator/ItemStorageController.php b/core/modules/aggregator/lib/Drupal/aggregator/ItemStorageController.php
index 69876ab..016e8ae 100644
--- a/core/modules/aggregator/lib/Drupal/aggregator/ItemStorageController.php
+++ b/core/modules/aggregator/lib/Drupal/aggregator/ItemStorageController.php
@@ -8,6 +8,7 @@
 namespace Drupal\aggregator;
 
 use Drupal\Core\Entity\DatabaseStorageControllerNG;
+use Drupal\aggregator\Plugin\Core\Entity\Item;
 use Drupal\Core\Entity\EntityInterface;
 
 /**
@@ -16,55 +17,14 @@
  * This extends the Drupal\Core\Entity\DatabaseStorageController class, adding
  * required special handling for feed item entities.
  */
-class ItemStorageController extends DatabaseStorageControllerNG {
-
-  /**
-   * Overrides Drupal\Core\Entity\DataBaseStorageController::create().
-   */
-  public function create(array $values) {
-    $entity = parent::create($values);
-
-    // Set an initial timestamp, this will be overwritten if known.
-    $entity->timestamp->value = REQUEST_TIME;
-    return $entity;
-  }
+class ItemStorageController extends DatabaseStorageControllerNG implements ItemStorageControllerInterface {
 
   /**
    * Overrides Drupal\Core\Entity\DataBaseStorageController::attachLoad().
    */
   protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
     parent::attachLoad($queried_entities, $load_revision);
-    foreach ($queried_entities as $item) {
-      $item->categories = db_query('SELECT c.title, c.cid FROM {aggregator_category_item} ci LEFT JOIN {aggregator_category} c ON ci.cid = c.cid WHERE ci.iid = :iid ORDER BY c.title', array(':iid' => $item->id()))->fetchAll();
-    }
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DataBaseStorageController::preDelete().
-   */
-  protected function preDelete($entities) {
-    parent::preDelete($entities);
-
-    db_delete('aggregator_category_item')
-      ->condition('iid', array_keys($entities), 'IN')
-      ->execute();
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DataBaseStorageController::postSave().
-   */
-  protected function postSave(EntityInterface $entity, $update) {
-    parent::postSave($entity, $update);
-
-    $result = db_query('SELECT cid FROM {aggregator_category_feed} WHERE fid = :fid', array(':fid' => $entity->fid->value));
-    foreach ($result as $category) {
-      db_merge('aggregator_category_item')
-        ->key(array(
-          'iid' => $entity->id(),
-          'cid' => $category->cid,
-        ))
-        ->execute();
-    }
+    $this->loadCategories($queried_entities);
   }
 
   /**
@@ -120,4 +80,36 @@ public function baseFieldDefinitions() {
     return $fields;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function loadCategories(array $entities) {
+    foreach ($entities as $item) {
+      $item->categories = db_query('SELECT c.title, c.cid FROM {aggregator_category_item} ci LEFT JOIN {aggregator_category} c ON ci.cid = c.cid WHERE ci.iid = :iid ORDER BY c.title', array(':iid' => $item->id()))->fetchAll();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteCategories(array $entities) {
+    $this->database->delete('aggregator_category_item')
+      ->condition('iid', array_keys($entities))
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function saveCategories(Item $item) {
+    $result = $this->database->query('SELECT cid FROM {aggregator_category_feed} WHERE fid = :fid', array(':fid' => $item->fid->value));
+    foreach ($result as $category) {
+      $this->database->merge('aggregator_category_item')
+        ->key(array(
+          'iid' => $item->id(),
+          'cid' => $category->cid,
+        ))
+        ->execute();
+    }
+  }
 }
diff --git a/core/modules/aggregator/lib/Drupal/aggregator/ItemStorageControllerInterface.php b/core/modules/aggregator/lib/Drupal/aggregator/ItemStorageControllerInterface.php
new file mode 100644
index 0000000..5c00345
--- /dev/null
+++ b/core/modules/aggregator/lib/Drupal/aggregator/ItemStorageControllerInterface.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\aggregator\ItemStorageControllerInterface.
+ */
+
+namespace Drupal\aggregator;
+
+use Drupal\aggregator\Plugin\Core\Entity\Item;
+use Drupal\core\Entity\EntityStorageControllerInterface;
+
+/**
+ * Defines a common interface for aggregator item entity controller classes.
+ */
+interface ItemStorageControllerInterface extends EntityStorageControllerInterface {
+
+  /**
+   * Load the categories for aggregator items.
+   *
+   * @param array $entities
+   *   An array of aggregator item objects, keyed by the item id. Each object
+   *   will get a categories property added.
+   */
+  public function loadCategories(array $entities);
+
+  /**
+   * Delete the categories for aggregator items.
+   *
+   * @param array $entities
+   *   An array of aggregator item objects, keyed by the item id being
+   *   deleted. The storage backend should delete the category data of the
+   *   items.
+   */
+  public function deleteCategories(array $entities);
+
+  /**
+   * Store the categories for aggregator items.
+   *
+   * @param Item $item
+   *   The storage backend should save the categories of this item.
+   */
+  public function saveCategories(Item $item);
+}
diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/Core/Entity/Feed.php b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/Core/Entity/Feed.php
index f915caa..69c8e34 100644
--- a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/Core/Entity/Feed.php
+++ b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/Core/Entity/Feed.php
@@ -8,6 +8,8 @@
 namespace Drupal\aggregator\Plugin\Core\Entity;
 
 use Drupal\Core\Entity\EntityNG;
+use Symfony\Component\DependencyInjection\Container;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
 use Drupal\aggregator\FeedInterface;
@@ -157,7 +159,6 @@ public function init() {
     unset($this->etag);
     unset($this->modified);
     unset($this->block);
-
   }
 
   /**
@@ -173,4 +174,75 @@ public function id() {
   public function label($langcode = NULL) {
     return $this->get('title')->value;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
+    $values += array(
+      'link' => '',
+      'description' => '',
+      'image' => '',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    // Invalidate the block cache to update aggregator feed-based derivatives.
+    if (module_exists('block')) {
+      \Drupal::service('plugin.manager.block')->clearCachedDefinitions();
+    }
+    $storage_controller->deleteCategories($entities);
+    foreach ($entities as $entity) {
+      // Notify processors to remove stored items.
+      $manager = \Drupal::service('plugin.manager.aggregator.processor');
+      foreach ($manager->getDefinitions() as $id => $definition) {
+        $manager->createInstance($id)->remove($entity);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    foreach ($entities as $entity) {
+      // Make sure there is no active block for this feed.
+      $block_configs = config_get_storage_names_with_prefix('plugin.core.block');
+      foreach ($block_configs as $config_id) {
+        $config = config($config_id);
+        if ($config->get('id') == 'aggregator_feed_block:' . $entity->id()) {
+          $config->delete();
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageControllerInterface $storage_controller) {
+    $this->clearBlockCacheDefinitions();
+    $storage_controller->deleteCategories(array($this->id() => $this));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = FALSE) {
+    if (!empty($this->categories)) {
+      $storage_controller->saveCategories($this, $this->categories);
+    }
+  }
+
+  /**
+   * Invalidate the block cache to update aggregator feed-based derivatives.
+   */
+  protected function clearBlockCacheDefinitions() {
+    if ($block_manager = \Drupal::getContainer()->get('plugin.manager.block', Container::NULL_ON_INVALID_REFERENCE)) {
+      $block_manager->clearCachedDefinitions();
+    }
+  }
 }
diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/Core/Entity/Item.php b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/Core/Entity/Item.php
index 51f150a..b527752 100644
--- a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/Core/Entity/Item.php
+++ b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/Core/Entity/Item.php
@@ -8,6 +8,7 @@
 namespace Drupal\aggregator\Plugin\Core\Entity;
 
 use Drupal\Core\Entity\EntityNG;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
 use Drupal\aggregator\ItemInterface;
@@ -130,4 +131,25 @@ public function id() {
   public function label($langcode = NULL) {
     return $this->get('title')->value;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postCreate(EntityStorageControllerInterface $storage_controller) {
+    $this->timestamp->value = REQUEST_TIME;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    $storage_controller->saveCategories($this);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    $storage_controller->deleteCategories($entities);
+  }
 }
diff --git a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockStorageController.php b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockStorageController.php
index 046b065..755a91a 100644
--- a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockStorageController.php
+++ b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockStorageController.php
@@ -8,41 +8,15 @@
 namespace Drupal\custom_block;
 
 use Drupal\Core\Entity\DatabaseStorageControllerNG;
-use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 
 /**
  * Controller class for custom blocks.
  *
- * This extends the Drupal\Core\Entity\DatabaseStorageController class, adding
- * required special handling for custom block entities.
+ * This extends the Drupal\Core\Entity\DatabaseStorageControllerNG class,
+ * adding required special handling for custom block entities.
  */
-class CustomBlockStorageController extends DatabaseStorageControllerNG {
-
-  /**
-   * Overrides \Drupal\Core\Entity\DatabaseStorageController::preSaveRevision().
-   */
-  protected function preSaveRevision(\stdClass $record, EntityInterface $entity) {
-    if ($entity->isNewRevision()) {
-      // When inserting either a new custom block or a new custom_block
-      // revision, $entity->log must be set because {block_custom_revision}.log
-      // is a text column and therefore cannot have a default value. However,
-      // it might not be set at this point (for example, if the user submitting
-      // the form does not have permission to create revisions), so we ensure
-      // that it is at least an empty string in that case.
-      // @todo: Make the {block_custom_revision}.log column nullable so that we
-      // can remove this check.
-      if (!isset($record->log)) {
-        $record->log = '';
-      }
-    }
-    elseif (isset($entity->original) && (!isset($record->log) || $record->log === '')) {
-      // If we are updating an existing custom_block without adding a new
-      // revision, we need to make sure $entity->log is reset whenever it is
-      // empty. Therefore, this code allows us to avoid clobbering an existing
-      // log entry with an empty one.
-      $record->log = $entity->original->log->value;
-    }
-  }
+class CustomBlockStorageController extends DatabaseStorageControllerNG implements EntityStorageControllerInterface {
 
   /**
    * Overrides \Drupal\Core\Entity\DatabaseStorageController::attachLoad().
@@ -63,14 +37,6 @@ protected function attachLoad(&$blocks, $load_revision = FALSE) {
   }
 
   /**
-   * Overrides \Drupal\Core\Entity\DatabaseStorageController::postSave().
-   */
-  protected function postSave(EntityInterface $block, $update) {
-    // Invalidate the block cache to update custom block-based derivatives.
-    drupal_container()->get('plugin.manager.block')->clearCachedDefinitions();
-  }
-
-  /**
    * Implements \Drupal\Core\Entity\DataBaseStorageControllerNG::basePropertyDefinitions().
    */
   public function baseFieldDefinitions() {
diff --git a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockTypeStorageController.php b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockTypeStorageController.php
deleted file mode 100644
index 17f7b4d..0000000
--- a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockTypeStorageController.php
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\custom_block\CustomBlockTypeStorageController.
- */
-
-namespace Drupal\custom_block;
-
-use Drupal\Core\Config\Entity\ConfigStorageController;
-use Drupal\Core\Entity\EntityInterface;
-
-/**
- * Controller class for custom block types.
- */
-class CustomBlockTypeStorageController extends ConfigStorageController {
-
-  /**
-   * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postSave().
-   */
-  protected function postSave(EntityInterface $entity, $update) {
-    parent::postSave($entity, $update);
-
-    if (!$update) {
-      entity_invoke_bundle_hook('create', 'custom_block', $entity->id());
-      custom_block_add_body_field($entity->id());
-    }
-    elseif ($entity->original->id() != $entity->id()) {
-      entity_invoke_bundle_hook('rename', 'custom_block', $entity->original->id(), $entity->id());
-    }
-  }
-
-  /**
-   * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postDelete().
-   */
-  protected function postDelete($entities) {
-    parent::postDelete($entities);
-
-    foreach ($entities as $entity) {
-      entity_invoke_bundle_hook('delete', 'custom_block', $entity->id());
-    }
-  }
-
-}
diff --git a/core/modules/block/custom_block/lib/Drupal/custom_block/Plugin/Core/Entity/CustomBlock.php b/core/modules/block/custom_block/lib/Drupal/custom_block/Plugin/Core/Entity/CustomBlock.php
index f799e75..74dfc25 100644
--- a/core/modules/block/custom_block/lib/Drupal/custom_block/Plugin/Core/Entity/CustomBlock.php
+++ b/core/modules/block/custom_block/lib/Drupal/custom_block/Plugin/Core/Entity/CustomBlock.php
@@ -8,6 +8,7 @@
 namespace Drupal\custom_block\Plugin\Core\Entity;
 
 use Drupal\Core\Entity\EntityNG;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
 use Drupal\custom_block\CustomBlockInterface;
@@ -184,6 +185,14 @@ public function uri() {
   /**
    * {@inheritdoc}
    */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    // Invalidate the block cache to update custom block-based derivatives.
+    \Drupal::service('plugin.manager.block')->clearCachedDefinitions();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getInstances() {
     return entity_load_multiple_by_properties('block', array('plugin' => 'custom_block:' . $this->uuid->value));
   }
@@ -191,6 +200,30 @@ public function getInstances() {
   /**
    * {@inheritdoc}
    */
+  public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
+    if ($this->isNewRevision()) {
+      // When inserting either a new custom block or a new custom_block
+      // revision, $entity->log must be set because {block_custom_revision}.log
+      // is a text column and therefore cannot have a default value. However,
+      // it might not be set at this point (for example, if the user submitting
+      // the form does not have permission to create revisions), so we ensure
+      // that it is at least an empty string in that case.
+      // @todo: Make the {block_custom_revision}.log column nullable so that we
+      // can remove this check.
+      if (!isset($record->log)) {
+        $record->log = '';
+      }
+    }
+    elseif (isset($this->original) && (!isset($record->log) || $record->log === '')) {
+      // If we are updating an existing custom_block without adding a new
+      // revision and the user did not supply a log, keep the existing one.
+      $record->log = $this->original->log->value;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function delete() {
     foreach ($this->getInstances() as $instance) {
       $instance->delete();
diff --git a/core/modules/block/custom_block/lib/Drupal/custom_block/Plugin/Core/Entity/CustomBlockType.php b/core/modules/block/custom_block/lib/Drupal/custom_block/Plugin/Core/Entity/CustomBlockType.php
index e4d3ab2..e1c6ace 100644
--- a/core/modules/block/custom_block/lib/Drupal/custom_block/Plugin/Core/Entity/CustomBlockType.php
+++ b/core/modules/block/custom_block/lib/Drupal/custom_block/Plugin/Core/Entity/CustomBlockType.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Config\Entity\ConfigEntityBase;
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\custom_block\CustomBlockTypeInterface;
 
 /**
@@ -20,7 +21,7 @@
  *   label = @Translation("Custom block type"),
  *   module = "custom_block",
  *   controllers = {
- *     "storage" = "Drupal\custom_block\CustomBlockTypeStorageController",
+ *     "storage" = "Drupal\Core\Config\Entity\ConfigStorageController",
  *     "form" = {
  *       "default" = "Drupal\custom_block\CustomBlockTypeFormController"
  *     },
@@ -83,4 +84,23 @@ public function uri() {
       )
     );
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    if (!$update) {
+      entity_invoke_bundle_hook('create', 'custom_block', $this->id());
+      custom_block_add_body_field($this->id);
+    }
+    elseif ($this->originalID != $this->id) {
+      entity_invoke_bundle_hook('rename', 'custom_block', $this->originalID, $this->id);
+    }
+  }
+
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    foreach ($entities as $entity) {
+      entity_invoke_bundle_hook('delete', 'custom_block', $entity->id());
+    }
+  }
 }
diff --git a/core/modules/block/lib/Drupal/block/BlockStorageController.php b/core/modules/block/lib/Drupal/block/BlockStorageController.php
index d6c547d..747aa87 100644
--- a/core/modules/block/lib/Drupal/block/BlockStorageController.php
+++ b/core/modules/block/lib/Drupal/block/BlockStorageController.php
@@ -26,13 +26,4 @@ public function load(array $ids = NULL) {
     });
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function preSave(EntityInterface $entity) {
-    parent::preSave($entity);
-
-    $entity->set('settings', $entity->getPlugin()->getConfig());
-  }
-
 }
diff --git a/core/modules/block/lib/Drupal/block/Plugin/Core/Entity/Block.php b/core/modules/block/lib/Drupal/block/Plugin/Core/Entity/Block.php
index 3c8b14c..eb9b8a7 100644
--- a/core/modules/block/lib/Drupal/block/Plugin/Core/Entity/Block.php
+++ b/core/modules/block/lib/Drupal/block/Plugin/Core/Entity/Block.php
@@ -12,6 +12,7 @@
 use Drupal\Core\Annotation\Translation;
 use Drupal\block\BlockPluginBag;
 use Drupal\block\BlockInterface;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 
 /**
  * Defines a Block configuration entity class.
@@ -163,4 +164,11 @@ public function getExportProperties() {
     return $properties;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageControllerInterface $storage_controller) {
+    $this->set('settings', $this->getPlugin()->getConfig());
+  }
+
 }
diff --git a/core/modules/comment/lib/Drupal/comment/CommentStorageController.php b/core/modules/comment/lib/Drupal/comment/CommentStorageController.php
index 3231d4b..435a960 100644
--- a/core/modules/comment/lib/Drupal/comment/CommentStorageController.php
+++ b/core/modules/comment/lib/Drupal/comment/CommentStorageController.php
@@ -18,14 +18,15 @@
  * This extends the Drupal\Core\Entity\DatabaseStorageController class, adding
  * required special handling for comment entities.
  */
-class CommentStorageController extends DatabaseStorageControllerNG {
+class CommentStorageController extends DatabaseStorageControllerNG implements CommentStorageControllerInterface {
+
   /**
    * The thread for which a lock was acquired.
    */
   protected $threadLock = '';
 
   /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::buildQuery().
+   * {@inheritdoc}
    */
   protected function buildQuery($ids, $revision_id = FALSE) {
     $query = parent::buildQuery($ids, $revision_id);
@@ -39,7 +40,7 @@ protected function buildQuery($ids, $revision_id = FALSE) {
   }
 
   /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::attachLoad().
+   * {@inheritdoc}
    */
   protected function attachLoad(&$records, $load_revision = FALSE) {
     // Prepare standard comment fields.
@@ -52,156 +53,9 @@ protected function attachLoad(&$records, $load_revision = FALSE) {
   }
 
   /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageControllerNG::create().
-   */
-  public function create(array $values) {
-    if (empty($values['node_type']) && !empty($values['nid'])) {
-      $node = node_load(is_object($values['nid']) ? $values['nid']->value : $values['nid']);
-      $values['node_type'] = 'comment_node_' . $node->type;
-    }
-    return parent::create($values);
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::preSave().
-   *
-   * @see comment_int_to_alphadecimal()
-   * @see comment_alphadecimal_to_int()
-   */
-  protected function preSave(EntityInterface $comment) {
-    global $user;
-
-    if (!isset($comment->status->value)) {
-      $comment->status->value = user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED;
-    }
-    // Make sure we have a proper bundle name.
-    if (!isset($comment->node_type->value)) {
-      $comment->node_type->value = 'comment_node_' . $comment->nid->entity->type;
-    }
-    if ($comment->isNew()) {
-      // Add the comment to database. This next section builds the thread field.
-      // Also see the documentation for comment_view().
-      if (!empty($comment->thread->value)) {
-        // Allow calling code to set thread itself.
-        $thread = $comment->thread->value;
-      }
-      else {
-        if ($this->threadLock) {
-          // As preSave() is protected, this can only happen when this class
-          // is extended in a faulty manner.
-          throw new LogicException('preSave is called again without calling postSave() or releaseThreadLock()');
-        }
-        if ($comment->pid->target_id == 0) {
-          // This is a comment with no parent comment (depth 0): we start
-          // by retrieving the maximum thread level.
-          $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid->target_id))->fetchField();
-          // Strip the "/" from the end of the thread.
-          $max = rtrim($max, '/');
-          // We need to get the value at the correct depth.
-          $parts = explode('.', $max);
-          $n = comment_alphadecimal_to_int($parts[0]);
-          $prefix = '';
-        }
-        else {
-          // This is a comment with a parent comment, so increase the part of
-          // the thread value at the proper depth.
-
-          // Get the parent comment:
-          $parent = $comment->pid->entity;
-          // Strip the "/" from the end of the parent thread.
-          $parent->thread->value = (string) rtrim((string) $parent->thread->value, '/');
-          $prefix = $parent->thread->value . '.';
-          // Get the max value in *this* thread.
-          $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array(
-            ':thread' => $parent->thread->value . '.%',
-            ':nid' => $comment->nid->target_id,
-          ))->fetchField();
-
-          if ($max == '') {
-            // First child of this parent. As the other two cases do an
-            // increment of the thread number before creating the thread
-            // string set this to -1 so it requires an increment too.
-            $n = -1;
-          }
-          else {
-            // Strip the "/" at the end of the thread.
-            $max = rtrim($max, '/');
-            // Get the value at the correct depth.
-            $parts = explode('.', $max);
-            $parent_depth = count(explode('.', $parent->thread->value));
-            $n = comment_alphadecimal_to_int($parts[$parent_depth]);
-          }
-        }
-        // Finally, build the thread field for this new comment. To avoid
-        // race conditions, get a lock on the thread. If aother process already
-        // has the lock, just move to the next integer.
-        do {
-          $thread = $prefix . comment_int_to_alphadecimal(++$n) . '/';
-        } while (!lock()->acquire("comment:{$comment->nid->target_id}:$thread"));
-        $this->threadLock = $thread;
-      }
-      if (empty($comment->created->value)) {
-        $comment->created->value = REQUEST_TIME;
-      }
-      if (empty($comment->changed->value)) {
-        $comment->changed->value = $comment->created->value;
-      }
-      // We test the value with '===' because we need to modify anonymous
-      // users as well.
-      if ($comment->uid->target_id === $user->uid && $user->uid) {
-        $comment->name->value = $user->name;
-      }
-      // Add the values which aren't passed into the function.
-      $comment->thread->value = $thread;
-      $comment->hostname->value = \Drupal::request()->getClientIP();
-    }
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::postSave().
-   */
-  protected function postSave(EntityInterface $comment, $update) {
-    $this->releaseThreadLock();
-    // Update the {node_comment_statistics} table prior to executing the hook.
-    $this->updateNodeStatistics($comment->nid->target_id);
-    if ($comment->status->value == COMMENT_PUBLISHED) {
-      module_invoke_all('comment_publish', $comment);
-    }
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::postDelete().
+   * {@inheritdoc}
    */
-  protected function postDelete($comments) {
-    // Delete the comments' replies.
-    $query = db_select('comment', 'c')
-      ->fields('c', array('cid'))
-      ->condition('pid', array(array_keys($comments)), 'IN');
-    $child_cids = $query->execute()->fetchCol();
-    entity_delete_multiple('comment', $child_cids);
-
-    foreach ($comments as $comment) {
-      $this->updateNodeStatistics($comment->nid->target_id);
-    }
-  }
-
-  /**
-   * Updates the comment statistics for a given node.
-   *
-   * The {node_comment_statistics} table has the following fields:
-   * - last_comment_timestamp: The timestamp of the last comment for this node,
-   *   or the node created timestamp if no comments exist for the node.
-   * - last_comment_name: The name of the anonymous poster for the last comment.
-   * - last_comment_uid: The user ID of the poster for the last comment for
-   *   this node, or the node author's user ID if no comments exist for the
-   *   node.
-   * - comment_count: The total number of approved/published comments on this
-   *   node.
-   *
-   * @param $nid
-   *   The node ID.
-   */
-  protected function updateNodeStatistics($nid) {
+  public function updateNodeStatistics($nid) {
     // Allow bulk updates and inserts to temporarily disable the
     // maintenance of the {node_comment_statistics} table.
     if (!variable_get('comment_maintain_node_statistics', TRUE)) {
@@ -247,17 +101,7 @@ protected function updateNodeStatistics($nid) {
   }
 
   /**
-   * Release the lock acquired for the thread in preSave().
-   */
-  protected function releaseThreadLock() {
-    if ($this->threadLock) {
-      lock()->release($this->threadLock);
-      $this->threadLock = '';
-    }
-  }
-
-  /**
-   * Implements \Drupal\Core\Entity\DataBaseStorageControllerNG::basePropertyDefinitions().
+   * {@inheritdoc}
    */
   public function baseFieldDefinitions() {
     $properties['cid'] = array(
@@ -356,4 +200,32 @@ public function baseFieldDefinitions() {
     );
     return $properties;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMaxThread(EntityInterface $comment) {
+    return db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid->target_id))->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMaxThreadPerThread(EntityInterface $comment) {
+    return $this->database->query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array(
+      ':thread' => rtrim($comment->pid->entity->thread->value, '/') . '.%',
+      ':nid' => $comment->nid->target_id,
+    ))->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChildCids(array $comments) {
+    return $this->database->select('comment', 'c')
+      ->fields('c', array('cid'))
+      ->condition('pid', array_keys($comments))
+      ->execute()
+      ->fetchCol();
+  }
 }
diff --git a/core/modules/comment/lib/Drupal/comment/CommentStorageControllerInterface.php b/core/modules/comment/lib/Drupal/comment/CommentStorageControllerInterface.php
new file mode 100644
index 0000000..f0e7c84
--- /dev/null
+++ b/core/modules/comment/lib/Drupal/comment/CommentStorageControllerInterface.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\comment\CommentStorageControllerInterface.
+ */
+
+namespace Drupal\comment;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+
+/**
+ * Defines a common interface for comment entity controller classes.
+ */
+interface CommentStorageControllerInterface extends EntityStorageControllerInterface {
+
+  /**
+   * @todo Document this.
+   *
+   * @param EntityInterface $comment
+   *   A comment entity.
+   * @return string
+   *   A thread string.
+   */
+  public function getMaxThread(EntityInterface $comment);
+
+  /**
+   * @todo Document this.
+   *
+   * @param EntityInterface $comment
+   *   A comment entity.
+   * @return string
+   *   A thread string.
+   */
+  public function getMaxThreadPerThread(EntityInterface $comment);
+
+  /**
+   * Gets the comment ids of the passed comment entities' children.
+   *
+   * @param array $comments
+   *   An array of comment entities keyed by their ids.
+   * @return array
+   *   The entity ids of the passed comment entities' children as an array.
+   */
+  public function getChildCids(array $comments);
+
+  /**
+   * Updates the comment statistics for a given node.
+   *
+   * The {node_comment_statistics} table has the following fields:
+   * - last_comment_timestamp: The timestamp of the last comment for this node,
+   *   or the node created timestamp if no comments exist for the node.
+   * - last_comment_name: The name of the anonymous poster for the last comment.
+   * - last_comment_uid: The user ID of the poster for the last comment for
+   *   this node, or the node author's user ID if no comments exist for the
+   *   node.
+   * - comment_count: The total number of approved/published comments on this
+   *   node.
+   *
+   * @param $nid
+   *   The node ID.
+   */
+  public function updateNodeStatistics($nid);
+
+}
diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php
index 2f38ea8..b043e29 100644
--- a/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php
+++ b/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
 use Drupal\comment\CommentInterface;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\Core\Language\Language;
 
 /**
@@ -227,6 +228,139 @@ public function id() {
   /**
    * {@inheritdoc}
    */
+  public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
+    if (empty($values['node_type']) && !empty($values['nid'])) {
+      $node = node_load(is_object($values['nid']) ? $values['nid']->value : $values['nid']);
+      $values['node_type'] = 'comment_node_' . $node->type;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageControllerInterface $storage_controller) {
+    global $user;
+
+    if (!isset($this->status->value)) {
+      $this->status->value = user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED;
+    }
+    // Make sure we have a proper bundle name.
+    if (!isset($this->node_type->value)) {
+      $this->node_type->value = 'comment_node_' . $this->nid->entity->type;
+    }
+    if ($this->isNew()) {
+      // Add the comment to database. This next section builds the thread field.
+      // Also see the documentation for comment_view().
+      if (!empty($this->thread->value)) {
+        // Allow calling code to set thread itself.
+        $thread = $this->thread->value;
+      }
+      else {
+        if ($this->threadLock) {
+          // As preSave() is protected, this can only happen when this class
+          // is extended in a faulty manner.
+          throw new \LogicException('preSave is called again without calling postSave() or releaseThreadLock()');
+        }
+        if ($this->pid->target_id == 0) {
+          // This is a comment with no parent comment (depth 0): we start
+          // by retrieving the maximum thread level.
+          $max = $storage_controller->getMaxThread($this);
+          // Strip the "/" from the end of the thread.
+          $max = rtrim($max, '/');
+          // We need to get the value at the correct depth.
+          $parts = explode('.', $max);
+          $n = comment_alphadecimal_to_int($parts[0]);
+          $prefix = '';
+        }
+        else {
+          // This is a comment with a parent comment, so increase the part of
+          // the thread value at the proper depth.
+
+          // Get the parent comment:
+          $parent = $this->pid->entity;
+          // Strip the "/" from the end of the parent thread.
+          $parent->thread->value = (string) rtrim((string) $parent->thread->value, '/');
+          $prefix = $parent->thread->value . '.';
+          // Get the max value in *this* thread.
+          $max = $storage_controller->getMaxThreadPerThread($this);
+
+          if ($max == '') {
+            // First child of this parent. As the other two cases do an
+            // increment of the thread number before creating the thread
+            // string set this to -1 so it requires an increment too.
+            $n = -1;
+          }
+          else {
+            // Strip the "/" at the end of the thread.
+            $max = rtrim($max, '/');
+            // Get the value at the correct depth.
+            $parts = explode('.', $max);
+            $parent_depth = count(explode('.', $parent->thread->value));
+            $n = comment_alphadecimal_to_int($parts[$parent_depth]);
+          }
+        }
+        // Finally, build the thread field for this new comment. To avoid
+        // race conditions, get a lock on the thread. If aother process already
+        // has the lock, just move to the next integer.
+        do {
+          $thread = $prefix . comment_int_to_alphadecimal(++$n) . '/';
+        } while (!lock()->acquire("comment:{$this->nid->target_id}:$thread"));
+        $this->threadLock = $thread;
+      }
+      if (empty($this->created->value)) {
+        $this->created->value = REQUEST_TIME;
+      }
+      if (empty($this->changed->value)) {
+        $this->changed->value = $this->created->value;
+      }
+      // We test the value with '===' because we need to modify anonymous
+      // users as well.
+      if ($this->uid->target_id === $user->uid && $user->uid) {
+        $this->name->value = $user->name;
+      }
+      // Add the values which aren't passed into the function.
+      $this->thread->value = $thread;
+      $this->hostname->value = \Drupal::request()->getClientIP();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    $this->releaseThreadLock();
+    // Update the {node_comment_statistics} table prior to executing the hook.
+    $storage_controller->updateNodeStatistics($this->nid->target_id);
+    if ($this->status->value == COMMENT_PUBLISHED) {
+      module_invoke_all('comment_publish', $this);
+    }
+  }
+
+  /**
+   * Release the lock acquired for the thread in preSave().
+   */
+  protected function releaseThreadLock() {
+    if ($this->threadLock) {
+      lock()->release($this->threadLock);
+      $this->threadLock = '';
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    $child_cids = $storage_controller->getChildCids($entities);
+    entity_delete_multiple('comment', $child_cids);
+
+    foreach ($entities as $id => $entity) {
+      $storage_controller->updateNodeStatistics($entity->nid->target_id);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function permalink() {
 
     $url['path'] = 'node/' . $this->nid->value;
diff --git a/core/modules/contact/lib/Drupal/contact/CategoryStorageController.php b/core/modules/contact/lib/Drupal/contact/CategoryStorageController.php
index 9350613..90b74db 100644
--- a/core/modules/contact/lib/Drupal/contact/CategoryStorageController.php
+++ b/core/modules/contact/lib/Drupal/contact/CategoryStorageController.php
@@ -8,36 +8,10 @@
 namespace Drupal\contact;
 
 use Drupal\Core\Config\Entity\ConfigStorageController;
-use Drupal\Core\Entity\EntityInterface;
 
 /**
  * Controller class for contact categories.
  */
 class CategoryStorageController extends ConfigStorageController {
 
-  /**
-   * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postSave().
-   */
-  protected function postSave(EntityInterface $entity, $update) {
-    parent::postSave($entity, $update);
-
-    if (!$update) {
-      entity_invoke_bundle_hook('create', 'contact_message', $entity->id());
-    }
-    elseif ($entity->original->id() != $entity->id()) {
-      entity_invoke_bundle_hook('rename', 'contact_message', $entity->original->id(), $entity->id());
-    }
-  }
-
-  /**
-   * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postDelete().
-   */
-  protected function postDelete($entities) {
-    parent::postDelete($entities);
-
-    foreach ($entities as $entity) {
-      entity_invoke_bundle_hook('delete', 'contact_message', $entity->id());
-    }
-  }
-
 }
diff --git a/core/modules/contact/lib/Drupal/contact/Plugin/Core/Entity/Category.php b/core/modules/contact/lib/Drupal/contact/Plugin/Core/Entity/Category.php
index 12a9fc0..f1aca0d 100644
--- a/core/modules/contact/lib/Drupal/contact/Plugin/Core/Entity/Category.php
+++ b/core/modules/contact/lib/Drupal/contact/Plugin/Core/Entity/Category.php
@@ -8,6 +8,7 @@
 namespace Drupal\contact\Plugin\Core\Entity;
 
 use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
 use Drupal\contact\CategoryInterface;
@@ -81,4 +82,25 @@ class Category extends ConfigEntityBase implements CategoryInterface {
    */
   public $weight = 0;
 
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    if (!$update) {
+      entity_invoke_bundle_hook('create', 'contact_message', $this->id());
+    }
+    elseif ($this->original->id() != $this->id()) {
+      entity_invoke_bundle_hook('rename', 'contact_message', $this->original->id(), $this->id());
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    foreach ($entities as $entity) {
+      entity_invoke_bundle_hook('delete', 'contact_message', $entity->id());
+    }
+  }
+
 }
diff --git a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/BundleKeyTestEntity.php b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/BundleKeyTestEntity.php
index 7f524d6..d0e715a 100644
--- a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/BundleKeyTestEntity.php
+++ b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/BundleKeyTestEntity.php
@@ -18,7 +18,7 @@
  *   label = @Translation("Test Entity with a bundle key"),
  *   module = "field_test",
  *   controllers = {
- *     "storage" = "Drupal\field_test\TestEntityController",
+ *     "storage" = "Drupal\Core\Entity\DatabaseStorageController",
  *     "form" = {
  *       "default" = "Drupal\field_test\TestEntityFormController"
  *     }
diff --git a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/BundleTestEntity.php b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/BundleTestEntity.php
index 7875bc4..5ad5777 100644
--- a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/BundleTestEntity.php
+++ b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/BundleTestEntity.php
@@ -18,7 +18,7 @@
  *   label = @Translation("Test Entity with a specified bundle"),
  *   module = "field_test",
  *   controllers = {
- *     "storage" = "Drupal\field_test\TestEntityController",
+ *     "storage" = "Drupal\Core\Entity\DatabaseStorageController",
  *     "form" = {
  *       "default" = "Drupal\field_test\TestEntityFormController"
  *     }
diff --git a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/CacheableTestEntity.php b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/CacheableTestEntity.php
index 976690a..351d7c0 100644
--- a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/CacheableTestEntity.php
+++ b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/CacheableTestEntity.php
@@ -18,7 +18,7 @@
  *   label = @Translation("Test Entity, cacheable"),
  *   module = "field_test",
  *   controllers = {
- *     "storage" = "Drupal\field_test\TestEntityController"
+ *     "storage" = "Drupal\Core\Entity\DatabaseStorageController"
  *   },
  *   field_cache = TRUE,
  *   base_table = "test_entity",
diff --git a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/TestEntity.php b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/TestEntity.php
index 112ba6c..1d8d9d0 100644
--- a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/TestEntity.php
+++ b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/Core/Entity/TestEntity.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Entity\Entity;
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 
 /**
  * Test entity class.
@@ -19,7 +20,7 @@
  *   label = @Translation("Test Entity"),
  *   module = "field_test",
  *   controllers = {
- *     "storage" = "Drupal\field_test\TestEntityController",
+ *     "storage" = "Drupal\Core\Entity\DatabaseStorageController",
  *     "render" = "Drupal\Core\Entity\EntityRenderController",
  *     "form" = {
  *       "default" = "Drupal\field_test\TestEntityFormController"
@@ -86,5 +87,16 @@ public function getRevisionId() {
   public function bundle() {
     return !empty($this->fttype) ? $this->fttype : $this->entityType();
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
+    // Allow for predefined revision ids.
+    if (!empty($record->use_provided_revision_id)) {
+      $record->ftvid = $record->use_provided_revision_id;
+    }
+  }
+
 }
 
diff --git a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/TestEntityController.php b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/TestEntityController.php
deleted file mode 100644
index 0fe7602..0000000
--- a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/TestEntityController.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\field_test\TestEntityController.
- */
-
-namespace Drupal\field_test;
-
-use Drupal\Core\Entity\DatabaseStorageController;
-use Drupal\Core\Entity\EntityInterface;
-
-/**
- * Controller class for the test entity entity types.
- */
-class TestEntityController extends DatabaseStorageController {
-
-  /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::preSaveRevision().
-   */
-  public function preSaveRevision(\stdClass $record, EntityInterface $entity) {
-    // Allow for predefined revision ids.
-    if (!empty($record->use_provided_revision_id)) {
-      $record->ftvid = $record->use_provided_revision_id;
-    }
-  }
-
-}
diff --git a/core/modules/file/lib/Drupal/file/FileStorageController.php b/core/modules/file/lib/Drupal/file/FileStorageController.php
index d3ca601..a2fee25 100644
--- a/core/modules/file/lib/Drupal/file/FileStorageController.php
+++ b/core/modules/file/lib/Drupal/file/FileStorageController.php
@@ -8,76 +8,17 @@
 namespace Drupal\file;
 
 use Drupal\Core\Entity\DatabaseStorageController;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Language\Language;
 
 /**
  * File storage controller for files.
  */
-class FileStorageController extends DatabaseStorageController {
+class FileStorageController extends DatabaseStorageController implements FileStorageControllerInterface {
 
   /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::create().
-   */
-  public function create(array $values) {
-    // Automatically detect filename if not set.
-    if (!isset($values['filename']) && isset($values['uri'])) {
-      $values['filename'] = drupal_basename($values['uri']);
-    }
-
-    // Automatically detect filemime if not set.
-    if (!isset($values['filemime']) && isset($values['uri'])) {
-      $values['filemime'] = file_get_mimetype($values['uri']);
-    }
-    return parent::create($values);
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::presave().
-   */
-  protected function preSave(EntityInterface $entity) {
-    $entity->timestamp = REQUEST_TIME;
-    $entity->filesize = filesize($entity->uri);
-    if (!isset($entity->langcode)) {
-      // Default the file's language code to none, because files are language
-      // neutral more often than language dependent. Until we have better
-      // flexible settings.
-      // @todo See http://drupal.org/node/258785 and followups.
-      $entity->langcode = Language::LANGCODE_NOT_SPECIFIED;
-    }
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::preDelete().
-   */
-  public function preDelete($entities) {
-    foreach ($entities as $entity) {
-      // Delete the actual file. Failures due to invalid files and files that
-      // were already deleted are logged to watchdog but ignored, the
-      // corresponding file entity will be deleted.
-      file_unmanaged_delete($entity->uri);
-    }
-    // Delete corresponding file usage entries.
-    db_delete('file_usage')
-      ->condition('fid', array_keys($entities), 'IN')
-      ->execute();
-  }
-
-  /**
-   * Determines total disk space used by a single user or the whole filesystem.
-   *
-   * @param int $uid
-   *   Optional. A user id, specifying NULL returns the total space used by all
-   *   non-temporary files.
-   * @param $status
-   *   Optional. The file status to consider. The default is to only
-   *   consider files in status FILE_STATUS_PERMANENT.
-   *
-   * @return int
-   *   An integer containing the number of bytes used.
+   * {@inheritdoc}
    */
   public function spaceUsed($uid = NULL, $status = FILE_STATUS_PERMANENT) {
-    $query = db_select($this->entityInfo['base_table'], 'f')
+    $query = $this->database->select($this->entityInfo['base_table'], 'f')
       ->condition('f.status', $status);
     $query->addExpression('SUM(f.filesize)', 'filesize');
     if (isset($uid)) {
@@ -87,15 +28,12 @@ public function spaceUsed($uid = NULL, $status = FILE_STATUS_PERMANENT) {
   }
 
   /**
-   * Retrieve temporary files that are older than DRUPAL_MAXIMUM_TEMP_FILE_AGE.
-   *
-   *  @return
-   *    A list of files to be deleted.
+   * {@inheritdoc}
    */
   public function retrieveTemporaryFiles() {
     // Use separate placeholders for the status to avoid a bug in some versions
     // of PHP. See http://drupal.org/node/352956.
-    return db_query('SELECT fid FROM {' . $this->entityInfo['base_table'] . '} WHERE status <> :permanent AND timestamp < :timestamp', array(
+    return $this->database->query('SELECT fid FROM {' . $this->entityInfo['base_table'] . '} WHERE status <> :permanent AND timestamp < :timestamp', array(
       ':permanent' => FILE_STATUS_PERMANENT,
       ':timestamp' => REQUEST_TIME - DRUPAL_MAXIMUM_TEMP_FILE_AGE
     ));
diff --git a/core/modules/file/lib/Drupal/file/FileStorageControllerInterface.php b/core/modules/file/lib/Drupal/file/FileStorageControllerInterface.php
new file mode 100644
index 0000000..913d172
--- /dev/null
+++ b/core/modules/file/lib/Drupal/file/FileStorageControllerInterface.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\file\FileStorageControllerInterface.
+ */
+
+namespace Drupal\file;
+
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+
+/**
+ * Defines a common interface for file entity controller classes.
+ */
+interface FileStorageControllerInterface extends EntityStorageControllerInterface {
+
+  /**
+   * Determines total disk space used by a single user or the whole filesystem.
+   *
+   * @param int $uid
+   *   Optional. A user id, specifying NULL returns the total space used by all
+   *   non-temporary files.
+   * @param int $status
+   *   (Optional) The file status to consider. The default is to only
+   *   consider files in status FILE_STATUS_PERMANENT.
+   *
+   * @return int
+   *   An integer containing the number of bytes used.
+   */
+  public function spaceUsed($uid = NULL, $status = FILE_STATUS_PERMANENT);
+
+  /**
+   * Retrieve temporary files that are older than DRUPAL_MAXIMUM_TEMP_FILE_AGE.
+   *
+   *  @return array
+   *    A list of files to be deleted.
+   */
+  public function retrieveTemporaryFiles();
+
+}
diff --git a/core/modules/file/lib/Drupal/file/Plugin/Core/Entity/File.php b/core/modules/file/lib/Drupal/file/Plugin/Core/Entity/File.php
index 1390cc7..7215d14 100644
--- a/core/modules/file/lib/Drupal/file/Plugin/Core/Entity/File.php
+++ b/core/modules/file/lib/Drupal/file/Plugin/Core/Entity/File.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Entity\Entity;
 use Drupal\Core\Entity\Annotation\EntityType;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\Core\Annotation\Translation;
 use Drupal\Core\Language\Language;
 use Drupal\file\FileInterface;
@@ -118,4 +119,53 @@ public function id() {
     return $this->fid;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
+    // Automatically detect filename if not set.
+    if (!isset($values['filename']) && isset($values['uri'])) {
+      $values['filename'] = drupal_basename($values['uri']);
+    }
+
+    // Automatically detect filemime if not set.
+    if (!isset($values['filemime']) && isset($values['uri'])) {
+      $values['filemime'] = file_get_mimetype($values['uri']);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageControllerInterface $storage_controller) {
+    $this->timestamp = REQUEST_TIME;
+    $this->filesize = filesize($this->uri);
+    if (!isset($this->langcode)) {
+      // Default the file's language code to none, because files are language
+      // neutral more often than language dependent. Until we have better
+      // flexible settings.
+      // @todo See http://drupal.org/node/258785 and followups.
+      $this->langcode = Language::LANGCODE_NOT_SPECIFIED;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    foreach ($entities as $entity) {
+      // Delete all remaining references to this file.
+      $file_usage = file_usage()->listUsage($entity);
+      if (!empty($file_usage)) {
+        foreach ($file_usage as $module => $usage) {
+          file_usage()->delete($entity, $module);
+        }
+      }
+      // Delete the actual file. Failures due to invalid files and files that
+      // were already deleted are logged to watchdog but ignored, the
+      // corresponding file entity will be deleted.
+      file_unmanaged_delete($entity->uri);
+    }
+  }
+
 }
diff --git a/core/modules/filter/lib/Drupal/filter/FilterFormatStorageController.php b/core/modules/filter/lib/Drupal/filter/FilterFormatStorageController.php
deleted file mode 100644
index 4c19d8b..0000000
--- a/core/modules/filter/lib/Drupal/filter/FilterFormatStorageController.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\filter\FilterFormatStorageController.
- */
-
-namespace Drupal\filter;
-
-use Drupal\Core\Config\Entity\ConfigStorageController;
-use Drupal\Core\Entity\EntityInterface;
-
-/**
- * Defines the storage controller class for Filter Format entities.
- */
-class FilterFormatStorageController extends ConfigStorageController {
-
-  /**
-   * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::preSave().
-   */
-  protected function preSave(EntityInterface $entity) {
-    parent::preSave($entity);
-
-    $entity->name = trim($entity->label());
-
-    // @todo Do not save disabled filters whose properties are identical to
-    //   all default properties.
-
-    // Determine whether the format can be cached.
-    // @todo This is a derived/computed definition, not configuration.
-    $entity->cache = TRUE;
-    foreach ($entity->filters() as $filter) {
-      if ($filter->status && !$filter->cache) {
-        $entity->cache = FALSE;
-      }
-    }
-  }
-
-  /**
-   * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postSave().
-   */
-  protected function postSave(EntityInterface $entity, $update) {
-    parent::postSave($entity, $update);
-
-    // Clear the static caches of filter_formats() and others.
-    filter_formats_reset();
-
-    if ($update) {
-      // Clear the filter cache whenever a text format is updated.
-      cache('filter')->deleteTags(array('filter_format' => $entity->id()));
-    }
-    else {
-      // Default configuration of modules and installation profiles is allowed
-      // to specify a list of user roles to grant access to for the new format;
-      // apply the defined user role permissions when a new format is inserted
-      // and has a non-empty $roles property.
-      // Note: user_role_change_permissions() triggers a call chain back into
-      // filter_permission() and lastly filter_formats(), so its cache must be
-      // reset upfront.
-      if (($roles = $entity->get('roles')) && $permission = filter_permission_name($entity)) {
-        foreach (user_roles() as $rid => $name) {
-          $enabled = in_array($rid, $roles, TRUE);
-          user_role_change_permissions($rid, array($permission => $enabled));
-        }
-      }
-    }
-  }
-
-}
diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/Core/Entity/FilterFormat.php b/core/modules/filter/lib/Drupal/filter/Plugin/Core/Entity/FilterFormat.php
index d6b1fe6..09b4df9 100644
--- a/core/modules/filter/lib/Drupal/filter/Plugin/Core/Entity/FilterFormat.php
+++ b/core/modules/filter/lib/Drupal/filter/Plugin/Core/Entity/FilterFormat.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Config\Entity\ConfigEntityBase;
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\filter\FilterFormatInterface;
 use Drupal\filter\FilterBag;
 
@@ -21,7 +22,7 @@
  *   label = @Translation("Text format"),
  *   module = "filter",
  *   controllers = {
- *     "storage" = "Drupal\filter\FilterFormatStorageController"
+ *     "storage" = "Drupal\Core\Config\Entity\ConfigStorageController"
  *   },
  *   config_prefix = "filter.format",
  *   entity_keys = {
@@ -179,4 +180,50 @@ public function disable() {
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageControllerInterface $storage_controller) {
+    $this->name = trim($this->label());
+
+    // @todo Do not save disabled filters whose properties are identical to
+    //   all default properties.
+
+    // Determine whether the format can be cached.
+    // @todo This is a derived/computed definition, not configuration.
+    $this->cache = TRUE;
+    foreach ($this->filters() as $filter) {
+      if ($filter->status && !$filter->cache) {
+        $this->cache = FALSE;
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    // Clear the static caches of filter_formats() and others.
+    filter_formats_reset();
+
+    if ($update) {
+      // Clear the filter cache whenever a text format is updated.
+      cache('filter')->deleteTags(array('filter_format' => $this->id()));
+    }
+    else {
+      // Default configuration of modules and installation profiles is allowed
+      // to specify a list of user roles to grant access to for the new format;
+      // apply the defined user role permissions when a new format is inserted
+      // and has a non-empty $roles property.
+      // Note: user_role_change_permissions() triggers a call chain back into
+      // filter_permission() and lastly filter_formats(), so its cache must be
+      // reset upfront.
+      if (($roles = $this->get('roles')) && $permission = filter_permission_name($this)) {
+        foreach (user_roles() as $rid => $name) {
+          $enabled = in_array($rid, $roles, TRUE);
+          user_role_change_permissions($rid, array($permission => $enabled));
+        }
+      }
+    }
+  }
 }
diff --git a/core/modules/image/lib/Drupal/image/ImageStyleStorageController.php b/core/modules/image/lib/Drupal/image/ImageStyleStorageController.php
index 97766b5..3f35f5b 100644
--- a/core/modules/image/lib/Drupal/image/ImageStyleStorageController.php
+++ b/core/modules/image/lib/Drupal/image/ImageStyleStorageController.php
@@ -9,8 +9,6 @@
 
 use Drupal\Core\Config\Entity\ConfigStorageController;
 use Drupal\Core\Config\Config;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\image\Plugin\Core\Entity\ImageStyle;
 
 /**
  * Defines a controller class for image styles.
@@ -35,73 +33,4 @@ protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
     parent::attachLoad($queried_entities, $revision_id);
   }
 
-  /**
-   * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postSave().
-   */
-  protected function postSave(EntityInterface $entity, $update) {
-    if ($update && !empty($entity->original) && $entity->{$this->idKey} !== $entity->original->{$this->idKey}) {
-      // The old image style name needs flushing after a rename.
-      image_style_flush($entity->original);
-      // Update field instance settings if necessary.
-      $this->replaceImageStyle($entity);
-    }
-  }
-
-  /**
-   * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::postDelete().
-   */
-  protected function postDelete($entities) {
-    foreach ($entities as $style) {
-      // Flush cached media for the deleted style.
-      image_style_flush($style);
-      // Check whether field instance settings need to be updated.
-      // In case no replacement style was specified, all image fields that are
-      // using the deleted style are left in a broken state.
-      if ($new_id = $style->get('replacementID')) {
-        // The deleted ID is still set as originalID.
-        $style->set('name', $new_id);
-        $this->replaceImageStyle($style);
-      }
-    }
-  }
-
-  /**
-   * Update field instance settings if the image style name is changed.
-   *
-   * @param ImageStyle $style
-   *   The image style.
-   */
-  protected function replaceImageStyle(ImageStyle $style) {
-    if ($style->id() != $style->getOriginalID()) {
-      $instances = field_read_instances();
-      // Loop through all fields searching for image fields.
-      foreach ($instances as $instance) {
-        if ($instance->getField()->type == 'image') {
-          $view_modes = entity_get_view_modes($instance['entity_type']);
-          $view_modes = array('default') + array_keys($view_modes);
-          foreach ($view_modes as $view_mode) {
-            $display = entity_get_display($instance['entity_type'], $instance['bundle'], $view_mode);
-            $display_options = $display->getComponent($instance['field_name']);
-
-            // Check if the formatter involves an image style.
-            if ($display_options && $display_options['type'] == 'image' && $display_options['settings']['image_style'] == $style->getOriginalID()) {
-              // Update display information for any instance using the image
-              // style that was just deleted.
-              $display_options['settings']['image_style'] = $style->id();
-              $display->setComponent($instance['field_name'], $display_options)
-                ->save();
-            }
-          }
-          $entity_form_display = entity_get_form_display($instance['entity_type'], $instance['bundle'], 'default');
-          $widget_configuration = $entity_form_display->getComponent($instance['field_name']);
-          if ($widget_configuration['settings']['preview_image_style'] == $style->getOriginalID()) {
-            $widget_options['settings']['preview_image_style'] = $style->id();
-            $entity_form_display->setComponent($instance['field_name'], $widget_options)
-              ->save();
-          }
-        }
-      }
-    }
-  }
-
 }
diff --git a/core/modules/image/lib/Drupal/image/Plugin/Core/Entity/ImageStyle.php b/core/modules/image/lib/Drupal/image/Plugin/Core/Entity/ImageStyle.php
index 60f020e..1145c1b 100644
--- a/core/modules/image/lib/Drupal/image/Plugin/Core/Entity/ImageStyle.php
+++ b/core/modules/image/lib/Drupal/image/Plugin/Core/Entity/ImageStyle.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Config\Entity\ConfigEntityBase;
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\image\ImageStyleInterface;
 
 /**
@@ -68,4 +69,73 @@ public function id() {
     return $this->name;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    if ($update && !empty($this->original) && $this->id() !== $this->original->id()) {
+      // The old image style name needs flushing after a rename.
+      image_style_flush($this->original);
+      // Update field instance settings if necessary.
+      static::replaceImageStyle($this);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    foreach ($entities as $style) {
+      // Flush cached media for the deleted style.
+      image_style_flush($style);
+      // Check whether field instance settings need to be updated.
+      // In case no replacement style was specified, all image fields that are
+      // using the deleted style are left in a broken state.
+      if ($new_id = $style->get('replacementID')) {
+        // The deleted ID is still set as originalID.
+        $style->set('name', $new_id);
+        static::replaceImageStyle($style);
+      }
+    }
+  }
+
+  /**
+   * Update field instance settings if the image style name is changed.
+   *
+   * @param \Drupal\image\Plugin\Core\Entity\ImageStyle $style
+   *   The image style.
+   */
+  protected static function replaceImageStyle(ImageStyle $style) {
+    if ($style->id() != $style->getOriginalID()) {
+      $instances = field_read_instances();
+      // Loop through all fields searching for image fields.
+      foreach ($instances as $instance) {
+        if ($instance->getField()->type == 'image') {
+          $view_modes = entity_get_view_modes($instance['entity_type']);
+          $view_modes = array('default') + array_keys($view_modes);
+          foreach ($view_modes as $view_mode) {
+            $display = entity_get_display($instance['entity_type'], $instance['bundle'], $view_mode);
+            $display_options = $display->getComponent($instance['field_name']);
+
+            // Check if the formatter involves an image style.
+            if ($display_options && $display_options['type'] == 'image' && $display_options['settings']['image_style'] == $style->getOriginalID()) {
+              // Update display information for any instance using the image
+              // style that was just deleted.
+              $display_options['settings']['image_style'] = $style->id();
+              $display->setComponent($instance['field_name'], $display_options)
+                ->save();
+            }
+          }
+          $entity_form_display = entity_get_form_display($instance['entity_type'], $instance['bundle'], 'default');
+          $widget_configuration = $entity_form_display->getComponent($instance['field_name']);
+          if ($widget_configuration['settings']['preview_image_style'] == $style->getOriginalID()) {
+            $widget_options['settings']['preview_image_style'] = $style->id();
+            $entity_form_display->setComponent($instance['field_name'], $widget_options)
+              ->save();
+          }
+        }
+      }
+    }
+  }
+
 }
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkInterface.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkInterface.php
index 3d74bde..c185dc4 100644
--- a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkInterface.php
+++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkInterface.php
@@ -9,6 +9,8 @@
 
 use Symfony\Component\Routing\Route;
 use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 
 /**
  * Provides an interface defining a menu link entity.
@@ -53,4 +55,44 @@ public function reset();
    */
   public static function buildFromRouterItem(array $item);
 
+  /**
+   * Returns the route_name matching a URL.
+   *
+   * @param string $link_path
+   *   The link path to find a route name for.
+   *
+   * @return string
+   *   The route name.
+   */
+  public static function findRouteName($link_path);
+
+  /**
+   * Sets the p1 through p9 properties for a menu link entity being saved.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $parent
+   *   A menu link entity.
+   */
+  public function setParents(EntityInterface $parent);
+
+  /**
+   * Finds a possible parent for a given menu link entity.
+   *
+   * Because the parent of a given link might not exist anymore in the database,
+   * we apply a set of heuristics to determine a proper parent:
+   *
+   *  - use the passed parent link if specified and existing.
+   *  - else, use the first existing link down the previous link hierarchy
+   *  - else, for system menu links (derived from hook_menu()), reparent
+   *    based on the path hierarchy.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage_controller
+   *   Storage controller object.
+   * @param array $parent_candidates
+   *   An array of menu link entities keyed by mlid.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface|false
+   *   A menu link entity structure of the possible parent or FALSE if no valid
+   *   parent has been found.
+   */
+  public function findParent(EntityStorageControllerInterface $storage_controller, array $parent_candidates = array());
 }
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php
index 229892a..493b939 100644
--- a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php
+++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php
@@ -13,7 +13,6 @@
 use Drupal\Core\Database\Connection;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Cmf\Component\Routing\RouteProviderInterface;
-use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Controller class for menu links.
@@ -21,7 +20,7 @@
  * This extends the Drupal\entity\DatabaseStorageController class, adding
  * required special handling for menu_link entities.
  */
-class MenuLinkStorageController extends DatabaseStorageController {
+class MenuLinkStorageController extends DatabaseStorageController implements MenuLinkStorageControllerInterface {
 
   /**
    * Indicates whether the delete operation should re-parent children items.
@@ -144,6 +143,8 @@ protected function attachLoad(&$menu_links, $load_revision = FALSE) {
    * Overrides DatabaseStorageController::save().
    */
   public function save(EntityInterface $entity) {
+    $entity_class = $this->entityInfo['class'];
+
     // We return SAVED_UPDATED by default because the logic below might not
     // update the entity if its values haven't changed, so returning FALSE
     // would be confusing in that situation.
@@ -165,7 +166,7 @@ public function save(EntityInterface $entity) {
       // 'presave' hook first because we want to allow modules to alter the
       // entity before all the logic from our preSave() method.
       $this->invokeHook('presave', $entity);
-      $this->preSave($entity);
+      $entity->preSave($this);
 
       // If every value in $entity->original is the same in the $entity, there
       // is no reason to run the update queries or clear the caches. We use
@@ -178,7 +179,7 @@ public function save(EntityInterface $entity) {
         if ($return) {
           if (!$entity->isNew()) {
             $this->resetCache(array($entity->{$this->idKey}));
-            $this->postSave($entity, TRUE);
+            $entity->postSave($this, TRUE);
             $this->invokeHook('update', $entity);
           }
           else {
@@ -186,7 +187,7 @@ public function save(EntityInterface $entity) {
             $this->resetCache();
 
             $entity->enforceIsNew(FALSE);
-            $this->postSave($entity, FALSE);
+            $entity->postSave($this, FALSE);
             $this->invokeHook('insert', $entity);
           }
         }
@@ -206,178 +207,21 @@ public function save(EntityInterface $entity) {
   }
 
   /**
-   * Overrides DatabaseStorageController::preSave().
-   */
-  protected function preSave(EntityInterface $entity) {
-    // This is the easiest way to handle the unique internal path '<front>',
-    // since a path marked as external does not need to match a router path.
-    $entity->external = (url_is_external($entity->link_path) || $entity->link_path == '<front>') ? 1 : 0;
-
-    // Try to find a parent link. If found, assign it and derive its menu.
-    $parent_candidates = !empty($entity->parentCandidates) ? $entity->parentCandidates : array();
-    $parent = $this->findParent($entity, $parent_candidates);
-    if ($parent) {
-      $entity->plid = $parent->id();
-      $entity->menu_name = $parent->menu_name;
-    }
-    // If no corresponding parent link was found, move the link to the top-level.
-    else {
-      $entity->plid = 0;
-    }
-
-    // Directly fill parents for top-level links.
-    if ($entity->plid == 0) {
-      $entity->p1 = $entity->id();
-      for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
-        $parent_property = "p$i";
-        $entity->$parent_property = 0;
-      }
-      $entity->depth = 1;
-    }
-    // Otherwise, ensure that this link's depth is not beyond the maximum depth
-    // and fill parents based on the parent link.
-    else {
-      if ($entity->has_children && $entity->original) {
-        $limit = MENU_MAX_DEPTH - $this->findChildrenRelativeDepth($entity->original) - 1;
-      }
-      else {
-        $limit = MENU_MAX_DEPTH - 1;
-      }
-      if ($parent->depth > $limit) {
-        return FALSE;
-      }
-      $entity->depth = $parent->depth + 1;
-      $this->setParents($entity, $parent);
-    }
-
-    // Need to check both plid and menu_name, since plid can be 0 in any menu.
-    if (isset($entity->original) && ($entity->plid != $entity->original->plid || $entity->menu_name != $entity->original->menu_name)) {
-      $this->moveChildren($entity, $entity->original);
-    }
-    // Find the router_path.
-    if (empty($entity->router_path) || empty($entity->original) || (isset($entity->original) && $entity->original->link_path != $entity->link_path)) {
-      if ($entity->external) {
-        $entity->router_path = '';
-      }
-      else {
-        // Find the router path which will serve this path.
-        $entity->parts = explode('/', $entity->link_path, MENU_MAX_PARTS);
-        $entity->router_path = _menu_find_router_path($entity->link_path);
-      }
-    }
-    // Find the route_name.
-    if (!isset($entity->route_name)) {
-      $entity->route_name = $this->findRouteName($entity->link_path);
-    }
-  }
-
-  /**
-   * Returns the route_name matching a URL.
-   *
-   * @param string $link_path
-   *   The link path to find a route name for.
-   *
-   * @return string
-   *   The route name.
-   */
-  protected function findRouteName($link_path) {
-    // Look up the route_name used for the given path.
-    $request = Request::create('/' . $link_path);
-    $request->attributes->set('system_path', $link_path);
-    try {
-      // Use router.dynamic instead of router, because router will call the
-      // legacy router which will call hook_menu() and you will get back to
-      // this method.
-      $result = \Drupal::service('router.dynamic')->matchRequest($request);
-      return isset($result['_route']) ? $result['_route'] : '';
-    }
-    catch (\Exception $e) {
-      return '';
-    }
-
-  }
-
-  /**
-   * DatabaseStorageController::postSave().
-   */
-  function postSave(EntityInterface $entity, $update) {
-    // Check the has_children status of the parent.
-    $this->updateParentalStatus($entity);
-
-    menu_cache_clear($entity->menu_name);
-    if (isset($entity->original) && $entity->menu_name != $entity->original->menu_name) {
-      menu_cache_clear($entity->original->menu_name);
-    }
-
-    // Now clear the cache.
-    _menu_clear_page_cache();
-  }
-
-  /**
-   * Sets an internal flag that allows us to prevent the reparenting operations
-   * executed during deletion.
-   *
-   * @param bool $value
+   * {@inheritdoc}
    */
-  public function preventReparenting($value = FALSE) {
+  public function setPreventReparenting($value = FALSE) {
     $this->preventReparenting = $value;
   }
 
   /**
-   * Overrides DatabaseStorageController::preDelete().
-   */
-  protected function preDelete($entities) {
-    // Nothing to do if we don't want to reparent children.
-    if ($this->preventReparenting) {
-      return;
-    }
-
-    foreach ($entities as $entity) {
-      // Children get re-attached to the item's parent.
-      if ($entity->has_children) {
-        $children = $this->loadByProperties(array('plid' => $entity->plid));
-        foreach ($children as $child) {
-          $child->plid = $entity->plid;
-          $this->save($child);
-        }
-      }
-    }
-  }
-
-  /**
-   * Overrides DatabaseStorageController::postDelete().
+   * {@inheritdoc}
    */
-  protected function postDelete($entities) {
-    $affected_menus = array();
-    // Update the has_children status of the parent.
-    foreach ($entities as $entity) {
-      if (!$this->preventReparenting) {
-        $this->updateParentalStatus($entity);
-      }
-
-      // Store all menu names for which we need to clear the cache.
-      if (!isset($affected_menus[$entity->menu_name])) {
-        $affected_menus[$entity->menu_name] = $entity->menu_name;
-      }
-    }
-
-    foreach ($affected_menus as $menu_name) {
-      menu_cache_clear($menu_name);
-    }
-    _menu_clear_page_cache();
+  public function getPreventReparenting() {
+    return $this->preventReparenting;
   }
 
   /**
-   * Loads updated and customized menu links for specific router paths.
-   *
-   * Note that this is a low-level method and it doesn't return fully populated
-   * menu link entities. (e.g. no fields are attached)
-   *
-   * @param array $router_paths
-   *   An array of router paths.
-   *
-   * @return array
-   *   An array of menu link objects indexed by their ids.
+   * {@inheritdoc}
    */
   public function loadUpdatedCustomized(array $router_paths) {
     $query = parent::buildQuery(NULL);
@@ -403,10 +247,7 @@ public function loadUpdatedCustomized(array $router_paths) {
   }
 
   /**
-   * Loads system menu link as needed by system_get_module_admin_tasks().
-   *
-   * @return array
-   *   An array of menu link entities indexed by their IDs.
+   * {@inheritdoc}
    */
   public function loadModuleAdminTasks() {
     $query = $this->buildQuery(NULL);
@@ -422,12 +263,9 @@ public function loadModuleAdminTasks() {
   }
 
   /**
-   * Checks and updates the 'has_children' property for the parent of a link.
-   *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   A menu link entity.
+   * {@inheritdoc}
    */
-  protected function updateParentalStatus(EntityInterface $entity, $exclude = FALSE) {
+  public function updateParentalStatus(EntityInterface $entity, $exclude = FALSE) {
     // If plid == 0, there is nothing to update.
     if ($entity->plid) {
       // Check if at least one visible child exists in the table.
@@ -451,125 +289,7 @@ protected function updateParentalStatus(EntityInterface $entity, $exclude = FALS
   }
 
   /**
-   * Finds a possible parent for a given menu link entity.
-   *
-   * Because the parent of a given link might not exist anymore in the database,
-   * we apply a set of heuristics to determine a proper parent:
-   *
-   *  - use the passed parent link if specified and existing.
-   *  - else, use the first existing link down the previous link hierarchy
-   *  - else, for system menu links (derived from hook_menu()), reparent
-   *    based on the path hierarchy.
-   *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   A menu link entity.
-   * @param array $parent_candidates
-   *   An array of menu link entities keyed by mlid.
-   *
-   * @return \Drupal\Core\Entity\EntityInterface|false
-   *   A menu link entity structure of the possible parent or FALSE if no valid
-   *   parent has been found.
-   */
-  protected function findParent(EntityInterface $entity, array $parent_candidates = array()) {
-    $parent = FALSE;
-
-    // This item is explicitely top-level, skip the rest of the parenting.
-    if (isset($entity->plid) && empty($entity->plid)) {
-      return $parent;
-    }
-
-    // If we have a parent link ID, try to use that.
-    $candidates = array();
-    if (isset($entity->plid)) {
-      $candidates[] = $entity->plid;
-    }
-
-    // Else, if we have a link hierarchy try to find a valid parent in there.
-    if (!empty($entity->depth) && $entity->depth > 1) {
-      for ($depth = $entity->depth - 1; $depth >= 1; $depth--) {
-        $parent_property = "p$depth";
-        $candidates[] = $entity->$parent_property;
-      }
-    }
-
-    foreach ($candidates as $mlid) {
-      if (isset($parent_candidates[$mlid])) {
-        $parent = $parent_candidates[$mlid];
-      }
-      else {
-        $parent = $this->load(array($mlid));
-        $parent = reset($parent);
-      }
-      if ($parent) {
-        return $parent;
-      }
-    }
-
-    // If everything else failed, try to derive the parent from the path
-    // hierarchy. This only makes sense for links derived from menu router
-    // items (ie. from hook_menu()).
-    if ($entity->module == 'system') {
-      // Find the parent - it must be unique.
-      $parent_path = $entity->link_path;
-      do {
-        $parent = FALSE;
-        $parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
-
-        $query = \Drupal::entityQuery($this->entityType);
-        $query
-          ->condition('mlid', $entity->id(), '<>')
-          ->condition('module', 'system')
-          // We always respect the link's 'menu_name'; inheritance for router
-          // items is ensured in _menu_router_build().
-          ->condition('menu_name', $entity->menu_name)
-          ->condition('link_path', $parent_path);
-
-        $result = $query->execute();
-        // Only valid if we get a unique result.
-        if (count($result) == 1) {
-          $parent = $this->load($result);
-          $parent = reset($parent);
-        }
-      } while ($parent === FALSE && $parent_path);
-    }
-
-    return $parent;
-  }
-
-  /**
-   * Sets the p1 through p9 properties for a menu link entity being saved.
-   *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   A menu link entity.
-   * @param \Drupal\Core\Entity\EntityInterface $parent
-   *   A menu link entity.
-   */
-  protected function setParents(EntityInterface $entity, EntityInterface $parent) {
-    $i = 1;
-    while ($i < $entity->depth) {
-      $p = 'p' . $i++;
-      $entity->{$p} = $parent->{$p};
-    }
-    $p = 'p' . $i++;
-    // The parent (p1 - p9) corresponding to the depth always equals the mlid.
-    $entity->{$p} = $entity->id();
-    while ($i <= MENU_MAX_DEPTH) {
-      $p = 'p' . $i++;
-      $entity->{$p} = 0;
-    }
-  }
-
-  /**
-   * Finds the depth of an item's children relative to its depth.
-   *
-   * For example, if the item has a depth of 2 and the maximum of any child in
-   * the menu link tree is 5, the relative depth is 3.
-   *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   A menu link entity.
-   *
-   * @return int
-   *   The relative depth, or zero.
+   * {@inheritdoc}
    */
   public function findChildrenRelativeDepth(EntityInterface $entity) {
     // @todo Since all we need is a specific field from the base table, does it
@@ -593,15 +313,9 @@ public function findChildrenRelativeDepth(EntityInterface $entity) {
   }
 
   /**
-   * Updates the children of a menu link that is being moved.
-   *
-   * The menu name, parents (p1 - p6), and depth are updated for all children of
-   * the link, and the has_children status of the previous parent is updated.
-   *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   A menu link entity.
+   * {@inheritdoc}
    */
-  protected function moveChildren(EntityInterface $entity) {
+  public function moveChildren(EntityInterface $entity) {
     $query = $this->database->update($this->entityInfo['base_table']);
 
     $query->fields(array('menu_name' => $entity->menu_name));
@@ -645,10 +359,7 @@ protected function moveChildren(EntityInterface $entity) {
   }
 
   /**
-   * Returns the number of menu links from a menu.
-   *
-   * @param string $menu_name
-   *   The unique name of a menu.
+   * {@inheritdoc}
    */
   public function countMenuLinks($menu_name) {
     $query = \Drupal::entityQuery($this->entityType);
@@ -657,4 +368,34 @@ public function countMenuLinks($menu_name) {
       ->count();
     return $query->execute();
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getParentFromHierarchy(EntityInterface $entity) {
+    $parent_path = $entity->link_path;
+    do {
+      $parent = FALSE;
+      $parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
+
+      $query = \Drupal::entityQuery($this->entityType);
+      $query
+        ->condition('mlid', $entity->id(), '<>')
+        ->condition('module', 'system')
+        // We always respect the link's 'menu_name'; inheritance for router
+        // items is ensured in _menu_router_build().
+        ->condition('menu_name', $entity->menu_name)
+        ->condition('link_path', $parent_path);
+
+      $result = $query->execute();
+      // Only valid if we get a unique result.
+      if (count($result) == 1) {
+        $parent = $this->load($result);
+        $parent = reset($parent);
+      }
+    } while ($parent === FALSE && $parent_path);
+
+    return $parent;
+  }
+
 }
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageControllerInterface.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageControllerInterface.php
new file mode 100644
index 0000000..c76b76c
--- /dev/null
+++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageControllerInterface.php
@@ -0,0 +1,108 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_link\MenuLinkStorageControllerInterface.
+*/
+
+namespace Drupal\menu_link;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+
+/**
+ * Defines a common interface for menu link entity controller classes.
+ */
+interface MenuLinkStorageControllerInterface extends EntityStorageControllerInterface {
+
+  /**
+   * Sets an internal flag that allows us to prevent the reparenting operations
+   * executed during deletion.
+   *
+   * @param bool $value
+   */
+  public function setPreventReparenting($value = FALSE);
+
+  /**
+   * Gets value of internal flag that allows/prevents reparenting operations
+   * executed during deletion.
+   *
+   * @return bool
+   */
+  public function getPreventReparenting();
+
+  /**
+   * Loads updated and customized menu links for specific router paths.
+   *
+   * Note that this is a low-level method and it doesn't return fully populated
+   * menu link entities. (e.g. no fields are attached)
+   *
+   * @param array $router_paths
+   *   An array of router paths.
+   *
+   * @return array
+   *   An array of menu link objects indexed by their ids.
+   */
+  public function loadUpdatedCustomized(array $router_paths);
+
+  /**
+   * Loads system menu link as needed by system_get_module_admin_tasks().
+   *
+   * @return array
+   *   An array of menu link entities indexed by their IDs.
+   */
+  public function loadModuleAdminTasks();
+
+  /**
+   * Checks and updates the 'has_children' property for the parent of a link.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   A menu link entity.
+   */
+  public function updateParentalStatus(EntityInterface $entity, $exclude = FALSE);
+
+  /**
+   * Finds the depth of an item's children relative to its depth.
+   *
+   * For example, if the item has a depth of 2 and the maximum of any child in
+   * the menu link tree is 5, the relative depth is 3.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   A menu link entity.
+   *
+   * @return int
+   *   The relative depth, or zero.
+   */
+  public function findChildrenRelativeDepth(EntityInterface $entity);
+
+  /**
+   * Updates the children of a menu link that is being moved.
+   *
+   * The menu name, parents (p1 - p6), and depth are updated for all children of
+   * the link, and the has_children status of the previous parent is updated.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   A menu link entity.
+   */
+  public function moveChildren(EntityInterface $entity);
+
+  /**
+   * Returns the number of menu links from a menu.
+   *
+   * @param string $menu_name
+   *   The unique name of a menu.
+   */
+  public function countMenuLinks($menu_name);
+
+  /**
+   * Tries to derive menu link's parent from the path hierarchy.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   A menu link entity.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface|false
+   *   A menu link entity or FALSE if not valid parent was found.
+   */
+  public function getParentFromHierarchy(EntityInterface $entity);
+
+}
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/Plugin/Core/Entity/MenuLink.php b/core/modules/menu_link/lib/Drupal/menu_link/Plugin/Core/Entity/MenuLink.php
index 5af23d7..e9b2cd7 100644
--- a/core/modules/menu_link/lib/Drupal/menu_link/Plugin/Core/Entity/MenuLink.php
+++ b/core/modules/menu_link/lib/Drupal/menu_link/Plugin/Core/Entity/MenuLink.php
@@ -9,9 +9,13 @@
 
 use Drupal\menu_link\MenuLinkInterface;
 use Symfony\Component\Routing\Route;
+use Symfony\Component\HttpFoundation\Request;
 
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\Entity;
 
@@ -372,4 +376,217 @@ public function offsetSet($offset, $value) {
   public function offsetUnset($offset) {
     unset($this->{$offset});
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    // Nothing to do if we don't want to reparent children.
+    if ($storage_controller->getPreventReparenting()) {
+      return;
+    }
+
+    foreach ($entities as $entity) {
+      // Children get re-attached to the item's parent.
+      if ($entity->has_children) {
+        $children = $storage_controller->loadByProperties(array('plid' => $entity->plid));
+        foreach ($children as $child) {
+          $child->plid = $entity->plid;
+          $storage_controller->save($child);
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    $affected_menus = array();
+    // Update the has_children status of the parent.
+    foreach ($entities as $entity) {
+      if (!$storage_controller->getPreventReparenting()) {
+        $storage_controller->updateParentalStatus($entity);
+      }
+
+      // Store all menu names for which we need to clear the cache.
+      if (!isset($affected_menus[$entity->menu_name])) {
+        $affected_menus[$entity->menu_name] = $entity->menu_name;
+      }
+    }
+
+    foreach ($affected_menus as $menu_name) {
+      menu_cache_clear($menu_name);
+    }
+    _menu_clear_page_cache();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageControllerInterface $storage_controller) {
+    // This is the easiest way to handle the unique internal path '<front>',
+    // since a path marked as external does not need to match a router path.
+    $this->external = (url_is_external($this->link_path) || $this->link_path == '<front>') ? 1 : 0;
+
+    // Try to find a parent link. If found, assign it and derive its menu.
+    $parent_candidates = !empty($this->parentCandidates) ? $this->parentCandidates : array();
+    $parent = $this->findParent($storage_controller, $parent_candidates);
+    if ($parent) {
+      $this->plid = $parent->id();
+      $this->menu_name = $parent->menu_name;
+    }
+    // If no corresponding parent link was found, move the link to the top-level.
+    else {
+      $this->plid = 0;
+    }
+
+    // Directly fill parents for top-level links.
+    if ($this->plid == 0) {
+      $this->p1 = $this->id();
+      for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
+        $parent_property = "p$i";
+        $this->{$parent_property} = 0;
+      }
+      $this->depth = 1;
+    }
+    // Otherwise, ensure that this link's depth is not beyond the maximum depth
+    // and fill parents based on the parent link.
+    else {
+      if ($this->has_children && $this->original) {
+        $limit = MENU_MAX_DEPTH - $storage_controller->findChildrenRelativeDepth($this->original) - 1;
+      }
+      else {
+        $limit = MENU_MAX_DEPTH - 1;
+      }
+      if ($parent->depth > $limit) {
+        return FALSE;
+      }
+      $this->depth = $parent->depth + 1;
+      $this->setParents($parent);
+    }
+
+    // Need to check both plid and menu_name, since plid can be 0 in any menu.
+    if (isset($this->original) && ($this->plid != $this->original->plid || $this->menu_name != $this->original->menu_name)) {
+      $storage_controller->moveChildren($this, $this->original);
+    }
+    // Find the router_path.
+    if (empty($this->router_path) || empty($this->original) || (isset($this->original) && $this->original->link_path != $this->link_path)) {
+      if ($this->external) {
+        $this->router_path = '';
+      }
+      else {
+        // Find the router path which will serve this path.
+        $this->parts = explode('/', $this->link_path, MENU_MAX_PARTS);
+        $this->router_path = _menu_find_router_path($this->link_path);
+      }
+    }
+    // Find the route_name.
+    if (!isset($this->route_name)) {
+      $this->route_name = $this::findRouteName($this->link_path);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    // Check the has_children status of the parent.
+    $storage_controller->updateParentalStatus($this);
+
+    menu_cache_clear($this->menu_name);
+    if (isset($this->original) && $this->menu_name != $this->original->menu_name) {
+      menu_cache_clear($this->original->menu_name);
+    }
+
+    // Now clear the cache.
+    _menu_clear_page_cache();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function findRouteName($link_path) {
+    // Look up the route_name used for the given path.
+    $request = Request::create('/' . $link_path);
+    $request->attributes->set('system_path', $link_path);
+    try {
+      // Use router.dynamic instead of router, because router will call the
+      // legacy router which will call hook_menu() and you will get back to
+      // this method.
+      $result = \Drupal::service('router.dynamic')->matchRequest($request);
+      return isset($result['_route']) ? $result['_route'] : '';
+    }
+    catch (\Exception $e) {
+      return '';
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setParents(EntityInterface $parent) {
+    $i = 1;
+    while ($i < $this->depth) {
+      $p = 'p' . $i++;
+      $this->{$p} = $parent->{$p};
+    }
+    $p = 'p' . $i++;
+    // The parent (p1 - p9) corresponding to the depth always equals the mlid.
+    $this->{$p} = $this->id();
+    while ($i <= MENU_MAX_DEPTH) {
+      $p = 'p' . $i++;
+      $this->{$p} = 0;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function findParent(EntityStorageControllerInterface $storage_controller, array $parent_candidates = array()) {
+    $parent = FALSE;
+
+    // This item is explicitely top-level, skip the rest of the parenting.
+    if (isset($this->plid) && empty($this->plid)) {
+      return $parent;
+    }
+
+    // If we have a parent link ID, try to use that.
+    $candidates = array();
+    if (isset($this->plid)) {
+      $candidates[] = $this->plid;
+    }
+
+    // Else, if we have a link hierarchy try to find a valid parent in there.
+    if (!empty($this->depth) && $this->depth > 1) {
+      for ($depth = $this->depth - 1; $depth >= 1; $depth--) {
+        $parent_property = "p$depth";
+        $candidates[] = $this->$parent_property;
+      }
+    }
+
+    foreach ($candidates as $mlid) {
+      if (isset($parent_candidates[$mlid])) {
+        $parent = $parent_candidates[$mlid];
+      }
+      else {
+        $parent = $storage_controller->load(array($mlid));
+        $parent = reset($parent);
+      }
+      if ($parent) {
+        return $parent;
+      }
+    }
+
+    // If everything else failed, try to derive the parent from the path
+    // hierarchy. This only makes sense for links derived from menu router
+    // items (ie. from hook_menu()).
+    if ($this->module == 'system') {
+      $parent = $storage_controller->getParentFromHierarchy($this);
+    }
+
+    return $parent;
+  }
+
+
 }
diff --git a/core/modules/menu_link/menu_link.module b/core/modules/menu_link/menu_link.module
index 45e1923..80785f9 100644
--- a/core/modules/menu_link/menu_link.module
+++ b/core/modules/menu_link/menu_link.module
@@ -103,7 +103,7 @@ function menu_link_delete_multiple(array $mlids, $force = FALSE, $prevent_repare
   else {
     $entities = $controller->load($mlids);
   }
-  $controller->preventReparenting($prevent_reparenting);
+  $controller->setPreventReparenting($prevent_reparenting);
   $controller->delete($entities);
 }
 
diff --git a/core/modules/node/lib/Drupal/node/NodeStorageController.php b/core/modules/node/lib/Drupal/node/NodeStorageController.php
index db3ba75..ce2d209 100644
--- a/core/modules/node/lib/Drupal/node/NodeStorageController.php
+++ b/core/modules/node/lib/Drupal/node/NodeStorageController.php
@@ -124,63 +124,6 @@ protected function mapToDataStorageRecord(EntityInterface $entity, $langcode) {
   }
 
   /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::preSave().
-   */
-  protected function preSave(EntityInterface $node) {
-    // Before saving the node, set changed and revision times.
-    $node->changed->value = REQUEST_TIME;
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::preSaveRevision().
-   */
-  protected function preSaveRevision(\stdClass $record, EntityInterface $entity) {
-    if ($entity->isNewRevision()) {
-      // When inserting either a new node or a new node revision, $node->log
-      // must be set because {node_field_revision}.log is a text column and
-      // therefore cannot have a default value. However, it might not be set at
-      // this point (for example, if the user submitting a node form does not
-      // have permission to create revisions), so we ensure that it is at least
-      // an empty string in that case.
-      // @todo Make the {node_field_revision}.log column nullable so that we
-      //   can remove this check.
-      if (!isset($record->log)) {
-        $record->log = '';
-      }
-    }
-    elseif (isset($entity->original) && (!isset($record->log) || $record->log === '')) {
-      // If we are updating an existing node without adding a new revision, we
-      // need to make sure $entity->log is reset whenever it is empty.
-      // Therefore, this code allows us to avoid clobbering an existing log
-      // entry with an empty one.
-      $record->log = $entity->original->log;
-    }
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::postSave().
-   */
-  public function postSave(EntityInterface $node, $update) {
-    // Update the node access table for this node, but only if it is the
-    // default revision. There's no need to delete existing records if the node
-    // is new.
-    if ($node->isDefaultRevision()) {
-      node_access_acquire_grants($node->getBCEntity(), $update);
-    }
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::preDelete().
-   */
-  public function preDelete($entities) {
-    if (module_exists('search')) {
-      foreach ($entities as $id => $entity) {
-        search_reindex($entity->nid->value, 'node');
-      }
-    }
-  }
-
-  /**
    * Overrides Drupal\Core\Entity\DatabaseStorageController::postDelete().
    */
   protected function postDelete($nodes) {
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
index 3343da4..0791287 100644
--- a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
+++ b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
@@ -8,6 +8,7 @@
 namespace Drupal\node\Plugin\Core\Entity;
 
 use Drupal\Core\Entity\EntityNG;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
 use Drupal\node\NodeInterface;
@@ -244,6 +245,52 @@ public function getRevisionId() {
   /**
    * {@inheritdoc}
    */
+  public function preSave(EntityStorageControllerInterface $storage_controller) {
+    // Before saving the node, set changed and revision times.
+    $this->changed->value = REQUEST_TIME;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
+    if ($this->newRevision) {
+      // When inserting either a new node or a new node revision, $node->log
+      // must be set because {node_field_revision}.log is a text column and
+      // therefore cannot have a default value. However, it might not be set at
+      // this point (for example, if the user submitting a node form does not
+      // have permission to create revisions), so we ensure that it is at least
+      // an empty string in that case.
+      // @todo Make the {node_field_revision}.log column nullable so that we
+      //   can remove this check.
+      if (!isset($record->log)) {
+        $record->log = '';
+      }
+    }
+    elseif (isset($this->original) && (!isset($record->log) || $record->log === '')) {
+      // If we are updating an existing node without adding a new revision, we
+      // need to make sure $entity->log is reset whenever it is empty.
+      // Therefore, this code allows us to avoid clobbering an existing log
+      // entry with an empty one.
+      $record->log = $this->original->log;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    // Update the node access table for this node, but only if it is the
+    // default revision. There's no need to delete existing records if the node
+    // is new.
+    if ($this->isDefaultRevision()) {
+      node_access_acquire_grants($this->getBCEntity(), $update);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getBCEntity() {
     if (!isset($this->bcEntity)) {
       $this->getPropertyDefinitions();
@@ -252,4 +299,16 @@ public function getBCEntity() {
     return $this->bcEntity;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    if (module_exists('search')) {
+      foreach ($entities as $id => $entity) {
+        search_reindex($entity->nid->value, 'node');
+      }
+    }
+  }
+
+
 }
diff --git a/core/modules/node/tests/modules/node_test/lib/Drupal/node_test/NodeTest.php b/core/modules/node/tests/modules/node_test/lib/Drupal/node_test/NodeTest.php
new file mode 100644
index 0000000..a77ca76
--- /dev/null
+++ b/core/modules/node/tests/modules/node_test/lib/Drupal/node_test/NodeTest.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node_test\NodeTest.
+ */
+
+namespace Drupal\node_test;
+
+use Drupal\node\Plugin\Core\Entity\Node;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+
+/**
+ * Overrides the default node entity class for testing.
+ */
+class NodeTest extends Node {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageControllerInterface $storage_controller) {
+    // Allow test nodes to specify their updated ('changed') time.
+  }
+
+}
diff --git a/core/modules/node/tests/modules/node_test/lib/Drupal/node_test/NodeTestStorageController.php b/core/modules/node/tests/modules/node_test/lib/Drupal/node_test/NodeTestStorageController.php
deleted file mode 100644
index 3e58980..0000000
--- a/core/modules/node/tests/modules/node_test/lib/Drupal/node_test/NodeTestStorageController.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\node_test\NodeTestStorageController.
- */
-
-namespace Drupal\node_test;
-
-use Drupal\node\NodeStorageController;
-use Drupal\Core\Entity\EntityInterface;
-
-/**
- * Provides a test storage controller for nodes.
- */
-class NodeTestStorageController extends NodeStorageController {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function preSave(EntityInterface $node) {
-    // Allow test nodes to specify their updated ('changed') time.
-  }
-
-}
diff --git a/core/modules/node/tests/modules/node_test/node_test.module b/core/modules/node/tests/modules/node_test/node_test.module
index e65f9bc..12e0eed 100644
--- a/core/modules/node/tests/modules/node_test/node_test.module
+++ b/core/modules/node/tests/modules/node_test/node_test.module
@@ -186,6 +186,6 @@ function node_test_node_insert(EntityInterface $node) {
  */
 function node_test_entity_info_alter(&$entity_info) {
   if (Drupal::state()->get('node_test.storage_controller')) {
-    $entity_info['node']['controllers']['storage'] = 'Drupal\node_test\NodeTestStorageController';
+    $entity_info['node']['class'] = 'Drupal\node_test\NodeTest';
   }
 }
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Form/SetDelete.php b/core/modules/shortcut/lib/Drupal/shortcut/Form/SetDelete.php
index 2b24574..105b382 100644
--- a/core/modules/shortcut/lib/Drupal/shortcut/Form/SetDelete.php
+++ b/core/modules/shortcut/lib/Drupal/shortcut/Form/SetDelete.php
@@ -95,7 +95,7 @@ public function buildForm(array $form, array &$form_state, Shortcut $shortcut =
 
     // Find out how many users are directly assigned to this shortcut set, and
     // make a message.
-    $number = $this->database->query('SELECT COUNT(*) FROM {shortcut_set_users} WHERE set_name = :name', array(':name' => $this->shortcut->id()))->fetchField();
+    $number = \Drupal::entityManager()->getStorageController('shortcut')->countAssignedUsers($shortcut);
     $info = '';
     if ($number) {
       $info .= '<p>' . format_plural($number,
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Plugin/Core/Entity/Shortcut.php b/core/modules/shortcut/lib/Drupal/shortcut/Plugin/Core/Entity/Shortcut.php
index 95802d8..90cadf3 100644
--- a/core/modules/shortcut/lib/Drupal/shortcut/Plugin/Core/Entity/Shortcut.php
+++ b/core/modules/shortcut/lib/Drupal/shortcut/Plugin/Core/Entity/Shortcut.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Config\Entity\ConfigEntityBase;
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\shortcut\ShortcutInterface;
 
 /**
@@ -79,4 +80,65 @@ public function uri() {
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function postCreate(EntityStorageControllerInterface $storage_controller) {
+    // Generate menu-compatible set name.
+    if (!$this->getOriginalID()) {
+      // Save a new shortcut set with links copied from the user's default set.
+      $default_set = shortcut_default_set();
+      // Generate a name to have no collisions with menu.
+      // Size of menu_name is 32 so id could be 23 = 32 - strlen('shortcut-').
+      $id = substr($this->id(), 0, 23);
+      $this->set('id', $id);
+      if ($default_set->id() != $id) {
+        foreach ($default_set->links as $link) {
+          $link = $link->createDuplicate();
+          $link->enforceIsNew();
+          $link->menu_name = $id;
+          $link->save();
+          $this->links[$link->uuid()] = $link;
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSAve(EntityStorageControllerInterface $storage_controller) {
+    // Just store the UUIDs.
+    foreach ($this->links as $uuid => $link) {
+      $this->links[$uuid] = $uuid;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    foreach ($this->links as $uuid) {
+      if ($menu_link = entity_load_by_uuid('menu_link', $uuid)) {
+        // Do not specifically associate these links with the shortcut module,
+        // since other modules may make them editable via the menu system.
+        // However, we do need to specify the correct menu name.
+        $menu_link->menu_name = 'shortcut-' . $this->id();
+        $menu_link->plid = 0;
+        $menu_link->save();
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    foreach ($entities as $entity) {
+      $storage_controller->deleteAssignedShortcutSets($entity);
+      // Next, delete the menu links for this set.
+      menu_delete_links('shortcut-' . $entity->id());
+
+    }
+  }
 }
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutStorageController.php b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutStorageController.php
index 4463a86..95d95bb 100644
--- a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutStorageController.php
+++ b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutStorageController.php
@@ -8,12 +8,12 @@
 namespace Drupal\shortcut;
 
 use Drupal\Core\Config\Entity\ConfigStorageController;
-use Drupal\Core\Entity\EntityInterface;
+use Drupal\shortcut\Plugin\Core\Entity\Shortcut;
 
 /**
  * Defines a storage controller for shortcut entities.
  */
-class ShortcutStorageController extends ConfigStorageController {
+class ShortcutStorageController extends ConfigStorageController implements ShortcutStorageControllerInterface {
 
   /**
    * Overrides \Drupal\config\ConfigStorageController::attachLoad().
@@ -29,81 +29,53 @@ protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
     }
   }
 
+
   /**
-   * Overrides \Drupal\config\ConfigStorageController::create().
+   * {@inheritdoc}
    */
-  public function create(array $values) {
-    $entity = parent::create($values);
-
-    // Generate menu-compatible set name.
-    if (!$entity->getOriginalID()) {
-      // Save a new shortcut set with links copied from the user's default set.
-      $default_set = shortcut_default_set();
-      // Generate a name to have no collisions with menu.
-      // Size of menu_name is 32 so id could be 23 = 32 - strlen('shortcut-').
-      $id = substr($entity->id(), 0, 23);
-      $entity->set('id', $id);
-      if ($default_set->id() != $id) {
-        foreach ($default_set->links as $link) {
-          $link = $link->createDuplicate();
-          $link->enforceIsNew();
-          $link->menu_name = $id;
-          $link->save();
-          $entity->links[$link->uuid()] = $link;
-        }
-      }
-    }
-
-    return $entity;
+  public function deleteAssignedShortcutSets(Shortcut $entity) {
+    // First, delete any user assignments for this set, so that each of these
+    // users will go back to using whatever default set applies.
+    db_delete('shortcut_set_users')
+      ->condition('set_name', $entity->id())
+      ->execute();
   }
 
   /**
-   * Overrides \Drupal\config\ConfigStorageController::preSave().
+   * {@inheritdoc}
    */
-  public function preSave(EntityInterface $entity) {
-    // Just store the UUIDs.
-    foreach ($entity->links as $uuid => $link) {
-      $entity->links[$uuid] = $uuid;
-    }
-
-    parent::preSave($entity);
+  public function assignUser($shortcut_set, $account) {
+    db_merge('shortcut_set_users')
+      ->key(array('uid' => $account->uid))
+      ->fields(array('set_name' => $shortcut_set->id()))
+      ->execute();
+    drupal_static_reset('shortcut_current_displayed_set');
   }
 
   /**
-   * Overrides \Drupal\config\ConfigStorageController::postSave().
+   * {@inheritdoc}
    */
-  function postSave(EntityInterface $entity, $update) {
-    // Process links in shortcut set.
-    foreach ($entity->links as $uuid) {
-      if ($menu_link = entity_load_by_uuid('menu_link', $uuid)) {
-        // Do not specifically associate these links with the shortcut module,
-        // since other modules may make them editable via the menu system.
-        // However, we do need to specify the correct menu name.
-        $menu_link->menu_name = 'shortcut-' . $entity->id();
-        $menu_link->plid = 0;
-        $menu_link->save();
-      }
-    }
-
-    parent::postSave($entity, $update);
+  public function unAssignUser($account) {
+    $deleted = db_delete('shortcut_set_users')
+      ->condition('uid', $account->uid)
+      ->execute();
+    return (bool) $deleted;
   }
 
   /**
-   * Overrides \Drupal\Core\Entity\ConfigStorageController::preDelete().
+   * {@inheritdoc}
    */
-  protected function preDelete($entities) {
-    foreach ($entities as $entity) {
-      // First, delete any user assignments for this set, so that each of these
-      // users will go back to using whatever default set applies.
-      db_delete('shortcut_set_users')
-        ->condition('set_name', $entity->id())
-        ->execute();
-
-      // Next, delete the menu links for this set.
-      menu_delete_links('shortcut-' . $entity->id());
-    }
-
-    parent::preDelete($entities);
+  public function getAssignedToUser($account) {
+    $query = db_select('shortcut_set_users', 'ssu');
+    $query->fields('ssu', array('set_name'));
+    $query->condition('ssu.uid', $account->uid);
+    return $query->execute()->fetchField();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function countAssignedUsers(Shortcut $shortcut) {
+    return db_query('SELECT COUNT(*) FROM {shortcut_set_users} WHERE set_name = :name', array(':name' => $shortcut->id()))->fetchField();
+  }
 }
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutStorageControllerInterface.php b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutStorageControllerInterface.php
new file mode 100644
index 0000000..77e2e4a
--- /dev/null
+++ b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutStorageControllerInterface.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\shortcut\ShortcutStorageControllerInterface.
+ */
+
+namespace Drupal\shortcut;
+
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+use Drupal\shortcut\Plugin\Core\Entity\Shortcut;
+
+/**
+ * Defines a common interface for shortcut entity controller classes.
+ */
+interface ShortcutStorageControllerInterface extends EntityStorageControllerInterface {
+
+  /**
+   * Assigns a user to a particular shortcut set.
+   *
+   * @param \Drupal\shortcut\Plugin\Core\Entity\Shortcut $shortcut_set
+   *   An object representing the shortcut set.
+   * @param $account
+   *   A user account that will be assigned to use the set.
+   */
+  public function assignUser($shortcut_set, $account);
+
+  /**
+   * Unassigns a user from any shortcut set they may have been assigned to.
+   *
+   * The user will go back to using whatever default set applies.
+   *
+   * @param $account
+   *   A user account that will be removed from the shortcut set assignment.
+   *
+   * @return bool
+   *   TRUE if the user was previously assigned to a shortcut set and has been
+   *   successfully removed from it. FALSE if the user was already not assigned
+   *   to any set.
+   */
+  public function unAssignUser($account);
+
+  /**
+   * Delete shortcut sets assigned to users.
+   *
+   * @param \Drupal\shortcut\Plugin\Core\Entity\Shortcut $entity
+   *   Delete the user assigned sets belonging to this shortcut.
+   */
+  public function deleteAssignedShortcutSets(Shortcut $entity);
+
+  /**
+   * Get the set name assigned to this user.
+   *
+   * @param \Drupal\user\Plugin\Core\Entity\User
+   *   The user account.
+   *
+   * @return string
+   *   The name of the shortcut set assigned to this user.
+   */
+  public function getAssignedToUser($account);
+
+  /**
+   * Get the number of users who have this set assigned to them.
+   *
+   * @param \Drupal\shortcut\Plugin\Core\Entity\Shortcut $shortcut
+   *   The shortcut to count the users assigned to.
+   *
+   * @return int
+   *   The number of users who have this set assigned to them.
+   */
+  public function countAssignedUsers(Shortcut $shortcut);
+}
diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module
index 8de14a0..7699958 100644
--- a/core/modules/shortcut/shortcut.module
+++ b/core/modules/shortcut/shortcut.module
@@ -311,11 +311,9 @@ function shortcut_set_reset_link_weights(&$shortcut_set) {
  *   A user account that will be assigned to use the set.
  */
 function shortcut_set_assign_user($shortcut_set, $account) {
-  db_merge('shortcut_set_users')
-    ->key(array('uid' => $account->uid))
-    ->fields(array('set_name' => $shortcut_set->id()))
-    ->execute();
-  drupal_static_reset('shortcut_current_displayed_set');
+  Drupal::entityManager()
+    ->getStorageController('shortcut')
+    ->assignUser($shortcut_set, $account);
 }
 
 /**
@@ -332,10 +330,9 @@ function shortcut_set_assign_user($shortcut_set, $account) {
  *   to any set.
  */
 function shortcut_set_unassign_user($account) {
-  $deleted = db_delete('shortcut_set_users')
-    ->condition('uid', $account->uid)
-    ->execute();
-  return (bool) $deleted;
+  return (bool) Drupal::entityManager()
+    ->getStorageController('shortcut')
+    ->unAssignUser($account);
 }
 
 /**
@@ -362,10 +359,9 @@ function shortcut_current_displayed_set($account = NULL) {
   }
   // If none was found, try to find a shortcut set that is explicitly assigned
   // to this user.
-  $query = db_select('shortcut_set_users', 'ssu');
-  $query->fields('ssu', array('set_name'));
-  $query->condition('ssu.uid', $account->uid);
-  $shortcut_set_name = $query->execute()->fetchField();
+  $shortcut_set_name = Drupal::entityManager()
+    ->getStorageController('shortcut')
+    ->getAssignedToUser($account);
   if ($shortcut_set_name) {
     $shortcut_set = shortcut_set_load($shortcut_set_name);
   }
diff --git a/core/modules/system/lib/Drupal/system/ActionStorageController.php b/core/modules/system/lib/Drupal/system/ActionStorageController.php
deleted file mode 100644
index 9fa0e0f..0000000
--- a/core/modules/system/lib/Drupal/system/ActionStorageController.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\system\ActionStorageController.
- */
-
-namespace Drupal\system;
-
-use Drupal\Core\Action\ConfigurableActionInterface;
-use Drupal\Core\Config\Entity\ConfigStorageController;
-use Drupal\Core\Entity\EntityInterface;
-
-/**
- * Defines the storage controller class for Action entities.
- */
-class ActionStorageController extends ConfigStorageController {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function preSave(EntityInterface $entity) {
-    parent::preSave($entity);
-
-    $plugin = $entity->getPlugin();
-    // If this plugin has any configuration, ensure that it is set.
-    if ($plugin instanceof ConfigurableActionInterface) {
-      $entity->set('configuration', $plugin->getConfiguration());
-    }
-  }
-
-}
diff --git a/core/modules/system/lib/Drupal/system/Plugin/Core/Entity/Action.php b/core/modules/system/lib/Drupal/system/Plugin/Core/Entity/Action.php
index 6086102..582804b 100644
--- a/core/modules/system/lib/Drupal/system/Plugin/Core/Entity/Action.php
+++ b/core/modules/system/lib/Drupal/system/Plugin/Core/Entity/Action.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Config\Entity\ConfigEntityBase;
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\system\ActionConfigEntityInterface;
 use Drupal\Core\Action\ActionBag;
 use Drupal\Core\Action\ConfigurableActionInterface;
@@ -22,7 +23,7 @@
  *   label = @Translation("Action"),
  *   module = "system",
  *   controllers = {
- *     "storage" = "Drupal\system\ActionStorageController"
+ *     "storage" = "Drupal\Core\Config\Entity\ConfigStorageController"
  *   },
  *   config_prefix = "action.action",
  *   entity_keys = {
@@ -176,4 +177,15 @@ public function getExportProperties() {
     return $properties;
   }
 
+    /**
+   * {@inheritdoc}
+   */
+  function preSave(EntityStorageControllerInterface $storage_controller) {
+    $plugin = $this->getPlugin();
+    // If this plugin has any configuration, ensure that it is set.
+    if ($plugin instanceof ConfigurableActionInterface) {
+      $this->set('configuration', $plugin->getConfiguration());
+    }
+  }
+
 }
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php
index 5f3dc78..0bf0aac 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Entity\EntityNG;
 use Drupal\Core\Entity\Annotation\EntityType;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\Core\Annotation\Translation;
 use Drupal\Core\Language\Language;
 use Drupal\taxonomy\TermInterface;
@@ -151,4 +152,46 @@ protected function init() {
     unset($this->description);
     unset($this->parent);
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    // See if any of the term's children are about to be become orphans.
+    $orphans = array();
+    foreach (array_keys($entities) as $tid) {
+      if ($children = taxonomy_term_load_children($tid)) {
+        foreach ($children as $child) {
+          // If the term has multiple parents, we don't delete it.
+          $parents = taxonomy_term_load_parents($child->id());
+          // Because the parent has already been deleted, the parent count might
+          // be 0.
+          if (count($parents) <= 1) {
+            $orphans[] = $child->id();
+          }
+        }
+      }
+    }
+
+    // Delete term hierarchy information after looking up orphans but before
+    // deleting them so that their children/parent information is consistent.
+    $storage_controller->deleteTermHierarchy(array_keys($entities));
+
+    if (!empty($orphans)) {
+      entity_delete_multiple('taxonomy_term', $orphans);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    // Only change the parents if a value is set, keep the existing values if
+    // not.
+    if (isset($this->parent->value)) {
+      $storage_controller->deleteTermHierarchy(array($this->id()));
+      $storage_controller->updateTermHierarchy($this);
+    }
+  }
+
 }
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Vocabulary.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Vocabulary.php
index 31d2e2f..dab74c6 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Vocabulary.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Vocabulary.php
@@ -8,6 +8,7 @@
 namespace Drupal\taxonomy\Plugin\Core\Entity;
 
 use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
 use Drupal\taxonomy\VocabularyInterface;
@@ -104,4 +105,78 @@ public function uri() {
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    if (!$update) {
+      entity_invoke_bundle_hook('create', 'taxonomy_term', $this->id());
+    }
+    elseif ($this->getOriginalID() != $this->id()) {
+      // Reflect machine name changes in the definitions of existing 'taxonomy'
+      // fields.
+      $fields = field_read_fields();
+      foreach ($fields as $field_name => $field) {
+        $update_field = FALSE;
+        if ($field['type'] == 'taxonomy_term_reference') {
+          foreach ($field['settings']['allowed_values'] as $key => &$value) {
+            if ($value['vocabulary'] == $this->getOriginalID()) {
+              $value['vocabulary'] = $this->id();
+              $update_field = TRUE;
+            }
+          }
+          if ($update_field) {
+            field_update_field($field);
+          }
+        }
+      }
+      // Update bundles.
+      entity_invoke_bundle_hook('rename', 'taxonomy_term', $this->getOriginalID(), $this->id());
+    }
+    $storage_controller->resetCache($update ? array($this->getOriginalID()) : array());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    // Only load terms without a parent, child terms will get deleted too.
+    entity_delete_multiple('taxonomy_term', $storage_controller->getToplevelTids(array_keys($entities)));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    $vocabularies = array();
+    foreach ($entities as $vocabulary) {
+      $vocabularies[$vocabulary->id()] = $vocabulary->id();
+    }
+    // Load all Taxonomy module fields and delete those which use only this
+    // vocabulary.
+    $taxonomy_fields = field_read_fields(array('module' => 'taxonomy'));
+    foreach ($taxonomy_fields as $field_name => $taxonomy_field) {
+      $modified_field = FALSE;
+      // Term reference fields may reference terms from more than one
+      // vocabulary.
+      foreach ($taxonomy_field['settings']['allowed_values'] as $key => $allowed_value) {
+        if (isset($vocabularies[$allowed_value['vocabulary']])) {
+          unset($taxonomy_field['settings']['allowed_values'][$key]);
+          $modified_field = TRUE;
+        }
+      }
+      if ($modified_field) {
+        if (empty($taxonomy_field['settings']['allowed_values'])) {
+          field_delete_field($field_name);
+        }
+        else {
+          // Update the field definition with the new allowed values.
+          field_update_field($taxonomy_field);
+        }
+      }
+    }
+    // Reset caches.
+    $storage_controller->resetCache(array_keys($vocabularies));
+  }
+
 }
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TermStorageController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermStorageController.php
index 9bb08c0..58860a0 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/TermStorageController.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermStorageController.php
@@ -14,7 +14,7 @@
 /**
  * Defines a Controller class for taxonomy terms.
  */
-class TermStorageController extends DatabaseStorageControllerNG {
+class TermStorageController extends DatabaseStorageControllerNG implements TermStorageControllerInterface {
 
   /**
    * Overrides Drupal\Core\Entity\DatabaseStorageController::create().
@@ -44,61 +44,6 @@ protected function buildPropertyQuery(QueryInterface $entity_query, array $value
   }
 
   /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::postDelete().
-   */
-  protected function postDelete($entities) {
-    // See if any of the term's children are about to be become orphans.
-    $orphans = array();
-    foreach (array_keys($entities) as $tid) {
-      if ($children = taxonomy_term_load_children($tid)) {
-        foreach ($children as $child) {
-          // If the term has multiple parents, we don't delete it.
-          $parents = taxonomy_term_load_parents($child->id());
-          // Because the parent has already been deleted, the parent count might
-          // be 0.
-          if (count($parents) <= 1) {
-            $orphans[] = $child->id();
-          }
-        }
-      }
-    }
-
-    // Delete term hierarchy information after looking up orphans but before
-    // deleting them so that their children/parent information is consistent.
-    db_delete('taxonomy_term_hierarchy')
-      ->condition('tid', array_keys($entities))
-      ->execute();
-
-    if (!empty($orphans)) {
-      entity_delete_multiple('taxonomy_term', $orphans);
-    }
-  }
-
-  /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::postSave().
-   */
-  protected function postSave(EntityInterface $entity, $update) {
-    // Only change the parents if a value is set, keep the existing values if
-    // not.
-    if (isset($entity->parent->value)) {
-      db_delete('taxonomy_term_hierarchy')
-        ->condition('tid', $entity->id())
-        ->execute();
-
-      $query = db_insert('taxonomy_term_hierarchy')
-        ->fields(array('tid', 'parent'));
-
-      foreach ($entity->parent as $parent) {
-        $query->values(array(
-          'tid' => $entity->id(),
-          'parent' => (int) $parent->value,
-        ));
-      }
-      $query->execute();
-    }
-  }
-
-  /**
    * Overrides Drupal\Core\Entity\DatabaseStorageController::resetCache().
    */
   public function resetCache(array $ids = NULL) {
@@ -167,4 +112,30 @@ public function baseFieldDefinitions() {
     );
     return $properties;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteTermHierarchy($tids) {
+    $this->database->delete('taxonomy_term_hierarchy')
+      ->condition('tid', $tids)
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateTermHierarchy(EntityInterface $term) {
+    $query = $this->database->insert('taxonomy_term_hierarchy')
+      ->fields(array('tid', 'parent'));
+
+    foreach ($term->parent as $parent) {
+      $query->values(array(
+        'tid' => $term->id(),
+        'parent' => (int) $parent->value,
+      ));
+    }
+    $query->execute();
+  }
+
 }
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TermStorageControllerInterface.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermStorageControllerInterface.php
new file mode 100644
index 0000000..732a58f
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermStorageControllerInterface.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\taxonomy\TermStorageControllerInterface.
+*/
+
+namespace Drupal\taxonomy;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+
+/**
+ * Defines a common interface for taxonomy term entity controller classes.
+ */
+interface TermStorageControllerInterface extends EntityStorageControllerInterface {
+
+  /**
+   * Removed reference to terms from term_hierarchy.
+   *
+   * @param array
+   *   Array of terms that need to be removed from hierarchy.
+   */
+  public function deleteTermHierarchy($tids);
+
+  /**
+   * Updates terms hierarchy information with the hierarchy trail of it.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $term
+   *   Term entity that needs to be added to term hierarchy information.
+   */
+  public function updateTermHierarchy(EntityInterface $term);
+
+}
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyStorageController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyStorageController.php
index c0e377b..a60f140 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyStorageController.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyStorageController.php
@@ -8,91 +8,11 @@
 namespace Drupal\taxonomy;
 
 use Drupal\Core\Config\Entity\ConfigStorageController;
-use Drupal\Core\Entity\EntityInterface;
 
 /**
  * Defines a controller class for taxonomy vocabularies.
  */
-class VocabularyStorageController extends ConfigStorageController {
-
-  /**
-   * Overrides Drupal\Core\Config\Entity\ConfigStorageController::postSave().
-   */
-  protected function postSave(EntityInterface $entity, $update) {
-    if (!$update) {
-      entity_invoke_bundle_hook('create', 'taxonomy_term', $entity->id());
-    }
-    elseif ($entity->getOriginalID() != $entity->id()) {
-      // Reflect machine name changes in the definitions of existing 'taxonomy'
-      // fields.
-      $fields = field_read_fields();
-      foreach ($fields as $field_name => $field) {
-        $update_field = FALSE;
-        if ($field['type'] == 'taxonomy_term_reference') {
-          foreach ($field['settings']['allowed_values'] as $key => &$value) {
-            if ($value['vocabulary'] == $entity->getOriginalID()) {
-              $value['vocabulary'] = $entity->id();
-              $update_field = TRUE;
-            }
-          }
-          if ($update_field) {
-            field_update_field($field);
-          }
-        }
-      }
-      // Update bundles.
-      entity_invoke_bundle_hook('rename', 'taxonomy_term', $entity->getOriginalID(), $entity->id());
-    }
-    parent::postSave($entity, $update);
-    $this->resetCache($update ? array($entity->getOriginalID()) : array());
-  }
-
-  /**
-   * Overrides Drupal\Core\Config\Entity\ConfigStorageController::preDelete().
-   */
-  protected function preDelete($entities) {
-    parent::preDelete($entities);
-    // Only load terms without a parent, child terms will get deleted too.
-    $tids = db_query('SELECT t.tid FROM {taxonomy_term_data} t INNER JOIN {taxonomy_term_hierarchy} th ON th.tid = t.tid WHERE t.vid IN (:vids) AND th.parent = 0', array(':vids' => array_keys($entities)))->fetchCol();
-    entity_delete_multiple('taxonomy_term', $tids);
-  }
-
-  /**
-   * Overrides Drupal\Core\Config\Entity\ConfigStorageController::postDelete().
-   */
-  protected function postDelete($entities) {
-    parent::postDelete($entities);
-
-    $vocabularies = array();
-    foreach ($entities as $vocabulary) {
-      $vocabularies[$vocabulary->id()] = $vocabulary->id();
-    }
-    // Load all Taxonomy module fields and delete those which use only this
-    // vocabulary.
-    $taxonomy_fields = field_read_fields(array('module' => 'taxonomy'));
-    foreach ($taxonomy_fields as $field_name => $taxonomy_field) {
-      $modified_field = FALSE;
-      // Term reference fields may reference terms from more than one
-      // vocabulary.
-      foreach ($taxonomy_field['settings']['allowed_values'] as $key => $allowed_value) {
-        if (isset($vocabularies[$allowed_value['vocabulary']])) {
-          unset($taxonomy_field['settings']['allowed_values'][$key]);
-          $modified_field = TRUE;
-        }
-      }
-      if ($modified_field) {
-        if (empty($taxonomy_field['settings']['allowed_values'])) {
-          field_delete_field($field_name);
-        }
-        else {
-          // Update the field definition with the new allowed values.
-          field_update_field($taxonomy_field);
-        }
-      }
-    }
-    // Reset caches.
-    $this->resetCache(array_keys($vocabularies));
-  }
+class VocabularyStorageController extends ConfigStorageController implements VocabularyStorageControllerInterface {
 
   /**
    * Overrides Drupal\Core\Config\Entity\ConfigStorageController::resetCache().
@@ -104,4 +24,11 @@ public function resetCache(array $ids = NULL) {
     entity_info_cache_clear();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getToplevelTids($vids) {
+    return db_query('SELECT t.tid FROM {taxonomy_term_data} t INNER JOIN {taxonomy_term_hierarchy} th ON th.tid = t.tid WHERE t.vid IN (:vids) AND th.parent = 0', array(':vids' => $vids))->fetchCol();
+  }
+
 }
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyStorageControllerInterface.php b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyStorageControllerInterface.php
new file mode 100644
index 0000000..b9e82ef
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyStorageControllerInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\taxonomy\VocabularyStorageControllerInterface.
+ */
+
+namespace Drupal\taxonomy;
+
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+
+/**
+ * Defines a common interface for taxonomy vocabulary entity controller classes.
+ */
+interface VocabularyStorageControllerInterface extends EntityStorageControllerInterface {
+
+  /**
+   * Gets top-level term IDs of vocabularies.
+   *
+   * @param array $vids
+   *   Array of vocabulary IDs.
+   *
+   * @return array
+   *   Array of top-level term IDs.
+   */
+  public function getToplevelTids($vids);
+
+}
diff --git a/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/Role.php b/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/Role.php
index 5a288c8..f24f7e9 100644
--- a/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/Role.php
+++ b/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/Role.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
 use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\user\RoleInterface;
 
 /**
@@ -78,4 +79,23 @@ public function uri() {
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageControllerInterface $storage_controller) {
+    if (!isset($this->weight) && ($roles = $storage_controller->load())) {
+      // Set a role weight to make this new role last.
+      $max = array_reduce($roles, function($max, $role) {
+        return $max > $role->weight ? $max : $role->weight;
+      });
+      $this->weight = $max + 1;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    $storage_controller->deleteRoleReferences(array_keys($entities));
+  }
 }
diff --git a/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php b/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php
index 1f55b49..ffc4d0e 100644
--- a/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php
+++ b/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\user\Plugin\Core\Entity;
 
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+use Drupal\Core\Entity\EntityMalformedException;
 use Drupal\Core\Entity\EntityNG;
 use Drupal\Core\Entity\Annotation\EntityType;
 use Drupal\Core\Annotation\Translation;
@@ -231,6 +233,96 @@ protected function init() {
   /**
    * {@inheritdoc}
    */
+  static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
+    if (!isset($values['created'])) {
+      $values['created'] = REQUEST_TIME;
+    }
+    // Users always have the authenticated user role.
+    $values['roles'][] = DRUPAL_AUTHENTICATED_RID;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageControllerInterface $storage_controller) {
+    // Update the user password if it has changed.
+    if ($this->isNew() || ($this->pass->value && $this->pass->value != $this->original->pass->value)) {
+      // Allow alternate password hashing schemes.
+      $this->pass->value = \Drupal::service('password')->hash(trim($this->pass->value));
+      // Abort if the hashing failed and returned FALSE.
+      if (!$this->pass->value) {
+        throw new EntityMalformedException('The entity does not have a password.');
+      }
+    }
+
+    if (!$this->isNew()) {
+      // If the password is empty, that means it was not changed, so use the
+      // original password.
+      if (empty($this->pass->value)) {
+        $this->pass->value = $this->original->pass->value;
+      }
+    }
+
+    // Store account cancellation information.
+    foreach (array('user_cancel_method', 'user_cancel_notify') as $key) {
+      if (isset($this->{$key})) {
+        \Drupal::service('user.data')->set('user', $this->id(), substr($key, 5), $this->{$key});
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    if ($update) {
+      // If the password has been changed, delete all open sessions for the
+      // user and recreate the current one.
+      if ($this->pass->value != $this->original->pass->value) {
+        drupal_session_destroy_uid($this->id());
+        if ($this->id() == $GLOBALS['user']->uid) {
+          drupal_session_regenerate();
+        }
+      }
+
+      // Update user roles if changed.
+      if ($this->roles->getValue() != $this->original->roles->getValue()) {
+        $storage_controller->deleteUserRoles(array($this->id()));
+        $storage_controller->saveRoles($this);
+      }
+
+      // If the user was blocked, delete the user's sessions to force a logout.
+      if ($this->original->status->value != $this->status->value && $this->status->value == 0) {
+        drupal_session_destroy_uid($this->id());
+      }
+
+      // Send emails after we have the new user object.
+      if ($this->status->value != $this->original->status->value) {
+        // The user's status is changing; conditionally send notification email.
+        $op = $this->status->value == 1 ? 'status_activated' : 'status_blocked';
+        _user_mail_notify($op, $this->getBCEntity());
+      }
+    }
+    else {
+      // Save user roles.
+      if (count($this->roles) > 1) {
+        $storage_controller->saveRoles($this);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    $uids = array_keys($entities);
+    \Drupal::service('user.data')->delete(NULL, $uids);
+    $storage_controller->deleteUserRoles($uids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getBCEntity() {
     if (!isset($this->bcEntity)) {
       // Initialize field definitions so that we can pass them by reference.
diff --git a/core/modules/user/lib/Drupal/user/RoleStorageController.php b/core/modules/user/lib/Drupal/user/RoleStorageController.php
index 680b412..4b6ca87 100644
--- a/core/modules/user/lib/Drupal/user/RoleStorageController.php
+++ b/core/modules/user/lib/Drupal/user/RoleStorageController.php
@@ -8,26 +8,11 @@
 namespace Drupal\user;
 
 use Drupal\Core\Config\Entity\ConfigStorageController;
-use Drupal\Core\Entity\EntityInterface;
 
 /**
  * Controller class for user roles.
  */
-class RoleStorageController extends ConfigStorageController {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function preSave(EntityInterface $entity) {
-    if (!isset($entity->weight) && $roles = entity_load_multiple('user_role')) {
-      // Set a role weight to make this new role last.
-      $max = array_reduce($roles, function($max, $entity) {
-        return $max > $entity->weight ? $max : $entity->weight;
-      });
-      $entity->weight = $max + 1;
-    }
-    parent::preSave($entity);
-  }
+class RoleStorageController extends ConfigStorageController implements RoleStorageControllerInterface {
 
   /**
    * {@inheritdoc}
@@ -43,9 +28,7 @@ public function resetCache(array $ids = NULL) {
   /**
    * {@inheritdoc}
    */
-  protected function postDelete($entities) {
-    $rids = array_keys($entities);
-
+  public function deleteRoleReferences(array $rids) {
     // Delete permission assignments.
     db_delete('role_permission')
       ->condition('rid', $rids)
diff --git a/core/modules/user/lib/Drupal/user/RoleStorageControllerInterface.php b/core/modules/user/lib/Drupal/user/RoleStorageControllerInterface.php
new file mode 100644
index 0000000..651beca
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/RoleStorageControllerInterface.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\RoleStorageControllerInterface.
+ */
+
+namespace Drupal\user;
+
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+
+/**
+ * Defines a common interface for roel entity controller classes.
+ */
+interface RoleStorageControllerInterface extends EntityStorageControllerInterface {
+
+  /**
+   * Delete role references.
+   *
+   * @param array $rids
+   *   The list of role IDs being deleted. The storage controller should
+   *   remove permission and user references to this role.
+   */
+  function deleteRoleReferences(array $rids);
+}
diff --git a/core/modules/user/lib/Drupal/user/UserStorageController.php b/core/modules/user/lib/Drupal/user/UserStorageController.php
index c8d40fd..897c543 100644
--- a/core/modules/user/lib/Drupal/user/UserStorageController.php
+++ b/core/modules/user/lib/Drupal/user/UserStorageController.php
@@ -21,7 +21,7 @@
  * This extends the Drupal\Core\Entity\DatabaseStorageController class, adding
  * required special handling for user objects.
  */
-class UserStorageController extends DatabaseStorageControllerNG {
+class UserStorageController extends DatabaseStorageControllerNG implements UserStorageControllerInterface {
 
   /**
    * Provides the password hashing service object.
@@ -86,10 +86,7 @@ function attachLoad(&$queried_users, $load_revision = FALSE) {
     }
 
     // Add any additional roles from the database.
-    $result = db_query('SELECT rid, uid FROM {users_roles} WHERE uid IN (:uids)', array(':uids' => array_keys($queried_users)));
-    foreach ($result as $record) {
-      $queried_users[$record->uid]->roles[] = $record->rid;
-    }
+    $this->addRoles($queried_users);
 
     // Call the default attachLoad() method. This will add fields and call
     // hook_user_load().
@@ -97,15 +94,9 @@ function attachLoad(&$queried_users, $load_revision = FALSE) {
   }
 
   /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::create().
+   * {@inheritdoc}
    */
-  public function create(array $values) {
-    if (!isset($values['created'])) {
-      $values['created'] = REQUEST_TIME;
-    }
-    // Users always have the authenticated user role.
-    $values['roles'][] = DRUPAL_AUTHENTICATED_RID;
-
+  function create(array $values) {
     return parent::create($values)->getBCEntity();
   }
 
@@ -126,106 +117,38 @@ public function save(EntityInterface $entity) {
   }
 
   /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::preSave().
+   * {@inheritdoc}
    */
-  protected function preSave(EntityInterface $entity) {
-    // Update the user password if it has changed.
-    if ($entity->isNew() || ($entity->pass->value && $entity->pass->value != $entity->original->pass->value)) {
-      // Allow alternate password hashing schemes.
-      $entity->pass->value = $this->password->hash(trim($entity->pass->value));
-      // Abort if the hashing failed and returned FALSE.
-      if (!$entity->pass->value) {
-        throw new EntityMalformedException('The entity does not have a password.');
-      }
-    }
-
-    if (!$entity->isNew()) {
-      // If the password is empty, that means it was not changed, so use the
-      // original password.
-      if (empty($entity->pass->value)) {
-        $entity->pass->value = $entity->original->pass->value;
-      }
-    }
-
-    // Store account cancellation information.
-    foreach (array('user_cancel_method', 'user_cancel_notify') as $key) {
-      if (isset($entity->{$key})) {
-        $this->userData->set('user', $entity->id(), substr($key, 5), $entity->{$key});
+  public function saveRoles(EntityInterface $user) {
+    $query = $this->database->insert('users_roles')->fields(array('uid', 'rid'));
+    foreach ($user->roles as $role) {
+      if (!in_array($role->value, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
+        $query->values(array(
+          'uid' => $user->id(),
+          'rid' => $role->value,
+        ));
       }
     }
+    $query->execute();
   }
 
   /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::postSave().
+   * {@inheritdoc}
    */
-  protected function postSave(EntityInterface $entity, $update) {
-
-    if ($update) {
-      // If the password has been changed, delete all open sessions for the
-      // user and recreate the current one.
-      if ($entity->pass->value != $entity->original->pass->value) {
-        drupal_session_destroy_uid($entity->id());
-        if ($entity->id() == $GLOBALS['user']->uid) {
-          drupal_session_regenerate();
-        }
-      }
-
-      // Update user roles if changed.
-      if ($entity->roles->getValue() != $entity->original->roles->getValue()) {
-        db_delete('users_roles')
-          ->condition('uid', $entity->id())
-          ->execute();
-
-        $query = $this->database->insert('users_roles')->fields(array('uid', 'rid'));
-        foreach ($entity->roles as $role) {
-          if (!in_array($role->value, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
-
-            $query->values(array(
-              'uid' => $entity->id(),
-              'rid' => $role->value,
-            ));
-          }
-        }
-        $query->execute();
-      }
-
-      // If the user was blocked, delete the user's sessions to force a logout.
-      if ($entity->original->status->value != $entity->status->value && $entity->status->value == 0) {
-        drupal_session_destroy_uid($entity->id());
-      }
-
-      // Send emails after we have the new user object.
-      if ($entity->status->value != $entity->original->status->value) {
-        // The user's status is changing; conditionally send notification email.
-        $op = $entity->status->value == 1 ? 'status_activated' : 'status_blocked';
-        _user_mail_notify($op, $entity->getBCEntity());
-      }
-    }
-    else {
-      // Save user roles.
-      if (count($entity->roles) > 1) {
-        $query = $this->database->insert('users_roles')->fields(array('uid', 'rid'));
-        foreach ($entity->roles as $role) {
-          if (!in_array($role->value, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
-            $query->values(array(
-              'uid' => $entity->id(),
-              'rid' => $role->value,
-            ));
-          }
-        }
-        $query->execute();
-      }
+  public function addRoles(array $users) {
+    $result = db_query('SELECT rid, uid FROM {users_roles} WHERE uid IN (:uids)', array(':uids' => array_keys($users)));
+    foreach ($result as $record) {
+      $users[$record->uid]->roles[] = $record->rid;
     }
   }
 
   /**
-   * Overrides Drupal\Core\Entity\DatabaseStorageController::postDelete().
+   * {@inheritdoc}
    */
-  protected function postDelete($entities) {
+  public function deleteUserRoles(array $uids) {
     $this->database->delete('users_roles')
-      ->condition('uid', array_keys($entities), 'IN')
+      ->condition('uid', $uids)
       ->execute();
-    $this->userData->delete(NULL, array_keys($entities));
   }
 
   /**
diff --git a/core/modules/user/lib/Drupal/user/UserStorageControllerInterface.php b/core/modules/user/lib/Drupal/user/UserStorageControllerInterface.php
new file mode 100644
index 0000000..8768869
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/UserStorageControllerInterface.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\UserStorageControllerInterface.
+ */
+
+namespace Drupal\user;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+
+/**
+ * Defines a common interface for user entity controller classes.
+ */
+interface UserStorageControllerInterface extends EntityStorageControllerInterface {
+
+  /**
+   * Add any roles from the storage to the user.
+   *
+   * @param array $users
+   */
+  public function addRoles(array $users);
+
+  /**
+   * Save the user's roles.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $user
+   */
+  public function saveRoles(EntityInterface $user);
+
+  /**
+   * Remove the roles of a user.
+   *
+   * @param array $uids
+   */
+  public function deleteUserRoles(array $uids);
+
+}
diff --git a/core/modules/views/lib/Drupal/views/Plugin/Core/Entity/View.php b/core/modules/views/lib/Drupal/views/Plugin/Core/Entity/View.php
index 6e65758..f655945 100644
--- a/core/modules/views/lib/Drupal/views/Plugin/Core/Entity/View.php
+++ b/core/modules/views/lib/Drupal/views/Plugin/Core/Entity/View.php
@@ -8,6 +8,7 @@
 namespace Drupal\views\Plugin\Core\Entity;
 
 use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\views\Views;
 use Drupal\views_ui\ViewUI;
 use Drupal\views\ViewStorageInterface;
@@ -361,4 +362,63 @@ public function getExportProperties() {
     return $properties;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    views_invalidate_cache();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
+    // If there is no information about displays available add at least the
+    // default display.
+    $values += array(
+      'display' => array(
+        'default' => array(
+          'display_plugin' => 'default',
+          'id' => 'default',
+          'display_title' => 'Master',
+          'position' => 0,
+          'display_options' => array(),
+        ),
+      )
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postCreate(EntityStorageControllerInterface $storage_controller) {
+    $this->mergeDefaultDisplaysOptions();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function mergeDefaultDisplaysOptions() {
+    $displays = array();
+    foreach ($this->get('display') as $key => $options) {
+      $options += array(
+        'display_options' => array(),
+        'display_plugin' => NULL,
+        'id' => NULL,
+        'display_title' => '',
+        'position' => NULL,
+      );
+      // Add the defaults for the display.
+      $displays[$key] = $options;
+    }
+    // Sort the displays.
+    uasort($displays, function ($display1, $display2) {
+      if ($display1['position'] != $display2['position']) {
+        return $display1['position'] < $display2['position'] ? -1 : 1;
+      }
+      return 0;
+    });
+    $this->set('display', $displays);
+  }
+
 }
diff --git a/core/modules/views/lib/Drupal/views/ViewStorageController.php b/core/modules/views/lib/Drupal/views/ViewStorageController.php
index 3ac6731..a7e1319 100644
--- a/core/modules/views/lib/Drupal/views/ViewStorageController.php
+++ b/core/modules/views/lib/Drupal/views/ViewStorageController.php
@@ -35,72 +35,11 @@ public function load(array $ids = NULL) {
    */
   protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
     foreach ($queried_entities as $id => $entity) {
-      $this->mergeDefaultDisplaysOptions($entity);
+      $entity->mergeDefaultDisplaysOptions();
     }
 
     parent::attachLoad($queried_entities, $revision_id);
   }
 
-  /**
-   * Overrides Drupal\config\ConfigStorageController::postSave().
-   */
-  protected function postSave(EntityInterface $entity, $update) {
-    parent::postSave($entity, $update);
-    // Clear caches.
-    views_invalidate_cache();
-  }
-
-  /**
-   * Overrides Drupal\config\ConfigStorageController::create().
-   */
-  public function create(array $values) {
-    // If there is no information about displays available add at least the
-    // default display.
-    $values += array(
-      'display' => array(
-        'default' => array(
-          'display_plugin' => 'default',
-          'id' => 'default',
-          'display_title' => 'Master',
-          'position' => 0,
-          'display_options' => array(),
-        ),
-      )
-    );
-
-    $entity = parent::create($values);
-
-    $this->mergeDefaultDisplaysOptions($entity);
-    return $entity;
-  }
-
-  /**
-   * Add defaults to the display options.
-   *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   The view entity to attach default displays options.
-   */
-  protected function mergeDefaultDisplaysOptions(EntityInterface $entity) {
-    $displays = array();
-    foreach ($entity->get('display') as $key => $options) {
-      $options += array(
-        'display_options' => array(),
-        'display_plugin' => NULL,
-        'id' => NULL,
-        'display_title' => '',
-        'position' => NULL,
-      );
-      // Add the defaults for the display.
-      $displays[$key] = $options;
-    }
-    // Sort the displays.
-    uasort($displays, function ($display1, $display2) {
-      if ($display1['position'] != $display2['position']) {
-        return $display1['position'] < $display2['position'] ? -1 : 1;
-      }
-      return 0;
-    });
-    $entity->set('display', $displays);
-  }
 
 }
diff --git a/core/modules/views/lib/Drupal/views/ViewStorageInterface.php b/core/modules/views/lib/Drupal/views/ViewStorageInterface.php
index b09acb2..7009bae 100644
--- a/core/modules/views/lib/Drupal/views/ViewStorageInterface.php
+++ b/core/modules/views/lib/Drupal/views/ViewStorageInterface.php
@@ -25,4 +25,8 @@
    */
   public function &getDisplay($display_id);
 
+  /**
+   * Add defaults to the display options.
+   */
+  public function mergeDefaultDisplaysOptions();
 }
diff --git a/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php b/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php
index cdc3bd7..93a3443 100644
--- a/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php
+++ b/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php
@@ -8,6 +8,7 @@
 namespace Drupal\views_ui;
 
 use Drupal\views\Views;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\views\ViewExecutable;
 use Drupal\Core\Database\Database;
 use Drupal\Core\TypedData\TypedDataInterface;
@@ -1139,9 +1140,66 @@ public function onChange($property_name) {
   /**
    * {@inheritdoc}
    */
-  public function uriRelationships() {
-    return $this->storage->uriRelationships();
+  public function preSave(EntityStorageControllerInterface $storage_controller) {
+    $this->storage->presave($storage_controller);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    $this->storage->postSave($storage_controller, $update);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preCreate(EntityStorageControllerInterface $storage_controller, array &$values) {
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function postCreate(EntityStorageControllerInterface $storage_controller) {
+    $this->storage->postCreate($storage_controller);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+  }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postLoad(EntityStorageControllerInterface $storage_controller, array $entities) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
+    $this->storage->preSaveRevision($storage_controller, $record);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function mergeDefaultDisplaysOptions() {
+    $this->storage->mergeDefaultDisplaysOptions();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function uriRelationships() {
+    return $this->storage->uriRelationships();
+  }
 }
