diff --git a/core/lib/Drupal/Core/Access/AccessResult.php b/core/lib/Drupal/Core/Access/AccessResult.php
index 76f021264608a2b07846332dc68362e85cbe29f8..d515933dab895438dedb051fde1295e038c14488 100644
--- a/core/lib/Drupal/Core/Access/AccessResult.php
+++ b/core/lib/Drupal/Core/Access/AccessResult.php
@@ -217,10 +217,15 @@ public function isNeutral() {
 
   /**
    * {@inheritdoc}
-   *
-   * AccessResult objects solely return cache context tokens, no static strings.
    */
   public function getCacheKeys() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
     sort($this->contexts);
     return $this->contexts;
   }
@@ -329,22 +334,22 @@ public function setCacheMaxAge($max_age) {
   }
 
   /**
-   * Convenience method, adds the "cache_context.user.roles" cache context.
+   * Convenience method, adds the "user.roles" cache context.
    *
    * @return $this
    */
   public function cachePerRole() {
-    $this->addCacheContexts(array('cache_context.user.roles'));
+    $this->addCacheContexts(array('user.roles'));
     return $this;
   }
 
   /**
-   * Convenience method, adds the "cache_context.user" cache context.
+   * Convenience method, adds the "user" cache context.
    *
    * @return $this
    */
   public function cachePerUser() {
-    $this->addCacheContexts(array('cache_context.user'));
+    $this->addCacheContexts(array('user'));
     return $this;
   }
 
