 core/lib/Drupal/Core/Field/FormatterBase.php       |  10 +-
 core/modules/shortcut/shortcut.module              |   7 ++
 core/modules/shortcut/src/Entity/Shortcut.php      |  51 ++++++++-
 .../Tests/Entity/EntityCacheTagsTestBase.php       | 116 +++++++++++++--------
 4 files changed, 131 insertions(+), 53 deletions(-)

diff --git a/core/lib/Drupal/Core/Field/FormatterBase.php b/core/lib/Drupal/Core/Field/FormatterBase.php
index e1646ff..45e3200 100644
--- a/core/lib/Drupal/Core/Field/FormatterBase.php
+++ b/core/lib/Drupal/Core/Field/FormatterBase.php
@@ -6,6 +6,7 @@
  */
 
 namespace Drupal\Core\Field;
+use Drupal\Component\Utility\NestedArray;
 
 /**
  * Base class for 'Field formatter' plugin implementations.
@@ -100,8 +101,13 @@ public function view(FieldItemListInterface $items) {
         }
 
         if (isset($item->entity)) {
-          $info['#cache']['tags'][$item->entity->getEntityTypeId()][] = $item->entity->id();
-          $info['#cache']['tags'][$item->entity->getEntityTypeId() . '_view'] = TRUE;
+          $info['#cache']['tags'] = NestedArray::mergeDeep($info['#cache']['tags'], $item->entity->getCacheTag());
+          if ($item->entity->getEntityType()->hasControllerClass('view_builder')) {
+            $view_cache_tag = \Drupal::entityManager()
+              ->getViewBuilder($item->entity->getEntityTypeId())
+              ->getCacheTag();
+            $info['#cache']['tags'] = NestedArray::mergeDeep($info['#cache']['tags'], $view_cache_tag);
+          }
         }
       }
 
diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module
index 870bd8a..269ac09 100644
--- a/core/modules/shortcut/shortcut.module
+++ b/core/modules/shortcut/shortcut.module
@@ -5,6 +5,7 @@
  * Allows users to manage customizable lists of shortcut links.
  */
 
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Routing\UrlMatcher;
 use Drupal\Core\Url;
@@ -299,12 +300,15 @@ function shortcut_renderable_links($shortcut_set = NULL) {
     $shortcut_set = shortcut_current_displayed_set();
   }
 
+  /** @var \Drupal\shortcut\ShortcutInterface[] $shortcuts  */
   $shortcuts = \Drupal::entityManager()->getStorage('shortcut')->loadByProperties(array('shortcut_set' => $shortcut_set->id()));
+  $all_cache_tags = array();
   foreach ($shortcuts as $shortcut) {
     $links[] = array(
       'title' => $shortcut->label(),
       'href' => $shortcut->path->value,
     );
+    $all_cache_tags[] = $shortcut->getCacheTag();
   }
 
   if (!empty($links)) {
@@ -314,6 +318,9 @@ function shortcut_renderable_links($shortcut_set = NULL) {
       '#attributes' => array(
         'class' => array('menu'),
       ),
+      '#cache' => array(
+        'tags' => NestedArray::mergeDeepArray($all_cache_tags),
+      ),
     );
   }
 
diff --git a/core/modules/shortcut/src/Entity/Shortcut.php b/core/modules/shortcut/src/Entity/Shortcut.php
index 71a33e4..031b241 100644
--- a/core/modules/shortcut/src/Entity/Shortcut.php
+++ b/core/modules/shortcut/src/Entity/Shortcut.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\shortcut\Entity;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Entity\ContentEntityBase;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
@@ -25,6 +26,7 @@
  *     "form" = {
  *       "default" = "Drupal\shortcut\ShortcutForm",
  *       "add" = "Drupal\shortcut\ShortcutForm",
+ *       "edit" = "Drupal\shortcut\ShortcutForm",
  *       "delete" = "Drupal\shortcut\Form\ShortcutDeleteForm"
  *     },
  *     "translation" = "Drupal\content_translation\ContentTranslationHandler"
