 .../Core/Cache/CacheableDependencyInterface.php    | 56 ++++++++++++++++++++++
 core/lib/Drupal/Core/Cache/CacheableInterface.php  | 35 +-------------
 core/lib/Drupal/Core/Config/ConfigBase.php         | 23 +++++++--
 core/lib/Drupal/Core/Entity/ContentEntityBase.php  | 11 +++++
 core/lib/Drupal/Core/Entity/Entity.php             | 16 +++++++
 core/lib/Drupal/Core/Entity/EntityInterface.php    | 11 +----
 core/lib/Drupal/Core/Entity/EntityViewBuilder.php  | 29 ++++++++---
 core/lib/Drupal/Core/Render/BubbleableMetadata.php | 17 +++++++
 core/lib/Drupal/Core/Render/Renderer.php           | 10 ++++
 core/lib/Drupal/Core/Render/RendererInterface.php  | 15 ++++++
 core/modules/book/src/BookManager.php              |  3 +-
 core/modules/comment/src/CommentForm.php           | 10 ++--
 core/modules/comment/src/CommentViewBuilder.php    |  3 +-
 .../contact/src/Controller/ContactController.php   |  3 +-
 .../forum/src/Controller/ForumController.php       |  3 +-
 .../language/src/Config/LanguageConfigOverride.php |  8 ++++
 .../src/Tests/Entity/EntityViewBuilderTest.php     |  8 ++--
 core/modules/user/src/Form/UserLoginForm.php       |  3 +-
 core/modules/views_ui/src/ViewUI.php               | 14 ++++++
 19 files changed, 209 insertions(+), 69 deletions(-)

diff --git a/core/lib/Drupal/Core/Cache/CacheableDependencyInterface.php b/core/lib/Drupal/Core/Cache/CacheableDependencyInterface.php
new file mode 100644
index 0000000..6ae7d41
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/CacheableDependencyInterface.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Core\CacheableInterface
+ */
+
+namespace Drupal\Core\Cache;
+
+/**
+ * Defines an interface for objects which may be used by other cached objects.
+ *
+ * All cacheability metadata exposed in this interface is bubbled to parent
+ * objects when they are cached: if a child object needs to be varied by certain
+ * cache contexts, invalidated by certain cache tags, expire after a certain
+ * maximum age, then so should any parent object.
+ *
+ * @ingroup cache
+ */
+interface CacheableDependencyInterface {
+
+  /**
+   * The cache contexts associated with this object.
+   *
+   * These identify a specific variation/representation of the object.
+   *
+   * Cache contexts are tokens: placeholders that are converted to cache keys by
+   * the @cache_contexts service. The replacement value depends on the request
+   * context (the current URL, language, and so on). They're converted before
+   * storing an object in cache.
+   *
+   * @return string[]
+   *   An array of cache context tokens, used to generate a cache ID.
+   *
+   * @see \Drupal\Core\Cache\CacheContexts::convertTokensToKeys()
+   */
+  public function getCacheContexts();
+
+  /**
+   * The cache tags associated with this object.
+   *
+   * When this object is modified, these cache tags will be invalidated.
+   *
+   * @return string[]
+   *  A set of cache tags.
+   */
+  public function getCacheTags();
+
+  /**
+   * The maximum age for which this object may be cached.
+   *
+   * @return int
+   *   The maximum time in seconds that this object may be cached.
+   */
+  public function getCacheMaxAge();
+
+}
diff --git a/core/lib/Drupal/Core/Cache/CacheableInterface.php b/core/lib/Drupal/Core/Cache/CacheableInterface.php
index 70b0a87..f57f236 100644
--- a/core/lib/Drupal/Core/Cache/CacheableInterface.php
+++ b/core/lib/Drupal/Core/Cache/CacheableInterface.php
@@ -20,7 +20,7 @@
  *
  * @ingroup cache
  */