@@ -459,7 +464,7 @@ public function andIf(AccessResultInterface $other) {
   public function inheritCacheability(AccessResultInterface $other) {
     if ($other instanceof CacheableInterface) {
       $this->setCacheable($other->isCacheable());
-      $this->addCacheContexts($other->getCacheKeys());
+      $this->addCacheContexts($other->getCacheContexts());
       $this->addCacheTags($other->getCacheTags());
       // Use the lowest max-age.
       if ($this->getCacheMaxAge() === Cache::PERMANENT) {
diff --git a/core/lib/Drupal/Core/Block/BlockBase.php b/core/lib/Drupal/Core/Block/BlockBase.php
index 83dd5deffd76711e28bdd4c9758dc1b11495d2d2..e0c6a12200fd858db905a6cd0f8d886032b8aa59 100644
--- a/core/lib/Drupal/Core/Block/BlockBase.php
+++ b/core/lib/Drupal/Core/Block/BlockBase.php
@@ -205,8 +205,8 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
     $contexts = \Drupal::service("cache_contexts")->getLabels();
     // Blocks are always rendered in the "per language" and "per theme" cache
     // contexts. No need to show those options to the end user.
-    unset($contexts['cache_context.language']);
-    unset($contexts['cache_context.theme']);
+    unset($contexts['language']);
+    unset($contexts['theme']);
     $form['cache']['contexts'] = array(
       '#type' => 'checkboxes',
       '#title' => t('Vary by context'),
@@ -350,6 +350,13 @@ protected function getRequiredCacheContexts() {
    * {@inheritdoc}
    */
   public function getCacheKeys() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
     // Return the required cache contexts, merged with the user-configured cache
     // contexts, if any.
     return array_merge($this->getRequiredCacheContexts(), $this->configuration['cache']['contexts']);
diff --git a/core/lib/Drupal/Core/Cache/CacheContexts.php b/core/lib/Drupal/Core/Cache/CacheContexts.php
index 7f7badc1fb9ac19072bf7043a886b2b58afcd971..a2d8bdb6ea0f239e45f8aec073efefd1629dba01 100644
--- a/core/lib/Drupal/Core/Cache/CacheContexts.php
+++ b/core/lib/Drupal/Core/Cache/CacheContexts.php
@@ -7,14 +7,15 @@
 
 namespace Drupal\Core\Cache;
 
+use Drupal\Component\Utility\String;
 use Drupal\Core\Database\Query\SelectInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Defines the CacheContexts service.
  *
- * Converts string placeholders into their final string values, to be used as a
- * cache key.
+ * Converts cache context IDs into their final string values, to be used as
+ * cache keys.
  */
 class CacheContexts {
 
@@ -26,9 +27,9 @@ class CacheContexts {
   protected $container;
 
   /**
-   * Available cache contexts and corresponding labels.
+   * Available cache context IDs and corresponding labels.
    *
-   * @var array
+   * @var string[]
    */
   protected $contexts;
 
@@ -37,10 +38,8 @@ class CacheContexts {
    *
    * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
    *   The current service container.
-   * @param array $contexts
-   *   An array of key-value pairs, where the keys are service names (which also
-   *   serve as the corresponding cache context token) and the values are the
-   *   cache context labels.
+   * @param string[] $contexts
+   *   An array of the available cache context IDs.
    */
   public function __construct(ContainerInterface $container, array $contexts) {
     $this->container = $container;
@@ -50,8 +49,8 @@ public function __construct(ContainerInterface $container, array $contexts) {
   /**
    * Provides an array of available cache contexts.
    *
-   * @return array
-   *   An array of available cache contexts.
+   * @return string[]
+   *   An array of available cache context IDs.
    */
   public function getAll() {
     return $this->contexts;
@@ -76,33 +75,30 @@ public function getLabels() {
   /**
    * Converts cache context tokens to string representations of the context.
    *
-   * Cache keys may either be static (just strings) or tokens (placeholders
-   * that are converted to static keys by the @cache_contexts service, depending
-   * depending on the request). This is the default cache contexts service.
-   *
-   * @param array $keys
-   *   An array of cache keys that may or may not contain cache context tokens.
+   * @param string[] $contexts
+   *   An array of cache context IDs.
    *
-   * @return array
+   * @return string[]
    *   A copy of the input, with cache context tokens converted.
+   *
+   * @throws \InvalidArgumentException
    */
-  public function convertTokensToKeys(array $keys) {
-    $context_keys = array_intersect($keys, $this->getAll());
-    $new_keys = $keys;
-
-    // Iterate over the indices instead of the values so that the order of the
-    // cache keys are preserved.
-    foreach (array_keys($context_keys) as $index) {
-      $new_keys[$index] = $this->getContext($keys[$index]);
+  public function convertTokensToKeys(array $contexts) {
+    $materialized_contexts = [];
+    foreach ($contexts as $context) {
+      if (!in_array($context, $this->contexts)) {
+        throw new \InvalidArgumentException(String::format('"@context" is not a valid cache context ID.', ['@context' => $context]));
+      }
+      $materialized_contexts[] = $this->getContext($context);
     }
-    return $new_keys;
+    return $materialized_contexts;
   }
 
   /**
    * Provides the string representation of a cache context.
    *
    * @param string $context
-   *   A cache context token of an available cache context service.
+   *   A cache context ID of an available cache context service.
    *
    * @return string
    *   The string representation of a cache context.
@@ -112,16 +108,17 @@ protected function getContext($context) {
   }
 
   /**
-   * Retrieves a service from the container.
+   * Retrieves a cache context service from the container.
    *
-   * @param string $service
-   *   The ID of the service to retrieve.
+   * @param string $context
+   *   The context ID, which together with the service ID prefix allows the
+   *   corresponding cache context service to be retrieved.
    *
-   * @return mixed
-   *   The specified service.
+   * @return \Drupal\Core\Cache\CacheContextInterface
+   *   The requested cache context service.
    */
-  protected function getService($service) {
-    return $this->container->get($service);
+  protected function getService($context) {
+    return $this->container->get('cache_context.' . $context);
   }
 
 }
diff --git a/core/lib/Drupal/Core/Cache/CacheContextsPass.php b/core/lib/Drupal/Core/Cache/CacheContextsPass.php
index 645df0364d4134c093835406b459550d1835722a..b1cb74e6886fa0c8218f0e81307c248735063e28 100644
--- a/core/lib/Drupal/Core/Cache/CacheContextsPass.php
+++ b/core/lib/Drupal/Core/Cache/CacheContextsPass.php
@@ -21,7 +21,13 @@ class CacheContextsPass implements CompilerPassInterface {
    * Collects the cache contexts into the cache_contexts parameter.
    */
   public function process(ContainerBuilder $container) {
-    $cache_contexts = array_keys($container->findTaggedServiceIds('cache.context'));
+    $cache_contexts = [];
+    foreach (array_keys($container->findTaggedServiceIds('cache.context')) as $id) {
+      if (strpos($id, 'cache_context.') !== 0) {
+        throw new \InvalidArgumentException(sprintf('The service "%s" has an invalid service ID: cache context service IDs must use the "cache_context." prefix. (The suffix is the cache context ID developers may use.)', $id));
+      }
+      $cache_contexts[] = substr($id, 14);
+    }
     $container->setParameter('cache_contexts', $cache_contexts);
   }
 
diff --git a/core/lib/Drupal/Core/Cache/CacheableInterface.php b/core/lib/Drupal/Core/Cache/CacheableInterface.php
index 49388016edb780ffae7fc0b38a5723f0d7355e99..4af960ac65025fca77c60231c5b0d8e4906ed831 100644
--- a/core/lib/Drupal/Core/Cache/CacheableInterface.php
+++ b/core/lib/Drupal/Core/Cache/CacheableInterface.php
@@ -9,6 +9,15 @@
 /**
  * Defines an interface for objects which are potentially cacheable.
  *
+ * 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. And if a child object is not
+ * cacheable, then neither is any parent object.
+ * The only cacheability metadata that must not be bubbled, are the cache keys:
+ * they're explicitly intended to be used to generate the cache item ID when
+ * caching the object they're on.
+ *
  * @ingroup cache
  */
 interface CacheableInterface {
@@ -16,21 +25,30 @@ interface CacheableInterface {
   /**
    * The cache keys associated with this potentially cacheable object.
    *
-   * Cache keys may either be static (just strings) or tokens (placeholders
-   * that are converted to static keys by the @cache_contexts service, depending
-   * depending on the request).
+   * @return string[]
+   *   An array of strings, used to generate a cache ID.
+   */
+  public function getCacheKeys();
+
+  /**
+   * The cache contexts associated with this potentially cacheable 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 array
-   *   An array of strings or cache context tokens, used to generate a cache ID.
+   * @return string[]
+   *   An array of cache context tokens, used to generate a cache ID.
    *
    * @see \Drupal\Core\Cache\CacheContexts::convertTokensToKeys()
    */
-  public function getCacheKeys();
+  public function getCacheContexts();
 
   /**
    * The cache tags associated with this potentially cacheable object.
    *
-   * @return array
+   * @return string[]
    *  An array of cache tags.
    */
   public function getCacheTags();
diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
index 420e78731f1ef34c33e3511ea860b89729409e92..2f506c243045239b62893a04ffb88c9fc7225dbf 100644
--- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
+++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
@@ -181,11 +181,13 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco
           $this->entityTypeId,
           $entity->id(),
           $view_mode,
-          'cache_context.theme',
-          'cache_context.user.roles',
+        ),
+        'contexts' => array(
+          'theme',
+          'user.roles',
           // @todo Move this out of here and into field formatters that depend
           //       on the timezone. Blocked on https://drupal.org/node/2099137.
-          'cache_context.timezone',
+          'timezone',
         ),
         'bin' => $this->cacheBin,
       );
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 69e99a0acf83241930967a94cdd11e3962f2b6e7..e0253b63ec7e1078c5ca3d5d76f0c1d2cfbd126e 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -537,7 +537,8 @@ protected function cacheSet(array &$elements) {
    * Creates the cache ID for a renderable element.
    *
    * This creates the cache ID string, either by returning the #cache['cid']
-   * property if present or by building the cache ID out of the #cache['keys'].
+   * property if present or by building the cache ID out of the #cache['keys'] +
+   * #cache['contexts'].
    *
    * @param array $elements
    *   A renderable array.
@@ -550,11 +551,12 @@ protected function createCacheID(array $elements) {
       return $elements['#cache']['cid'];
     }
     elseif (isset($elements['#cache']['keys'])) {
-      // Cache keys may either be static (just strings) or tokens (placeholders
-      // that are converted to static keys by the @cache_contexts service,
-      // depending on the request).
-      $keys = $this->cacheContexts->convertTokensToKeys($elements['#cache']['keys']);
-      return implode(':', $keys);
+      $cid_parts = $elements['#cache']['keys'];
+      if (isset($elements['#cache']['contexts'])) {
+        $contexts = $this->cacheContexts->convertTokensToKeys($elements['#cache']['contexts']);
+        $cid_parts = array_merge($cid_parts, $contexts);
+      }
+      return implode(':', $cid_parts);
     }
     return FALSE;
   }
diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php
index f2bf6c40abcf80e187db530bb1f80fc0a185086a..06f49685e0ce40eee35a8d13de3006f9e084b0a5 100644
--- a/core/lib/Drupal/Core/Render/RendererInterface.php
+++ b/core/lib/Drupal/Core/Render/RendererInterface.php
@@ -96,9 +96,9 @@ public function renderPlain(&$elements);
    *     associative array with one or several of the following keys:
    *     - 'keys': An array of one or more keys that identify the element. If
    *       'keys' is set, the cache ID is created automatically from these keys.
-   *       Cache keys may either be static (just strings) or tokens
-   *       (placeholders that are converted to static keys by the
-   *       'cache_contexts' service, depending on the request).
+   *     - 'contexts': An array of one or more cache context IDs. These are
+   *       converted to a final value depending on the request. (e.g. 'user' is
+   *       mapped to the current user's ID.)
    *     - 'cid': Specify the cache ID directly. Either 'keys' or 'cid' is
    *       required. If 'cid' is set, 'keys' is ignored. Use only if you have
    *       special requirements.
diff --git a/core/modules/block/src/BlockViewBuilder.php b/core/modules/block/src/BlockViewBuilder.php
index 5dcbd9ff21063dc2857a3308eeaeee8d6f1bc155..e56b24dc87a22bec29855ff17bfe747a81f1de54 100644
--- a/core/modules/block/src/BlockViewBuilder.php
+++ b/core/modules/block/src/BlockViewBuilder.php
@@ -78,20 +78,20 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
 
       if ($plugin->isCacheable()) {
         $build[$entity_id]['#pre_render'][] = array($this, 'buildBlock');
-        // Generic cache keys, with the block plugin's custom keys appended
-        // (usually cache context keys like 'cache_context.user.roles').
+        // Generic cache keys, with the block plugin's custom keys appended.
         $default_cache_keys = array(
           'entity_view',
           'block',
           $entity->id(),
-          // Blocks are always rendered in a "per language" cache context.
-          'cache_context.language',
-          // Blocks are always rendered in a "per theme" cache context.
-          'cache_context.theme',
+        );
+        $default_cache_contexts = array(
+          'language',
+          'theme',
         );
         $max_age = $plugin->getCacheMaxAge();
         $build[$entity_id]['#cache'] += array(
           'keys' => array_merge($default_cache_keys, $plugin->getCacheKeys()),
+          'contexts' => array_merge($default_cache_contexts, $plugin->getCacheContexts()),
           'expire' => ($max_age === Cache::PERMANENT) ? Cache::PERMANENT : REQUEST_TIME + $max_age,
         );
       }
diff --git a/core/modules/block/src/Tests/BlockCacheTest.php b/core/modules/block/src/Tests/BlockCacheTest.php
index 5ea01425eb4db62326dd5c569f776e4853ac05e4..615751da47d36d50928f8623cf3f2e9e32df9cfc 100644
--- a/core/modules/block/src/Tests/BlockCacheTest.php
+++ b/core/modules/block/src/Tests/BlockCacheTest.php
@@ -72,12 +72,12 @@ protected function setUp() {
   }
 
   /**
-   * Test "cache_context.user.roles" cache context.
+   * Test "user.roles" cache context.
    */
   function testCachePerRole() {
     $this->setBlockCacheConfig(array(
       'max_age' => 600,
-      'contexts' => array('cache_context.user.roles'),
+      'contexts' => array('user.roles'),
     ));
 
     // Enable our test block. Set some content for it to display.
@@ -167,12 +167,12 @@ function testNoCache() {
   }
 
   /**
-   * Test "cache_context.user" cache context.
+   * Test "user" cache context.
    */
   function testCachePerUser() {
     $this->setBlockCacheConfig(array(
       'max_age' => 600,
-      'contexts' => array('cache_context.user'),
+      'contexts' => array('user'),
     ));
 
     $current_content = $this->randomMachineName();
@@ -199,12 +199,12 @@ function testCachePerUser() {
   }
 
   /**
-   * Test "cache_context.url" cache context.
+   * Test "url" cache context.
    */
   function testCachePerPage() {
     $this->setBlockCacheConfig(array(
       'max_age' => 600,
-      'contexts' => array('cache_context.url'),
+      'contexts' => array('url'),
     ));
 
     $current_content = $this->randomMachineName();
diff --git a/core/modules/block/src/Tests/BlockInterfaceTest.php b/core/modules/block/src/Tests/BlockInterfaceTest.php
index 11b676294f0e2106bd5b93bd6b3b0307ec0877ed..3c81acbf314635be189dd9a83199e79ff14e1438 100644
--- a/core/modules/block/src/Tests/BlockInterfaceTest.php
+++ b/core/modules/block/src/Tests/BlockInterfaceTest.php
@@ -65,8 +65,8 @@ public function testBlockInterface() {
     $period[0] = '<' . t('no caching') . '>';
     $period[\Drupal\Core\Cache\Cache::PERMANENT] = t('Forever');
     $contexts = \Drupal::service("cache_contexts")->getLabels();
-    unset($contexts['cache_context.theme']);
-    unset($contexts['cache_context.language']);
+    unset($contexts['theme']);
+    unset($contexts['language']);
     $expected_form = array(
       'provider' => array(
         '#type' => 'value',
diff --git a/core/modules/block/src/Tests/BlockViewBuilderTest.php b/core/modules/block/src/Tests/BlockViewBuilderTest.php
index 972ded1df4a95b4744b930ce12787029506e5fdd..506ce511bf79de9931cfa047ea9c9d96b3fa2dde 100644
--- a/core/modules/block/src/Tests/BlockViewBuilderTest.php
+++ b/core/modules/block/src/Tests/BlockViewBuilderTest.php
@@ -214,7 +214,7 @@ public function testBlockViewBuilderAlter() {
     $request_method = $request->server->get('REQUEST_METHOD');
     $request->setMethod('GET');
 
-    $default_keys = array('entity_view', 'block', 'test_block', 'cache_context.language', 'cache_context.theme');
+    $default_keys = array('entity_view', 'block', 'test_block');
     $default_tags = array('block_view', 'config:block.block.test_block', 'block_plugin:test_cache');
 
     // Advanced: cached block, but an alter hook adds an additional cache key.
@@ -223,7 +223,7 @@ public function testBlockViewBuilderAlter() {
     ));
     $alter_add_key = $this->randomMachineName();
     \Drupal::state()->set('block_test_view_alter_cache_key', $alter_add_key);
-    $cid = 'entity_view:block:test_block:en:core:' . $alter_add_key;
+    $cid = 'entity_view:block:test_block:' . $alter_add_key . ':en:core';
     $expected_keys = array_merge($default_keys, array($alter_add_key));
     $build = $this->getBlockRenderArray();
     $this->assertIdentical($expected_keys, $build['#cache']['keys'], 'An altered cacheable block has the expected cache keys.');
@@ -290,11 +290,12 @@ public function testBlockViewBuilderCacheContexts() {
     // Second: the "per URL" cache context.
     $this->setBlockCacheConfig(array(
       'max_age' => 600,
-      'contexts' => array('cache_context.url'),
+      'contexts' => array('url'),
     ));
     $old_cid = $cid;
     $build = $this->getBlockRenderArray();
-    $cid = implode(':', $cache_contexts->convertTokensToKeys($build['#cache']['keys']));
+    $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts->convertTokensToKeys($build['#cache']['contexts']));
+    $cid = implode(':', $cid_parts);
     drupal_render($build);
     $this->assertTrue($this->container->get('cache.render', $cid), 'The block render element has been cached.');
     $this->assertNotEqual($cid, $old_cid, 'The cache ID has changed.');
@@ -307,7 +308,8 @@ public function testBlockViewBuilderCacheContexts() {
     $this->container->set('cache_context.url', $temp_context);
     $old_cid = $cid;
     $build = $this->getBlockRenderArray();
-    $cid = implode(':', $cache_contexts->convertTokensToKeys($build['#cache']['keys']));
+    $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts->convertTokensToKeys($build['#cache']['contexts']));
+    $cid = implode(':', $cid_parts);
     drupal_render($build);
     $this->assertTrue($this->container->get('cache.render', $cid), 'The block render element has been cached.');
     $this->assertNotEqual($cid, $old_cid, 'The cache ID has changed.');
diff --git a/core/modules/book/src/Plugin/Block/BookNavigationBlock.php b/core/modules/book/src/Plugin/Block/BookNavigationBlock.php
index 4369756d022d3abbacb22c799b6b07a94c7c7532..4a080af3a98c501ac2729f9c9e4eac2c285a2daa 100644
--- a/core/modules/book/src/Plugin/Block/BookNavigationBlock.php
+++ b/core/modules/book/src/Plugin/Block/BookNavigationBlock.php
@@ -201,7 +201,7 @@ public function getCacheKeys() {
   protected function getRequiredCacheContexts() {
     // The "Book navigation" block must be cached per role: different roles may
     // have access to different menu links.
-    return array('cache_context.user.roles');
+    return array('user.roles');
   }
 
 }
diff --git a/core/modules/help/src/Plugin/Block/HelpBlock.php b/core/modules/help/src/Plugin/Block/HelpBlock.php
index 7e490f5c773fcf91d5d42ecf936f0304475e3115..7038c9dc855f97ede6693da43d0e6b336f2b57e4 100644
--- a/core/modules/help/src/Plugin/Block/HelpBlock.php
+++ b/core/modules/help/src/Plugin/Block/HelpBlock.php
@@ -141,7 +141,7 @@ public function defaultConfiguration() {
   protected function getRequiredCacheContexts() {
     // The "Help" block must be cached per URL: help is defined for a
     // given path, and does not come with any access restrictions.
-    return array('cache_context.url');
+    return array('url');
   }
 
 }
diff --git a/core/modules/node/src/NodeGrantDatabaseStorage.php b/core/modules/node/src/NodeGrantDatabaseStorage.php
index d9e8d5cc5fc091481622d624e06c4de2b4fed73c..da6522dc0efa0a8686e23ed3f362b30a3d727f9f 100644
--- a/core/modules/node/src/NodeGrantDatabaseStorage.php
+++ b/core/modules/node/src/NodeGrantDatabaseStorage.php
@@ -107,7 +107,7 @@ public function access(NodeInterface $node, $operation, $langcode, AccountInterf
     // know it for a fact.
     $set_cacheability = function (AccessResult $access_result) use ($operation) {
       if ($operation === 'view') {
-        return $access_result->addCacheContexts(['cache_context.node_view_grants']);
+        return $access_result->addCacheContexts(['node_view_grants']);
       }
       else {
         return $access_result->setCacheable(FALSE);
diff --git a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
index 0bfade440a3864d334351bf35d418aca4fbecedc..8ddcc46508ef19970b8b2320e8cc4ddf9d672034 100644
--- a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
@@ -208,7 +208,7 @@ public function getCacheTags() {
   protected function getRequiredCacheContexts() {
     // Menu blocks must be cached per role: different roles may have access to
     // different menu links.
-    return array('cache_context.user.roles');
+    return array('user.roles');
   }
 
 }
diff --git a/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php b/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php
index 955d75b544cdcb6d4f3330372080b415c4242943..b0a63432d4bed76e518e975b5a8b20953d5d380b 100644
--- a/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php
+++ b/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php
@@ -57,7 +57,8 @@ public function testEntityViewBuilderCache() {
     // Get a fully built entity view render array.
     $entity_test->save();
     $build = $this->container->get('entity.manager')->getViewBuilder('entity_test')->view($entity_test, 'full');
-    $cid = implode(':', $cache_contexts->convertTokensToKeys($build['#cache']['keys']));
+    $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts->convertTokensToKeys($build['#cache']['contexts']));
+    $cid = implode(':', $cid_parts);
     $bin = $build['#cache']['bin'];
 
     // Mock the build array to not require the theme registry.
@@ -106,7 +107,8 @@ public function testEntityViewBuilderCacheWithReferences() {
 
     // Get a fully built entity view render array for the referenced entity.
     $build = $this->container->get('entity.manager')->getViewBuilder('entity_test')->view($entity_test_reference, 'full');
-    $cid_reference = implode(':', $cache_contexts->convertTokensToKeys($build['#cache']['keys']));
+    $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts->convertTokensToKeys($build['#cache']['contexts']));
+    $cid_reference = implode(':', $cid_parts);
     $bin_reference = $build['#cache']['bin'];
 
     // Mock the build array to not require the theme registry.
@@ -124,7 +126,8 @@ public function testEntityViewBuilderCacheWithReferences() {
 
     // Get a fully built entity view render array.
     $build = $this->container->get('entity.manager')->getViewBuilder('entity_test')->view($entity_test, 'full');
-    $cid = implode(':', $cache_contexts->convertTokensToKeys($build['#cache']['keys']));
+    $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts->convertTokensToKeys($build['#cache']['contexts']));
+    $cid = implode(':', $cid_parts);
     $bin = $build['#cache']['bin'];
 
     // Mock the build array to not require the theme registry.
@@ -154,7 +157,7 @@ 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']) == array('tags', 'keys', 'bin') , 'A view mode with render cache enabled has the correct output (cache tags, keys and bin).');
+    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == array('tags', 'keys', 'contexts', 'bin') , 'A view mode with render cache enabled has the correct output (cache tags, keys, contexts 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');
diff --git a/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
index da5651c6ee5e19e77b22f6892f3062cc509d7ac8..42d2d2a4100fd0340084e86de8f46216b95d9a69 100644
--- a/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
+++ b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
@@ -23,6 +23,7 @@ class AccessResultTest extends UnitTestCase {
   protected function assertDefaultCacheability(AccessResult $access) {
     $this->assertTrue($access->isCacheable());
     $this->assertSame([], $access->getCacheKeys());
+    $this->assertSame([], $access->getCacheContexts());
     $this->assertSame([], $access->getCacheTags());
     $this->assertSame('default', $access->getCacheBin());
     $this->assertSame(Cache::PERMANENT, $access->getCacheMaxAge());
@@ -318,7 +319,7 @@ public function testCacheMaxAge() {
   /**
    * @covers ::addCacheContexts
    * @covers ::resetCacheContexts
-   * @covers ::getCacheKeys
+   * @covers ::getCacheContexts
    * @covers ::cachePerRole
    * @covers ::cachePerUser
    * @covers ::allowedIfHasPermission
@@ -331,31 +332,31 @@ public function testCacheContexts() {
       $this->assertTrue($access->isCacheable());
       $this->assertSame('default', $access->getCacheBin());
       $this->assertSame(Cache::PERMANENT, $access->getCacheMaxAge());
-      $this->assertSame($contexts, $access->getCacheKeys());
+      $this->assertSame($contexts, $access->getCacheContexts());
       $this->assertSame([], $access->getCacheTags());
     };
 
-    $access = AccessResult::neutral()->addCacheContexts(['cache_context.foo']);
-    $verify($access, ['cache_context.foo']);
+    $access = AccessResult::neutral()->addCacheContexts(['foo']);
+    $verify($access, ['foo']);
     // Verify resetting works.
     $access->resetCacheContexts();
     $verify($access, []);
     // Verify idempotency.
-    $access->addCacheContexts(['cache_context.foo'])
-      ->addCacheContexts(['cache_context.foo']);
-    $verify($access, ['cache_context.foo']);
+    $access->addCacheContexts(['foo'])
+      ->addCacheContexts(['foo']);
+    $verify($access, ['foo']);
     // Verify same values in different call order yields the same result.
     $access->resetCacheContexts()
-      ->addCacheContexts(['cache_context.foo'])
-      ->addCacheContexts(['cache_context.bar']);
-    $verify($access, ['cache_context.bar', 'cache_context.foo']);
+      ->addCacheContexts(['foo'])
+      ->addCacheContexts(['bar']);
+    $verify($access, ['bar', 'foo']);
     $access->resetCacheContexts()
-      ->addCacheContexts(['cache_context.bar'])
-      ->addCacheContexts(['cache_context.foo']);
-    $verify($access, ['cache_context.bar', 'cache_context.foo']);
+      ->addCacheContexts(['bar'])
+      ->addCacheContexts(['foo']);
+    $verify($access, ['bar', 'foo']);
 
     // ::cachePerRole() convenience method.
-    $contexts = array('cache_context.user.roles');
+    $contexts = array('user.roles');
     $a = AccessResult::neutral()->addCacheContexts($contexts);
     $verify($a, $contexts);
     $b = AccessResult::neutral()->cachePerRole();
@@ -363,7 +364,7 @@ public function testCacheContexts() {
     $this->assertEquals($a, $b);
 
     // ::cachePerUser() convenience method.
-    $contexts = array('cache_context.user');
+    $contexts = array('user');
     $a = AccessResult::neutral()->addCacheContexts($contexts);
     $verify($a, $contexts);
     $b = AccessResult::neutral()->cachePerUser();
@@ -371,7 +372,7 @@ public function testCacheContexts() {
     $this->assertEquals($a, $b);
 
     // Both.
-    $contexts = array('cache_context.user', 'cache_context.user.roles');
+    $contexts = array('user', 'user.roles');
     $a = AccessResult::neutral()->addCacheContexts($contexts);
     $verify($a, $contexts);
     $b = AccessResult::neutral()->cachePerRole()->cachePerUser();
@@ -387,7 +388,7 @@ public function testCacheContexts() {
       ->method('hasPermission')
       ->with('may herd llamas')
       ->will($this->returnValue(FALSE));
-    $contexts = array('cache_context.user.roles');
+    $contexts = array('user.roles');
 
     // Verify the object when using the ::allowedIfHasPermission() convenience
     // static method.
@@ -410,7 +411,7 @@ public function testCacheTags() {
       $this->assertTrue($access->isCacheable());
       $this->assertSame('default', $access->getCacheBin());
       $this->assertSame(Cache::PERMANENT, $access->getCacheMaxAge());
-      $this->assertSame([], $access->getCacheKeys());
+      $this->assertSame([], $access->getCacheContexts());
       $this->assertSame($tags, $access->getCacheTags());
     };
 
@@ -471,7 +472,7 @@ public function testInheritCacheability() {
     $other = AccessResult::allowed()->setCacheMaxAge(1500)->cachePerRole()->addCacheTags(['node:20011988']);
     $this->assertTrue($access->inheritCacheability($other) instanceof AccessResult);
     $this->assertTrue($access->isCacheable());
-    $this->assertSame(['cache_context.user.roles'], $access->getCacheKeys());
+    $this->assertSame(['user.roles'], $access->getCacheContexts());
     $this->assertSame(['node:20011988'], $access->getCacheTags());
     $this->assertSame('default', $access->getCacheBin());
     $this->assertSame(1500, $access->getCacheMaxAge());
@@ -481,7 +482,7 @@ public function testInheritCacheability() {
     $other = AccessResult::forbidden()->addCacheTags(['node:14031991'])->setCacheMaxAge(86400);
     $this->assertTrue($access->inheritCacheability($other) instanceof AccessResult);
     $this->assertTrue($access->isCacheable());
-    $this->assertSame(['cache_context.user'], $access->getCacheKeys());
+    $this->assertSame(['user'], $access->getCacheContexts());
     $this->assertSame(['node:14031991'], $access->getCacheTags());
     $this->assertSame('default', $access->getCacheBin());
     $this->assertSame(43200, $access->getCacheMaxAge());
diff --git a/core/tests/Drupal/Tests/Core/Cache/CacheContextsTest.php b/core/tests/Drupal/Tests/Core/Cache/CacheContextsTest.php
index ca868e397903615e6bff4df0b42e723e4e97145f..fff9df226adba13d056e08b8b4eb3bcc0095f152 100644
--- a/core/tests/Drupal/Tests/Core/Cache/CacheContextsTest.php
+++ b/core/tests/Drupal/Tests/Core/Cache/CacheContextsTest.php
@@ -27,17 +27,32 @@ public function testContextPlaceholdersAreReplaced() {
     $cache_contexts = new CacheContexts($container, $this->getContextsFixture());
 
     $new_keys = $cache_contexts->convertTokensToKeys(
-      array("non-cache-context", "cache_context.foo")
+      ['foo']
     );
 
-    $expected = array("non-cache-context", "bar");
+    $expected = ['bar'];
     $this->assertEquals($expected, $new_keys);
   }
 
+  /**
+   * @covers ::convertTokensToKeys
+   *
+   * @expectedException \InvalidArgumentException
+   * @expectedExceptionMessage "non-cache-context" is not a valid cache context ID.
+   */
+  public function testInvalidContext() {
+    $container = $this->getMockContainer();
+    $cache_contexts = new CacheContexts($container, $this->getContextsFixture());
+
+    $cache_contexts->convertTokensToKeys(
+      ["non-cache-context"]
+    );
+  }
+
   public function testAvailableContextStrings() {
     $cache_contexts = new CacheContexts($this->getMockContainer(), $this->getContextsFixture());
     $contexts = $cache_contexts->getAll();
-    $this->assertEquals(array("cache_context.foo"), $contexts);
+    $this->assertEquals(array("foo"), $contexts);
   }
 
   public function testAvailableContextLabels() {
@@ -49,12 +64,12 @@ public function testAvailableContextLabels() {
 
     $cache_contexts = new CacheContexts($container, $this->getContextsFixture());
     $labels = $cache_contexts->getLabels();
-    $expected = array("cache_context.foo" => "Foo");
+    $expected = array("foo" => "Foo");
     $this->assertEquals($expected, $labels);
   }
 
   protected function getContextsFixture() {
-    return array('cache_context.foo');
+    return array('foo');
   }
 
   protected function getMockContainer() {