From d7fa47b49bb0f85bee51453c37cf7c0bccd75900 Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Sun, 22 Sep 2013 09:31:05 +0100
Subject: [PATCH] Issue #1605290 by amateescu, msonnabaum, Caseledde,
 damiankloip, beejeebus, catch, Berdir, Wim Leers: Enable entity render
 caching with cache tag support.

---
 core/includes/common.inc                      |  46 ++++-
 core/includes/entity.inc                      |  38 +++-
 .../Core/Entity/Annotation/EntityType.php     |   8 +
 core/lib/Drupal/Core/Entity/Entity.php        |  48 +++++
 .../Drupal/Core/Entity/EntityInterface.php    |  13 ++
 .../Core/Entity/EntityRenderController.php    |  64 ++++++-
 .../EntityRenderControllerInterface.php       |  10 +
 .../Tests/Views/IntegrationTest.php           |   2 +-
 .../entity.view_mode.custom_block.full.yml    |   1 +
 .../Drupal/block/BlockRenderController.php    |   5 +
 .../config/entity.view_mode.node.print.yml    |   1 +
 .../config/entity.view_mode.comment.full.yml  |   1 +
 .../comment/Controller/CommentController.php  |   1 +
 .../lib/Drupal/comment/Entity/Comment.php     |   1 +
 .../editor/Tests/EditorFileUsageTest.php      |  19 +-
 .../lib/Drupal/entity/EntityDisplayBase.php   |  14 ++
 .../Drupal/entity/EntityDisplayModeBase.php   |   7 +
 .../Plugin/Type/Formatter/FormatterBase.php   |  13 ++
 .../Drupal/field/Tests/FieldUnitTestBase.php  |   3 +-
 .../lib/Drupal/filter/Tests/FilterAPITest.php |   1 -
 .../config/entity.view_mode.node.full.yml     |   1 +
 .../node/config/entity.view_mode.node.rss.yml |   1 +
 .../config/entity.view_mode.node.teaser.yml   |   1 +
 .../node/lib/Drupal/node/Entity/Node.php      |   1 +
 .../lib/Drupal/node/NodeRenderController.php  |   4 +
 .../Tests/Condition/NodeConditionTest.php     |  19 +-
 .../Drupal/node/Tests/NodeLastChangedTest.php |   3 +-
 .../Drupal/node/Tests/NodeValidationTest.php  |  13 +-
 core/modules/node/node.module                 |   2 +
 .../rdf/lib/Drupal/rdf/Entity/RdfMapping.php  |  12 ++
 .../entity.view_mode.node.search_index.yml    |   1 +
 .../entity.view_mode.node.search_result.yml   |   1 +
 .../Tests/NormalizerTestBase.php              |   3 +-
 core/modules/statistics/statistics.module     |  11 ++
 .../Form/DateFormatLocalizeResetForm.php      |   1 +
 .../system/Tests/Action/ActionUnitTest.php    |   2 +-
 .../Drupal/system/Tests/Common/RenderTest.php |  17 ++
 .../system/Tests/Entity/EntityAccessTest.php  |   1 -
 .../Tests/Entity/EntityCrudHookTest.php       |   2 +-
 .../system/Tests/Entity/EntityFieldTest.php   |   2 +-
 .../system/Tests/Entity/EntityRenderTest.php  | 177 ++++++++++++++++++
 .../Tests/Entity/EntityUnitTestBase.php       |   2 +-
 .../Tests/Entity/EntityValidationTest.php     |   2 +-
 .../Tests/Entity/EntityViewControllerTest.php |   2 +-
 .../system/Tests/TypedData/TypedDataTest.php  |   2 +-
 ...tity.view_mode.entity_test_render.full.yml |   1 +
 ...tity.view_mode.entity_test_render.test.yml |   1 +
 .../modules/entity_test/entity_test.module    |   4 +-
 .../entity_test/Entity/EntityTestLabel.php    |   4 +-
 .../entity.view_mode.taxonomy_term.full.yml   |   1 +
 .../lib/Drupal/taxonomy/Entity/Term.php       |   2 +-
 .../config/entity.view_mode.user.compact.yml  |   1 +
 .../config/entity.view_mode.user.full.yml     |   1 +
 .../Drupal/user/Tests/UserValidationTest.php  |   2 +-
 .../user/Tests/Views/UserUnitTestBase.php     |   2 +-
 .../Tests/Plugin/RelationshipJoinTestBase.php |   2 +-
 .../views_ui/lib/Drupal/views_ui/ViewUI.php   |  15 ++
 57 files changed, 558 insertions(+), 57 deletions(-)
 create mode 100644 core/modules/system/lib/Drupal/system/Tests/Entity/EntityRenderTest.php

diff --git a/core/includes/common.inc b/core/includes/common.inc
index 1c5dd17092b9..df552ed01e4c 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -4139,7 +4139,7 @@ function drupal_render_cache_set(&$markup, $elements) {
   }
   $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'cache';
   $expire = isset($elements['#cache']['expire']) ? $elements['#cache']['expire'] : CacheBackendInterface::CACHE_PERMANENT;
-  $tags = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array();
+  $tags = drupal_render_collect_cache_tags($elements);
   cache($bin)->set($cid, $data, $expire, $tags);
 }
 
@@ -4186,6 +4186,50 @@ function drupal_render_collect_attached($elements, $return = FALSE) {
   }
 }
 
+/**
+ * Collects cache tags for an element and its children into a single array.
+ *
+ * The cache tags array is returned in a format that is valid for
+ * \Drupal\Core\Cache\CacheBackendInterface::set().
+ *
+ * When caching elements, it is necessary to collect all cache tags into a
+ * single array, from both the element itself and all child elements. This
+ * allows items to be invalidated based on all tags attached to the content
+ * they're constituted from.
+ *
+ * @param array $element
+ *   The element to collect cache tags from.
+ * @param array $tags
+ *   (optional) An array of already collected cache tags (i.e. from a parent
+ *   element). Defaults to an empty array.
+ *
+ * @return array
+ *   The cache tags array for this element and its descendants.
+ */
+function drupal_render_collect_cache_tags($element, $tags = array()) {
+  if (isset($element['#cache']['tags'])) {
+    foreach ($element['#cache']['tags'] as $namespace => $values) {
+      if (is_array($values)) {
+        foreach ($values as $value) {
+          $tags[$namespace][$value] = $value;
+        }
+      }
+      else {
+        if (!isset($tags[$namespace])) {
+          $tags[$namespace] = $values;
+        }
+      }
+    }
+  }
+  if ($children = element_children($element)) {
+    foreach ($children as $child) {
+      $tags = drupal_render_collect_cache_tags($element[$child], $tags);
+    }
+  }
+
+  return $tags;
+}
+
 /**
  * Prepares an element for caching based on a query.
  *
diff --git a/core/includes/entity.inc b/core/includes/entity.inc
index fbc79b26fd2d..e8a0e956b1b0 100644
--- a/core/includes/entity.inc
+++ b/core/includes/entity.inc
@@ -46,6 +46,18 @@ function entity_info_cache_clear() {
   \Drupal::entityManager()->clearCachedFieldDefinitions();
 }
 
+/**
+ * Clears the entity render cache for all entity types.
+ */
+function entity_render_cache_clear() {
+  $entity_manager = Drupal::entityManager();
+  foreach ($entity_manager->getDefinitions() as $entity_type => $info) {
+    if ($entity_manager->hasController($entity_type, 'render')) {
+      $entity_manager->getRenderController($entity_type)->resetCache();
+    }
+  }
+}
+
 /**
  * Returns the entity bundle info.
  *
@@ -611,14 +623,19 @@ function entity_render_controller($entity_type) {
  * @param string $langcode
  *   (optional) For which language the entity should be rendered, defaults to
  *   the current content language.
+ * @param bool $reset
+ *   (optional) Whether to reset the render cache for the requested entity.
+ *   Defaults to FALSE.
  *
  * @return array
  *   A render array for the entity.
  */