-interface CacheableInterface {
+interface CacheableInterface extends CacheableDependencyInterface {
 
   /**
    * The cache keys associated with this potentially cacheable object.
@@ -33,39 +33,6 @@
   public function getCacheKeys();
 
   /**
-   * The cache contexts associated with this potentially cacheable object.
-   *
-   * These identify a specific variation/representation of the object.
-   *
-   * Cache contexts are tokens: placeholders that are converted to cache keys by
-   * the @cache_contexts service. The replacement value depends on the request
-   * context (the current URL, language, and so on). They're converted before
-   * storing an object in cache.
-   *
-   * @return string[]
-   *   An array of cache context tokens, used to generate a cache ID.
-   *
-   * @see \Drupal\Core\Cache\CacheContexts::convertTokensToKeys()
-   */
-  public function getCacheContexts();
-
-  /**
-   * The cache tags associated with this potentially cacheable object.
-   *
-   * @return string[]
-   *  An array of cache tags.
-   */
-  public function getCacheTags();
-
-  /**
-   * The maximum age for which this object may be cached.
-   *
-   * @return int
-   *   The maximum time in seconds that this object may be cached.
-   */
-  public function getCacheMaxAge();
-
-  /**
    * Indicates whether this object is cacheable.
    *
    * @return bool
diff --git a/core/lib/Drupal/Core/Config/ConfigBase.php b/core/lib/Drupal/Core/Config/ConfigBase.php
index e94db9b..8a2397a 100644
--- a/core/lib/Drupal/Core/Config/ConfigBase.php
+++ b/core/lib/Drupal/Core/Config/ConfigBase.php
@@ -9,6 +9,8 @@
 
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\String;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
 
 /**
@@ -26,7 +28,7 @@
  * @see \Drupal\Core\Config\Config
  * @see \Drupal\Core\Theme\ThemeSettings
  */
-abstract class ConfigBase {
+abstract class ConfigBase implements CacheableDependencyInterface {
   use DependencySerializationTrait;
 
   /**
@@ -264,13 +266,24 @@ public function merge(array $data_to_merge) {
   }
 
   /**
-   * The unique cache tag associated with this configuration object.
-   *
-   * @return string[]
-   *   An array of cache tags.
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
    */
   public function getCacheTags() {
     return ['config:' . $this->name];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return Cache::PERMANENT;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
index d315b27..ae8c2a5 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -990,6 +990,17 @@ public function referencedEntities() {
   }
 
   /**
+   * {@inheritdoc}
+   *
+   * Because we know that this class implements TranslatableInterface, we can
+   * optimize this compared to \Drupal\Core\Entity\Entity::getCacheContexts().
+   */
+  public function getCacheContexts() {
+    // @todo use 'languages:content', blocked on https://www.drupal.org/node/2432837
+    return count($this->getTranslationLanguages()) > 1 ? ['language'] : [];
+  }
+
+  /**
    * Returns the value of the given entity key, if defined.
    *
    * @param string $key
diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 455607e..bbee272 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -17,6 +17,7 @@
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Link;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\TypedData\TranslatableInterface;
 use Drupal\Core\Url;
 
 /**
@@ -429,6 +430,14 @@ public function referencedEntities() {
   /**
    * {@inheritdoc}
    */
+  public function getCacheContexts() {
+    // @todo use 'languages:content', blocked on https://www.drupal.org/node/2432837
+    return ($this instanceof TranslatableInterface && count($this->getTranslationLanguages()) > 1) ? ['language'] : [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getCacheTags() {
     // @todo Add bundle-specific listing cache tag? https://drupal.org/node/2145751
     return [$this->entityTypeId . ':' . $this->id()];
@@ -436,6 +445,13 @@ public function getCacheTags() {
 
   /**
    * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return Cache::PERMANENT;
+  }
+
+  /**
+   * {@inheritdoc}
    *
    * @return static|null
    *   The entity object or NULL if there is no entity with the given ID.
diff --git a/core/lib/Drupal/Core/Entity/EntityInterface.php b/core/lib/Drupal/Core/Entity/EntityInterface.php
index fac0380..7b5ebc7 100644
--- a/core/lib/Drupal/Core/Entity/EntityInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityInterface.php
@@ -8,13 +8,14 @@
 namespace Drupal\Core\Entity;
 
 use Drupal\Core\Access\AccessibleInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 
 /**
  * Defines a common interface for all entity objects.
  *
  * @ingroup entity_api
  */
-interface EntityInterface extends AccessibleInterface {
+interface EntityInterface extends AccessibleInterface, CacheableDependencyInterface {
 
   /**
    * Returns the entity UUID (Universally Unique Identifier).
@@ -397,14 +398,6 @@ public function toArray();
   public function getTypedData();
 
   /**
-   * The unique cache tag associated with this entity.
-   *
-   * @return string[]
-   *   An array of cache tags.
-   */
-  public function getCacheTags();
-
-  /**
    * Gets the key that is used to store configuration dependencies.
    *
    * @return string
diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
index 071de3e..0a8fdbb 100644
--- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
+++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
@@ -169,13 +169,20 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco
       // Collect cache defaults for this entity.
       '#cache' => array(
         'tags' => Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags()),
-        'contexts' => [
-          'theme',
-          'user.roles',
-        ],
+        'contexts' => $entity->getCacheContexts(),
+        'max-age' => $entity->getCacheMaxAge(),
       ),
     );
 
+    // @todo This will go away thanks to:
+    //   - https://www.drupal.org/node/2099137 (will remove 'user.roles')
+    //   - https://www.drupal.org/node/2453059 (will remove 'theme')
+    // It's only moved out o/t above code to show what the end result will be.
+    $build['#cache']['contexts'] = Cache::mergeContexts($build['#cache']['contexts'], [
+      'theme',
+      'user.roles',
+    ]);
+
     // Cache the rendered output if permitted by the view mode and global entity
     // type configuration.
     if ($this->isViewModeCacheable($view_mode) && !$entity->isNew() && $entity->isDefaultRevision() && $this->entityType->isRenderCacheable()) {
@@ -189,8 +196,18 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco
         'bin' => $this->cacheBin,
       );
 
-      if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) {
-        $build['#cache']['keys'][] = $langcode;
+      // There are two cases:
+      // 1. $langcode is the negotiated content language. Then $langcode matches
+      //    the 'language' cache context, which is set by translated entities.
+      //    Hence no work is necessary in this case.
+      // 2. $langcode is set to a specific value when calling ::view() or
+      //    ::viewMultiple() and therefore doesn't match the negotiated content
+      //    language. In that case, the 'language' cache context set by the
+      //    entity would be *wrong*. Thus, the 'language' cache context is
+      //    removed and a cache key is added instead.
+      if ($langcode !== $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId()) {
+        $build['#cache']['contexts'] = array_diff($build['#cache']['contexts'], ['language']);
+        $build['#cache']['keys'][] = 'forced-' . $langcode;
       }
     }
 
diff --git a/core/lib/Drupal/Core/Render/BubbleableMetadata.php b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
index d822d82..5f28d01 100644
--- a/core/lib/Drupal/Core/Render/BubbleableMetadata.php
+++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 
 /**
  * Value object used for bubbleable rendering metadata.
@@ -108,6 +109,22 @@ public static function createFromRenderArray(array $build) {
   }
 
   /**
+   * Creates a bubbleable metadata object from a cacheable depended object.
+   *
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface $object
+   *   The object whose cacheability metadata to retrieve.
+   *
+   * @return static
+   */
+  public static function createFromObject(CacheableDependencyInterface $object) {
+    $meta = new static();
+    $meta->contexts = $object->getCacheContexts();
+    $meta->tags = $object->getCacheTags();
+    $meta->maxAge = $object->getCacheMaxAge();
+    return $meta;
+  }
+
+  /**
    * Gets cache tags.
    *
    * @return string[]
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 06f9c4f..b979f51 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -10,6 +10,7 @@
 use Drupal\Component\Utility\Crypt;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Cache\CacheContexts;
 use Drupal\Core\Cache\CacheFactoryInterface;
 use Drupal\Core\Controller\ControllerResolverInterface;
@@ -771,6 +772,15 @@ public static function mergeBubbleableMetadata(array $a, array $b) {
   /**
    * {@inheritdoc}
    */
+  public static function dependsOn(array &$elements, CacheableDependencyInterface $dependency) {
+    $meta_a = BubbleableMetadata::createFromRenderArray($elements);
+    $meta_b = BubbleableMetadata::createFromObject($dependency);
+    $meta_a->merge($meta_b)->applyTo($elements);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public static function mergeAttachments(array $a, array $b) {
     // If both #attached arrays contain drupalSettings, then merge them
     // correctly; adding the same settings multiple times needs to behave
diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php
index 33502ba..a3fe59e 100644
--- a/core/lib/Drupal/Core/Render/RendererInterface.php
+++ b/core/lib/Drupal/Core/Render/RendererInterface.php
@@ -6,6 +6,7 @@
  */
 
 namespace Drupal\Core\Render;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 
 /**
  * Defines an interface for turning a render array into a string.
@@ -343,6 +344,20 @@ public function getCacheableRenderArray(array $elements);
   public static function mergeBubbleableMetadata(array $a, array $b);
 
   /**
+   * Merges in the bubbleable cacheability metadata of a depended object.
+   *
+   * E.g. when a render array depends on some configuration, an entity, or an
+   * access result, we must make sure their cacheability metadata is present on
+   * the render array. This method makes doing that simple.
+   *
+   * @param array &$elements
+   *   The render array to update.
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface $dependency
+   *   The dependency.
+   */
+  public static function dependsOn(array &$elements, CacheableDependencyInterface $dependency);
+
+  /**
    * Merges two attachments arrays (which live under the '#attached' key).
    *
    * The values under the 'drupalSettings' key are merged in a special way, to
diff --git a/core/modules/book/src/BookManager.php b/core/modules/book/src/BookManager.php
index 26c02fa..9180c80 100644
--- a/core/modules/book/src/BookManager.php
+++ b/core/modules/book/src/BookManager.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Renderer;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -353,7 +354,7 @@ protected function addParentSelectFormElements(array $book_link) {
         '#suffix' => '</div>',
       );
     }
-    $form['#cache']['tags'] = Cache::mergeTags(isset($form['#cache']['tags']) ? $form['#cache']['tags'] : [],  $config->getCacheTags());
+    Renderer::dependsOn($form, $config);
 
     return $form;
   }
diff --git a/core/modules/comment/src/CommentForm.php b/core/modules/comment/src/CommentForm.php
index 8f7368a..53bde29 100644
--- a/core/modules/comment/src/CommentForm.php
+++ b/core/modules/comment/src/CommentForm.php
@@ -17,6 +17,7 @@
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Render\Renderer;
 use Drupal\Core\Session\AccountInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -214,12 +215,9 @@ public function form(array $form, FormStateInterface $form_state) {
       '#access' => $is_admin,
     );
 
-    $form['#cache']['tags'] = Cache::mergeTags(
-      isset($form['#cache']['tags']) ? $form['#cache']['tags'] : [],
-      $config->getCacheTags(),
-      // The form depends on the field definition.
-      $field_definition->getConfig($entity->bundle())->getCacheTags()
-    );
+    Renderer::dependsOn($form, $config);
+    // The form depends on the field definition.
+    Renderer::dependsOn($form, $field_definition->getConfig($entity->bundle()));
 
     return parent::form($form, $form_state, $comment);
   }
diff --git a/core/modules/comment/src/CommentViewBuilder.php b/core/modules/comment/src/CommentViewBuilder.php
index bf0f526..121a154 100644
--- a/core/modules/comment/src/CommentViewBuilder.php
+++ b/core/modules/comment/src/CommentViewBuilder.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityViewBuilder;
+use Drupal\Core\Render\Renderer;
 
 /**
  * Render controller for comments.
@@ -104,7 +105,7 @@ public function buildComponents(array &$build, array $entities, array $displays,
 
       $account = comment_prepare_author($entity);
       $config = \Drupal::config('user.settings');
-      $build['#cache']['tags'] = Cache::mergeTags(isset($build['#cache']['tags']) ? $build['#cache']['tags'] : [],  $config->getCacheTags());
+      Renderer::dependsOn($build, $config);
       if ($config->get('signatures') && $account->getSignature()) {
         $build[$id]['signature'] = array(
           '#type' => 'processed_text',
diff --git a/core/modules/contact/src/Controller/ContactController.php b/core/modules/contact/src/Controller/ContactController.php
index 648a064..101ceec 100644
--- a/core/modules/contact/src/Controller/ContactController.php
+++ b/core/modules/contact/src/Controller/ContactController.php
@@ -12,6 +12,7 @@
 use Drupal\Core\Datetime\DateFormatter;
 use Drupal\Core\Flood\FloodInterface;
 use Drupal\contact\ContactFormInterface;
+use Drupal\Core\Render\Renderer;
 use Drupal\user\UserInterface;
 use Drupal\Component\Utility\String;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -106,7 +107,7 @@ public function contactSitePage(ContactFormInterface $contact_form = NULL) {
 
     $form = $this->entityFormBuilder()->getForm($message);
     $form['#title'] = String::checkPlain($contact_form->label());
-    $form['#cache']['tags'] = Cache::mergeTags(isset($form['#cache']['tags']) ? $form['#cache']['tags'] : [],  $config->getCacheTags());
+    Renderer::dependsOn($form, $config->getCacheTags());
     return $form;
   }
 
diff --git a/core/modules/forum/src/Controller/ForumController.php b/core/modules/forum/src/Controller/ForumController.php
index 18d7325..42fde11 100644
--- a/core/modules/forum/src/Controller/ForumController.php
+++ b/core/modules/forum/src/Controller/ForumController.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Entity\EntityAccessControlHandlerInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Render\Renderer;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
 use Drupal\forum\ForumManagerInterface;
@@ -191,7 +192,7 @@ protected function build($forums, TermInterface $term, $topics = array(), $paren
     if (empty($term->forum_container->value)) {
       $build['#attached']['feed'][] = array('taxonomy/term/' . $term->id() . '/feed', 'RSS - ' . $term->getName());
     }
-    $build['#cache']['tags'] = Cache::mergeTags(isset($build['#cache']['tags']) ? $build['#cache']['tags'] : [],  $config->getCacheTags());
+    Renderer::dependsOn($build, $config);
 
     return [
       'action' => $this->buildActionLinks($config->get('vocabulary'), $term),
diff --git a/core/modules/language/src/Config/LanguageConfigOverride.php b/core/modules/language/src/Config/LanguageConfigOverride.php
index 5be48ad..c13725f 100644
--- a/core/modules/language/src/Config/LanguageConfigOverride.php
+++ b/core/modules/language/src/Config/LanguageConfigOverride.php
@@ -92,4 +92,12 @@ public function getLangcode() {
     return $this->getLangcodeFromCollectionName($this->getStorage()->getCollectionName());
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    // @todo use 'languages:interface', blocked on https://www.drupal.org/node/2432837
+    return ['language'];
+  }
+
 }
diff --git a/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php b/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php
index 1588150..9c7ed7a 100644
--- a/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php
+++ b/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php
@@ -56,7 +56,7 @@ public function testEntityViewBuilderCache() {
     // Test that new entities (before they are saved for the first time) do not
     // generate a cache entry.
     $build = $this->container->get('entity.manager')->getViewBuilder('entity_test')->view($entity_test, 'full');
-    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts'], 'The render array element of new (unsaved) entities is not cached, but does have cache tags set.');
+    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts', 'max-age'], 'The render array element of new (unsaved) entities is not cached, but does have cache tags set.');
 
     // Get a fully built entity view render array.
     $entity_test->save();
@@ -161,17 +161,17 @@ public function testEntityViewBuilderCacheToggling() {
     // 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')->getViewBuilder('entity_test')->view($entity_test, 'full');
-    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts', 'keys', 'bin'] , 'A view mode with render cache enabled has the correct output (cache tags, keys, contexts and bin).');
+    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts', 'max-age', 'keys', 'bin'] , 'A view mode with render cache enabled has the correct output (cache tags, keys, contexts, max-age and bin).');
 
     // Test that a view mode can opt out of render caching.
     $build = $this->container->get('entity.manager')->getViewBuilder('entity_test')->view($entity_test, 'test');
-    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts'], 'A view mode with render cache disabled has the correct output (only cache tags).');
+    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts', 'max-age'], 'A view mode with render cache disabled has the correct output (only cache tags, contexts and max-age).');
 
     // 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')->getViewBuilder('entity_test_label')->view($entity_test_no_cache, 'full');
-    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts'], 'An entity type can opt out of render caching regardless of view mode configuration, but always has cache tags set.');
+    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts', 'max-age'], 'An entity type can opt out of render caching regardless of view mode configuration, but always has cache tags, contexts and max-age set.');
   }
 
   /**
diff --git a/core/modules/user/src/Form/UserLoginForm.php b/core/modules/user/src/Form/UserLoginForm.php
index a0c0876..1338766 100644
--- a/core/modules/user/src/Form/UserLoginForm.php
+++ b/core/modules/user/src/Form/UserLoginForm.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Flood\FloodInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Renderer;
 use Drupal\user\UserAuthInterface;
 use Drupal\user\UserStorageInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -112,7 +113,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $form['#validate'][] = '::validateAuthentication';
     $form['#validate'][] = '::validateFinal';
 
-    $form['#cache']['tags'] = Cache::mergeTags(isset($form['#cache']['tags']) ? $form['#cache']['tags'] : [],  $config->getCacheTags());
+    Renderer::dependsOn($form, $config);
 
     return $form;
   }
diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php
index 5aeac87..31d29b3 100644
--- a/core/modules/views_ui/src/ViewUI.php
+++ b/core/modules/views_ui/src/ViewUI.php
@@ -1211,6 +1211,13 @@ public function getDependencies() {
   /**
    * {@inheritdoc}
    */
+  public function getCacheContexts() {
+    return $this->storage->getCacheContexts();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getCacheTags() {
     return $this->storage->getCacheTags();
   }
@@ -1218,6 +1225,13 @@ public function getCacheTags() {
   /**
    * {@inheritdoc}
    */
+  public function getCacheMaxAge() {
+    return $this->storage->getCacheMaxAge();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function getTypedData() {
     $this->storage->getTypedData();
   }
