diff --git a/core/lib/Drupal/Component/Plugin/Context/Context.php b/core/lib/Drupal/Component/Plugin/Context/Context.php
index 04cb7b2..bde72ec 100644
--- a/core/lib/Drupal/Component/Plugin/Context/Context.php
+++ b/core/lib/Drupal/Component/Plugin/Context/Context.php
@@ -57,8 +57,7 @@ public function getContextValue() {
       $default_value = $definition->getDefaultValue();
 
       if (!isset($default_value) && $definition->isRequired()) {
-        $type = $definition->getDataType();
-        throw new ContextException(sprintf("The %s context is required and not present.", $type));
+        return NULL;
       }
       // Keep the default value here so that subsequent calls don't have to look
       // it up again.
diff --git a/core/lib/Drupal/Component/Plugin/ContextAwarePluginBase.php b/core/lib/Drupal/Component/Plugin/ContextAwarePluginBase.php
index b684630..0732d4d 100644
--- a/core/lib/Drupal/Component/Plugin/ContextAwarePluginBase.php
+++ b/core/lib/Drupal/Component/Plugin/ContextAwarePluginBase.php
@@ -22,7 +22,7 @@
    *
    * @var \Drupal\Component\Plugin\Context\ContextInterface[]
    */
-  protected $context;
+  protected $context = [];
 
   /**
    * Overrides \Drupal\Component\Plugin\PluginBase::__construct().
diff --git a/core/lib/Drupal/Core/Block/BlockBase.php b/core/lib/Drupal/Core/Block/BlockBase.php
index 262fd0c..7b82dba 100644
--- a/core/lib/Drupal/Core/Block/BlockBase.php
+++ b/core/lib/Drupal/Core/Block/BlockBase.php
@@ -10,6 +10,7 @@
 use Drupal\block\BlockInterface;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContextAwarePluginBase;
 use Drupal\Component\Utility\Unicode;
@@ -303,21 +304,42 @@ public function setTransliteration(TransliterationInterface $transliteration) {
    * {@inheritdoc}
    */
   public function getCacheContexts() {
-    return [];
+    $cache_contexts = [];
+    foreach ($this->getContexts() as $context) {
+      /** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
+      if ($context instanceof CacheableDependencyInterface) {
+        $cache_contexts = Cache::mergeContexts($cache_contexts, $context->getCacheContexts());
+      }
+    }
+    return $cache_contexts;
   }
 
   /**
    * {@inheritdoc}
    */
   public function getCacheTags() {
-    return [];
+    $tags = [];
+    foreach ($this->getContexts() as $context) {
+      /** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
+      if ($context instanceof CacheableDependencyInterface) {
+        $tags = Cache::mergeTags($tags, $context->getCacheTags());
+      }
+    }
+    return $tags;
   }
 
   /**
    * {@inheritdoc}
    */
   public function getCacheMaxAge() {
-    return (int)$this->configuration['cache']['max_age'];
+    $max_age = (int)$this->configuration['cache']['max_age'];
+    foreach ($this->getContexts() as $context) {
+      /** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
+      if ($context instanceof CacheableDependencyInterface) {
+        $max_age = Cache::mergeMaxAges($max_age, $context->getCacheMaxAge());
+      }
+    }
+    return $max_age;
   }
 
 }
diff --git a/core/lib/Drupal/Core/Condition/ConditionInterface.php b/core/lib/Drupal/Core/Condition/ConditionInterface.php
index e0b6115..c53059d 100644
--- a/core/lib/Drupal/Core/Condition/ConditionInterface.php
+++ b/core/lib/Drupal/Core/Condition/ConditionInterface.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Plugin\ConfigurablePluginInterface;
 use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Executable\ExecutableInterface;
 use Drupal\Core\Executable\ExecutableManagerInterface;
 use Drupal\Core\Plugin\PluginFormInterface;
@@ -46,7 +47,7 @@
  *
  * @ingroup plugin_api
  */
-interface ConditionInterface extends ExecutableInterface, PluginFormInterface, ConfigurablePluginInterface, PluginInspectionInterface {
+interface ConditionInterface extends ExecutableInterface, PluginFormInterface, ConfigurablePluginInterface, PluginInspectionInterface, CacheableDependencyInterface {
 
   /**
    * Determines whether condition result will be negated.
diff --git a/core/lib/Drupal/Core/Condition/ConditionPluginBase.php b/core/lib/Drupal/Core/Condition/ConditionPluginBase.php
index 4b4a07b..4bef311 100644
--- a/core/lib/Drupal/Core/Condition/ConditionPluginBase.php
+++ b/core/lib/Drupal/Core/Condition/ConditionPluginBase.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\Condition;
 
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Executable\ExecutablePluginBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait;
@@ -107,4 +109,46 @@ public function calculateDependencies() {
     return array();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    $cache_contexts = [];
+    foreach ($this->getContexts() as $context) {
+      /** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
+      if ($context instanceof CacheableDependencyInterface) {
+        $cache_contexts = Cache::mergeContexts($cache_contexts, $context->getCacheContexts());
+      }
+    }
+    return $cache_contexts;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    $tags = [];
+    foreach ($this->getContexts() as $context) {
+      /** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
+      if ($context instanceof CacheableDependencyInterface) {
+        $tags = Cache::mergeTags($tags, $context->getCacheTags());
+      }
+    }
+    return $tags;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    $max_age = Cache::PERMANENT;
+    foreach ($this->getContexts() as $context) {
+      /** @var $context \Drupal\Core\Cache\CacheableDependencyInterface */
+      if ($context instanceof CacheableDependencyInterface) {
+        $max_age = Cache::mergeMaxAges($max_age, $context->getCacheMaxAge());
+      }
+    }
+    return $max_age;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Plugin/Context/Context.php b/core/lib/Drupal/Core/Plugin/Context/Context.php
index 9835dd7..599f73e 100644
--- a/core/lib/Drupal/Core/Plugin/Context/Context.php
+++ b/core/lib/Drupal/Core/Plugin/Context/Context.php
@@ -8,8 +8,11 @@
 namespace Drupal\Core\Plugin\Context;
 
 use Drupal\Component\Plugin\Context\Context as ComponentContext;
+use Drupal\Component\Plugin\Context\ContextDefinitionInterface;
 use Drupal\Component\Plugin\Exception\ContextException;
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\TypedData\TypedDataInterface;
 use Drupal\Core\TypedData\TypedDataTrait;
 
@@ -35,6 +38,21 @@ class Context extends ComponentContext implements ContextInterface {
   protected $contextDefinition;
 
   /**
+   * The cacheability metadata.
+   *
+   * @var \Drupal\Core\Cache\CacheableMetadata
+   */
+  protected $cacheabilityMetadata;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(ContextDefinitionInterface $context_definition) {
+    parent::__construct($context_definition);
+    $this->cacheabilityMetadata = new CacheableMetadata();
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function getContextValue() {
@@ -48,8 +66,7 @@ public function getContextValue() {
         $this->setContextValue($default_value);
       }
       elseif ($definition->isRequired()) {
-        $type = $definition->getDataType();
-        throw new ContextException(SafeMarkup::format("The @type context is required and not present.", array('@type' => $type)));
+        return NULL;
       }
       return $default_value;
     }
@@ -60,6 +77,11 @@ public function getContextValue() {
    * {@inheritdoc}
    */
   public function setContextValue($value) {
+    // Add the value as a cacheable dependency only if implements to interface
+    // to prevent it from disabling caching with a max-age 0.
+    if ($value instanceof CacheableDependencyInterface) {
+      $this->addCacheableDependency($value);
+    }
     if ($value instanceof TypedDataInterface) {
       return $this->setContextData($value);
     }
@@ -113,4 +135,33 @@ public function validate() {
     return $this->getContextData()->validate();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function addCacheableDependency($dependency) {
+    $this->cacheabilityMetadata = $this->cacheabilityMetadata->merge(CacheableMetadata::createFromObject($dependency));
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return $this->cacheabilityMetadata->getCacheContexts();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return $this->cacheabilityMetadata->getCacheTags();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return $this->cacheabilityMetadata->getCacheMaxAge();
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
index fe9b78e..370f693 100644
--- a/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
+++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Plugin\Exception\ContextException;
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Plugin\ContextAwarePluginInterface;
 
 /**
@@ -74,16 +75,36 @@ public function getMatchingContexts(array $contexts, ContextDefinitionInterface
   public function applyContextMapping(ContextAwarePluginInterface $plugin, $contexts, $mappings = array()) {
     $mappings += $plugin->getContextMapping();
     // Loop through each of the expected contexts.
-    foreach (array_keys($plugin->getContextDefinitions()) as $plugin_context_id) {
+
+    $missing_value = [];
+
+    foreach ($plugin->getContextDefinitions() as $plugin_context_id => $plugin_context_definition) {
       // If this context was given a specific name, use that.
       $context_id = isset($mappings[$plugin_context_id]) ? $mappings[$plugin_context_id] : $plugin_context_id;
       if (!empty($contexts[$context_id])) {
         // This assignment has been used, remove it.
         unset($mappings[$plugin_context_id]);
+
+        $plugin_context = $plugin->getContext($plugin_context_id);
+        if ($plugin_context instanceof ContextInterface && $contexts[$context_id] instanceof CacheableDependencyInterface) {
+          $plugin_context->addCacheableDependency($contexts[$context_id]);
+        }
+
+        // Collect contexts that exist but are missing a value.
+        if ($plugin_context_definition->isRequired() && !$contexts[$context_id]->getContextValue()) {
+          $missing_value[] = $plugin_context_id;
+          continue;
+        }
+
         $plugin->setContextValue($plugin_context_id, $contexts[$context_id]->getContextValue());
       }
     }
 
+    // if there are any required contexts without a value, throw an exception.
+    if ($missing_value) {
+      throw new ContextException(SafeMarkup::format("Required contexts without a value: @contexts.", array('@contexts' => implode(', ', $missing_value))));
+    }
+
     // If there are any mappings that were not satisfied, throw an exception.
     if (!empty($mappings)) {
       throw new ContextException(SafeMarkup::format('Assigned contexts were not satisfied: @mappings', ['@mappings' => implode(',', array_keys($mappings))]));
diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextInterface.php b/core/lib/Drupal/Core/Plugin/Context/ContextInterface.php
index bb38617..5b1e6bd 100644
--- a/core/lib/Drupal/Core/Plugin/Context/ContextInterface.php
+++ b/core/lib/Drupal/Core/Plugin/Context/ContextInterface.php
@@ -8,12 +8,13 @@
 namespace Drupal\Core\Plugin\Context;
 
 use Drupal\Component\Plugin\Context\ContextInterface as ComponentContextInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\TypedData\TypedDataInterface;
 
 /**
  * Interface for context.
  */
-interface ContextInterface extends ComponentContextInterface {
+interface ContextInterface extends ComponentContextInterface, CacheableDependencyInterface {
 
   /**
    * Gets the context value as typed data object.
@@ -32,4 +33,22 @@ public function getContextData();
    */
   public function setContextData(TypedDataInterface $data);
 
+  /**
+   * Adds a dependency on an object: merges its cacheability metadata.
+   *
+   * E.g. when a context depends on some configuration, an entity, or an access
+   * result, we must make sure their cacheability metadata is present on the
+   * response. This method makes doing that simple.
+   *
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $dependency
+   *   The dependency. If the object implements CacheableDependencyInterface,
+   *   then its cacheability metadata will be used. Otherwise, the passed in
+   *   object must be assumed to be uncacheable, so max-age 0 is set.
+   *
+   * @return $this
+   *
+   * @see \Drupal\Core\Cache\CacheableMetadata::createFromObject()
+   */
+  public function addCacheableDependency($dependency);
+
 }
diff --git a/core/modules/block/src/BlockAccessControlHandler.php b/core/modules/block/src/BlockAccessControlHandler.php
index 148e3e3..9187125 100644
--- a/core/modules/block/src/BlockAccessControlHandler.php
+++ b/core/modules/block/src/BlockAccessControlHandler.php
@@ -9,6 +9,9 @@
 
 use Drupal\Component\Plugin\Exception\ContextException;
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Condition\ConditionAccessResolverTrait;
 use Drupal\Core\Entity\EntityAccessControlHandler;
 use Drupal\Core\Entity\EntityHandlerInterface;
@@ -87,31 +90,59 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
     else {
       $contexts = $entity->getContexts();
       $conditions = [];
+      $missing_context = FALSE;
       foreach ($entity->getVisibilityConditions() as $condition_id => $condition) {
         if ($condition instanceof ContextAwarePluginInterface) {
           try {
             $this->contextHandler->applyContextMapping($condition, $contexts);
           }
           catch (ContextException $e) {
-            return AccessResult::forbidden()->setCacheMaxAge(0);
+            $missing_context = TRUE;
           }
         }
         $conditions[$condition_id] = $condition;
       }
-      if ($this->resolveConditions($conditions, 'and') !== FALSE) {
+
+      if ($missing_context) {
+        // @todo Find a reliable way to avoid max-age 0 in all or more cases.
+        //   Treat missing context vs. context without value as a different
+        //   exception, for example.
+        $access = AccessResult::forbidden()->setCacheMaxAge(0);
+      }
+      elseif ($this->resolveConditions($conditions, 'and') !== FALSE) {
         // Delegate to the plugin.
         $access = $entity->getPlugin()->access($account, TRUE);
       }
       else {
         $access = AccessResult::forbidden();
       }
-      // This should not be hardcoded to an uncacheable access check result, but
-      // in order to fix that, we need condition plugins to return cache contexts,
-      // otherwise it will be impossible to determine by which cache contexts the
-      // result should be varied.
-      // @todo Change this to use $access->cacheUntilEntityChanges($entity) once
-      //   https://www.drupal.org/node/2375695 is resolved.
-      return $access->setCacheMaxAge(0);
+
+      $this->mergeCacheabilityFromConditions($conditions, $access);
+
+
+      // Ensure that access is evaluated again when the block changes.
+      return $access->cacheUntilEntityChanges($entity);
+    }
+  }
+
+  /**
+   * Merges cacheablity metadata from the conditions
+   *
+   * @param \Drupal\Core\Condition\ConditionInterface[] $conditions
+   *   List of visibility conditions.
+   * @param \Drupal\Core\Access\AccessResult $access
+   *   The access result object.
+   */
+  protected function mergeCacheabilityFromConditions($conditions, AccessResult $access) {
+    // Add cacheability metadata from the conditions.
+    foreach ($conditions as $condition) {
+      if ($condition instanceof CacheableDependencyInterface) {
+        $access->addCacheTags($condition->getCacheTags());
+        $access->addCacheContexts($condition->getCacheContexts());
+        if ($condition->getCacheMaxAge() != Cache::PERMANENT && $condition->getCacheMaxAge() < $access->getCacheMaxAge()) {
+          $access->setCacheMaxAge($condition->getCacheMaxAge());
+        }
+      }
     }
   }
 
diff --git a/core/modules/block/src/BlockRepository.php b/core/modules/block/src/BlockRepository.php
index 485a66f..e2aa34e 100644
--- a/core/modules/block/src/BlockRepository.php
+++ b/core/modules/block/src/BlockRepository.php
@@ -77,9 +77,8 @@ public function getVisibleBlocksPerRegion(array $contexts) {
     foreach ($this->blockStorage->loadByProperties(array('theme' => $this->getTheme())) as $block_id => $block) {
       /** @var \Drupal\block\BlockInterface $block */
       // Set the contexts on the block before checking access.
-      if ($block->setContexts($contexts)->access('view')) {
-        $full[$block->getRegion()][$block_id] = $block;
-      }
+      $block->setContexts($contexts);
+      $full[$block->getRegion()][$block_id] = $block;
     }
 
     // Merge it with the actual values to maintain the region ordering.
diff --git a/core/modules/block/src/BlockViewBuilder.php b/core/modules/block/src/BlockViewBuilder.php
index a68b6eb..e7f8087 100644
--- a/core/modules/block/src/BlockViewBuilder.php
+++ b/core/modules/block/src/BlockViewBuilder.php
@@ -82,6 +82,12 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
         '#block' => $entity,
       );
       $build[$entity_id]['#configuration']['label'] = SafeMarkup::checkPlain($configuration['label']);
+      /** @var $context \Drupal\Core\Plugin\Context\ContextInterface */
+      foreach ($entity->getVisibilityConditions()->getConditionContexts() as $context) {
+        CacheableMetadata::createFromRenderArray($build[$entity_id])
+          ->merge($context->getCacheableMetadata())
+          ->applyTo($build[$entity_id]);
+      }
 
       // Don't run in ::buildBlock() to ensure cache keys can be altered. If an
       // alter hook wants to modify the block contents, it can append another
diff --git a/core/modules/block/src/EventSubscriber/CurrentLanguageContext.php b/core/modules/block/src/EventSubscriber/CurrentLanguageContext.php
index 023f7cc..7b1a206 100644
--- a/core/modules/block/src/EventSubscriber/CurrentLanguageContext.php
+++ b/core/modules/block/src/EventSubscriber/CurrentLanguageContext.php
@@ -8,6 +8,7 @@
 namespace Drupal\block\EventSubscriber;
 
 use Drupal\block\Event\BlockContextEvent;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Plugin\Context\Context;
 use Drupal\Core\Plugin\Context\ContextDefinition;
@@ -48,6 +49,11 @@ public function onBlockActiveContext(BlockContextEvent $event) {
       if (isset($info[$type_key]['name'])) {
         $context = new Context(new ContextDefinition('language', $info[$type_key]['name']));
         $context->setContextValue($this->languageManager->getCurrentLanguage($type_key));
+
+        $cacheability = new CacheableMetadata();
+        $cacheability->setCacheContexts(['languages:' . $type_key]);
+        $context->addCacheableDependency($cacheability);
+
         $event->setContext('language.' . $type_key, $context);
       }
     }
diff --git a/core/modules/block/src/EventSubscriber/CurrentUserContext.php b/core/modules/block/src/EventSubscriber/CurrentUserContext.php
index cb70f3d..b116d53 100644
--- a/core/modules/block/src/EventSubscriber/CurrentUserContext.php
+++ b/core/modules/block/src/EventSubscriber/CurrentUserContext.php
@@ -8,6 +8,7 @@
 namespace Drupal\block\EventSubscriber;
 
 use Drupal\block\Event\BlockContextEvent;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Plugin\Context\Context;
 use Drupal\Core\Plugin\Context\ContextDefinition;
@@ -56,6 +57,9 @@ public function onBlockActiveContext(BlockContextEvent $event) {
 
     $context = new Context(new ContextDefinition('entity:user', $this->t('Current user')));
     $context->setContextValue($current_user);
+    $cacheability = new CacheableMetadata();
+    $cacheability->setCacheContexts(['user']);
+    $context->addCacheableDependency($cacheability);
     $event->setContext('user.current_user', $context);
   }
 
diff --git a/core/modules/block/src/EventSubscriber/NodeRouteContext.php b/core/modules/block/src/EventSubscriber/NodeRouteContext.php
index 66458c0..89d24f5 100644
--- a/core/modules/block/src/EventSubscriber/NodeRouteContext.php
+++ b/core/modules/block/src/EventSubscriber/NodeRouteContext.php
@@ -8,6 +8,7 @@
 namespace Drupal\block\EventSubscriber;
 
 use Drupal\block\Event\BlockContextEvent;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Plugin\Context\Context;
 use Drupal\Core\Plugin\Context\ContextDefinition;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -39,8 +40,8 @@ public function __construct(RouteMatchInterface $route_match) {
    * {@inheritdoc}
    */
   public function onBlockActiveContext(BlockContextEvent $event) {
+    $context = new Context(new ContextDefinition('entity:node', NULL, FALSE));
     if (($route_object = $this->routeMatch->getRouteObject()) && ($route_contexts = $route_object->getOption('parameters')) && isset($route_contexts['node'])) {
-      $context = new Context(new ContextDefinition($route_contexts['node']['type']));
       if ($node = $this->routeMatch->getParameter('node')) {
         $context->setContextValue($node);
       }
@@ -48,10 +49,12 @@ public function onBlockActiveContext(BlockContextEvent $event) {
     }
     elseif ($this->routeMatch->getRouteName() == 'node.add') {
       $node_type = $this->routeMatch->getParameter('node_type');
-      $context = new Context(new ContextDefinition('entity:node'));
       $context->setContextValue(Node::create(array('type' => $node_type->id())));
-      $event->setContext('node.node', $context);
     }
+    $cacheability = new CacheableMetadata();
+    $cacheability->setCacheContexts(['route']);
+    $context->addCacheableDependency($cacheability);
+    $event->setContext('node.node', $context);
   }
 
   /**
diff --git a/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
index 7b67093..56610ec 100644
--- a/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
+++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
@@ -12,6 +12,7 @@
 use Drupal\block\Event\BlockEvents;
 use Drupal\Core\Block\MainContentBlockPluginInterface;
 use Drupal\Core\Block\MessagesBlockPluginInterface;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Display\PageVariantInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\EntityViewBuilderInterface;
@@ -133,6 +134,19 @@ public function build() {
     foreach ($this->blockRepository->getVisibleBlocksPerRegion($contexts) as $region => $blocks) {
       /** @var $blocks \Drupal\block\BlockInterface[] */
       foreach ($blocks as $key => $block) {
+        $access = $block->access('view', NULL, TRUE);
+
+        if (!$access->isAllowed()) {
+          // Add the cache metadata from the access result directly to the
+          // build array, to avoid issues with empty checks and alters that
+          // might be checking incorrectly for a non-empty block.
+          CacheableMetadata::createFromRenderArray($build)
+            ->merge(CacheableMetadata::createFromObject($access))
+            ->merge(CacheableMetadata::createFromObject($block))
+            ->applyTo($build);
+
+          continue;
+        }
         $block_plugin = $block->getPlugin();
         if ($block_plugin instanceof MainContentBlockPluginInterface) {
           $block_plugin->setMainContent($this->mainContent);
@@ -143,6 +157,10 @@ public function build() {
         }
         $build[$region][$key] = $this->blockViewBuilder->view($block);
 
+        CacheableMetadata::createFromRenderArray($build[$region][$key])
+          ->merge(CacheableMetadata::createFromObject($access))
+          ->applyTo($build[$region][$key]);
+
         // The main content block cannot be cached: it is a placeholder for the
         // render array returned by the controller. It should be rendered as-is,
         // with other placed blocks "decorating" it.
diff --git a/core/modules/block/tests/src/Unit/BlockRepositoryTest.php b/core/modules/block/tests/src/Unit/BlockRepositoryTest.php
index f589e82..149d6ab 100644
--- a/core/modules/block/tests/src/Unit/BlockRepositoryTest.php
+++ b/core/modules/block/tests/src/Unit/BlockRepositoryTest.php
@@ -89,10 +89,7 @@ public function testGetVisibleBlocksPerRegion(array $blocks_config, array $expec
       $block->expects($this->once())
         ->method('setContexts')
         ->willReturnSelf();
-      $block->expects($this->once())
-        ->method('access')
-        ->will($this->returnValue($block_config[0]));
-      $block->expects($block_config[0] ? $this->atLeastOnce() : $this->never())
+      $block->expects($this->atLeastOnce())
         ->method('getRegion')
         ->willReturn($block_config[1]);
       $blocks[$block_id] = $block;
@@ -117,10 +114,6 @@ public function providerBlocksConfig() {
       'block1' => array(
         TRUE, 'top', 0
       ),
-      // Test a block without access.
-      'block2' => array(
-        FALSE, 'bottom', 0
-      ),
       // Test two blocks in the same region with specific weight.
       'block3' => array(
         TRUE, 'bottom', 5
@@ -152,9 +145,6 @@ public function testGetVisibleBlocksPerRegionWithContext() {
       ->method('setContexts')
       ->willReturnSelf();
     $block->expects($this->once())
-      ->method('access')
-      ->willReturn(TRUE);
-    $block->expects($this->once())
       ->method('getRegion')
       ->willReturn('top');
     $blocks['block_id'] = $block;
diff --git a/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php
index bb5390c..5458384 100644
--- a/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php
+++ b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\block\Unit\Plugin\DisplayVariant;
 
+use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -55,6 +56,18 @@ class BlockPageVariantTest extends UnitTestCase {
    *   A mocked display variant plugin.
    */
   public function setUpDisplayVariant($configuration = array(), $definition = array()) {
+
+    $container = new ContainerBuilder();
+    $cache_context_manager = $this->getMockBuilder('Drupal\Core\Cache\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $container->set('cache_contexts_manager', $cache_context_manager);
+    $cache_context_manager->expects($this->any())
+      ->method('validateTokens')
+      ->with([])
+      ->willReturn([]);
+    \Drupal::setContainer($container);
+
     $this->blockRepository = $this->getMock('Drupal\block\BlockRepositoryInterface');
     $this->blockViewBuilder = $this->getMock('Drupal\Core\Entity\EntityViewBuilderInterface');
     $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
@@ -99,18 +112,48 @@ public function providerBuild() {
           ],
         ],
         'top' => [
-          'block1' => [],
+          'block1' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
         // The main content was rendered via a block.
         'center' => [
-          'block4' => [],
-          'block5' => [],
+          'block4' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
+          'block5' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
         'bottom' => [
-          'block2' => [],
-          'block3' => [],
+          'block2' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
+          'block3' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
       ],
@@ -124,16 +167,40 @@ public function providerBuild() {
           ],
         ],
         'top' => [
-          'block1' => [],
+          'block1' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
         'center' => [
-          'block4' => [],
+          'block4' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
         'bottom' => [
-          'block2' => [],
-          'block3' => [],
+          'block2' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
+          'block3' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
         // The messages are rendered via the fallback in case there is no block
@@ -155,12 +222,30 @@ public function providerBuild() {
           ],
         ],
         'top' => [
-          'block1' => [],
+          'block1' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
         'bottom' => [
-          'block2' => [],
-          'block3' => [],
+          'block2' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
+          'block3' => [
+            '#cache' => [
+              'contexts' => [],
+              'tags' => [],
+              'max-age' => -1,
+            ],
+          ],
           '#sorted' => TRUE,
         ],
         // The main content & messages are rendered via the fallback in case
@@ -192,11 +277,31 @@ public function testBuild(array $blocks_config, $visible_block_count, array $exp
     $block_plugin = $this->getMock('Drupal\Core\Block\BlockPluginInterface');
     $main_content_block_plugin = $this->getMock('Drupal\Core\Block\MainContentBlockPluginInterface');
     $messages_block_plugin = $this->getMock('Drupal\Core\Block\MessagesBlockPluginInterface');
+    $access_result = $this->getMockBuilder('\Drupal\Core\Access\AccessResultAllowed')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $access_result->expects($this->atLeastOnce())
+      ->method('isAllowed')
+      ->willReturn(TRUE);
+    $access_result->expects($this->atLeastOnce())
+      ->method('getCacheContexts')
+      ->willReturn([]);
+    $access_result->expects($this->atLeastOnce())
+      ->method('getCacheTags')
+      ->willReturn([]);
+    $access_result->expects($this->atLeastOnce())
+      ->method('getCacheMaxAge')
+      ->willReturn(-1);
+
     foreach ($blocks_config as $block_id => $block_config) {
       $block = $this->getMock('Drupal\block\BlockInterface');
       $block->expects($this->atLeastOnce())
         ->method('getPlugin')
         ->willReturn($block_config[1] ? $main_content_block_plugin : ($block_config[2] ? $messages_block_plugin : $block_plugin));
+      $block->expects($this->atLeastOnce())
+        ->method('access')
+        ->willReturn($access_result);
       $blocks[$block_config[0]][$block_id] = $block;
     }
 
diff --git a/core/modules/node/src/Tests/NodeBlockFunctionalTest.php b/core/modules/node/src/Tests/NodeBlockFunctionalTest.php
index 84f2452..3b436b8 100644
--- a/core/modules/node/src/Tests/NodeBlockFunctionalTest.php
+++ b/core/modules/node/src/Tests/NodeBlockFunctionalTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\node\Tests;
 
 use Drupal\block\Entity\Block;
+use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
 use Drupal\user\RoleInterface;
 
 /**
@@ -17,6 +18,8 @@
  */
 class NodeBlockFunctionalTest extends NodeTestBase {
 
+  use AssertPageCacheContextsAndTagsTrait;
+
   /**
    * An administrative user for testing.
    *
@@ -122,6 +125,8 @@ public function testRecentNodeBlock() {
     $this->assertText($node3->label(), 'Node found in block.');
     $this->assertText($node4->label(), 'Node found in block.');
 
+    $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'user']);
+
     // Enable the "Powered by Drupal" block only on article nodes.
     $edit = [
       'id' => strtolower($this->randomMachineName()),
@@ -145,12 +150,16 @@ public function testRecentNodeBlock() {
     $this->drupalGet('');
     $label = $block->label();
     $this->assertNoText($label, 'Block was not displayed on the front page.');
+    $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'user', 'route']);
     $this->drupalGet('node/add/article');
     $this->assertText($label, 'Block was displayed on the node/add/article page.');
+    $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'user', 'route']);
     $this->drupalGet('node/' . $node1->id());
     $this->assertText($label, 'Block was displayed on the node/N when node is of type article.');
+    $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'user', 'route', 'timezone']);
     $this->drupalGet('node/' . $node5->id());
     $this->assertNoText($label, 'Block was not displayed on nodes of type page.');
+    $this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'user', 'route', 'timezone']);
 
     $this->drupalLogin($this->adminUser);
     $this->drupalGet('admin/structure/block');
diff --git a/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php b/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
index 95bd739..ae92792 100644
--- a/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
+++ b/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
@@ -71,13 +71,11 @@ function testPageCacheTags() {
 
     $cache_contexts = [
       'languages:' . LanguageInterface::TYPE_INTERFACE,
-      'route.menu_active_trails:account',
-      'route.menu_active_trails:footer',
-      'route.menu_active_trails:main',
-      'route.menu_active_trails:tools',
+      'route',
       'theme',
       'timezone',
       'user.permissions',
+      'user.roles:anonymous',
       // The cache contexts associated with the (in)accessible menu links are
       // bubbled.
       'user.roles:authenticated',
@@ -93,6 +91,9 @@ function testPageCacheTags() {
       'config:block.block.bartik_tools',
       'config:block.block.bartik_login',
       'config:block.block.bartik_footer',
+      'config:block.block.bartik_help',
+      'config:block.block.bartik_search',
+      'config:block.block.' . $block->id(),
       'config:block.block.bartik_powered',
       'config:block.block.bartik_main_menu',
       'config:block.block.bartik_account_menu',
@@ -123,6 +124,8 @@ function testPageCacheTags() {
       'config:block.block.bartik_content',
       'config:block.block.bartik_tools',
       'config:block.block.bartik_login',
+      'config:block.block.bartik_help',
+      'config:block.block.bartik_search',
       'config:block.block.' . $block->id(),
       'config:block.block.bartik_footer',
       'config:block.block.bartik_powered',
diff --git a/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php b/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
index 4893c54..e4a9c1c 100644
--- a/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
+++ b/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
@@ -77,8 +77,8 @@ protected function assertPageCacheContextsAndTags(Url $url, array $expected_cont
     sort($cache_entry->tags);
     $this->assertEqual($cache_entry->tags, $expected_tags);
     if ($cache_entry->tags !== $expected_tags) {
-      debug('Missing cache tags: ' . implode(',', array_diff($cache_entry->tags, $expected_tags)));
-      debug('Unwanted cache tags: ' . implode(',', array_diff($expected_tags, $cache_entry->tags)));
+      debug('Unwanted cache tags: ' . implode(',', array_diff($cache_entry->tags, $expected_tags)));
+      debug('Missing cache tags: ' . implode(',', array_diff($expected_tags, $cache_entry->tags)));
     }
   }
 
@@ -92,8 +92,8 @@ protected function assertCacheTags(array $expected_tags) {
     $actual_tags = $this->getCacheHeaderValues('X-Drupal-Cache-Tags');
     $this->assertIdentical($actual_tags, $expected_tags);
     if ($actual_tags !== $expected_tags) {
-      debug('Missing cache tags: ' . implode(',', array_diff($actual_tags, $expected_tags)));
-      debug('Unwanted cache tags: ' . implode(',', array_diff($expected_tags, $actual_tags)));
+      debug('Unwanted cache tags: ' . implode(',', array_diff($actual_tags, $expected_tags)));
+      debug('Missing cache tags: ' . implode(',', array_diff($expected_tags, $actual_tags)));
     }
   }
 
@@ -114,8 +114,8 @@ protected function assertCacheContexts(array $expected_contexts, $message = NULL
     sort($actual_contexts);
     $return = $this->assertIdentical($actual_contexts, $expected_contexts, $message);
     if (!$return) {
-      debug('Missing cache contexts: ' . implode(',', array_diff($actual_contexts, $expected_contexts)));
-      debug('Unwanted cache contexts: ' . implode(',', array_diff($expected_contexts, $actual_contexts)));
+      debug('Unwanted cache contexts: ' . implode(',', array_diff($actual_contexts, $expected_contexts)));
+      debug('Missing cache contexts: ' . implode(',', array_diff($expected_contexts, $actual_contexts)));
     }
     return $return;
   }
diff --git a/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php
index 97702c7..6c00c7f 100644
--- a/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php
+++ b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php
@@ -7,8 +7,11 @@
 
 namespace Drupal\Tests\Core\Plugin\Context;
 
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Plugin\Context\Context;
+use Drupal\Core\TypedData\TypedDataInterface;
 use Drupal\Tests\UnitTestCase;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
 
 /**
  * @coversDefaultClass \Drupal\Core\Plugin\Context\Context
@@ -43,6 +46,91 @@ class ContextTest extends UnitTestCase {
   public function setUp() {
     parent::setUp();
 
+    $this->typedDataManager = $this->getMockBuilder('Drupal\Core\TypedData\TypedDataManager')
+      ->disableOriginalConstructor()
+      ->setMethods(array('create'))
+      ->getMock();
+  }
+
+  /**
+   * @covers ::getContextValue
+   */
+  public function testDefaultValue() {
+    $this->setUpDefaultValue();
+
+    $context = new Context($this->contextDefinition);
+    $context->setTypedDataManager($this->typedDataManager);
+    $this->assertEquals('test', $context->getContextValue());
+  }
+
+  /**
+   * @covers ::getContextData
+   */
+  public function testDefaultDataValue() {
+    $this->setUpDefaultValue();
+
+    $context = new Context($this->contextDefinition);
+    $context->setTypedDataManager($this->typedDataManager);
+    $this->assertEquals($this->typedData, $context->getContextData());
+  }
+
+  /**
+   * @covers ::setContextValue
+   */
+  public function testSetContextValueTypedData() {
+
+    $this->contextDefinition = $this->getMockBuilder('Drupal\Core\Plugin\Context\ContextDefinitionInterface')
+      ->setMethods(array('getDefaultValue', 'getDataDefinition'))
+      ->getMockForAbstractClass();
+
+    $context = new Context($this->contextDefinition);
+    $context->setTypedDataManager($this->typedDataManager);
+    $typed_data = $this->getMock('Drupal\Core\TypedData\TypedDataInterface');
+    $context->setContextValue($typed_data);
+    $this->assertSame($typed_data, $context->getContextData());
+  }
+
+  /**
+   * @covers ::setContextValue
+   */
+  public function testSetContextValueCacheableDependency() {
+    $container = new ContainerBuilder();
+    $cache_context_manager = $this->getMockBuilder('Drupal\Core\Cache\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $container->set('cache_contexts_manager', $cache_context_manager);
+    $cache_context_manager->expects($this->any())
+      ->method('validateTokens')
+      ->with(['route'])
+      ->willReturn(['route']);
+    \Drupal::setContainer($container);
+
+    $this->contextDefinition = $this->getMock('Drupal\Core\Plugin\Context\ContextDefinitionInterface');
+
+    $context = new Context($this->contextDefinition);
+    $context->setTypedDataManager($this->typedDataManager);
+    $cacheable_dependency = $this->getMock('Drupal\Tests\Core\Plugin\Context\TypedDataCacheableDepencencyInterface');
+    $cacheable_dependency->expects($this->once())
+      ->method('getCacheTags')
+      ->willReturn(['node:1']);
+    $cacheable_dependency->expects($this->once())
+      ->method('getCacheContexts')
+      ->willReturn(['route']);
+    $cacheable_dependency->expects($this->once())
+      ->method('getCacheMaxAge')
+      ->willReturn(60);
+
+    $context->setContextValue($cacheable_dependency);
+    $this->assertSame($cacheable_dependency, $context->getContextData());
+    $this->assertEquals(['node:1'], $context->getCacheableMetadata()->getCacheTags());
+    $this->assertEquals(['route'], $context->getCacheableMetadata()->getCacheContexts());
+    $this->assertEquals(60, $context->getCacheableMetadata()->getCacheMaxAge());
+  }
+
+  /**
+   * Set up mocks for the getDefaultValue() method call.
+   */
+  protected function setUpDefaultValue() {
     $mock_data_definition = $this->getMock('Drupal\Core\TypedData\DataDefinitionInterface');
 
     $this->contextDefinition = $this->getMockBuilder('Drupal\Core\Plugin\Context\ContextDefinitionInterface')
@@ -59,33 +147,14 @@ public function setUp() {
 
     $this->typedData = $this->getMock('Drupal\Core\TypedData\TypedDataInterface');
 
-    $this->typedDataManager = $this->getMockBuilder('Drupal\Core\TypedData\TypedDataManager')
-      ->disableOriginalConstructor()
-      ->setMethods(array('create'))
-      ->getMock();
-
     $this->typedDataManager->expects($this->once())
       ->method('create')
       ->with($mock_data_definition, 'test')
       ->willReturn($this->typedData);
   }
-
-  /**
-   * @covers ::getContextValue
-   */
-  public function testDefaultValue() {
-    $context = new Context($this->contextDefinition);
-    $context->setTypedDataManager($this->typedDataManager);
-    $this->assertEquals('test', $context->getContextValue());
-  }
-
-  /**
-   * @covers ::getContextData
-   */
-  public function testDefaultDataValue() {
-    $context = new Context($this->contextDefinition);
-    $context->setTypedDataManager($this->typedDataManager);
-    $this->assertEquals($this->typedData, $context->getContextData());
-  }
-
 }
+
+/**
+ * Test interface used for mocking.
+ */
+interface TypedDataCacheableDepencencyInterface extends CacheableDependencyInterface, TypedDataInterface { }