@@ -39,9 +41,12 @@
  *     "label" = "title"
  *   },
  *   links = {
+ *     "canonical" = "shortcut.link_edit",
  *     "delete-form" = "shortcut.link_delete",
- *     "edit-form" = "shortcut.link_edit"
- *   }
+ *     "edit-form" = "shortcut.link_edit",
+ *     "admin-form" = "shortcut.link_edit"
+ *   },
+ *   bundle_entity_type = "shortcut_set"
  * )
  */
 class Shortcut extends ContentEntityBase implements ShortcutInterface {
@@ -124,9 +129,26 @@ public static function preCreate(EntityStorageInterface $storage, array &$values
   public function preSave(EntityStorageInterface $storage) {
     parent::preSave($storage);
 
-    $url = Url::createFromPath($this->path->value);
-    $this->setRouteName($url->getRouteName());
-    $this->setRouteParams($url->getRouteParameters());
+    if ($this->isNew() || !empty($this->path->value)) {
+      $url = Url::createFromPath($this->path->value);
+      $this->setRouteName($url->getRouteName());
+      $this->setRouteParams($url->getRouteParameters());
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+    parent::postSave($storage);
+
+    // Entity::postSave() calls Entity::invalidateTagsOnSave(), which only
+    // handles the regular cases. The Shortcut entity has one special case: a
+    // newly created shortcut is *also* added to a shortcut set, so we must
+    // invalidate the associated shortcut set's cache tag.
+    if (!$update) {
+      Cache::invalidateTags($this->getCacheTag());
+    }
   }
 
   /**
@@ -199,4 +221,23 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
     return $fields;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTag() {
+    $field_value = $this->get('shortcut_set')
+      ->getValue(TRUE);
+    $shortcut_set = $field_value[0]['entity'];
+    return $shortcut_set->getCacheTag();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getListCacheTags() {
+    $field_value = $this->get('shortcut_set')->getValue(TRUE);
+    $shortcut_set = $field_value[0]['entity'];
+    return $shortcut_set->getListCacheTags();
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCacheTagsTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCacheTagsTestBase.php
index e4a6a52..0175cd2 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCacheTagsTestBase.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityCacheTagsTestBase.php
@@ -7,8 +7,9 @@
 
 namespace Drupal\system\Tests\Entity;
 
-use Drupal\Component\Utility\String;
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\EventSubscriber\HtmlViewSubscriber;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\system\Tests\Cache\PageCacheTagsTestBase;
 
@@ -212,22 +213,26 @@ public function testReferencedEntity() {
 
     $theme_cache_tags = array('content:1', 'theme:stark', 'theme_global_settings:1');
 
-    // Generate the standardized entity cache tags.
-    $cache_tag = $entity_type . ':' . $this->entity->id();
-    $view_cache_tag = $entity_type . '_view:1';
+    $view_cache_tag = array();
+    if ($this->entity->getEntityType()->hasControllerClass('view_builder')) {
+      $view_cache_tag = \Drupal::entityManager()->getViewBuilder($entity_type)
+        ->getCacheTag();
+    }
 
     // Generate the cache tags for the (non) referencing entities.
-    $referencing_entity_cache_tags = array(
-      'entity_test_view:1',
-      'entity_test:' . $this->referencing_entity->id(),
+    $referencing_entity_cache_tags = NestedArray::mergeDeep(
+      $this->referencing_entity->getCacheTag(),
+      \Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTag(),
       // Includes the main entity's cache tags, since this entity references it.
-      $cache_tag,
-      $view_cache_tag,
+      $this->entity->getCacheTag(),
+      $view_cache_tag
     );
-    $non_referencing_entity_cache_tags = array(
-      'entity_test_view:1',
-      'entity_test:' . $this->non_referencing_entity->id(),
+    $referencing_entity_cache_tags = explode(' ', HtmlViewSubscriber::convertCacheTagsToHeader($referencing_entity_cache_tags));
+    $non_referencing_entity_cache_tags = NestedArray::mergeDeep(
+      $this->non_referencing_entity->getCacheTag(),
+      \Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTag()
     );
+    $non_referencing_entity_cache_tags = explode(' ', HtmlViewSubscriber::convertCacheTagsToHeader($non_referencing_entity_cache_tags));
 
 
     // Prime the page cache for the referencing entity.
@@ -239,9 +244,7 @@ public function testReferencedEntity() {
 
     // Also verify the existence of an entity render cache entry.
     $cid = 'entity_view:entity_test:' . $this->referencing_entity->id() . ':full:stark:r.anonymous';
-    $cache_entry = \Drupal::cache('render')->get($cid);
-    $this->assertIdentical($cache_entry->tags, $referencing_entity_cache_tags);
-
+    $this->verifyRenderCache($cid, $referencing_entity_cache_tags);
 
     // Prime the page cache for the non-referencing entity.
     $this->verifyPageCache($non_referencing_entity_path, 'MISS');
@@ -252,8 +255,7 @@ public function testReferencedEntity() {
 
     // Also verify the existence of an entity render cache entry.
     $cid = 'entity_view:entity_test:' . $this->non_referencing_entity->id() . ':full:stark:r.anonymous';
-    $cache_entry = \Drupal::cache('render')->get($cid);
-    $this->assertIdentical($cache_entry->tags, $non_referencing_entity_cache_tags);
+    $this->verifyRenderCache($cid, $non_referencing_entity_cache_tags);
 
 
 
@@ -306,19 +308,21 @@ public function testReferencedEntity() {
     $this->verifyPageCache($non_referencing_entity_path, 'HIT');
 
 
-    // Verify that after modifying the entity's "full" display, there is a cache
-    // miss for both the referencing entity, and the listing of referencing
-    // entities, but not for the non-referencing entity.
-    $this->pass("Test modification of referenced entity's 'full' display.", 'Debug');
-    $entity_display = entity_get_display($entity_type, $this->entity->bundle(), 'full');
-    $entity_display->save();
-    $this->verifyPageCache($referencing_entity_path, 'MISS');
-    $this->verifyPageCache($listing_path, 'MISS');
-    $this->verifyPageCache($non_referencing_entity_path, 'HIT');
+    if ($this->entity->getEntityType()->hasControllerClass('view_builder')) {
+      // Verify that after modifying the entity's "full" display, there is a cache
+      // miss for both the referencing entity, and the listing of referencing
+      // entities, but not for the non-referencing entity.
+      $this->pass("Test modification of referenced entity's 'full' display.", 'Debug');
+      $entity_display = entity_get_display($entity_type, $this->entity->bundle(), 'full');
+      $entity_display->save();
+      $this->verifyPageCache($referencing_entity_path, 'MISS');
+      $this->verifyPageCache($listing_path, 'MISS');
+      $this->verifyPageCache($non_referencing_entity_path, 'HIT');
 
-    // Verify cache hits.
-    $this->verifyPageCache($referencing_entity_path, 'HIT');
-    $this->verifyPageCache($listing_path, 'HIT');
+      // Verify cache hits.
+      $this->verifyPageCache($referencing_entity_path, 'HIT');
+      $this->verifyPageCache($listing_path, 'HIT');
+    }
 
 
     $bundle_entity_type = $this->entity->getEntityType()->getBundleEntityType();
@@ -375,7 +379,7 @@ public function testReferencedEntity() {
     // a cache miss for both the referencing entity, and the listing of
     // referencing entities, but not for the non-referencing entity.
     $this->pass("Test invalidation of referenced entity's cache tag.", 'Debug');
-    Cache::invalidateTags(array($entity_type => array($this->entity->id())));
+    Cache::invalidateTags($this->entity->getCacheTag());
     $this->verifyPageCache($referencing_entity_path, 'MISS');
     $this->verifyPageCache($listing_path, 'MISS');
     $this->verifyPageCache($non_referencing_entity_path, 'HIT');
@@ -385,19 +389,20 @@ public function testReferencedEntity() {
     $this->verifyPageCache($listing_path, 'HIT');
 
 
-    // Verify that after invalidating the generic entity type's view cache tag
-    // directly, there is a cache miss for both the referencing entity, and the
-    // listing of referencing entities, but not for the non-referencing entity.
-    $this->pass("Test invalidation of referenced entity's 'view' cache tag.", 'Debug');
-    Cache::invalidateTags(array($entity_type . '_view' => TRUE));
-    $this->verifyPageCache($referencing_entity_path, 'MISS');
-    $this->verifyPageCache($listing_path, 'MISS');
-    $this->verifyPageCache($non_referencing_entity_path, 'HIT');
-
-    // Verify cache hits.
-    $this->verifyPageCache($referencing_entity_path, 'HIT');
-    $this->verifyPageCache($listing_path, 'HIT');
+    if (!empty($view_cache_tag)) {
+      // Verify that after invalidating the generic entity type's view cache tag
+      // directly, there is a cache miss for both the referencing entity, and the
+      // listing of referencing entities, but not for the non-referencing entity.
+      $this->pass("Test invalidation of referenced entity's 'view' cache tag.", 'Debug');
+      Cache::invalidateTags($view_cache_tag);
+      $this->verifyPageCache($referencing_entity_path, 'MISS');
+      $this->verifyPageCache($listing_path, 'MISS');
+      $this->verifyPageCache($non_referencing_entity_path, 'HIT');
 
+      // Verify cache hits.
+      $this->verifyPageCache($referencing_entity_path, 'HIT');
+      $this->verifyPageCache($listing_path, 'HIT');
+    }
 
     // Verify that after deleting the entity, there is a cache miss for both the
     // referencing entity, and the listing of referencing entities, but not for
@@ -409,12 +414,31 @@ public function testReferencedEntity() {
     $this->verifyPageCache($non_referencing_entity_path, 'HIT');
 
     // Verify cache hits.
-    $tags = array_merge($theme_cache_tags, array(
-      'entity_test_view:1',
-      'entity_test:' . $this->referencing_entity->id(),
-    ));
+    $referencing_entity_cache_tags = NestedArray::mergeDeep(
+      $this->referencing_entity->getCacheTag(),
+      \Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTag()
+    );
+    $referencing_entity_cache_tags = explode(' ', HtmlViewSubscriber::convertCacheTagsToHeader($referencing_entity_cache_tags));
+    $tags = array_merge($theme_cache_tags, $referencing_entity_cache_tags);
     $this->verifyPageCache($referencing_entity_path, 'HIT', $tags);
     $this->verifyPageCache($listing_path, 'HIT', $theme_cache_tags);
   }
 
+  /**
+   * Verify that a given render cache entry exists, with the correct cache tags.
+   *
+   * @param string $cid
+   *   The render cache item ID.
+   * @param array $tags
+   *   An array of expected cache tags.
+   */
+  protected function verifyRenderCache($cid, array $tags) {
+    // Also verify the existence of an entity render cache entry.
+    $cache_entry = \Drupal::cache('render')->get($cid);
+    $this->assertTrue($cache_entry, 'A render cache entry exists.');
+    sort($cache_entry->tags);
+    sort($tags);
+    $this->assertIdentical($cache_entry->tags, $tags);
+  }
+
 }