-function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL) {
-  return \Drupal::entityManager()
-    ->getRenderController($entity->entityType())
-    ->view($entity, $view_mode, $langcode);
+function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL, $reset = FALSE) {
+  $render_controller = \Drupal::entityManager()->getRenderController($entity->entityType());
+  if ($reset) {
+    $render_controller->resetCache(array($entity->id()));
+  }
+  return $render_controller->view($entity, $view_mode, $langcode);
 }
 
 /**
@@ -631,15 +648,20 @@ function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL) {
  * @param string $langcode
  *   (optional) For which language the entity should be rendered, defaults to
  *   the current content language.
+ * @param bool $reset
+ *   (optional) Whether to reset the render cache for the requested entities.
+ *   Defaults to FALSE.
  *
  * @return array
  *   A render array for the entities, indexed by the same keys as the
  *   entities array passed in $entities.
  */
-function entity_view_multiple(array $entities, $view_mode, $langcode = NULL) {
-  return \Drupal::entityManager()
-    ->getRenderController(reset($entities)->entityType())
-    ->viewMultiple($entities, $view_mode, $langcode);
+function entity_view_multiple(array $entities, $view_mode, $langcode = NULL, $reset = FALSE) {
+  $render_controller = \Drupal::entityManager()->getRenderController(reset($entities)->entityType());
+  if ($reset) {
+    $render_controller->resetCache(array_keys($entities));
+  }
+  return $render_controller->viewMultiple($entities, $view_mode, $langcode);
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Entity/Annotation/EntityType.php b/core/lib/Drupal/Core/Entity/Annotation/EntityType.php
index 0bf08f0a7d98..016767d218ff 100644
--- a/core/lib/Drupal/Core/Entity/Annotation/EntityType.php
+++ b/core/lib/Drupal/Core/Entity/Annotation/EntityType.php
@@ -137,6 +137,14 @@ class EntityType extends Plugin {
    */
   public $static_cache = TRUE;
 
+  /**
+   * Boolean indicating whether the rendered output of entities should be
+   * cached.
+   *
+   * @var bool (optional)
+   */
+  public $render_cache = TRUE;
+
   /**
    * Boolean indicating whether entities of this type have multilingual support.
    *
diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 683ef530d209..d4b4ea5d015e 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core\Entity;
 
 use Drupal\Component\Uuid\Uuid;
+use Drupal\Core\Entity\Plugin\DataType\EntityReferenceItem;
 use Drupal\Core\Language\Language;
 use Drupal\Core\TypedData\TranslatableInterface;
 use Drupal\Core\TypedData\TypedDataInterface;
@@ -611,6 +612,7 @@ public function preSave(EntityStorageControllerInterface $storage_controller) {
    * {@inheritdoc}
    */
   public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    $this->changed();
   }
 
   /**
@@ -635,6 +637,9 @@ public static function preDelete(EntityStorageControllerInterface $storage_contr
    * {@inheritdoc}
    */
   public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    foreach ($entities as $entity) {
+      $entity->changed();
+    }
   }
 
   /**
@@ -698,4 +703,47 @@ public static function baseFieldDefinitions($entity_type) {
     return array();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function referencedEntities() {
+    $referenced_entities = array();
+
+    // @todo Remove when all entities are converted to EntityNG.
+    if (!$this->getPropertyDefinitions()) {
+      return $referenced_entities;
+    }
+
+    // Gather a list of referenced entities.
+    foreach ($this->getProperties() as $name => $definition) {
+      $field_items = $this->get($name);
+      foreach ($field_items as $offset => $field_item) {
+        if ($field_item instanceof EntityReferenceItem && $entity = $field_item->entity) {
+          $referenced_entities[] = $entity;
+        }
+      }
+    }
+
+    return $referenced_entities;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function changed() {
+    $referenced_entity_ids = array(
+      $this->entityType() => array($this->id() => TRUE),
+    );
+
+    foreach ($this->referencedEntities() as $referenced_entity) {
+      $referenced_entity_ids[$referenced_entity->entityType()][$referenced_entity->id()] = TRUE;
+    }
+
+    foreach ($referenced_entity_ids as $entity_type => $entity_ids) {
+      if (\Drupal::entityManager()->hasController($entity_type, 'render')) {
+        \Drupal::entityManager()->getRenderController($entity_type)->resetCache(array_keys($entity_ids));
+      }
+    }
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityInterface.php b/core/lib/Drupal/Core/Entity/EntityInterface.php
index 7d0056cdfcab..c0949f71ac01 100644
--- a/core/lib/Drupal/Core/Entity/EntityInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityInterface.php
@@ -325,4 +325,17 @@ public function initTranslation($langcode);
    */
   public static function baseFieldDefinitions($entity_type);
 
+  /**
+   * Returns a list of entities referenced by this entity.
+   *
+   * @return array
+   *   An array of entities.
+   */
+  public function referencedEntities();
+
+  /**
+   * Acts on an entity after it was saved or deleted.
+   */
+  public function changed();
+
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityRenderController.php b/core/lib/Drupal/Core/Entity/EntityRenderController.php
index a2d078104ed1..fe6bd103c967 100644
--- a/core/lib/Drupal/Core/Entity/EntityRenderController.php
+++ b/core/lib/Drupal/Core/Entity/EntityRenderController.php
@@ -6,8 +6,8 @@
  */
 
 namespace Drupal\Core\Entity;
-use Drupal\entity\Entity\EntityDisplay;
 
+use Drupal\entity\Entity\EntityDisplay;
 use Drupal\Core\Language\Language;
 
 /**
@@ -22,8 +22,37 @@ class EntityRenderController implements EntityRenderControllerInterface {
    */
   protected $entityType;
 
+  /**
+   * The entity info array.
+   *
+   * @var array
+   *
+   * @see entity_get_info()
+   */
+  protected $entityInfo;
+
+  /**
+   * An array of view mode info for the type of entities for which this
+   * controller is instantiated.
+   *
+   * @var array
+   */
+  protected $viewModesInfo;
+
+  /**
+   * The cache bin used to store the render cache.
+   *
+   * @todo Defaults to 'cache' for now, until http://drupal.org/node/1194136 is
+   * fixed.
+   *
+   * @var string
+   */
+  protected $cacheBin = 'cache';
+
   public function __construct($entity_type) {
     $this->entityType = $entity_type;
+    $this->entityInfo = entity_get_info($entity_type);
+    $this->viewModesInfo = entity_get_view_modes($entity_type);
   }
 
   /**
@@ -80,6 +109,23 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco
       '#view_mode' => $view_mode,
       '#langcode' => $langcode,
     );
+
+    // Cache the rendered output if permitted by the view mode and global entity
+    // type configuration. The isset() checks below are necessary because
+    // 'default' is not an actual view mode.
+    $view_mode_is_cacheable = !isset($this->viewModesInfo[$view_mode]) || (isset($this->viewModesInfo[$view_mode]) && $this->viewModesInfo[$view_mode]['cache']);
+    if ($view_mode_is_cacheable && !$entity->isNew() && !isset($entity->in_preview) && $this->entityInfo['render_cache']) {
+      $return['#cache'] = array(
+        'keys' => array('entity_view', $this->entityType, $entity->id(), $view_mode),
+        'granularity' => DRUPAL_CACHE_PER_ROLE,
+        'bin' => $this->cacheBin,
+        'tags' => array(
+          $this->entityType . '_view' => TRUE,
+          $this->entityType => array($entity->id()),
+        ),
+      );
+    }
+
     return $return;
   }
 
@@ -182,4 +228,20 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
 
     return $build;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function resetCache(array $ids = NULL) {
+    if (isset($ids)) {
+      $tags = array();
+      foreach ($ids as $entity_id) {
+        $tags[$this->entityType][$entity_id] = $entity_id;
+      }
+      \Drupal::cache($this->cacheBin)->deleteTags($tags);
+    }
+    else {
+      \Drupal::cache($this->cacheBin)->deleteTags(array($this->entityType . '_view' => TRUE));
+    }
+  }
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityRenderControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityRenderControllerInterface.php
index 03ba014cd05e..a112fc2870dc 100644
--- a/core/lib/Drupal/Core/Entity/EntityRenderControllerInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityRenderControllerInterface.php
@@ -75,4 +75,14 @@ public function view(EntityInterface $entity, $view_mode = 'full', $langcode = N
    *   be available for loading.
    */
   public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL);
+
+  /**
+   * Resets the entity render cache.
+   *
+   * @param array|null $ids
+   *   (optional) If specified, the cache is reset for the given entity IDs
+   *   only.
+   */
+  public function resetCache(array $ids = NULL);
+
 }
diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Tests/Views/IntegrationTest.php b/core/modules/aggregator/lib/Drupal/aggregator/Tests/Views/IntegrationTest.php
index 53621cfef298..77393d6536f2 100644
--- a/core/modules/aggregator/lib/Drupal/aggregator/Tests/Views/IntegrationTest.php
+++ b/core/modules/aggregator/lib/Drupal/aggregator/Tests/Views/IntegrationTest.php
@@ -20,7 +20,7 @@ class IntegrationTest extends ViewUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('aggregator', 'aggregator_test_views', 'system', 'field');
+  public static $modules = array('aggregator', 'aggregator_test_views', 'system', 'entity', 'field');
 
   /**
    * Views used by this test.
diff --git a/core/modules/block/custom_block/config/entity.view_mode.custom_block.full.yml b/core/modules/block/custom_block/config/entity.view_mode.custom_block.full.yml
index ebacec5a62ad..6c2a3d153135 100644
--- a/core/modules/block/custom_block/config/entity.view_mode.custom_block.full.yml
+++ b/core/modules/block/custom_block/config/entity.view_mode.custom_block.full.yml
@@ -1,4 +1,5 @@
 id: custom_block.full
 label: Full
 status: '0'
+cache: '1'
 targetEntityType: custom_block
diff --git a/core/modules/block/lib/Drupal/block/BlockRenderController.php b/core/modules/block/lib/Drupal/block/BlockRenderController.php
index 57a5d621ef32..68599a5533f1 100644
--- a/core/modules/block/lib/Drupal/block/BlockRenderController.php
+++ b/core/modules/block/lib/Drupal/block/BlockRenderController.php
@@ -62,4 +62,9 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
     return $build;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function resetCache(array $ids = NULL) { }
+
 }
diff --git a/core/modules/book/config/entity.view_mode.node.print.yml b/core/modules/book/config/entity.view_mode.node.print.yml
index fe45f505b8f8..6f7333f9af93 100644
--- a/core/modules/book/config/entity.view_mode.node.print.yml
+++ b/core/modules/book/config/entity.view_mode.node.print.yml
@@ -1,4 +1,5 @@
 id: node.print
 label: Print
 status: '0'
+cache: '1'
 targetEntityType: node
diff --git a/core/modules/comment/config/entity.view_mode.comment.full.yml b/core/modules/comment/config/entity.view_mode.comment.full.yml
index e48fbd764229..abfc64613713 100644
--- a/core/modules/comment/config/entity.view_mode.comment.full.yml
+++ b/core/modules/comment/config/entity.view_mode.comment.full.yml
@@ -1,4 +1,5 @@
 id: comment.full
 label: Full comment
 status: '0'
+cache: '1'
 targetEntityType: comment
diff --git a/core/modules/comment/lib/Drupal/comment/Controller/CommentController.php b/core/modules/comment/lib/Drupal/comment/Controller/CommentController.php
index 137def5179fd..2470b2ac9ce6 100644
--- a/core/modules/comment/lib/Drupal/comment/Controller/CommentController.php
+++ b/core/modules/comment/lib/Drupal/comment/Controller/CommentController.php
@@ -219,6 +219,7 @@ public function getReplyForm(Request $request, NodeInterface $node, $pid = NULL)
       elseif ($account->hasPermission('access content')) {
         // Display the node.
         $build['comment_node'] = $this->entityManager()->getRenderController('node')->view($node);
+        unset($build['comment_node']['#cache']);
       }
     }
     else {
diff --git a/core/modules/comment/lib/Drupal/comment/Entity/Comment.php b/core/modules/comment/lib/Drupal/comment/Entity/Comment.php
index dfe08c10000e..c30d380eb122 100644
--- a/core/modules/comment/lib/Drupal/comment/Entity/Comment.php
+++ b/core/modules/comment/lib/Drupal/comment/Entity/Comment.php
@@ -36,6 +36,7 @@
  *   uri_callback = "comment_uri",
  *   fieldable = TRUE,
  *   translatable = TRUE,
+ *   render_cache = FALSE,
  *   route_base_path = "admin/structure/types/manage/{bundle}/comment",
  *   bundle_prefix = "comment_node_",
  *   entity_keys = {
diff --git a/core/modules/editor/lib/Drupal/editor/Tests/EditorFileUsageTest.php b/core/modules/editor/lib/Drupal/editor/Tests/EditorFileUsageTest.php
index 80384ae0f3a1..97715b590655 100644
--- a/core/modules/editor/lib/Drupal/editor/Tests/EditorFileUsageTest.php
+++ b/core/modules/editor/lib/Drupal/editor/Tests/EditorFileUsageTest.php
@@ -7,19 +7,19 @@
 
 namespace Drupal\editor\Tests;
 
-use Drupal\simpletest\DrupalUnitTestBase;
+use Drupal\system\Tests\Entity\EntityUnitTestBase;
 
 /**
  * Unit tests for editor.module's entity hooks to track file usage.
  */
-class EditorFileUsageTest extends DrupalUnitTestBase {
+class EditorFileUsageTest extends EntityUnitTestBase {
 
   /**
    * Modules to enable.
    *
    * @var array
    */
-  public static $modules = array('system', 'editor', 'editor_test', 'filter', 'node', 'entity', 'field', 'text', 'field_sql_storage', 'file');
+  public static $modules = array('editor', 'editor_test', 'node', 'file');
 
   public static function getInfo() {
     return array(
@@ -31,13 +31,8 @@ public static function getInfo() {
 
   function setUp() {
     parent::setUp();
-    $this->installSchema('system', 'url_alias');
-    $this->installSchema('node', 'node');
-    $this->installSchema('node', 'node_access');
-    $this->installSchema('node', 'node_field_data');
-    $this->installSchema('node', 'node_field_revision');
-    $this->installSchema('file', 'file_managed');
-    $this->installSchema('file', 'file_usage');
+    $this->installSchema('node', array('node', 'node_access', 'node_field_data', 'node_field_revision'));
+    $this->installSchema('file', array('file_managed', 'file_usage'));
 
     // Add text formats.
     $filtered_html_format = entity_create('filter_format', array(
@@ -71,13 +66,15 @@ function testEditorEntityHooks() {
     $this->assertIdentical(array(), file_usage()->listUsage($image), 'The image has zero usages.');
 
     // Test editor_entity_insert(): increment.
+    $this->createUser();
     $node = entity_create('node', array(
       'type' => 'page',
       'title' => 'test',
       'body' => array(
         'value' => '<p>Hello, world!</p><img src="awesome-llama.jpg" data-editor-file-uuid="' . $image->uuid() . '" />',
         'format' => 'filtered_html',
-      )
+      ),
+      'uid' => 1,
     ));
     $node->save();
     $this->assertIdentical(array('editor' => array('node' => array(1 => '1'))), file_usage()->listUsage($image), 'The image has 1 usage.');
diff --git a/core/modules/entity/lib/Drupal/entity/EntityDisplayBase.php b/core/modules/entity/lib/Drupal/entity/EntityDisplayBase.php
index 128bc7cea2bc..eabce2509f34 100644
--- a/core/modules/entity/lib/Drupal/entity/EntityDisplayBase.php
+++ b/core/modules/entity/lib/Drupal/entity/EntityDisplayBase.php
@@ -118,6 +118,20 @@ public function id() {
     return $this->targetEntityType . '.' . $this->bundle . '.' . $this->mode;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function save() {
+    $return = parent::save();
+
+    // Reset the render cache for the target entity type.
+    if (\Drupal::entityManager()->hasController($this->targetEntityType, 'render')) {
+      \Drupal::entityManager()->getRenderController($this->targetEntityType)->resetCache();
+    }
+
+    return $return;
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/entity/lib/Drupal/entity/EntityDisplayModeBase.php b/core/modules/entity/lib/Drupal/entity/EntityDisplayModeBase.php
index 8fe73cb9c89d..1541143bb1d7 100644
--- a/core/modules/entity/lib/Drupal/entity/EntityDisplayModeBase.php
+++ b/core/modules/entity/lib/Drupal/entity/EntityDisplayModeBase.php
@@ -57,6 +57,13 @@ abstract class EntityDisplayModeBase extends ConfigEntityBase implements EntityD
    */
   public $status = TRUE;
 
+  /**
+   * Whether or not the rendered output of this view mode is cached by default.
+   *
+   * @var bool
+   */
+  public $cache = TRUE;
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/field/lib/Drupal/field/Plugin/Type/Formatter/FormatterBase.php b/core/modules/field/lib/Drupal/field/Plugin/Type/Formatter/FormatterBase.php
index bb9edd389803..34cadaeaaa8a 100644
--- a/core/modules/field/lib/Drupal/field/Plugin/Type/Formatter/FormatterBase.php
+++ b/core/modules/field/lib/Drupal/field/Plugin/Type/Formatter/FormatterBase.php
@@ -96,8 +96,21 @@ public function view(FieldInterface $items) {
         '#object' => $entity,
         '#items' => $items->getValue(TRUE),
         '#formatter' => $this->getPluginId(),
+        '#cache' => array('tags' => array())
       );
 
+      // Gather cache tags from reference fields.
+      foreach ($items as $item) {
+        if (isset($item->format)) {
+          $info['#cache']['tags']['filter_format'] = $item->format;
+        }
+
+        if (isset($item->entity)) {
+          $info['#cache']['tags'][$item->entity->entityType()][] = $item->entity->id();
+          $info['#cache']['tags'][$item->entity->entityType() . '_view'] = TRUE;
+        }
+      }
+
       $addition[$field_name] = array_merge($info, $elements);
     }
 
diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php b/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php
index b619aa2956ff..cd809a6198e2 100644
--- a/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php
+++ b/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php
@@ -35,8 +35,9 @@ abstract class FieldUnitTestBase extends DrupalUnitTestBase {
    */
   function setUp() {
     parent::setUp();
-    $this->installSchema('system', array('sequences', 'variable', 'config_snapshot'));
     $this->installSchema('entity_test', 'entity_test');
+    $this->installSchema('system', array('sequences', 'variable', 'config_snapshot'));
+    $this->installSchema('user', array('users', 'users_roles'));
 
     // Set default storage backend and configure the theme system.
     $this->installConfig(array('field', 'system'));
diff --git a/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php b/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php
index 0efcb30170bc..945e30b8a962 100644
--- a/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php
+++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php
@@ -31,7 +31,6 @@ function setUp() {
     parent::setUp();
 
     $this->installConfig(array('system', 'filter'));
-    $this->installSchema('user', array('users_roles'));
 
     // Create Filtered HTML format.
     $filtered_html_format = entity_create('filter_format', array(
diff --git a/core/modules/node/config/entity.view_mode.node.full.yml b/core/modules/node/config/entity.view_mode.node.full.yml
index af6d938b1b85..e4d8bd0f76dd 100644
--- a/core/modules/node/config/entity.view_mode.node.full.yml
+++ b/core/modules/node/config/entity.view_mode.node.full.yml
@@ -1,4 +1,5 @@
 id: node.full
 label: Full content
 status: '0'
+cache: '1'
 targetEntityType: node
diff --git a/core/modules/node/config/entity.view_mode.node.rss.yml b/core/modules/node/config/entity.view_mode.node.rss.yml
index 984b05fbec63..0dbf7c12f007 100644
--- a/core/modules/node/config/entity.view_mode.node.rss.yml
+++ b/core/modules/node/config/entity.view_mode.node.rss.yml
@@ -1,4 +1,5 @@
 id: node.rss
 label: RSS
 status: '0'
+cache: '1'
 targetEntityType: node
diff --git a/core/modules/node/config/entity.view_mode.node.teaser.yml b/core/modules/node/config/entity.view_mode.node.teaser.yml
index 2089b94199cf..636de1539574 100644
--- a/core/modules/node/config/entity.view_mode.node.teaser.yml
+++ b/core/modules/node/config/entity.view_mode.node.teaser.yml
@@ -1,4 +1,5 @@
 id: node.teaser
 label: Teaser
 status: '1'
+cache: '1'
 targetEntityType: node
diff --git a/core/modules/node/lib/Drupal/node/Entity/Node.php b/core/modules/node/lib/Drupal/node/Entity/Node.php
index 5105d29da251..8367e16a8c8f 100644
--- a/core/modules/node/lib/Drupal/node/Entity/Node.php
+++ b/core/modules/node/lib/Drupal/node/Entity/Node.php
@@ -40,6 +40,7 @@
  *   uri_callback = "node_uri",
  *   fieldable = TRUE,
  *   translatable = TRUE,
+ *   render_cache = FALSE,
  *   entity_keys = {
  *     "id" = "nid",
  *     "revision" = "vid",
diff --git a/core/modules/node/lib/Drupal/node/NodeRenderController.php b/core/modules/node/lib/Drupal/node/NodeRenderController.php
index 6da0b29fe9e3..433adb3e6969 100644
--- a/core/modules/node/lib/Drupal/node/NodeRenderController.php
+++ b/core/modules/node/lib/Drupal/node/NodeRenderController.php
@@ -85,6 +85,10 @@ protected function alterBuild(array &$build, EntityInterface $entity, EntityDisp
     if ($entity->id()) {
       $build['#contextual_links']['node'] = array('node', array($entity->id()));
     }
+
+    // The node 'submitted' info is not rendered in a standard way (renderable
+    // array) so we have to add a cache tag manually.
+    $build['#cache']['tags']['user'][] = $entity->uid;
   }
 
 }
diff --git a/core/modules/node/lib/Drupal/node/Tests/Condition/NodeConditionTest.php b/core/modules/node/lib/Drupal/node/Tests/Condition/NodeConditionTest.php
index 31d515f9a65d..4718c857c240 100644
--- a/core/modules/node/lib/Drupal/node/Tests/Condition/NodeConditionTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/Condition/NodeConditionTest.php
@@ -7,14 +7,14 @@
 
 namespace Drupal\node\Tests\Condition;
 
-use Drupal\simpletest\DrupalUnitTestBase;
+use Drupal\system\Tests\Entity\EntityUnitTestBase;
 
 /**
  * Tests the node conditions.
  */
-class NodeConditionTest extends DrupalUnitTestBase {
+class NodeConditionTest extends EntityUnitTestBase {
 
-  public static $modules = array('system', 'node', 'field');
+  public static $modules = array('node');
 
   public static function getInfo() {
     return array(
@@ -24,11 +24,9 @@ public static function getInfo() {
     );
   }
 
-  protected function setUp() {
+  public function setUp() {
     parent::setUp();
-    $this->installSchema('node', 'node');
-    $this->installSchema('node', 'node_field_data');
-    $this->installSchema('node', 'node_field_revision');
+    $this->installSchema('node', array('node', 'node_field_data', 'node_field_revision'));
   }
 
   /**
@@ -36,13 +34,14 @@ protected function setUp() {
    */
   function testConditions() {
     $manager = $this->container->get('plugin.manager.condition', $this->container->get('container.namespaces'));
+    $this->createUser();
 
     // Get some nodes of various types to check against.
-    $page = entity_create('node', array('type' => 'page', 'title' => $this->randomName()));
+    $page = entity_create('node', array('type' => 'page', 'title' => $this->randomName(), 'uid' => 1));
     $page->save();
-    $article = entity_create('node', array('type' => 'article', 'title' => $this->randomName()));
+    $article = entity_create('node', array('type' => 'article', 'title' => $this->randomName(), 'uid' => 1));
     $article->save();
-    $test = entity_create('node', array('type' => 'test', 'title' => $this->randomName()));
+    $test = entity_create('node', array('type' => 'test', 'title' => $this->randomName(), 'uid' => 1));
     $test->save();
 
     // Grab the node type condition and configure it to check against node type
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeLastChangedTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeLastChangedTest.php
index 96e6f1a83729..e33d6a7a76f4 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeLastChangedTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeLastChangedTest.php
@@ -19,7 +19,7 @@ class NodeLastChangedTest extends DrupalUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('node', 'field');
+  public static $modules = array('entity', 'user', 'node', 'field');
 
   public static function getInfo() {
     return array(
@@ -34,6 +34,7 @@ public function setUp() {
     $this->installSchema('node', 'node');
     $this->installSchema('node', 'node_field_data');
     $this->installSchema('node', 'node_field_revision');
+    $this->installSchema('user', array('users'));
   }
 
   /**
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeValidationTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeValidationTest.php
index e5ecd7d83e5a..2d3efe8a9382 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeValidationTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeValidationTest.php
@@ -7,19 +7,19 @@
 
 namespace Drupal\node\Tests;
 
-use Drupal\simpletest\DrupalUnitTestBase;
+use Drupal\system\Tests\Entity\EntityUnitTestBase;
 
 /**
  * Tests node validation constraints.
  */
-class NodeValidationTest extends DrupalUnitTestBase {
+class NodeValidationTest extends EntityUnitTestBase {
 
   /**
    * Modules to enable.
    *
    * @var array
    */
-  public static $modules = array('node', 'entity', 'field', 'text', 'field_sql_storage', 'filter');
+  public static $modules = array('node');
 
   public static function getInfo() {
     return array(
@@ -34,9 +34,7 @@ public static function getInfo() {
    */
   public function setUp() {
     parent::setUp();
-    $this->installSchema('node', 'node');
-    $this->installSchema('node', 'node_field_data');
-    $this->installSchema('node', 'node_field_revision');
+    $this->installSchema('node', array('node', 'node_field_data', 'node_field_revision'));
 
     // Create a node type for testing.
     $type = entity_create('node_type', array('type' => 'page', 'name' => 'page'));
@@ -47,7 +45,8 @@ public function setUp() {
    * Tests the node validation constraints.
    */
   public function testValidation() {
-    $node = entity_create('node', array('type' => 'page', 'title' => 'test'));
+    $this->createUser();
+    $node = entity_create('node', array('type' => 'page', 'title' => 'test', 'uid' => 1));
     $violations = $node->validate();
     $this->assertEqual(count($violations), 0, 'No violations when validating a default node.');
 
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index b924c129d0a1..e1d2b63f3f9f 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -585,6 +585,8 @@ function node_show(EntityInterface $node, $message = FALSE) {
 
   if ($message) {
     $page['#title'] = t('Revision of %title from %date', array('%title' => $node->label(), '%date' => format_date($node->getRevisionCreationTime())));
+    // Don't use the render cache when a revision is displayed.
+    unset($page['nodes'][$node->id()]['#cache']);
   }
 
   // Update the history table, stating that this user viewed this node.
diff --git a/core/modules/rdf/lib/Drupal/rdf/Entity/RdfMapping.php b/core/modules/rdf/lib/Drupal/rdf/Entity/RdfMapping.php
index 4a5972e249cc..9464a569b760 100644
--- a/core/modules/rdf/lib/Drupal/rdf/Entity/RdfMapping.php
+++ b/core/modules/rdf/lib/Drupal/rdf/Entity/RdfMapping.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\rdf\RdfMappingInterface;
 
 /**
@@ -165,4 +166,15 @@ public function getExportProperties() {
     return $properties;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    parent::postSave($storage_controller, $update);
+
+    if (\Drupal::entityManager()->hasController($this->targetEntityType, 'render')) {
+      \Drupal::entityManager()->getRenderController($this->targetEntityType)->resetCache();
+    }
+  }
+
 }
diff --git a/core/modules/search/config/entity.view_mode.node.search_index.yml b/core/modules/search/config/entity.view_mode.node.search_index.yml
index ed22c30962a4..e12156d6a08f 100644
--- a/core/modules/search/config/entity.view_mode.node.search_index.yml
+++ b/core/modules/search/config/entity.view_mode.node.search_index.yml
@@ -1,4 +1,5 @@
 id: node.search_index
 label: Search index
 status: '0'
+cache: '1'
 targetEntityType: node
diff --git a/core/modules/search/config/entity.view_mode.node.search_result.yml b/core/modules/search/config/entity.view_mode.node.search_result.yml
index 1608657df63a..776ada9877ff 100644
--- a/core/modules/search/config/entity.view_mode.node.search_result.yml
+++ b/core/modules/search/config/entity.view_mode.node.search_result.yml
@@ -1,4 +1,5 @@
 id: node.search_result
 label: Search result
 status: '0'
+cache: '1'
 targetEntityType: node
diff --git a/core/modules/serialization/lib/Drupal/serialization/Tests/NormalizerTestBase.php b/core/modules/serialization/lib/Drupal/serialization/Tests/NormalizerTestBase.php
index 1966e3a3994b..977437b87b8f 100644
--- a/core/modules/serialization/lib/Drupal/serialization/Tests/NormalizerTestBase.php
+++ b/core/modules/serialization/lib/Drupal/serialization/Tests/NormalizerTestBase.php
@@ -16,12 +16,13 @@ abstract class NormalizerTestBase extends DrupalUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('serialization', 'system', 'entity', 'field', 'entity_test', 'text', 'filter');
+  public static $modules = array('serialization', 'system', 'entity', 'field', 'entity_test', 'text', 'filter', 'user');
 
   protected function setUp() {
     parent::setUp();
 
     $this->installSchema('entity_test', array('entity_test_mulrev', 'entity_test_mulrev_property_revision', 'entity_test_mulrev_property_data'));
+    $this->installSchema('user', array('users', 'users_roles'));
     $this->installSchema('system', array('url_alias'));
     $this->installConfig(array('field'));
 
diff --git a/core/modules/statistics/statistics.module b/core/modules/statistics/statistics.module
index a28ea38582e1..37dca75eb0ba 100644
--- a/core/modules/statistics/statistics.module
+++ b/core/modules/statistics/statistics.module
@@ -72,6 +72,17 @@ function statistics_node_view(EntityInterface $node, EntityDisplay $display, $vi
   }
 }
 
+/**
+ * Implements hook_node_view_alter().
+ */
+function statistics_node_view_alter(&$build, EntityInterface $node, EntityDisplay $display) {
+  // If statistics were added to the node render array, we can't use the render
+  // cache.
+  if (isset($build['links']['statistics'])) {
+    unset($build['#cache']);
+  }
+}
+
 /**
  * Implements hook_menu().
  */
diff --git a/core/modules/system/lib/Drupal/system/Form/DateFormatLocalizeResetForm.php b/core/modules/system/lib/Drupal/system/Form/DateFormatLocalizeResetForm.php
index bb313c502aa2..e1e5e8b05255 100644
--- a/core/modules/system/lib/Drupal/system/Form/DateFormatLocalizeResetForm.php
+++ b/core/modules/system/lib/Drupal/system/Form/DateFormatLocalizeResetForm.php
@@ -109,6 +109,7 @@ public function submitForm(array &$form, array &$form_state) {
     foreach (config_get_storage_names_with_prefix('locale.config.' . $this->language->id . '.system.date_format.') as $config_id) {
       $this->configFactory->get($config_id)->delete();
     }
+    entity_render_cache_clear();
 
     $form_state['redirect'] = 'admin/config/regional/date-time/locale';
   }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Action/ActionUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Action/ActionUnitTest.php
index 03cc551648d0..96f2d73c9525 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Action/ActionUnitTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Action/ActionUnitTest.php
@@ -18,7 +18,7 @@ class ActionUnitTest extends DrupalUnitTestBase {
   /**
    * {@inheritdoc}
    */
-  public static $modules = array('system', 'field', 'user', 'action_test');
+  public static $modules = array('system','entity' , 'field', 'user', 'action_test');
 
   /**
    * The action manager.
diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php
index be4d7e533f03..4a871d510d65 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/RenderTest.php
@@ -553,8 +553,16 @@ function testDrupalRenderCache() {
     $test_element = array(
       '#cache' => array(
         'cid' => 'render_cache_test',
+        'tags' => array('render_cache_tag' => TRUE),
       ),
       '#markup' => '',
+      'child' => array(
+        '#cache' => array(
+          'cid' => 'render_cache_test_child',
+          'tags' => array('render_cache_tag_child' => array(1, 2))
+        ),
+        '#markup' => '',
+      ),
     );
 
     // Render the element and confirm that it goes through the rendering
@@ -569,6 +577,15 @@ function testDrupalRenderCache() {
     drupal_render($element);
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
 
+    // Test that cache tags are correctly collected from the render element,
+    // including the ones from its subchild.
+    $expected_tags = array(
+      'render_cache_tag' => TRUE,
+      'render_cache_tag_child' => array(1 => 1, 2 => 2),
+    );
+    $actual_tags = drupal_render_collect_cache_tags($test_element);
+    $this->assertEqual($expected_tags, $actual_tags, 'Cache tags were collected from the element and its subchild.');
+
     // Restore the previous request method.
     $_SERVER['REQUEST_METHOD'] = $request_method;
   }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityAccessTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityAccessTest.php
index be02e7f8b9e1..440d2d0a46f9 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityAccessTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityAccessTest.php
@@ -29,7 +29,6 @@ public static function getInfo() {
 
   function setUp() {
     parent::setUp();
-    $this->installSchema('user', array('users_roles'));
     $this->installSchema('system', array('variable', 'url_alias'));
     $this->installConfig(array('language'));
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCrudHookTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCrudHookTest.php
index 2c452d4df50b..84231d3a6e91 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCrudHookTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCrudHookTest.php
@@ -43,7 +43,7 @@ public static function getInfo() {
 
   public function setUp() {
     parent::setUp();
-    $this->installSchema('user', array('users_roles', 'users_data'));
+    $this->installSchema('user', array('users_data'));
     $this->installSchema('node', array('node', 'node_field_data', 'node_field_revision', 'node_access'));
     $this->installSchema('comment', array('comment', 'node_comment_statistics'));
   }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php
index ebc328dacd89..9bf20e2f840b 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php
@@ -36,7 +36,7 @@ public static function getInfo() {
 
   public function setUp() {
     parent::setUp();
-    $this->installSchema('user', array('users_roles', 'users_data'));
+    $this->installSchema('user', array('users_data'));
     $this->installSchema('node', array('node', 'node_field_data', 'node_field_revision', 'node_access'));
     $this->installSchema('entity_test', array(
       'entity_test_mul',
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityRenderTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityRenderTest.php
new file mode 100644
index 000000000000..9836c7d30079
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityRenderTest.php
@@ -0,0 +1,177 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Entity\EntityRenderTest.
+ */
+
+namespace Drupal\system\Tests\Entity;
+
+/**
+ * Tests the Entity Render Controller.
+ */
+class EntityRenderTest extends EntityUnitTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('entity_reference');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Entity rendering',
+      'description' => 'Tests the entity render controller.',
+      'group' => 'Entity API',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->installConfig(array('entity_test'));
+  }
+
+  /**
+   * Tests entity render cache handling.
+   */
+  public function testEntityRenderCache() {
+    // Force a request via GET so we can get drupal_render() cache working.
+    $request_method = $_SERVER['REQUEST_METHOD'];
+    $this->container->get('request')->setMethod('GET');
+
+    $entity_test = $this->createTestEntity('entity_test_render');
+
+    // Test that new entities (before they are saved for the first time) do not
+    // generate a cache entry.
+    $build = $this->container->get('entity.manager')->getRenderController('entity_test_render')->view($entity_test, 'full');
+    $this->assertFalse(isset($build['#cache']), 'The render array element of new (unsaved) entities is not cached.');
+
+    // Get a fully built entity view render array.
+    $entity_test->save();
+    $build = $this->container->get('entity.manager')->getRenderController('entity_test_render')->view($entity_test, 'full');
+    $cid = drupal_render_cid_create($build);
+    $bin = $build['#cache']['bin'];
+
+    // Mock the build array to not require the theme registry.
+    unset($build['#theme']);
+    $build['#markup'] = 'entity_render_test';
+
+    // Test that a cache entry is created.
+    drupal_render($build);
+    $this->assertTrue($this->container->get('cache.' . $bin)->get($cid), 'The entity render element has been cached.');
+
+    // Re-save the entity and check that the cache entry has been deleted.
+    $entity_test->save();
+    $this->assertFalse($this->container->get('cache.' . $bin)->get($cid), 'The entity render cache has been cleared when the entity was saved.');
+
+    // Rebuild the render array (creating a new cache entry in the process) and
+    // delete the entity to check the cache entry is deleted.
+    unset($build['#printed']);
+    drupal_render($build);
+    $this->assertTrue($this->container->get('cache.' . $bin)->get($cid), 'The entity render element has been cached.');
+    $entity_test->delete();
+    $this->assertFalse($this->container->get('cache.' . $bin)->get($cid), 'The entity render cache has been cleared when the entity was deleted.');
+
+    // Restore the previous request method.
+    $this->container->get('request')->setMethod($request_method);
+  }
+
+  /**
+   * Tests entity render cache with references.
+   */
+  public function testEntityRenderCacheWithReferences() {
+    // Force a request via GET so we can get drupal_render() cache working.
+    $request_method = $_SERVER['REQUEST_METHOD'];
+    $this->container->get('request')->setMethod('GET');
+
+    // Create an entity reference field and an entity that will be referenced.
+    entity_reference_create_instance('entity_test_render', 'entity_test_render', 'reference_field', 'Reference', 'entity_test_render');
+    entity_get_display('entity_test_render', 'entity_test_render', 'full')->setComponent('reference_field')->save();
+    $entity_test_reference = $this->createTestEntity('entity_test_render');
+    $entity_test_reference->save();
+
+    // Get a fully built entity view render array for the referenced entity.
+    $build = $this->container->get('entity.manager')->getRenderController('entity_test_render')->view($entity_test_reference, 'full');
+    $cid_reference = drupal_render_cid_create($build);
+    $bin_reference = $build['#cache']['bin'];
+
+    // Mock the build array to not require the theme registry.
+    unset($build['#theme']);
+    $build['#markup'] = 'entity_render_test';
+    drupal_render($build);
+
+    // Test that a cache entry was created for the referenced entity.
+    $this->assertTrue($this->container->get('cache.' . $bin_reference)->get($cid_reference), 'The entity render element for the referenced entity has been cached.');
+
+    // Create another entity that references the first one.
+    $entity_test = $this->createTestEntity('entity_test_render');
+    $entity_test->reference_field->entity = $entity_test_reference;
+    $entity_test->save();
+
+    // Get a fully built entity view render array.
+    $build = $this->container->get('entity.manager')->getRenderController('entity_test_render')->view($entity_test, 'full');
+    $cid = drupal_render_cid_create($build);
+    $bin = $build['#cache']['bin'];
+
+    // Mock the build array to not require the theme registry.
+    unset($build['#theme']);
+    $build['#markup'] = 'entity_render_test';
+    drupal_render($build);
+
+    // Test that a cache entry is created.
+    $this->assertTrue($this->container->get('cache.' . $bin)->get($cid), 'The entity render element has been cached.');
+
+    // Save the entity and verify that both cache entries have been deleted.
+    $entity_test->save();
+    $this->assertFalse($this->container->get('cache.' . $bin)->get($cid), 'The entity render cache has been cleared when the entity was deleted.');
+    $this->assertFalse($this->container->get('cache.' . $bin_reference)->get($cid_reference), 'The entity render cache for the referenced entity has been cleared when the entity was deleted.');
+
+    // Restore the previous request method.
+    $this->container->get('request')->setMethod($request_method);
+  }
+
+  /**
+   * Tests entity render cache toggling.
+   */
+  public function testEntityRenderCacheToggling() {
+    $entity_test = $this->createTestEntity('entity_test_render');
+    $entity_test->save();
+
+    // Test a view mode in default conditions: render caching is enabled for
+    // the entity type and the view mode.
+    $build = $this->container->get('entity.manager')->getRenderController('entity_test_render')->view($entity_test, 'full');
+    $this->assertTrue(isset($build['#cache']), 'A view mode with render cache enabled has the correct output.');
+
+    // Test that a view mode can opt out of render caching.
+    $build = $this->container->get('entity.manager')->getRenderController('entity_test_render')->view($entity_test, 'test');
+    $this->assertFalse(isset($build['#cache']), 'A view mode with render cache disabled has the correct output.');
+
+    // Test that an entity type can opt out of render caching completely.
+    $entity_test_no_cache = $this->createTestEntity('entity_test_label');
+    $entity_test_no_cache->save();
+    $build = $this->container->get('entity.manager')->getRenderController('entity_test_label')->view($entity_test_no_cache, 'full');
+    $this->assertFalse(isset($build['#cache']), 'An entity type can opt out of render caching regardless of view mode configuration.');
+  }
+
+  /**
+   * Creates an entity for testing.
+   *
+   * @param string $entity_type
+   *   The entity type.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   The created entity.
+   */
+  protected function createTestEntity($entity_type) {
+    $data = array(
+      'bundle' => $entity_type,
+      'name' => $this->randomName(),
+    );
+    return $this->container->get('entity.manager')->getStorageController($entity_type)->create($data);
+  }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUnitTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUnitTestBase.php
index d3225b3d67dc..657cd0abcc81 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUnitTestBase.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUnitTestBase.php
@@ -42,7 +42,7 @@ public function setUp() {
     $this->entityManager = $this->container->get('entity.manager');
     $this->state = $this->container->get('state');
 
-    $this->installSchema('user', 'users');
+    $this->installSchema('user', array('users', 'users_roles'));
     $this->installSchema('system', 'sequences');
     $this->installSchema('entity_test', 'entity_test');
     $this->installConfig(array('field'));
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityValidationTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityValidationTest.php
index eab31a8c2983..b17a700a99d1 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityValidationTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityValidationTest.php
@@ -37,7 +37,7 @@ public static function getInfo() {
    */
   public function setUp() {
     parent::setUp();
-    $this->installSchema('user', array('users_roles', 'users_data'));
+    $this->installSchema('user', array('users_data'));
     $this->installSchema('entity_test', array(
       'entity_test_mul',
       'entity_test_mul_property_data',
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityViewControllerTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityViewControllerTest.php
index 3de68b50d679..ca3ac07d80ed 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityViewControllerTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityViewControllerTest.php
@@ -84,7 +84,7 @@ public function testFieldItemAttributes() {
       'field_name' => 'field_test_text',
       'bundle' => 'entity_test_render',
     ))->save();
-    entity_get_display('entity_test_render', 'entity_test_render', 'full')
+    entity_get_display('entity_test_render', 'entity_test_render', 'default')
       ->setComponent('field_test_text', array('type' => 'text_default'))
       ->save();
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php b/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php
index a0e4eb3ea5c5..6415f27a15b2 100644
--- a/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php
@@ -28,7 +28,7 @@ class TypedDataTest extends DrupalUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('system', 'field', 'file');
+  public static $modules = array('system', 'entity', 'field', 'file');
 
   public static function getInfo() {
     return array(
diff --git a/core/modules/system/tests/modules/entity_test/config/entity.view_mode.entity_test_render.full.yml b/core/modules/system/tests/modules/entity_test/config/entity.view_mode.entity_test_render.full.yml
index 8902bc3c2094..4ce40f1bddab 100644
--- a/core/modules/system/tests/modules/entity_test/config/entity.view_mode.entity_test_render.full.yml
+++ b/core/modules/system/tests/modules/entity_test/config/entity.view_mode.entity_test_render.full.yml
@@ -1,4 +1,5 @@
 id: entity_test_render.full
 label: Full
 status: '0'
+cache: '1'
 targetEntityType: entity_test_render
diff --git a/core/modules/system/tests/modules/entity_test/config/entity.view_mode.entity_test_render.test.yml b/core/modules/system/tests/modules/entity_test/config/entity.view_mode.entity_test_render.test.yml
index a0d108bb1237..8593b8cf78fe 100644
--- a/core/modules/system/tests/modules/entity_test/config/entity.view_mode.entity_test_render.test.yml
+++ b/core/modules/system/tests/modules/entity_test/config/entity.view_mode.entity_test_render.test.yml
@@ -1,4 +1,5 @@
 id: entity_test_render.test
 label: Test
 status: '0'
+cache: '0'
 targetEntityType: entity_test_render
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index 19161b37852f..6e2082a4c12c 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -143,15 +143,17 @@ function entity_test_entity_bundle_info() {
 function entity_test_entity_view_mode_info_alter(&$view_modes) {
   $entity_info = entity_get_info();
   foreach ($entity_info as $entity_type => $info) {
-    if ($entity_info[$entity_type]['module'] == 'entity_test') {
+    if ($entity_info[$entity_type]['module'] == 'entity_test' && !isset($view_modes[$entity_type])) {
       $view_modes[$entity_type] = array(
         'full' => array(
           'label' => t('Full object'),
           'status' => TRUE,
+          'cache' => TRUE,
         ),
         'teaser' => array(
           'label' => t('Teaser'),
           'status' => TRUE,
+          'cache' => TRUE,
         ),
       );
     }
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestLabel.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestLabel.php
index 224da5a5958f..fc969c3c2a4d 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestLabel.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestLabel.php
@@ -18,9 +18,11 @@
  *   label = @Translation("Entity Test label"),
  *   module = "entity_test",
  *   controllers = {
- *     "storage" = "Drupal\entity_test\EntityTestStorageController"
+ *     "storage" = "Drupal\entity_test\EntityTestStorageController",
+ *     "render" = "Drupal\entity_test\EntityTestRenderController"
  *   },
  *   base_table = "entity_test",
+ *   render_cache = FALSE,
  *   entity_keys = {
  *     "id" = "id",
  *     "label" = "name",
diff --git a/core/modules/taxonomy/config/entity.view_mode.taxonomy_term.full.yml b/core/modules/taxonomy/config/entity.view_mode.taxonomy_term.full.yml
index 100547ebc268..2666012234fb 100644
--- a/core/modules/taxonomy/config/entity.view_mode.taxonomy_term.full.yml
+++ b/core/modules/taxonomy/config/entity.view_mode.taxonomy_term.full.yml
@@ -1,4 +1,5 @@
 id: taxonomy_term.full
 label: Taxonomy term page
 status: '0'
+cache: '1'
 targetEntityType: taxonomy_term
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Entity/Term.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Entity/Term.php
index 08b11f8c89f8..03ddfe2d52e8 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Entity/Term.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Entity/Term.php
@@ -189,7 +189,7 @@ public function preSave(EntityStorageControllerInterface $storage_controller) {
    * {@inheritdoc}
    */
   public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
-    parent::preSave($storage_controller, $update);
+    parent::postSave($storage_controller, $update);
 
     // Only change the parents if a value is set, keep the existing values if
     // not.
diff --git a/core/modules/user/config/entity.view_mode.user.compact.yml b/core/modules/user/config/entity.view_mode.user.compact.yml
index c27265b188d2..c211e99284fb 100644
--- a/core/modules/user/config/entity.view_mode.user.compact.yml
+++ b/core/modules/user/config/entity.view_mode.user.compact.yml
@@ -1,4 +1,5 @@
 id: user.compact
 label: Compact
 status: '1'
+cache: '1'
 targetEntityType: user
diff --git a/core/modules/user/config/entity.view_mode.user.full.yml b/core/modules/user/config/entity.view_mode.user.full.yml
index ac1ca2005cdb..343e909b4ce2 100644
--- a/core/modules/user/config/entity.view_mode.user.full.yml
+++ b/core/modules/user/config/entity.view_mode.user.full.yml
@@ -1,4 +1,5 @@
 id: user.full
 label: User account
 status: '0'
+cache: '1'
 targetEntityType: user
diff --git a/core/modules/user/lib/Drupal/user/Tests/UserValidationTest.php b/core/modules/user/lib/Drupal/user/Tests/UserValidationTest.php
index 14cf2af41da6..99aaecb1cb97 100644
--- a/core/modules/user/lib/Drupal/user/Tests/UserValidationTest.php
+++ b/core/modules/user/lib/Drupal/user/Tests/UserValidationTest.php
@@ -20,7 +20,7 @@ class UserValidationTest extends DrupalUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('field', 'user', 'system');
+  public static $modules = array('entity', 'field', 'user', 'system');
 
   public static function getInfo() {
     return array(
diff --git a/core/modules/user/lib/Drupal/user/Tests/Views/UserUnitTestBase.php b/core/modules/user/lib/Drupal/user/Tests/Views/UserUnitTestBase.php
index 02265f918ea5..9446f66d2bb1 100644
--- a/core/modules/user/lib/Drupal/user/Tests/Views/UserUnitTestBase.php
+++ b/core/modules/user/lib/Drupal/user/Tests/Views/UserUnitTestBase.php
@@ -20,7 +20,7 @@ abstract class UserUnitTestBase extends ViewUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('user_test_views', 'user', 'system', 'field');
+  public static $modules = array('user_test_views', 'user', 'system', 'entity', 'field');
 
   /**
    * Users to use during this test.
diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/RelationshipJoinTestBase.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/RelationshipJoinTestBase.php
index d0bd49c2dede..fae35159ce54 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Plugin/RelationshipJoinTestBase.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/RelationshipJoinTestBase.php
@@ -20,7 +20,7 @@ abstract class RelationshipJoinTestBase extends PluginUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('system', 'user', 'field');
+  public static $modules = array('system', 'user', 'entity', 'field');
 
   /**
    * Overrides \Drupal\views\Tests\ViewUnitTestBase::setUpFixtures().
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 aa78fb980722..7d88791bf3d7 100644
--- a/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php
+++ b/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php
@@ -1232,4 +1232,19 @@ public static function baseFieldDefinitions($entity_type) {
     //   https://drupal.org/node/2004244.
     return array();
   }
+
+  /**
+   * {@inheritdoc}
+   */
+   public function referencedEntities() {
+     return $this->storage->referencedEntities();
+   }
+
+   /**
+    * {@inheritdoc}
+    */
+   public function changed() {
+     return $this->storage->changed();
+   }
+
 }
-- 
GitLab