diff --git a/core/core.services.yml b/core/core.services.yml
index 802d143..7dd2719 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -4,6 +4,24 @@ services:
     arguments: ['@settings']
     calls:
       - [setContainer, ['@service_container']]
+  cache_contexts:
+    class: Drupal\Core\Cache\CacheContexts
+    arguments: ['@service_container', '%cache_contexts%' ]
+  cache_context.url:
+    class: Drupal\Core\Cache\UrlCacheContext
+    arguments: ['@request']
+    tags:
+      - { name: cache.context}
+  cache_context.language:
+    class: Drupal\Core\Cache\LanguageCacheContext
+    arguments: ['@language_manager']
+    tags:
+      - { name: cache.context}
+  cache_context.theme:
+    class: Drupal\Core\Cache\ThemeCacheContext
+    arguments: ['@request', '@theme.negotiator']
+    tags:
+      - { name: cache.context}
   cache.backend.database:
     class: Drupal\Core\Cache\DatabaseBackendFactory
     arguments: ['@database']
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 9e5c50f..e38cfda 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -900,6 +900,13 @@ function drupal_serve_page_from_cache(stdClass $cache, Response $response, Reque
   // max-age > 0, allowing the page to be cached by external proxies, when a
   // session cookie is present unless the Vary header has been replaced.
   $max_age = !$request->cookies->has(session_name()) || isset($boot_headers['vary']) ? $config->get('cache.page.max_age') : 0;
+  // RFC 2616, section 14.21 says: 'To mark a response as "never expires," an
+  // origin server sends an Expires date approximately one year from the time
+  // the response is sent. HTTP/1.1 servers SHOULD NOT send Expires dates more
+  // than one year in the future.'
+  if ($max_age > 31536000 || $max_age === \Drupal\Core\Cache\Cache::PERMANENT) {
+    $max_age = 31536000;
+  }
   $response->headers->set('Cache-Control', 'public, max-age=' . $max_age);
 
   // Entity tag should change if the output changes.
diff --git a/core/includes/common.inc b/core/includes/common.inc
index a2226c8..cdb4380 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -132,78 +132,6 @@
 const JS_THEME = 100;
 
 /**
- * @defgroup block_caching Block Caching
- * @{
- * Constants that define each block's caching state.
- *
- * Modules specify how their blocks can be cached in their hook_block_info()
- * implementations. Caching can be turned off (DRUPAL_NO_CACHE), managed by the
- * module declaring the block (DRUPAL_CACHE_CUSTOM), or managed by the core
- * Block module. If the Block module is managing the cache, you can specify that
- * the block is the same for every page and user (DRUPAL_CACHE_GLOBAL), or that
- * it can change depending on the page (DRUPAL_CACHE_PER_PAGE) or by user
- * (DRUPAL_CACHE_PER_ROLE or DRUPAL_CACHE_PER_USER). Page and user settings can
- * be combined with a bitwise-binary or operator; for example,
- * DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE means that the block can change
- * depending on the user role or page it is on.
- *
- * The block cache is cleared when the 'content' cache tag is invalidated,
- * following the same pattern as the page cache (node, comment, user, taxonomy
- * added or updated...).
- *
- * Note that user 1 is excluded from block caching.
- */
-
-/**
- * The block should not get cached.
- *
- * This setting should be used:
- * - For simple blocks (notably those that do not perform any db query), where
- *   querying the db cache would be more expensive than directly generating the
- *   content.
- * - For blocks that change too frequently.
- */
-const DRUPAL_NO_CACHE = -1;
-
-/**
- * The block is handling its own caching in its hook_block_view().
- *
- * This setting is useful when time based expiration is needed or a site uses a
- * node access which invalidates standard block cache.
- */
-const DRUPAL_CACHE_CUSTOM = -2;
-
-/**
- * The block or element can change depending on the user's roles.
- *
- * This is the default setting for blocks, used when the block does not specify
- * anything.
- */
-const DRUPAL_CACHE_PER_ROLE = 0x0001;
-
-/**
- * The block or element can change depending on the user.
- *
- * This setting can be resource-consuming for sites with large number of users,
- * and thus should only be used when DRUPAL_CACHE_PER_ROLE is not sufficient.
- */
-const DRUPAL_CACHE_PER_USER = 0x0002;
-
-/**
- * The block or element can change depending on the page being viewed.
- */
-const DRUPAL_CACHE_PER_PAGE = 0x0004;
-
-/**
- * The block or element is the same for every user and page that it is visible.
- */
-const DRUPAL_CACHE_GLOBAL = 0x0008;
-
-/**
- * @} End of "defgroup block_caching".
- */
-
-/**
  * The delimiter used to split plural strings.
  *
  * This is the ETX (End of text) character and is used as a minimal means to
@@ -3718,16 +3646,12 @@ function drupal_render_page($page) {
  *     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.
- *       See drupal_render_cid_create().
- *     - 'granularity' (optional): Define the cache granularity using binary
- *       combinations of the cache granularity constants, e.g.
- *       DRUPAL_CACHE_PER_USER to cache for each user separately or
- *       DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE to cache separately for
- *       each page and role. If not specified the element is cached globally for
- *       each theme and language.
+ *       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). See drupal_render_cid_create().
  *     - 'cid': Specify the cache ID directly. Either 'keys' or 'cid' is
- *       required. If 'cid' is set, 'keys' and 'granularity' are ignored. Use
- *       only if you have special requirements.
+ *       required. If 'cid' is set, 'keys' is ignored. Use only if you have
+ *       special requirements.
  *     - 'expire': Set to one of the cache lifetime constants.
  *     - 'bin': Specify a cache bin to cache the element in. Default is 'cache'.
  *   - If this element has #type defined and the default attributes for this
@@ -4511,100 +4435,10 @@ function drupal_cache_tags_page_get(Response $response) {
 }
 
 /**
- * Prepares an element for caching based on a query.
- *
- * This smart caching strategy saves Drupal from querying and rendering to HTML
- * when the underlying query is unchanged.
- *
- * Expensive queries should use the query builder to create the query and then
- * call this function. Executing the query and formatting results should happen
- * in a #pre_render callback.
- *
- * @param $query
- *   A select query object as returned by db_select().
- * @param $function
- *   The name of the function doing this caching. A _pre_render suffix will be
- *   added to this string and is also part of the cache key in
- *   drupal_render_cache_set() and drupal_render_cache_get().
- * @param $expire
- *   The cache expire time, passed eventually to \Drupal::cache()->set().
- * @param $granularity
- *   One or more granularity constants passed to drupal_render_cid_parts().
- *
- * @return
- *   A renderable array with the following keys and values:
- *   - #query: The passed-in $query.
- *   - #pre_render: $function with a _pre_render suffix.
- *   - #cache: An associative array prepared for drupal_render_cache_set().
- */
-function drupal_render_cache_by_query($query, $function, $expire = Cache::PERMANENT, $granularity = NULL) {
-  $cache_keys = array_merge(array($function), drupal_render_cid_parts($granularity));
-  $query->preExecute();
-  $cache_keys[] = hash('sha256', serialize(array((string) $query, $query->getArguments())));
-  return array(
-    '#query' => $query,
-    '#pre_render' => array($function . '_pre_render'),
-    '#cache' => array(
-      'keys' => $cache_keys,
-      'expire' => $expire,
-    ),
-  );
-}
-
-/**
- * Returns cache ID parts for building a cache ID.
- *
- * @param $granularity
- *   One or more cache granularity constants. For example, to cache separately
- *   for each user, use DRUPAL_CACHE_PER_USER. To cache separately for each
- *   page and role, use the expression:
- *   @code
- *   DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE
- *   @endcode
- *
- * @return
- *   An array of cache ID parts, always containing the active theme. If the
- *   locale module is enabled it also contains the active language. If
- *   $granularity was passed in, more parts are added.
- */
-function drupal_render_cid_parts($granularity = NULL) {
-  global $theme, $base_root;
-
-  $cid_parts[] = $theme;
-
-  // If we have only one language enabled we do not need it as cid part.
-  $language_manager = \Drupal::languageManager();
-  if ($language_manager->isMultilingual()) {
-    foreach ($language_manager->getLanguageTypes() as $type) {
-      $cid_parts[] = $language_manager->getCurrentLanguage($type)->id;
-    }
-  }
-
-  if (!empty($granularity)) {
-    // 'PER_ROLE' and 'PER_USER' are mutually exclusive. 'PER_USER' can be a
-    // resource drag for sites with many users, so when a module is being
-    // equivocal, we favor the less expensive 'PER_ROLE' pattern.
-    if ($granularity & DRUPAL_CACHE_PER_ROLE) {
-      $cid_parts[] = 'r.' . implode(',', \Drupal::currentUser()->getRoles());
-    }
-    elseif ($granularity & DRUPAL_CACHE_PER_USER) {
-      $cid_parts[] = 'u.' . \Drupal::currentUser()->id();
-    }
-
-    if ($granularity & DRUPAL_CACHE_PER_PAGE) {
-      $cid_parts[] = $base_root . request_uri();
-    }
-  }
-
-  return $cid_parts;
-}
-
-/**
  * 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']
- * and, optionally, the #cache['granularity'] properties.
+ * property if present or by building the cache ID out of the #cache['keys'].
  *
  * @param $elements
  *   A renderable array.
@@ -4617,10 +4451,12 @@ function drupal_render_cid_create($elements) {
     return $elements['#cache']['cid'];
   }
   elseif (isset($elements['#cache']['keys'])) {
-    $granularity = isset($elements['#cache']['granularity']) ? $elements['#cache']['granularity'] : NULL;
-    // Merge in additional cache ID parts based provided by drupal_render_cid_parts().
-    $cid_parts = array_merge($elements['#cache']['keys'], drupal_render_cid_parts($granularity));
-    return implode(':', $cid_parts);
+    // 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).
+    $cache_contexts = \Drupal::service("cache_contexts");
+    $keys = $cache_contexts->convertTokensToKeys($elements['#cache']['keys']);
+    return implode(':', $keys);
   }
   return FALSE;
 }
diff --git a/core/lib/Drupal/Core/Cache/Cache.php b/core/lib/Drupal/Core/Cache/Cache.php
index c2f5fe6..a9c18ae 100644
--- a/core/lib/Drupal/Core/Cache/Cache.php
+++ b/core/lib/Drupal/Core/Cache/Cache.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\Cache;
 
+use Drupal\Core\Database\Query\SelectInterface;
+
 /**
  * Helper methods for cache.
  */
@@ -70,4 +72,26 @@ public static function getBins() {
     return $bins;
   }
 
+  /**
+   * Generates a hash from a query object, to be used as part of the cache key.
+   *
+   * This smart caching strategy saves Drupal from querying and rendering to
+   * HTML when the underlying query is unchanged.
+   *
+   * Expensive queries should use the query builder to create the query and then
+   * call this function. Executing the query and formatting results should
+   * happen in a #pre_render callback.
+   *
+   * @param \Drupal\Core\Database\Query\SelectInterface $query
+   *   A select query object.
+   *
+   * @return string
+   *   A hash of the query arguments.
+   */
+  public static function keyFromQuery(SelectInterface $query) {
+    $query->preExecute();
+    $keys = array((string) $query, $query->getArguments());
+    return hash('sha256', serialize($keys));
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Cache/CacheBackendInterface.php b/core/lib/Drupal/Core/Cache/CacheBackendInterface.php
index 3bdbc64..36fd5a3 100644
--- a/core/lib/Drupal/Core/Cache/CacheBackendInterface.php
+++ b/core/lib/Drupal/Core/Cache/CacheBackendInterface.php
@@ -74,7 +74,7 @@
   /**
    * Indicates that the item should never be removed unless explicitly deleted.
    */
-  const CACHE_PERMANENT = 0;
+  const CACHE_PERMANENT = -1;
 
   /**
    * Returns data from the persistent cache.
diff --git a/core/lib/Drupal/Core/Cache/CacheContextInterface.php b/core/lib/Drupal/Core/Cache/CacheContextInterface.php
new file mode 100644
index 0000000..fd9783a
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/CacheContextInterface.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\CacheContextInterface.
+ */
+
+namespace Drupal\Core\Cache;
+
+/**
+ * Provides an interface for defining a cache context service.
+ */
+interface CacheContextInterface {
+
+  /**
+   * Returns the label of the cache context.
+   *
+   * @return string
+   *   The label of the cache context.
+   */
+  public static function getLabel();
+
+  /**
+   * Returns the string representation of the cache context.
+   *
+   * A cache context service's name is used as a token (placeholder) cache key,
+   * and is then replaced with the string returned by this method.
+   *
+   * @return string
+   *   The string representation of the cache context.
+   */
+  public function getContext();
+
+}
diff --git a/core/lib/Drupal/Core/Cache/CacheContexts.php b/core/lib/Drupal/Core/Cache/CacheContexts.php
new file mode 100644
index 0000000..cbc2e9e
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/CacheContexts.php
@@ -0,0 +1,127 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\CacheContexts.
+ */
+
+namespace Drupal\Core\Cache;
+
+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.
+ */
+class CacheContexts {
+
+  /**
+   * The service container.
+   *
+   * @var \Symfony\Component\DependencyInjection\ContainerInterface
+   */
+  protected $container;
+
+  /**
+   * Available cache contexts and corresponding labels.
+   *
+   * @var array
+   */
+  protected $contexts;
+
+  /**
+   * Constructs a CacheContexts object.
+   *
+   * @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.
+   */
+  public function __construct(ContainerInterface $container, array $contexts) {
+    $this->container = $container;
+    $this->contexts = $contexts;
+  }
+
+  /**
+   * Provides an array of available cache contexts.
+   *
+   * @return array
+   *   An array of available cache contexts.
+   */
+  public function getAll() {
+    return $this->contexts;
+  }
+
+  /**
+   * Provides an array of available cache context labels.
+   *
+   * To be used in cache configuration forms.
+   *
+   * @return array
+   *   An array of available cache contexts and corresponding labels.
+   */
+  public function getLabels() {
+    $with_labels = array();
+    foreach ($this->contexts as $context) {
+      $with_labels[$context] = $this->getService($context)->getLabel();
+    }
+    return $with_labels;
+  }
+
+  /**
+   * 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.
+   *
+   * @return array
+   *   A copy of the input, with cache context tokens converted.
+   */
+  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]);
+    }
+    return $new_keys;
+  }
+
+  /**
+   * Provides the string representaton of a cache context.
+   *
+   * @param string $context
+   *   A cache context token of an available cache context service.
+   *
+   * @return string
+   *   The string representaton of a cache context.
+   */
+  protected function getContext($context) {
+    return $this->getService($context)->getContext();
+  }
+
+  /**
+   * Retrieves a service from the container.
+   *
+   * @param string $service
+   *   The ID of the service to retrieve.
+   *
+   * @return mixed
+   *   The specified service.
+   */
+  protected function getService($service) {
+    return $this->container->get($service);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Cache/CacheContextsPass.php b/core/lib/Drupal/Core/Cache/CacheContextsPass.php
new file mode 100644
index 0000000..645df03
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/CacheContextsPass.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\CacheContextsPass.
+ */
+
+namespace Drupal\Core\Cache;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+
+/**
+ * Adds cache_contexts parameter to the container.
+ */
+class CacheContextsPass implements CompilerPassInterface {
+
+  /**
+   * Implements CompilerPassInterface::process().
+   *
+   * Collects the cache contexts into the cache_contexts parameter.
+   */
+  public function process(ContainerBuilder $container) {
+    $cache_contexts = array_keys($container->findTaggedServiceIds('cache.context'));
+    $container->setParameter('cache_contexts', $cache_contexts);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Cache/LanguageCacheContext.php b/core/lib/Drupal/Core/Cache/LanguageCacheContext.php
new file mode 100644
index 0000000..b73652f
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/LanguageCacheContext.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\LanguageCacheContext.
+ */
+
+namespace Drupal\Core\Cache;
+
+use Drupal\Core\Language\LanguageManagerInterface;
+
+/**
+ * Defines the LanguageCacheContext service, for "per language" caching.
+ */
+class LanguageCacheContext implements CacheContextInterface {
+
+  /**
+   * The language manager.
+   *
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   */
+  protected $languageManager;
+
+  /**
+   * Constructs a new LanguageCacheContext service.
+   *
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   */
+  public function __construct(LanguageManagerInterface $language_manager) {
+    $this->languageManager = $language_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLabel() {
+    return t('Language');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext() {
+    $context_parts = array();
+    if ($this->language_manager->isMultilingual()) {
+      foreach ($this->language_manager->getLanguageTypes() as $type) {
+        $context_parts[] = $this->language_manager->getCurrentLanguage($type)->id;
+      }
+    }
+    return implode(':', $context_parts);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Cache/ThemeCacheContext.php b/core/lib/Drupal/Core/Cache/ThemeCacheContext.php
new file mode 100644
index 0000000..d777471
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/ThemeCacheContext.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\LanguageCacheContext.
+ */
+
+namespace Drupal\Core\Cache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Drupal\Core\Theme\ThemeNegotiatorInterface;
+
+/**
+ * Defines the ThemeCacheContext service, for "per theme" caching.
+ */
+class ThemeCacheContext implements CacheContextInterface {
+
+  /**
+   * The current request.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * The theme negotiator.
+   *
+   * @var \Drupal\Core\Theme\ThemeNegotiator
+   */
+  protected $themeNegotiator;
+
+  /**
+   * Constructs a new ThemeCacheContext service.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The HTTP request object.
+   * @param \Drupal\Core\Theme\ThemeNegotiatorInterface $theme_negotiator
+   *   The theme negotiator.
+   */
+  public function __construct(Request $request, ThemeNegotiatorInterface $theme_negotiator) {
+    $this->request = $request;
+    $this->themeNegotiator = $theme_negotiator;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLabel() {
+    return t('Theme');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext() {
+    return $this->themeNegotiator->determineActiveTheme($this->request) ?: 'stark';
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Cache/UrlCacheContext.php b/core/lib/Drupal/Core/Cache/UrlCacheContext.php
new file mode 100644
index 0000000..596e134
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/UrlCacheContext.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\UrlCacheContext.
+ */
+
+namespace Drupal\Core\Cache;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines the UrlCacheContext service, for "per page" caching.
+ */
+class UrlCacheContext implements CacheContextInterface {
+
+  /**
+   * The current request.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * Constructs a new UrlCacheContext service.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The HTTP request object.
+   */
+  public function __construct(Request $request) {
+    $this->request = $request;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLabel() {
+    return t('URL');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext() {
+    return $this->request->getUri();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php
index 927189e..aa822fd 100644
--- a/core/lib/Drupal/Core/CoreServiceProvider.php
+++ b/core/lib/Drupal/Core/CoreServiceProvider.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core;
 
+use Drupal\Core\Cache\CacheContextsPass;
 use Drupal\Core\Cache\ListCacheBinsPass;
 use Drupal\Core\Config\ConfigFactoryOverridePass;
 use Drupal\Core\DependencyInjection\ServiceProviderInterface;
@@ -72,6 +73,7 @@ public function register(ContainerBuilder $container) {
     $container->addCompilerPass(new RegisterPathProcessorsPass());
     $container->addCompilerPass(new RegisterRouteProcessorsPass());
     $container->addCompilerPass(new ListCacheBinsPass());
+    $container->addCompilerPass(new CacheContextsPass());
     // Add the compiler pass for appending string translators.
     $container->addCompilerPass(new RegisterStringTranslatorsPass());
     // Add the compiler pass that will process the tagged breadcrumb builder
diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
index 7cbadb2..2e48ecc 100644
--- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
+++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
@@ -158,8 +158,14 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco
     // type configuration.
     if ($this->isViewModeCacheable($view_mode) && !$entity->isNew() && $entity->isDefaultRevision() && $this->entityType->isRenderCacheable()) {
       $return['#cache'] += array(
-        'keys' => array('entity_view', $this->entityTypeId, $entity->id(), $view_mode),
-        'granularity' => DRUPAL_CACHE_PER_ROLE,
+        'keys' => array(
+          'entity_view',
+          $this->entityTypeId,
+          $entity->id(),
+          $view_mode,
+          'cache_context.theme',
+          'cache_context.user.roles',
+        ),
         'bin' => $this->cacheBin,
       );
 
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index 1bca18b..08fb82d 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -169,52 +169,16 @@ function block_page_build(&$page) {
 function block_get_blocks_by_region($region) {
   $build = array();
   if ($list = block_list($region)) {
-    $build = _block_get_renderable_region($list);
-  }
-  return $build;
-}
-
-/**
- * Gets an array of blocks suitable for drupal_render().
- *
- * @param $list
- *   A list of blocks such as that returned by block_list().
- *
- * @return
- *   A renderable array.
- */
-function _block_get_renderable_region($list = array()) {
-  $build = array();
-  // Block caching is not compatible with node_access modules. We also
-  // preserve the submission of forms in blocks, by fetching from cache
-  // only if the request method is 'GET' (or 'HEAD'). User 1 being out of
-  // the regular 'roles define permissions' schema, it brings too many
-  // chances of having unwanted output get in the cache and later be served
-  // to other users. We therefore exclude user 1 from block caching.
-  $not_cacheable = \Drupal::currentUser()->id() == 1 ||
-    count(\Drupal::moduleHandler()->getImplementations('node_grants')) ||
-    !\Drupal::request()->isMethodSafe();
-
-  foreach ($list as $key => $block) {
-    $settings = $block->get('settings');
-    if ($not_cacheable || in_array($settings['cache'], array(DRUPAL_NO_CACHE, DRUPAL_CACHE_CUSTOM))) {
-      // Non-cached blocks get built immediately.
+    foreach ($list as $key => $block) {
       if ($block->access()) {
         $build[$key] = entity_view($block, 'block');
       }
     }
-    else {
-      $build[$key] = array(
-        '#block' => $block,
-        '#weight' => $block->get('weight'),
-        '#pre_render' => array('_block_get_renderable_block'),
-        '#cache' => array(
-          'keys' => array($key, $settings['module']),
-          'granularity' => $settings['cache'],
-          'bin' => 'block',
-          'tags' => array('content' => TRUE),
-        ),
-      );
+    // If none of the blocks in this region are visible, then don't set anything
+    // else in the render array, because that would cause the region to show up.
+    if (!empty($build)) {
+      // block_list() already returned the blocks in sorted order.
+      $build['#sorted'] = TRUE;
     }
   }
   return $build;
@@ -342,9 +306,7 @@ function block_list($region) {
     $blocks[$region] = array();
   }
 
-  uasort($blocks[$region], function($first, $second) {
-    return $first->weight === $second->weight ? 0 : ($first->weight < $second->weight ? -1 : 1);
-  });
+  uasort($blocks[$region], 'Drupal\block\Entity\Block::sort');
 
   return $blocks[$region];
 }
@@ -365,26 +327,6 @@ function block_load($entity_id) {
 }
 
 /**
- * Builds the content and label for a block.
- *
- * For cacheable blocks, this is called during #pre_render.
- *
- * @param $element
- *   A renderable array.
- *
- * @return
- *   A renderable array.
- */
-function _block_get_renderable_block($element) {
-  $block = $element['#block'];
-  // Don't bother to build blocks that aren't accessible.
-  if ($element['#access'] = $block->access()) {
-    $element += entity_view($block, 'block');
-  }
-  return $element;
-}
-
-/**
  * Implements hook_rebuild().
  */
 function block_rebuild() {
@@ -456,6 +398,11 @@ function template_preprocess_block(&$variables) {
   $variables['derivative_plugin_id'] = $variables['elements']['#derivative_plugin_id'];
   $variables['label'] = !empty($variables['configuration']['label_display']) ? $variables['configuration']['label'] : '';
   $variables['content'] = $variables['elements']['content'];
+  // A block's label is configuration: it is static. Allow dynamic labels to be
+  // set in the render array.
+  if (isset($variables['elements']['content']['#title']) && !empty($variables['configuration']['label_display'])) {
+    $variables['label'] = $variables['elements']['content']['#title'];
+  }
 
   $variables['attributes']['class'][] = 'block';
   $variables['attributes']['class'][] = drupal_html_class('block-' . $variables['configuration']['module']);
diff --git a/core/modules/block/config/schema/block.schema.yml b/core/modules/block/config/schema/block.schema.yml
index 1ed3f49..dc7c0e0 100644
--- a/core/modules/block/config/schema/block.schema.yml
+++ b/core/modules/block/config/schema/block.schema.yml
@@ -73,8 +73,18 @@ block.block.*:
           type: string
           label: 'Display title'
         cache:
-          type: integer
-          label: 'Cache'
+          type: mapping
+          label: 'Cache settings'
+          mapping:
+            max_age:
+              type: integer
+              label: 'Maximum age'
+            contexts:
+              type: sequence
+              label: 'Vary by context'
+              sequence:
+                - type: string
+                  label: 'Context'
         status:
           type: boolean
           label: 'Status'
diff --git a/core/modules/block/custom_block/lib/Drupal/custom_block/Plugin/Block/CustomBlockBlock.php b/core/modules/block/custom_block/lib/Drupal/custom_block/Plugin/Block/CustomBlockBlock.php
index fe7f461..dab9b26 100644
--- a/core/modules/block/custom_block/lib/Drupal/custom_block/Plugin/Block/CustomBlockBlock.php
+++ b/core/modules/block/custom_block/lib/Drupal/custom_block/Plugin/Block/CustomBlockBlock.php
@@ -93,6 +93,12 @@ public function defaultConfiguration() {
       'status' => TRUE,
       'info' => '',
       'view_mode' => 'full',
+      // Modify the default max age for custom block blocks: modifications made
+      // to them will automatically invalidate corresponding cache tags, thus
+      // allowing us to cache custom block blocks forever.
+      'cache' => array(
+        'max_age' => \Drupal\Core\Cache\Cache::PERMANENT,
+      ),
     );
   }
 
diff --git a/core/modules/block/lib/Drupal/block/BlockBase.php b/core/modules/block/lib/Drupal/block/BlockBase.php
index d278663..2dd5cde 100644
--- a/core/modules/block/lib/Drupal/block/BlockBase.php
+++ b/core/modules/block/lib/Drupal/block/BlockBase.php
@@ -10,7 +10,10 @@
 use Drupal\Core\Plugin\PluginBase;
 use Drupal\block\BlockInterface;
 use Drupal\Component\Utility\Unicode;
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Language\Language;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableInterface;
 use Drupal\Core\Session\AccountInterface;
 
 /**
@@ -28,12 +31,7 @@
   public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
 
-    $this->configuration += $this->defaultConfiguration() + array(
-      'label' => '',
-      'module' => $plugin_definition['module'],
-      'label_display' => BlockInterface::BLOCK_LABEL_VISIBLE,
-      'cache' => DRUPAL_NO_CACHE,
-    );
+    $this->setConfiguration($configuration);
   }
 
   /**
@@ -47,7 +45,29 @@ public function getConfiguration() {
    * {@inheritdoc}
    */
   public function setConfiguration(array $configuration) {
-    $this->configuration = $configuration;
+    $this->configuration = NestedArray::mergeDeep(
+      $this->baseConfigurationDefaults(),
+      $this->defaultConfiguration(),
+      $configuration
+    );
+  }
+
+  /**
+   * Returns generic default configuration for block plugins.
+   *
+   * @return array
+   *   An associative array with the default configuration.
+   */
+  protected function baseConfigurationDefaults() {
+    return array(
+      'label' => '',
+      'module' => $this->pluginDefinition['module'],
+      'label_display' => BlockInterface::BLOCK_LABEL_VISIBLE,
+      'cache' => array(
+        'max_age' => 0,
+        'contexts' => array(),
+      ),
+    );
   }
 
   /**
@@ -108,10 +128,50 @@ public function buildConfigurationForm(array $form, array &$form_state) {
       '#default_value' => ($this->configuration['label_display'] === BlockInterface::BLOCK_LABEL_VISIBLE),
       '#return_value' => BlockInterface::BLOCK_LABEL_VISIBLE,
     );
+    // Identical options to the ones for page caching.
+    // @see \Drupal\system\Form\PerformanceForm::buildForm()
+    $period = array(0, 60, 180, 300, 600, 900, 1800, 2700, 3600, 10800, 21600, 32400, 43200, 86400);
+    $period = array_map('format_interval', array_combine($period, $period));
+    $period[0] = '<' . t('no caching') . '>';
+    $period[\Drupal\Core\Cache\Cache::PERMANENT] = t('Forever');
     $form['cache'] = array(
-      '#type' => 'value',
-      '#value' => $this->configuration['cache'],
+      '#type' => 'details',
+      '#title' => t('Cache settings'),
+    );
+    $form['cache']['max_age'] = array(
+      '#type' => 'select',
+      '#title' => t('Maximum age'),
+      '#description' => t('The maximum time this block may be cached.'),
+      '#default_value' => $this->configuration['cache']['max_age'],
+      '#options' => $period,
+    );
+    $contexts = \Drupal::service("cache_contexts")->getLabels();
+    // Blocks are always rendered in a "per theme" cache context. No need to
+    // show that option to the end user.
+    unset($contexts['cache_context.theme']);
+    $form['cache']['contexts'] = array(
+      '#type' => 'checkboxes',
+      '#title' => t('Vary by context'),
+      '#description' => t('The contexts this cached block must be varied by.'),
+      '#default_value' => $this->configuration['cache']['contexts'],
+      '#options' => $contexts,
+      '#states' => array(
+        'disabled' => array(
+          ':input[name="settings[cache][max_age]"]' => array('value' => (string) 0),
+        ),
+      ),
     );
+    if (count($this->getRequiredCacheContexts()) > 0) {
+      // Remove the required cache contexts from the list of contexts a user can
+      // choose to modify by: they must always be applied.
+      $context_labels = array();
+      foreach ($this->getRequiredCacheContexts() as $context) {
+        $context_labels[] = $form['cache']['contexts']['#options'][$context];
+        unset($form['cache']['contexts']['#options'][$context]);
+      }
+      $required_context_list = implode(', ', $context_labels);
+      $form['cache']['contexts']['#description'] .= ' ' . t('This block is <em>always</em> varied by the following contexts: %required-context-list.', array('%required-context-list' => $required_context_list));
+    }
 
     // Add plugin-specific settings for this block type.
     $form += $this->blockForm($form, $form_state);
@@ -134,6 +194,9 @@ public function blockForm($form, &$form_state) {
    * @see \Drupal\block\BlockBase::blockValidate()
    */
   public function validateConfigurationForm(array &$form, array &$form_state) {
+    // Transform the #type = checkboxes value to a numerically indexed array.
+    $form_state['values']['cache']['contexts'] = array_values(array_filter($form_state['values']['cache']['contexts']));
+
     $this->blockValidate($form, $form_state);
   }
 
@@ -156,6 +219,7 @@ public function submitConfigurationForm(array &$form, array &$form_state) {
       $this->configuration['label'] = $form_state['values']['label'];
       $this->configuration['label_display'] = $form_state['values']['label_display'];
       $this->configuration['module'] = $form_state['values']['module'];
+      $this->configuration['cache'] = $form_state['values']['cache'];
       $this->blockSubmit($form, $form_state);
     }
   }
@@ -189,4 +253,59 @@ public function getMachineNameSuggestion() {
     return $transliterated;
   }
 
+  /**
+   * Returns the cache contexts required for this block.
+   *
+   * @return array
+   *   The required cache contexts IDs.
+   */
+  protected function getRequiredCacheContexts() {
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheKeys() {
+    // Return the required cache contexts, merged with the user-configured cache
+    // contexts, if any.
+    return array_merge($this->getRequiredCacheContexts(), $this->configuration['cache']['contexts']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    // If a block plugin's output changes, then it must be able to invalidate a
+    // cache tag that affects all instances of this block: across themes and
+    // across regions.
+    $block_plugin_cache_tag = str_replace(':', '__', $this->getPluginID());
+    return array('block_plugin' => array($block_plugin_cache_tag));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheBin() {
+    return 'block';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return (int)$this->configuration['cache']['max_age'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    // Similar to the page cache, a block is cacheable if it has a max age.
+    // Blocks that should never be cached can override this method to simply
+    // return FALSE.
+    $max_age = $this->getCacheMaxAge();
+    return $max_age === Cache::PERMANENT || $max_age > 0;
+  }
+
 }
diff --git a/core/modules/block/lib/Drupal/block/BlockPluginInterface.php b/core/modules/block/lib/Drupal/block/BlockPluginInterface.php
index 047efd9..4f6bdc2 100644
--- a/core/modules/block/lib/Drupal/block/BlockPluginInterface.php
+++ b/core/modules/block/lib/Drupal/block/BlockPluginInterface.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\block;
 
+use Drupal\Core\Cache\CacheableInterface;
 use Drupal\Component\Plugin\PluginInspectionInterface;
 use Drupal\Component\Plugin\ConfigurablePluginInterface;
 use Drupal\Core\Plugin\PluginFormInterface;
@@ -20,7 +21,7 @@
  *   brif references to the important components that are not coupled to the
  *   interface.
  */
-interface BlockPluginInterface extends ConfigurablePluginInterface, PluginFormInterface, PluginInspectionInterface {
+interface BlockPluginInterface extends ConfigurablePluginInterface, PluginFormInterface, PluginInspectionInterface, CacheableInterface {
 
   /**
    * Indicates whether the block should be shown.
diff --git a/core/modules/block/lib/Drupal/block/BlockViewBuilder.php b/core/modules/block/lib/Drupal/block/BlockViewBuilder.php
index a076dc9..4ccfcf4 100644
--- a/core/modules/block/lib/Drupal/block/BlockViewBuilder.php
+++ b/core/modules/block/lib/Drupal/block/BlockViewBuilder.php
@@ -7,7 +7,9 @@
 
 namespace Drupal\block;
 
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\String;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Entity\EntityViewBuilder;
 use Drupal\Core\Entity\EntityViewBuilderInterface;
 use Drupal\Core\Entity\EntityInterface;
@@ -43,52 +45,62 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
       $plugin_id = $plugin->getPluginId();
       $base_id = $plugin->getBasePluginId();
       $derivative_id = $plugin->getDerivativeId();
+      $configuration = $plugin->getConfiguration();
 
-      if ($content = $plugin->build()) {
-        $configuration = $plugin->getConfiguration();
-
-        // Create the render array for the block as a whole.
-        // @see template_preprocess_block().
-        $build[$key] = array(
-          '#theme' => 'block',
-          '#attributes' => array(),
-          '#contextual_links' => array(
-            'block' => array(
-              'route_parameters' => array('block' => $entity_id),
-            ),
+      // Create the render array for the block as a whole.
+      // @see template_preprocess_block().
+      $build[$entity_id] = array(
+        '#theme' => 'block',
+        '#attributes' => array(),
+        // All blocks get a "Configure block" contextual link.
+        '#contextual_links' => array(
+          'block' => array(
+            'route_parameters' => array('block' => $entity->id()),
           ),
-          '#configuration' => $configuration,
-          '#plugin_id' => $plugin_id,
-          '#base_plugin_id' => $base_id,
-          '#derivative_plugin_id' => $derivative_id,
+        ),
+        '#weight' => $entity->get('weight'),
+        '#configuration' => $configuration,
+        '#plugin_id' => $plugin_id,
+        '#base_plugin_id' => $base_id,
+        '#derivative_plugin_id' => $derivative_id,
+        // @todo Remove after fixing http://drupal.org/node/1989568.
+        '#block' => $entity,
+      );
+      $build[$entity_id]['#configuration']['label'] = check_plain($configuration['label']);
+
+      // Set cache tags; these always need to be set, whether the block is
+      // cacheable or not, so that the page cache is correctly informed.
+      $default_cache_tags = array(
+        'content' => TRUE,
+        'block_view' => TRUE,
+        'block' => array($entity->id()),
+      );
+      $build[$entity_id]['#cache']['tags'] = NestedArray::mergeDeep($default_cache_tags, $plugin->getCacheTags());
+
+
+      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').
+        $default_cache_keys = array(
+          'entity_view',
+          'block',
+          $entity->id(),
+          $entity->langcode,
+          // Blocks are always rendered in a "per theme" cache context.
+          'cache_context.theme',
+        );
+        $max_age = $plugin->getCacheMaxAge();
+        $build[$entity_id]['#cache'] += array(
+          'keys' => array_merge($default_cache_keys, $plugin->getCacheKeys()),
+          'bin' => $plugin->getCacheBin(),
+          'expire' => ($max_age === Cache::PERMANENT) ? Cache::PERMANENT : REQUEST_TIME + $max_age,
         );
-        $build[$key]['#configuration']['label'] = String::checkPlain($configuration['label']);
-
-        // Place the $content returned by the block plugin into a 'content'
-        // child element, as a way to allow the plugin to have complete control
-        // of its properties and rendering (e.g., its own #theme) without
-        // conflicting with the properties used above, or alternate ones used
-        // by alternate block rendering approaches in contrib (e.g., Panels).
-        // However, the use of a child element is an implementation detail of
-        // this particular block rendering approach. Semantically, the content
-        // returned by the plugin "is the" block, and in particular,
-        // #attributes and #contextual_links is information about the *entire*
-        // block. Therefore, we must move these properties from $content and
-        // merge them into the top-level element.
-        foreach (array('#attributes', '#contextual_links') as $property) {
-          if (isset($content[$property])) {
-            $build[$key][$property] += $content[$property];
-            unset($content[$property]);
-          }
-        }
-        $build[$key]['content'] = $content;
       }
       else {
-        $build[$key] = array();
+        $build[$entity_id] = $this->buildBlock($build[$entity_id]);
       }
 
-      $this->moduleHandler()->alter(array('block_view', "block_view_$base_id"), $build[$key], $plugin);
-
       // @todo Remove after fixing http://drupal.org/node/1989568.
       $build[$key]['#block'] = $entity;
     }
@@ -96,8 +108,65 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
   }
 
   /**
+   * #pre_render callback for building a block.
+   *
+   * Renders the content using the provided block plugin, and then:
+   * - if there is no content, aborts rendering, and makes sure the block won't
+   *   be rendered.
+   * - if there is content, moves the contextual links from the block content to
+   *   the block itself, and invokes the alter hooks.
+   */
+  public function buildBlock($build) {
+    $plugin = $build['#block']->getPlugin();
+    $content = $plugin->build();
+
+    if (!empty($content)) {
+      // Place the $content returned by the block plugin into a 'content' child
+      // element, as a way to allow the plugin to have complete control of its
+      // properties and rendering (e.g., its own #theme) without conflicting
+      // with the properties used above, or alternate ones used by alternate
+      // block rendering approaches in contrib (e.g., Panels). However, the use
+      // of a child element is an implementation detail of this particular block
+      // rendering approach. Semantically, the content returned by the plugin
+      // "is the" block, and in particular, #attributes and #contextual_links is
+      // information about the *entire* block. Therefore, we must move these
+      // properties from $content and merge them into the top-level element.
+      foreach (array('#attributes', '#contextual_links') as $property) {
+        if (isset($content[$property])) {
+          $build[$property] += $content[$property];
+          unset($content[$property]);
+        }
+      }
+      $build['content'] = $content;
+
+      $base_id = $plugin->getBasePluginId();
+      $this->moduleHandler()->alter(array('block_view', "block_view_$base_id"), $build, $plugin);
+
+      return $build;
+    }
+    else {
+      // Abort rendering: render as the empty string and ensure this block is
+      // render cached, so we can avoid the work of having to repeatedly
+      // determine whether the block is empty. E.g. modifying or adding entities
+      // could cause the block to no longer be empty.
+      return array(
+        '#markup' => '',
+        '#cache' => $build['#cache'],
+      );
+    }
+    return $build;
+   }
+
+  /**
    * {@inheritdoc}
    */
-  public function resetCache(array $ids = NULL) { }
+  public function resetCache(array $entities = NULL) {
+    if (isset($entities)) {
+      Cache::invalidateTags(array('block' => array_keys($entities)));
+    }
+    else {
+      Cache::invalidateTags(array('block_view' => TRUE));
+    }
+  }
 
 }
diff --git a/core/modules/block/lib/Drupal/block/Entity/Block.php b/core/modules/block/lib/Drupal/block/Entity/Block.php
index cb49cac..93ce334 100644
--- a/core/modules/block/lib/Drupal/block/Entity/Block.php
+++ b/core/modules/block/lib/Drupal/block/Entity/Block.php
@@ -7,10 +7,12 @@
 
 namespace Drupal\block\Entity;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Config\Entity\ConfigEntityBase;
 use Drupal\block\BlockPluginBag;
 use Drupal\block\BlockInterface;
 use Drupal\Core\Config\Entity\EntityWithPluginBagInterface;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
 
 /**
  * Defines a Block configuration entity class.
@@ -146,6 +148,32 @@ public function getExportProperties() {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
+    parent::postSave($storage_controller, $update);
+
+    if ($update) {
+      Cache::invalidateTags(array('block' => $this->id()));
+    }
+    // When placing a new block, invalidate all cache entries for this theme,
+    // since any page that uses this theme might be affected.
+    else {
+      // @todo Replace with theme cache tag: https://drupal.org/node/2185617
+      Cache::invalidateTags(array('content' => TRUE));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
+    parent::postDelete($storage_controller, $entities);
+
+    Cache::invalidateTags(array('block' => array_keys($entities)));
+  }
+
+  /**
    * Sorts active blocks by weight; sorts inactive blocks by name.
    */
   public static function sort($a, $b) {
diff --git a/core/modules/block/lib/Drupal/block/Plugin/views/display/Block.php b/core/modules/block/lib/Drupal/block/Plugin/views/display/Block.php
index 657b635..94bea95 100644
--- a/core/modules/block/lib/Drupal/block/Plugin/views/display/Block.php
+++ b/core/modules/block/lib/Drupal/block/Plugin/views/display/Block.php
@@ -46,7 +46,6 @@ protected function defineOptions() {
 
     $options['block_description'] = array('default' => '', 'translatable' => TRUE);
     $options['block_category'] = array('default' => 'Lists (Views)', 'translatable' => TRUE);
-    $options['block_caching'] = array('default' => DRUPAL_NO_CACHE);
     $options['block_hide_empty'] = array('default' => FALSE);
 
     $options['allow'] = array(
@@ -131,13 +130,6 @@ public function optionsSummary(&$categories, &$options) {
       'value' => empty($filtered_allow) ? t('None') : t('Items per page'),
     );
 
-    $types = $this->blockCachingModes();
-    $options['block_caching'] = array(
-      'category' => 'other',
-      'title' => t('Block caching'),
-      'value' => $types[$this->getCacheType()],
-    );
-
     $options['block_hide_empty'] = array(
       'category' => 'other',
       'title' => t('Hide block if the view output is empty'),
@@ -146,33 +138,6 @@ public function optionsSummary(&$categories, &$options) {
   }
 
   /**
-   * Provide a list of core's block caching modes.
-   */
-  protected function blockCachingModes() {
-    return array(
-      DRUPAL_NO_CACHE => t('Do not cache'),
-      DRUPAL_CACHE_GLOBAL => t('Cache once for everything (global)'),
-      DRUPAL_CACHE_PER_PAGE => t('Per page'),
-      DRUPAL_CACHE_PER_ROLE => t('Per role'),
-      DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE => t('Per role per page'),
-      DRUPAL_CACHE_PER_USER => t('Per user'),
-      DRUPAL_CACHE_PER_USER | DRUPAL_CACHE_PER_PAGE => t('Per user per page'),
-    );
-  }
-
-  /**
-   * Provide a single method to figure caching type, keeping a sensible default
-   * for when it's unset.
-   */
-  public function getCacheType() {
-    $cache_type = $this->getOption('block_caching');
-    if (empty($cache_type)) {
-      $cache_type = DRUPAL_NO_CACHE;
-    }
-    return $cache_type;
-  }
-
-  /**
    * Provide the default form for setting options.
    */
   public function buildOptionsForm(&$form, &$form_state) {
@@ -196,16 +161,6 @@ public function buildOptionsForm(&$form, &$form_state) {
           '#default_value' => $this->getOption('block_category'),
         );
         break;
-      case 'block_caching':
-        $form['#title'] .= t('Block caching type');
-
-        $form['block_caching'] = array(
-          '#type' => 'radios',
-          '#description' => t("This sets the default status for Drupal's built-in block caching method; this requires that caching be turned on in block administration, and be careful because you have little control over when this cache is flushed."),
-          '#options' => $this->blockCachingModes(),
-          '#default_value' => $this->getCacheType(),
-        );
-        break;
       case 'block_hide_empty':
         $form['#title'] .= t('Block empty settings');
 
@@ -251,7 +206,6 @@ public function submitOptionsForm(&$form, &$form_state) {
     switch ($form_state['section']) {
       case 'block_description':
       case 'block_category':
-      case 'block_caching':
       case 'allow':
       case 'block_hide_empty':
         $this->setOption($form_state['section'], $form_state['values'][$form_state['section']]);
diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockCacheTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockCacheTest.php
index 7414efd..ce78720 100644
--- a/core/modules/block/lib/Drupal/block/Tests/BlockCacheTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/BlockCacheTest.php
@@ -61,10 +61,13 @@ function setUp() {
   }
 
   /**
-   * Test DRUPAL_CACHE_PER_ROLE.
+   * Test "cache_context.user.roles" cache context.
    */
   function testCachePerRole() {
-    $this->setCacheMode(DRUPAL_CACHE_PER_ROLE);
+    $this->setBlockCacheConfig(array(
+      'max_age' => 600,
+      'contexts' => array('cache_context.user.roles'),
+    ));
 
     // Enable our test block. Set some content for it to display.
     $current_content = $this->randomName();
@@ -108,10 +111,13 @@ function testCachePerRole() {
   }
 
   /**
-   * Test DRUPAL_CACHE_GLOBAL.
+   * Test a cacheable block without any cache context.
    */
   function testCacheGlobal() {
-    $this->setCacheMode(DRUPAL_CACHE_GLOBAL);
+    $this->setBlockCacheConfig(array(
+      'max_age' => 600,
+    ));
+
     $current_content = $this->randomName();
     \Drupal::state()->set('block_test.content', $current_content);
 
@@ -124,18 +130,21 @@ function testCacheGlobal() {
 
     $this->drupalLogout();
     $this->drupalGet('user');
-    $this->assertText($old_content, 'Block content served from global cache.');
+    $this->assertText($old_content, 'Block content served from cache.');
   }
 
   /**
-   * Test DRUPAL_NO_CACHE.
+   * Test non-cacheable block.
    */
   function testNoCache() {
-    $this->setCacheMode(DRUPAL_NO_CACHE);
+    $this->setBlockCacheConfig(array(
+      'max_age' => 0,
+    ));
+
     $current_content = $this->randomName();
     \Drupal::state()->set('block_test.content', $current_content);
 
-    // If DRUPAL_NO_CACHE has no effect, the next request would be cached.
+    // If max_age = 0 has no effect, the next request would be cached.
     $this->drupalGet('');
     $this->assertText($current_content, 'Block content displays.');
 
@@ -143,14 +152,18 @@ function testNoCache() {
     $current_content = $this->randomName();
     \Drupal::state()->set('block_test.content', $current_content);
     $this->drupalGet('');
-    $this->assertText($current_content, 'DRUPAL_NO_CACHE prevents blocks from being cached.');
+    $this->assertText($current_content, 'Maximum age of zero prevents blocks from being cached.');
   }
 
   /**
-   * Test DRUPAL_CACHE_PER_USER.
+   * Test "cache_context.user" cache context.
    */
   function testCachePerUser() {
-    $this->setCacheMode(DRUPAL_CACHE_PER_USER);
+    $this->setBlockCacheConfig(array(
+      'max_age' => 600,
+      'contexts' => array('cache_context.user'),
+    ));
+
     $current_content = $this->randomName();
     \Drupal::state()->set('block_test.content', $current_content);
     $this->drupalLogin($this->normal_user);
@@ -175,10 +188,14 @@ function testCachePerUser() {
   }
 
   /**
-   * Test DRUPAL_CACHE_PER_PAGE.
+   * Test "cache_context.url" cache context.
    */
   function testCachePerPage() {
-    $this->setCacheMode(DRUPAL_CACHE_PER_PAGE);
+    $this->setBlockCacheConfig(array(
+      'max_age' => 600,
+      'contexts' => array('cache_context.url'),
+    ));
+
     $current_content = $this->randomName();
     \Drupal::state()->set('block_test.content', $current_content);
 
@@ -196,10 +213,11 @@ function testCachePerPage() {
   }
 
   /**
-   * Private helper method to set the test block's cache mode.
+   * Private helper method to set the test block's cache configuration.
    */
-  private function setCacheMode($cache_mode) {
-    $this->block->getPlugin()->setConfigurationValue('cache', $cache_mode);
+  private function setBlockCacheConfig($cache_config) {
+    $block = $this->block->getPlugin();
+    $block->setConfigurationValue('cache', $cache_config);
     $this->block->save();
   }
 
diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockInterfaceTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockInterfaceTest.php
index a694b4e..5e7d9d0 100644
--- a/core/modules/block/lib/Drupal/block/Tests/BlockInterfaceTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/BlockInterfaceTest.php
@@ -45,10 +45,13 @@ public function testBlockInterface() {
     );
     $expected_configuration = array(
       'label' => 'Custom Display Message',
-      'display_message' => 'no message set',
       'module' => 'block_test',
       'label_display' => BlockInterface::BLOCK_LABEL_VISIBLE,
-      'cache' => DRUPAL_NO_CACHE,
+      'cache' => array(
+        'max_age' => 0,
+        'contexts' => array(),
+      ),
+      'display_message' => 'no message set',
     );
     // Initial configuration of the block at construction time.
     $display_block = $manager->createInstance('test_block_instantiation', $configuration);
@@ -60,6 +63,12 @@ public function testBlockInterface() {
     $this->assertIdentical($display_block->getConfiguration(), $expected_configuration, 'The block configuration was updated correctly.');
     $definition = $display_block->getPluginDefinition();
 
+    $period = array(0, 60, 180, 300, 600, 900, 1800, 2700, 3600, 10800, 21600, 32400, 43200, 86400);
+    $period = array_map('format_interval', array_combine($period, $period));
+    $period[0] = '<' . t('no caching') . '>';
+    $period[\Drupal\Core\Cache\Cache::PERMANENT] = t('Forever');
+    $contexts = \Drupal::service("cache_contexts")->getLabels();
+    unset($contexts['cache_context.theme']);
     $expected_form = array(
       'module' => array(
         '#type' => 'value',
@@ -84,8 +93,27 @@ public function testBlockInterface() {
         '#return_value' => 'visible',
       ),
       'cache' => array(
-        '#type' => 'value',
-        '#value' => DRUPAL_NO_CACHE,
+        '#type' => 'details',
+        '#title' => t('Cache settings'),
+        'max_age' => array(
+          '#type' => 'select',
+          '#title' => t('Maximum age'),
+          '#description' => t('The maximum time this block may be cached.'),
+          '#default_value' => 0,
+          '#options' => $period,
+        ),
+        'contexts' => array(
+          '#type' => 'checkboxes',
+          '#title' => t('Vary by context'),
+          '#description' => t('The contexts this cached block must be varied by.'),
+          '#default_value' => array(),
+          '#options' => $contexts,
+          '#states' => array(
+            'disabled' => array(
+              ':input[name="settings[cache][max_age]"]' => array('value' => (string) 0),
+            ),
+          ),
+        ),
       ),
       'display_message' => array(
         '#type' => 'textfield',
diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockRenderOrderTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockRenderOrderTest.php
index 0521aa9..1a7ea82 100644
--- a/core/modules/block/lib/Drupal/block/Tests/BlockRenderOrderTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/BlockRenderOrderTest.php
@@ -48,21 +48,24 @@ function testBlockRenderOrder() {
       'stark_powered' => array(
         'weight' => '-3',
         'id' => 'stark_powered',
+        'label' => 'Test block A',
       ),
       'stark_by' => array(
         'weight' => '3',
         'id' => 'stark_by',
+        'label' => 'Test block C',
       ),
       'stark_drupal' => array(
         'weight' => '3',
         'id' => 'stark_drupal',
+        'label' => 'Test block B',
       ),
     );
 
     // Place the test blocks.
     foreach ($test_blocks as $test_block) {
       $this->drupalPlaceBlock('system_powered_by_block', array(
-        'label' => 'Test Block',
+        'label' => $test_block['label'],
         'region' => $region,
         'weight' => $test_block['weight'],
         'id' => $test_block['id'],
@@ -81,6 +84,6 @@ function testBlockRenderOrder() {
       }
     }
     $this->assertTrue($position['stark_powered'] < $position['stark_by'], 'Blocks with different weight are rendered in the correct order.');
-    $this->assertTrue($position['stark_drupal'] < $position['stark_by'], 'Blocks with identical weight are rendered in reverse alphabetical order.');
+    $this->assertTrue($position['stark_drupal'] < $position['stark_by'], 'Blocks with identical weight are rendered in alphabetical order.');
   }
 }
diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockStorageUnitTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockStorageUnitTest.php
index 36ed74a..d95edca 100644
--- a/core/modules/block/lib/Drupal/block/Tests/BlockStorageUnitTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/BlockStorageUnitTest.php
@@ -102,7 +102,10 @@ protected function createTests() {
         'label' => '',
         'module' => 'block_test',
         'label_display' => BlockInterface::BLOCK_LABEL_VISIBLE,
-        'cache' => DRUPAL_NO_CACHE,
+        'cache' => array(
+          'max_age' => 0,
+          'contexts' => array(),
+        ),
       ),
       'visibility' => NULL,
     );
diff --git a/core/modules/block/lib/Drupal/block/Tests/BlockTest.php b/core/modules/block/lib/Drupal/block/Tests/BlockTest.php
index e7b99e1..cc25986 100644
--- a/core/modules/block/lib/Drupal/block/Tests/BlockTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/BlockTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\block\Tests;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -249,35 +250,106 @@ function moveBlockToRegion(array $block, $region) {
   }
 
   /**
-   * Test _block_rehash().
+   * Test that cache tags are properly set and bubbled up to the page cache.
+   *
+   * Verify that invalidation of these cache tags works:
+   * - "block:<block ID>"
+   * - "block_plugin:<block plugin ID>"
    */
-  function testBlockRehash() {
-    \Drupal::moduleHandler()->install(array('block_test'));
-    $this->assertTrue(\Drupal::moduleHandler()->moduleExists('block_test'), 'Test block module enabled.');
-
-    // Clear the block cache to load the block_test module's block definitions.
-    $this->container->get('plugin.manager.block')->clearCachedDefinitions();
-
-    // Add a test block.
-    $block = array();
-    $block['id'] = 'test_cache';
-    $block['theme'] = \Drupal::config('system.theme')->get('default');
-    $block['region'] = 'header';
-    $block = $this->drupalPlaceBlock('test_cache', array('region' => 'header'));
+  public function testBlockCacheTags() {
+    // The page cache only works for anonymous users.
+    $this->drupalLogout();
 
-    // Our test block's caching should default to DRUPAL_CACHE_PER_ROLE.
-    $settings = $block->get('settings');
-    $this->assertEqual($settings['cache'], DRUPAL_CACHE_PER_ROLE, 'Test block cache mode defaults to DRUPAL_CACHE_PER_ROLE.');
+    // Enable page caching.
+    $config = \Drupal::config('system.performance');
+    $config->set('cache.page.use_internal', 1);
+    $config->set('cache.page.max_age', 300);
+    $config->save();
+
+    // Place the "Powered by Drupal" block.
+    $block = $this->drupalPlaceBlock('system_powered_by_block', array('id' => 'powered', 'cache' => array('max_age' => 315360000)));
+
+    // Prime the page cache.
+    $this->drupalGet('<front>');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
+
+    // Verify a cache hit, but also the presence of the correct cache tags in
+    // both the page and block caches.
+    $this->drupalGet('<front>');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
+    $cid_parts = array(url('<front>', array('absolute' => TRUE)), 'html');
+    $cid = sha1(implode(':', $cid_parts));
+    $cache_entry = \Drupal::cache('page')->get($cid);
+    $expected_cache_tags = array(
+      'content:1',
+      'block_view:1',
+      'block:powered',
+      'block_plugin:system_powered_by_block',
+    );
+    $this->assertIdentical($cache_entry->tags, $expected_cache_tags);
+    $cache_entry = \Drupal::cache('block')->get('entity_view:block:powered:en:stark');
+    $this->assertIdentical($cache_entry->tags, $expected_cache_tags);
 
-    // Disable caching for this block.
-    $block->getPlugin()->setConfigurationValue('cache', DRUPAL_NO_CACHE);
+    // The "Powered by Drupal" block is modified; verify a cache miss.
+    $block->set('region', 'content');
     $block->save();
-    // Flushing all caches should call _block_rehash().
-    $this->resetAll();
-    // Verify that block is updated with the new caching mode.
-    $block = entity_load('block', $block->id());
-    $settings = $block->get('settings');
-    $this->assertEqual($settings['cache'], DRUPAL_NO_CACHE, "Test block's database entry updated to DRUPAL_NO_CACHE.");
+    $this->drupalGet('<front>');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
+
+    // Now we should have a cache hit again.
+    $this->drupalGet('<front>');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
+
+    // Place the "Powered by Drupal" block another time; verify a cache miss.
+    $block_2 = $this->drupalPlaceBlock('system_powered_by_block', array('id' => 'powered-2', 'cache' => array('max_age' => 315360000)));
+    $this->drupalGet('<front>');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
+
+    // Verify a cache hit, but also the presence of the correct cache tags.
+    $this->drupalGet('<front>');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
+    $cid_parts = array(url('<front>', array('absolute' => TRUE)), 'html');
+    $cid = sha1(implode(':', $cid_parts));
+    $cache_entry = \Drupal::cache('page')->get($cid);
+    $expected_cache_tags = array(
+      'content:1',
+      'block_view:1',
+      'block:powered-2',
+      'block:powered',
+      'block_plugin:system_powered_by_block',
+    );
+    $this->assertEqual($cache_entry->tags, $expected_cache_tags);
+    $expected_cache_tags = array(
+      'content:1',
+      'block_view:1',
+      'block:powered',
+      'block_plugin:system_powered_by_block',
+    );
+    $cache_entry = \Drupal::cache('block')->get('entity_view:block:powered:en:stark');
+    $this->assertIdentical($cache_entry->tags, $expected_cache_tags);
+    $expected_cache_tags = array(
+      'content:1',
+      'block_view:1',
+      'block:powered-2',
+      'block_plugin:system_powered_by_block',
+    );
+    $cache_entry = \Drupal::cache('block')->get('entity_view:block:powered-2:en:stark');
+    $this->assertIdentical($cache_entry->tags, $expected_cache_tags);
+
+    // The plugin providing the "Powered by Drupal" block is modified; verify a
+    // cache miss.
+    Cache::invalidateTags(array('block_plugin:system_powered_by_block'));
+    $this->drupalGet('<front>');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
+
+    // Now we should have a cache hit again.
+    $this->drupalGet('<front>');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
+
+    // Delete the "Powered by Drupal" blocks; verify a cache miss.
+    entity_delete_multiple('block', array('powered', 'powered-2'));
+    $this->drupalGet('<front>');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
   }
 
 }
diff --git a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
index 8f2ddc0..986ade7 100644
--- a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
@@ -176,19 +176,6 @@ protected function testDeleteBlockDisplay() {
   }
 
   /**
-   * Tests views block plugin definitions.
-   */
-  public function testViewsBlockPlugins() {
-    // Ensures that the cache setting gets to the block settings.
-    $instance = $this->container->get('plugin.manager.block')->createInstance('views_block:test_view_block2-block_2');
-    $configuration = $instance->getConfiguration();
-    $this->assertEqual($configuration['cache'], DRUPAL_NO_CACHE);
-    $instance = $this->container->get('plugin.manager.block')->createInstance('views_block:test_view_block2-block_3');
-    $configuration = $instance->getConfiguration();
-    $this->assertEqual($configuration['cache'], DRUPAL_CACHE_PER_USER);
-  }
-
-  /**
    * Test the block form for a Views block.
    */
   public function testViewsBlockForm() {
@@ -282,19 +269,23 @@ public function testBlockRendering() {
   public function testBlockContextualLinks() {
     $this->drupalLogin($this->drupalCreateUser(array('administer views', 'access contextual links', 'administer blocks')));
     $block = $this->drupalPlaceBlock('views_block:test_view_block-block_1');
+    $cached_block = $this->drupalPlaceBlock('views_block:test_view_block-block_1', array('cache' => array('max_age' => 3600)));
     $this->drupalGet('test-page');
 
     $id = 'block:block=' . $block->id() . ':|views_ui_edit:view=test_view_block:location=block&name=test_view_block&display_id=block_1';
+    $cached_id = 'block:block=' . $cached_block->id() . ':|views_ui_edit:view=test_view_block:location=block&name=test_view_block&display_id=block_1';
     // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder()
     $this->assertRaw('<div' . new Attribute(array('data-contextual-id' => $id)) . '></div>', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id)));
+    $this->assertRaw('<div' . new Attribute(array('data-contextual-id' => $cached_id)) . '></div>', format_string('Contextual link placeholder with id @id exists.', array('@id' => $cached_id)));
 
     // Get server-rendered contextual links.
     // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks()
-    $post = array('ids[0]' => $id);
+    $post = array('ids[0]' => $id, 'ids[1]' => $cached_id);
     $response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page')));
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
     $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '">Configure block</a></li><li class="views-uiedit"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1">Edit view</a></li></ul>');
+    $this->assertIdentical($json[$cached_id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $cached_block->id() . '">Configure block</a></li><li class="views-uiedit"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1">Edit view</a></li></ul>');
   }
 
 }
diff --git a/core/modules/block/tests/Drupal/block/Tests/BlockBaseTest.php b/core/modules/block/tests/Drupal/block/Tests/BlockBaseTest.php
index f0d04ec..8d96c33 100644
--- a/core/modules/block/tests/Drupal/block/Tests/BlockBaseTest.php
+++ b/core/modules/block/tests/Drupal/block/Tests/BlockBaseTest.php
@@ -12,11 +12,6 @@
 use Drupal\Core\Transliteration\PHPTransliteration;
 use Drupal\Tests\UnitTestCase;
 
-// @todo Remove once the constants are replaced with constants on classes.
-if (!defined('DRUPAL_NO_CACHE')) {
-  define('DRUPAL_NO_CACHE', -1);
-}
-
 /**
  * Tests the base block plugin.
  *
diff --git a/core/modules/block/tests/modules/block_test/config/block.block.test_block.yml b/core/modules/block/tests/modules/block_test/config/block.block.test_block.yml
index 9a8383c..9c00b0b 100644
--- a/core/modules/block/tests/modules/block_test/config/block.block.test_block.yml
+++ b/core/modules/block/tests/modules/block_test/config/block.block.test_block.yml
@@ -9,7 +9,6 @@ settings:
   label: 'Test HTML block'
   module: block_test
   label_display: 'hidden'
-  cache: 1
 visibility:
   path:
     visibility: 0
diff --git a/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestCacheBlock.php b/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestCacheBlock.php
index 2fa6122..0216fa1 100644
--- a/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestCacheBlock.php
+++ b/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestCacheBlock.php
@@ -21,21 +21,10 @@ class TestCacheBlock extends BlockBase {
 
   /**
    * {@inheritdoc}
-   *
-   * Sets a different caching strategy for testing purposes.
-   */
-  public function defaultConfiguration() {
-    return array(
-      'cache' => DRUPAL_CACHE_PER_ROLE,
-    );
-  }
-
-  /**
-   * {@inheritdoc}
    */
   public function build() {
     return array(
-      '#children' => \Drupal::state()->get('block_test.content'),
+      '#markup' => \Drupal::state()->get('block_test.content'),
     );
   }
 
diff --git a/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestXSSTitleBlock.php b/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestXSSTitleBlock.php
index 156abf9..3146b59 100644
--- a/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestXSSTitleBlock.php
+++ b/core/modules/block/tests/modules/block_test/lib/Drupal/block_test/Plugin/Block/TestXSSTitleBlock.php
@@ -16,16 +16,4 @@
  * )
  */
 class TestXSSTitleBlock extends TestCacheBlock {
-
-  /**
-   * {@inheritdoc}
-   *
-   * Sets a different caching strategy for testing purposes.
-   */
-  public function defaultConfiguration() {
-    return array(
-      'cache' => DRUPAL_NO_CACHE,
-    );
-  }
-
 }
diff --git a/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php b/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php
index 4350e53..6a9cfff 100644
--- a/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php
+++ b/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php
@@ -65,7 +65,6 @@ public static function create(ContainerInterface $container, array $configuratio
    */
   public function defaultConfiguration() {
     return array(
-      'cache' => DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE,
       'block_mode' => "all pages",
     );
   }
@@ -101,6 +100,7 @@ public function blockSubmit($form, &$form_state) {
    */
   public function build() {
     $current_bid = 0;
+
     if ($node = $this->request->get('node')) {
       $current_bid = empty($node->book['bid']) ? 0 : $node->book['bid'];
     }
@@ -145,15 +145,21 @@ public function build() {
         $data = array_shift($tree);
         $below = \Drupal::service('book.manager')->bookTreeOutput($data['below']);
         if (!empty($below)) {
-          $book_title_link = array('#theme' => 'book_title_link', '#link' => $data['link']);
-          return array(
-            '#title' => drupal_render($book_title_link),
-            $below,
-          );
+          return $below;
         }
       }
     }
     return array();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getRequiredCacheContexts() {
+    // The "Book navigation" block must be cached per URL and per role: the
+    // "active" menu link may differ per URL and different roles may have access
+    // to different menu links.
+    return array('cache_context.url', 'cache_context.user.roles');
+  }
+
 }
diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module
index 959a700..c0e8d3a 100644
--- a/core/modules/forum/forum.module
+++ b/core/modules/forum/forum.module
@@ -539,23 +539,6 @@ function forum_form_node_form_alter(&$form, &$form_state, $form_id) {
 }
 
 /**
- * Render API callback: Lists nodes based on the element's #query property.
- *
- * This function can be used as a #pre_render callback.
- *
- * @see \Drupal\forum\Plugin\block\block\NewTopicsBlock::build()
- * @see \Drupal\forum\Plugin\block\block\ActiveTopicsBlock::build()
- */
-function forum_block_view_pre_render($elements) {
-  $result = $elements['#query']->execute();
-  if ($node_title_list = node_title_list($result)) {
-    $elements['forum_list'] = $node_title_list;
-    $elements['forum_more'] = array('#theme' => 'more_link', '#url' => 'forum', '#title' => t('Read the latest forum topics.'));
-  }
-  return $elements;
-}
-
-/**
  * Implements hook_preprocess_HOOK() for block templates.
  */
 function forum_preprocess_block(&$variables) {
diff --git a/core/modules/forum/lib/Drupal/forum/Plugin/Block/ActiveTopicsBlock.php b/core/modules/forum/lib/Drupal/forum/Plugin/Block/ActiveTopicsBlock.php
index d83f736..133529a 100644
--- a/core/modules/forum/lib/Drupal/forum/Plugin/Block/ActiveTopicsBlock.php
+++ b/core/modules/forum/lib/Drupal/forum/Plugin/Block/ActiveTopicsBlock.php
@@ -21,17 +21,13 @@ class ActiveTopicsBlock extends ForumBlockBase {
   /**
    * {@inheritdoc}
    */
-  public function build() {
-    $query = db_select('forum_index', 'f')
+  protected function buildForumQuery() {
+    return db_select('forum_index', 'f')
       ->fields('f')
       ->addTag('node_access')
       ->addMetaData('base_table', 'forum_index')
       ->orderBy('f.last_comment_timestamp', 'DESC')
       ->range(0, $this->configuration['block_count']);
-
-    return array(
-      drupal_render_cache_by_query($query, 'forum_block_view'),
-    );
   }
 
 }
diff --git a/core/modules/forum/lib/Drupal/forum/Plugin/Block/ForumBlockBase.php b/core/modules/forum/lib/Drupal/forum/Plugin/Block/ForumBlockBase.php
index f1c2f4e..6b760a9 100644
--- a/core/modules/forum/lib/Drupal/forum/Plugin/Block/ForumBlockBase.php
+++ b/core/modules/forum/lib/Drupal/forum/Plugin/Block/ForumBlockBase.php
@@ -9,6 +9,7 @@
 
 use Drupal\block\BlockBase;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Cache\Cache;
 
 /**
  * Provides a base class for Forum blocks.
@@ -18,9 +19,32 @@
   /**
    * {@inheritdoc}
    */
+  public function build() {
+    $result = $this->buildForumQuery()->execute();
+    if ($node_title_list = node_title_list($result)) {
+      $elements['forum_list'] = $node_title_list;
+      $elements['forum_more'] = array(
+        '#theme' => 'more_link',
+        '#url' => 'forum',
+        '#title' => t('Read the latest forum topics.')
+      );
+    }
+    return $elements;
+  }
+
+  /**
+   * Builds the select query to use for this forum block.
+   *
+   * @return \Drupal\Core\Database\Query\Select
+   *   A Select object.
+   */
+  abstract protected function buildForumQuery();
+
+  /**
+   * {@inheritdoc}
+   */
   public function defaultConfiguration() {
     return array(
-      'cache' => DRUPAL_CACHE_CUSTOM,
       'properties' => array(
         'administrative' => TRUE,
       ),
@@ -56,4 +80,11 @@ public function blockSubmit($form, &$form_state) {
     $this->configuration['block_count'] = $form_state['values']['block_count'];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheKeys() {
+    return array_merge(parent::getCacheKeys(), Cache::keyFromQuery($this->buildForumQuery()));
+  }
+
 }
diff --git a/core/modules/forum/lib/Drupal/forum/Plugin/Block/NewTopicsBlock.php b/core/modules/forum/lib/Drupal/forum/Plugin/Block/NewTopicsBlock.php
index 8b75a83..635dab4 100644
--- a/core/modules/forum/lib/Drupal/forum/Plugin/Block/NewTopicsBlock.php
+++ b/core/modules/forum/lib/Drupal/forum/Plugin/Block/NewTopicsBlock.php
@@ -21,17 +21,12 @@ class NewTopicsBlock extends ForumBlockBase {
   /**
    * {@inheritdoc}
    */
-  public function build() {
-    $query = db_select('forum_index', 'f')
+  protected function buildForumQuery() {
+    return db_select('forum_index', 'f')
       ->fields('f')
       ->addTag('node_access')
       ->addMetaData('base_table', 'forum_index')
       ->orderBy('f.created', 'DESC')
       ->range(0, $this->configuration['block_count']);
-
-    return array(
-      drupal_render_cache_by_query($query, 'forum_block_view'),
-    );
   }
-
 }
diff --git a/core/modules/language/lib/Drupal/language/Plugin/Derivative/LanguageBlock.php b/core/modules/language/lib/Drupal/language/Plugin/Derivative/LanguageBlock.php
index 88c0736..b673d66 100644
--- a/core/modules/language/lib/Drupal/language/Plugin/Derivative/LanguageBlock.php
+++ b/core/modules/language/lib/Drupal/language/Plugin/Derivative/LanguageBlock.php
@@ -27,7 +27,6 @@ public function getDerivativeDefinitions($base_plugin_definition) {
       foreach ($configurable_types as $type) {
         $this->derivatives[$type] = $base_plugin_definition;
         $this->derivatives[$type]['admin_label'] = t('Language switcher (!type)', array('!type' => $info[$type]['name']));
-        $this->derivatives[$type]['cache'] = DRUPAL_NO_CACHE;
       }
       // If there is just one configurable type then change the title of the
       // block.
diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuCacheTagsTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuCacheTagsTest.php
index c09875c..e8f00a3 100644
--- a/core/modules/menu/lib/Drupal/menu/Tests/MenuCacheTagsTest.php
+++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuCacheTagsTest.php
@@ -58,7 +58,14 @@ public function testMenuBlock() {
     $this->verifyPageCache($path, 'MISS');
 
     // Verify a cache hit, but also the presence of the correct cache tags.
-    $this->verifyPageCache($path, 'HIT', array('content:1', 'menu:llama'));
+    $expected_tags = array(
+      'content:1',
+      'block_view:1',
+      'block:' . $block->id(),
+      'block_plugin:system_menu_block__llama',
+      'menu:llama',
+    );
+    $this->verifyPageCache($path, 'HIT', $expected_tags);
 
 
     // Verify that after modifying the menu, there is a cache miss.
@@ -101,7 +108,7 @@ public function testMenuBlock() {
     $this->verifyPageCache($path, 'MISS');
 
     // Verify a cache hit.
-    $this->verifyPageCache($path, 'HIT', array('content:1', 'menu:llama'));
+    $this->verifyPageCache($path, 'HIT', $expected_tags);
 
 
     // Verify that after deleting the menu, there is a cache miss.
diff --git a/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php b/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php
index ad96735..0635eb0 100644
--- a/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php
+++ b/core/modules/search/lib/Drupal/search/Form/SearchBlockForm.php
@@ -103,4 +103,13 @@ public function submitForm(array &$form, array &$form_state) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    // This is a block containing a simple form without state, which is safe to
+    // cache forever.
+    return array('cache' => array('max_age' => \Drupal\Core\Cache\Cache::PERMANENT));
+  }
+
 }
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
index 30a6280..6fd6ab2 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -353,6 +353,7 @@ protected function drupalCreateContentType(array $values = array()) {
    *   - region: 'sidebar_first'.
    *   - theme: The default theme.
    *   - visibility: Empty array.
+   *   - cache: array('max_age' => 0).
    *
    * @return \Drupal\block\Entity\Block
    *   The block entity.
@@ -369,6 +370,9 @@ protected function drupalPlaceBlock($plugin_id, array $settings = array()) {
       'label' => $this->randomName(8),
       'visibility' => array(),
       'weight' => 0,
+      'cache' => array(
+        'max_age' => 0,
+      ),
     );
     foreach (array('region', 'id', 'theme', 'plugin', 'visibility', 'weight') as $key) {
       $values[$key] = $settings[$key];
diff --git a/core/modules/system/lib/Drupal/system/Form/PerformanceForm.php b/core/modules/system/lib/Drupal/system/Form/PerformanceForm.php
index fe689d0..27d628a 100644
--- a/core/modules/system/lib/Drupal/system/Form/PerformanceForm.php
+++ b/core/modules/system/lib/Drupal/system/Form/PerformanceForm.php
@@ -79,10 +79,12 @@ public function buildForm(array $form, array &$form_state) {
       '#title' => t('Caching'),
       '#open' => TRUE,
     );
-
+    // Identical options to the ones for block caching.
+    // @see \Drupal\block\BlockBase::buildConfigurationForm()
     $period = array(0, 60, 180, 300, 600, 900, 1800, 2700, 3600, 10800, 21600, 32400, 43200, 86400);
     $period = array_map('format_interval', array_combine($period, $period));
-    $period[0] = '<' . t('none') . '>';
+    $period[0] = '<' . t('no caching') . '>';
+    $period[\Drupal\Core\Cache\Cache::PERMANENT] = t('Forever');
     $form['caching']['page_cache_maximum_age'] = array(
       '#type' => 'select',
       '#title' => t('Page cache maximum age'),
diff --git a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemHelpBlock.php b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemHelpBlock.php
index f669e61..c594b98 100644
--- a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemHelpBlock.php
+++ b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemHelpBlock.php
@@ -117,4 +117,24 @@ public function build() {
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    // Modify the default max age for the System Help block: help text is static
+    // for a given URL, except when a module is updated, in which case
+    // update.php must be run, which clears all caches. Thus it's safe to cache
+    // the output for this block forever on a per-URL basis.
+    return array('cache' => array('max_age' => \Drupal\Core\Cache\Cache::PERMANENT));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getRequiredCacheContexts() {
+    // The "System 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');
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMainBlock.php b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMainBlock.php
index a64951a..666b515 100644
--- a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMainBlock.php
+++ b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMainBlock.php
@@ -28,4 +28,26 @@ public function build() {
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, array &$form_state) {
+    $form = parent::buildConfigurationForm($form, $form_state);
+
+    // The main content block is never cacheable, because it may be dynamic.
+    $form['cache']['#disabled'] = TRUE;
+    $form['cache']['#description'] = t('This block is never cacheable, it is not configurable.');
+    $form['cache']['max_age']['#value'] = 0;
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    // The main content block is never cacheable, because it may be dynamic.
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php
index 8298193..b6d1316 100644
--- a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php
+++ b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php
@@ -7,8 +7,8 @@
 
 namespace Drupal\system\Plugin\Block;
 
+use Drupal\Component\Utility\NestedArray;
 use Drupal\block\BlockBase;
-use Drupal\Core\Session\AccountInterface;
 
 /**
  * Provides a generic Menu block.
@@ -30,4 +30,50 @@ public function build() {
     return menu_tree($menu);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    // Modify the default max age for menu blocks: modifications made to menus,
+    // menu links and menu blocks will automatically invalidate corresponding
+    // cache tags, therefore allowing us to cache menu blocks forever. This is
+    // only not the case if there are user-specific or dynamic alterations (e.g.
+    // hook_node_access()), but in that:
+    // 1) it is possible to set a different max age for individual blocks, since
+    //    this is just the default value.
+    // 2) modules can modify caching by implementing hook_block_view_alter()
+    return array('cache' => array('max_age' => \Drupal\Core\Cache\Cache::PERMANENT));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheKeys() {
+    // Return the required cache contexts, merged with the user-configured cache
+    // contexts, if any.
+    return array_merge($this->getRequiredCacheContexts(), parent::getCacheKeys());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    // Even when the menu block renders to the empty string for a user, we want
+    // the cache tag for this menu to be set: whenever the menu is changed, this
+    // menu block must also be re-rendered for that user, because maybe a menu
+    // link that is accessible for that user has been added.
+    $tags = array('menu' => array($this->getDerivativeId()));
+    return NestedArray::mergeDeep(parent::getCacheTags(), $tags);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getRequiredCacheContexts() {
+    // Menu blocks must be cached per URL and per role: the "active" menu link
+    // may differ per URL and different roles may have access to different menu
+    // links.
+    return array('cache_context.url', 'cache_context.user.roles');
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemPoweredByBlock.php b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemPoweredByBlock.php
index 742af7b..b0878f8 100644
--- a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemPoweredByBlock.php
+++ b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemPoweredByBlock.php
@@ -8,6 +8,7 @@
 namespace Drupal\system\Plugin\Block;
 
 use Drupal\block\BlockBase;
+use Drupal\Core\Cache\Cache;
 
 /**
  * Provides a 'Powered by Drupal' block.
@@ -26,4 +27,34 @@ public function build() {
     return array('#markup' => '<span>' . t('Powered by <a href="@poweredby">Drupal</a>', array('@poweredby' => 'http://drupal.org')) . '</span>');
   }
 
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, array &$form_state) {
+    $form = parent::buildConfigurationForm($form, $form_state);
+
+    // The 'Powered by Drupal' block is permanently cacheable, because its
+    // contents can never change.
+    $form['cache']['#disabled'] = TRUE;
+    $form['cache']['max_age']['#value'] = Cache::PERMANENT;
+    $form['cache']['#description'] = t('This block is always cached forever, it is not configurable.');
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return Cache::PERMANENT;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return TRUE;
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Plugin/Derivative/SystemMenuBlock.php b/core/modules/system/lib/Drupal/system/Plugin/Derivative/SystemMenuBlock.php
index 372de9b..72a4c49 100644
--- a/core/modules/system/lib/Drupal/system/Plugin/Derivative/SystemMenuBlock.php
+++ b/core/modules/system/lib/Drupal/system/Plugin/Derivative/SystemMenuBlock.php
@@ -52,7 +52,6 @@ public function getDerivativeDefinitions($base_plugin_definition) {
     foreach ($this->menuStorage->loadMultiple() as $menu => $entity) {
       $this->derivatives[$menu] = $base_plugin_definition;
       $this->derivatives[$menu]['admin_label'] = $entity->label();
-      $this->derivatives[$menu]['cache'] = DRUPAL_NO_CACHE;
     }
     return $this->derivatives;
   }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheTagsIntegrationTest.php b/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheTagsIntegrationTest.php
index b245c34..cc584a3 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheTagsIntegrationTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheTagsIntegrationTest.php
@@ -64,13 +64,35 @@ function testPageCacheTags() {
       'promote' => NODE_PROMOTED,
     ));
 
+    // Place a block, but only make it visible on full node page 2.
+    $block = $this->drupalPlaceBlock('views_block:comments_recent-block_1', array(
+      'visibility' => array(
+        'path' => array(
+          'visibility' => BLOCK_VISIBILITY_LISTED,
+          'pages' => 'node/' . $node_2->id(),
+        ),
+      )
+    ));
+
     // Full node page 1.
     $this->verifyPageCacheTags('node/' . $node_1->id(), array(
       'content:1',
+      'block_view:1',
+      'block:bartik_content',
+      'block:bartik_tools',
+      'block:bartik_login',
+      'block:bartik_footer',
+      'block:bartik_powered',
+      'block_plugin:system_main_block',
+      'block_plugin:system_menu_block__tools',
+      'block_plugin:user_login_block',
+      'block_plugin:system_menu_block__footer',
+      'block_plugin:system_powered_by_block',
       'node_view:1',
       'node:' . $node_1->id(),
       'user:' . $author_1->id(),
       'filter_format:basic_html',
+      'menu:tools',
       'menu:footer',
       'menu:main',
     ));
@@ -78,10 +100,24 @@ function testPageCacheTags() {
     // Full node page 2.
     $this->verifyPageCacheTags('node/' . $node_2->id(), array(
       'content:1',
+      'block_view:1',
+      'block:bartik_content',
+      'block:bartik_tools',
+      'block:bartik_login',
+      'block:' . $block->id(),
+      'block:bartik_footer',
+      'block:bartik_powered',
+      'block_plugin:system_main_block',
+      'block_plugin:system_menu_block__tools',
+      'block_plugin:user_login_block',
+      'block_plugin:views_block__comments_recent-block_1',
+      'block_plugin:system_menu_block__footer',
+      'block_plugin:system_powered_by_block',
       'node_view:1',
       'node:' . $node_2->id(),
       'user:' . $author_2->id(),
       'filter_format:full_html',
+      'menu:tools',
       'menu:footer',
       'menu:main',
     ));
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityViewBuilderTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityViewBuilderTest.php
index 2d0369b..0379aa7 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityViewBuilderTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityViewBuilderTest.php
@@ -144,7 +144,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', 'granularity', 'bin') , 'A view mode with render cache enabled has the correct output (cache tags, keys, granularity and bin).');
+    $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).');
 
     // 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/modules/user/lib/Drupal/user/Cache/UserCacheContext.php b/core/modules/user/lib/Drupal/user/Cache/UserCacheContext.php
new file mode 100644
index 0000000..d9d880e
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Cache/UserCacheContext.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Cache\UserCacheContext.
+ */
+
+namespace Drupal\user\Cache;
+
+use Drupal\Core\Cache\CacheContextInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the UserCacheContext service, for "per user" caching.
+ */
+class UserCacheContext implements CacheContextInterface {
+
+  /**
+   * Constructs a new UserCacheContext service.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $user
+   *   The current user.
+   */
+  public function __construct(AccountInterface $user) {
+    $this->user = $user;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLabel() {
+    return t('User');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext() {
+    return "u." . $this->user->id();
+  }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/Cache/UserRolesCacheContext.php b/core/modules/user/lib/Drupal/user/Cache/UserRolesCacheContext.php
new file mode 100644
index 0000000..0f7975d
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Cache/UserRolesCacheContext.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Cache\UserRolesCacheContext.
+ */
+
+namespace Drupal\user\Cache;
+
+use Drupal\Core\Cache\CacheContextInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the UserRolesCacheContext service, for "per role" caching.
+ */
+class UserRolesCacheContext implements CacheContextInterface {
+
+  /**
+   * Constructs a new UserRolesCacheContext service.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $user
+   *   The current user.
+   */
+  public function __construct(AccountInterface $user) {
+    $this->user = $user;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLabel() {
+    return t("User's roles");
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext() {
+    return 'r.' . implode(',', $this->user->getRoles());
+  }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/EventSubscriber/UserLoginBlockConfigChangeSubscriber.php b/core/modules/user/lib/Drupal/user/EventSubscriber/UserLoginBlockConfigChangeSubscriber.php
new file mode 100644
index 0000000..d307dee
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/EventSubscriber/UserLoginBlockConfigChangeSubscriber.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\EventSubscriber\UserLoginBlockConfigChangeSubscriber.
+ */
+
+namespace Drupal\user\EventSubscriber;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Config\ConfigCrudEvent;
+use Drupal\Core\Config\ConfigEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Invalidates the cache tag for the "User login" block when necessary.
+ */
+class UserLoginBlockConfigChangeSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Reacts to config event.
+   *
+   * @param \Drupal\Core\Config\ConfigCrudEvent $event
+   *   The configuration event.
+   */
+  public function onConfigSave(ConfigCrudEvent $event) {
+    if ($event->getConfig()->getName() === 'user.settings' && $event->isChanged('register')) {
+      Cache::invalidateTags(array('block_plugin' => array('user_login_block')));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  static function getSubscribedEvents() {
+    $events[ConfigEvents::SAVE][] = array('onConfigSave', 0);
+    return $events;
+  }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/Plugin/Block/UserLoginBlock.php b/core/modules/user/lib/Drupal/user/Plugin/Block/UserLoginBlock.php
index c087889..19266ab 100644
--- a/core/modules/user/lib/Drupal/user/Plugin/Block/UserLoginBlock.php
+++ b/core/modules/user/lib/Drupal/user/Plugin/Block/UserLoginBlock.php
@@ -64,4 +64,13 @@ public function build() {
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    // This is a block containing a simple form without state, which is safe to
+    // cache forever.
+    return array('cache' => array('max_age' => \Drupal\Core\Cache\Cache::PERMANENT));
+  }
+
 }
diff --git a/core/modules/user/user.services.yml b/core/modules/user/user.services.yml
index 36d1e79..45af7f1 100644
--- a/core/modules/user/user.services.yml
+++ b/core/modules/user/user.services.yml
@@ -15,6 +15,16 @@ services:
     class: Drupal\user\Access\LoginStatusCheck
     tags:
       - { name: access_check, applies_to: _user_is_logged_in }
+  cache_context.user:
+    class: Drupal\user\Cache\UserCacheContext
+    arguments: ['@current_user']
+    tags:
+      - { name: cache.context}
+  cache_context.user.roles:
+    class: Drupal\user\Cache\UserRolesCacheContext
+    arguments: ['@current_user']
+    tags:
+      - { name: cache.context}
   user.data:
     class: Drupal\user\UserData
     arguments: ['@database']
@@ -25,6 +35,10 @@ services:
     class: Drupal\user\EventSubscriber\MaintenanceModeSubscriber
     tags:
       - { name: event_subscriber }
+  user_login_block_config_change_subscriber:
+    class: Drupal\user\EventSubscriber\UserLoginBlockConfigChangeSubscriber
+    tags:
+      - { name: event_subscriber }
   theme.negotiator.admin_theme:
     class: Drupal\user\Theme\AdminNegotiator
     arguments: ['@current_user', '@config.factory', '@entity.manager']
diff --git a/core/modules/views/lib/Drupal/views/Plugin/Block/ViewsBlock.php b/core/modules/views/lib/Drupal/views/Plugin/Block/ViewsBlock.php
index 482c25f..2648919 100644
--- a/core/modules/views/lib/Drupal/views/Plugin/Block/ViewsBlock.php
+++ b/core/modules/views/lib/Drupal/views/Plugin/Block/ViewsBlock.php
@@ -29,13 +29,11 @@ public function build() {
     $this->view->display_handler->preBlockBuild($this);
 
     if ($output = $this->view->executeDisplay($this->displayID)) {
-      // Set the label to the title configured in the view.
-      if (empty($this->configuration['views_label'])) {
-        $this->configuration['label'] = Xss::filterAdmin($this->view->getTitle());
-      }
-      else {
-        $this->configuration['label'] = $this->configuration['views_label'];
+      // Override the label to the dynamic title configured in the view.
+      if (empty($this->configuration['views_label']) && $this->view->getTitle()) {
+        $output['#title'] = Xss::filterAdmin($this->view->getTitle());
       }
+
       // Before returning the block output, convert it to a renderable array
       // with contextual links.
       $this->addContextualLinks($output);
@@ -48,6 +46,20 @@ public function build() {
   /**
    * {@inheritdoc}
    */
+  public function getConfiguration() {
+    $configuration = parent::getConfiguration();
+
+    // Set the label to the static title configured in the view.
+    if (!empty($configuration['views_label'])) {
+      $configuration['label'] = $configuration['views_label'];
+    }
+
+    return $configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function defaultConfiguration() {
     $settings = parent::defaultConfiguration();
 
diff --git a/core/modules/views/lib/Drupal/views/Plugin/Block/ViewsBlockBase.php b/core/modules/views/lib/Drupal/views/Plugin/Block/ViewsBlockBase.php
index 56f3c86..3c228d1 100644
--- a/core/modules/views/lib/Drupal/views/Plugin/Block/ViewsBlockBase.php
+++ b/core/modules/views/lib/Drupal/views/Plugin/Block/ViewsBlockBase.php
@@ -99,10 +99,7 @@ public function access(AccountInterface $account) {
    * {@inheritdoc}
    */
   public function defaultConfiguration() {
-    $settings = array();
-    $settings['views_label'] = '';
-
-    return $settings;
+    return array('views_label' => '');
   }
 
   /**
diff --git a/core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsBlock.php b/core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsBlock.php
index a7fb041..9541124 100644
--- a/core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsBlock.php
+++ b/core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsBlock.php
@@ -102,7 +102,6 @@ public function getDerivativeDefinitions($base_plugin_definition) {
           $this->derivatives[$delta] = array(
             'category' => $display->getOption('block_category'),
             'admin_label' => $desc,
-            'cache' => $display->getCacheType()
           );
           $this->derivatives[$delta] += $base_plugin_definition;
         }
diff --git a/core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsExposedFilterBlock.php b/core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsExposedFilterBlock.php
index 0423bb5..29439a6 100644
--- a/core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsExposedFilterBlock.php
+++ b/core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsExposedFilterBlock.php
@@ -93,7 +93,6 @@ public function getDerivativeDefinitions($base_plugin_definition) {
             $desc = t('Exposed form: @view-@display_id', array('@view' => $view->id(), '@display_id' => $display->display['id']));
             $this->derivatives[$delta] = array(
               'admin_label' => $desc,
-              'cache' => DRUPAL_NO_CACHE,
             );
             $this->derivatives[$delta] += $base_plugin_definition;
           }
diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php
index bc99b02..b18fff3 100644
--- a/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php
+++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php
@@ -2383,7 +2383,6 @@ public function getSpecialBlocks() {
 
       $blocks[$delta] = array(
         'info' => $desc,
-        'cache' => DRUPAL_NO_CACHE,
       );
     }
 
diff --git a/core/modules/views/tests/Drupal/views/Tests/Plugin/Block/ViewsBlockTest.php b/core/modules/views/tests/Drupal/views/Tests/Plugin/Block/ViewsBlockTest.php
index 15d4d6e..d0a4d37 100644
--- a/core/modules/views/tests/Drupal/views/Tests/Plugin/Block/ViewsBlockTest.php
+++ b/core/modules/views/tests/Drupal/views/Tests/Plugin/Block/ViewsBlockTest.php
@@ -15,9 +15,6 @@
 if (!defined('BLOCK_LABEL_VISIBLE')) {
   define('BLOCK_LABEL_VISIBLE', 'visible');
 }
-if (!defined('DRUPAL_NO_CACHE')) {
-  define('DRUPAL_NO_CACHE', -1);
-}
 
 /**
  * Tests the views block plugin.
diff --git a/core/profiles/minimal/config/block.block.stark_admin.yml b/core/profiles/minimal/config/block.block.stark_admin.yml
index f3c1735..a2a51d1 100644
--- a/core/profiles/minimal/config/block.block.stark_admin.yml
+++ b/core/profiles/minimal/config/block.block.stark_admin.yml
@@ -9,7 +9,6 @@ settings:
   label: Administration
   module: system
   label_display: visible
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/profiles/minimal/config/block.block.stark_login.yml b/core/profiles/minimal/config/block.block.stark_login.yml
index 4ca8266..a4a17cc 100644
--- a/core/profiles/minimal/config/block.block.stark_login.yml
+++ b/core/profiles/minimal/config/block.block.stark_login.yml
@@ -9,7 +9,6 @@ settings:
   label: 'User login'
   module: user
   label_display: visible
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/profiles/minimal/config/block.block.stark_tools.yml b/core/profiles/minimal/config/block.block.stark_tools.yml
index a452643..648c266 100644
--- a/core/profiles/minimal/config/block.block.stark_tools.yml
+++ b/core/profiles/minimal/config/block.block.stark_tools.yml
@@ -9,7 +9,6 @@ settings:
   label: Tools
   module: system
   label_display: visible
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/profiles/standard/config/block.block.bartik_breadcrumbs.yml b/core/profiles/standard/config/block.block.bartik_breadcrumbs.yml
index 3465368..a525a35 100644
--- a/core/profiles/standard/config/block.block.bartik_breadcrumbs.yml
+++ b/core/profiles/standard/config/block.block.bartik_breadcrumbs.yml
@@ -9,7 +9,6 @@ settings:
   label: Breadcrumbs
   module: system
   label_display: '0'
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/profiles/standard/config/block.block.bartik_content.yml b/core/profiles/standard/config/block.block.bartik_content.yml
index 8146a54..537bcae 100644
--- a/core/profiles/standard/config/block.block.bartik_content.yml
+++ b/core/profiles/standard/config/block.block.bartik_content.yml
@@ -9,7 +9,6 @@ settings:
   label: 'Main page content'
   module: system
   label_display: '0'
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/profiles/standard/config/block.block.bartik_footer.yml b/core/profiles/standard/config/block.block.bartik_footer.yml
index 76f9508..4f4db57 100644
--- a/core/profiles/standard/config/block.block.bartik_footer.yml
+++ b/core/profiles/standard/config/block.block.bartik_footer.yml
@@ -9,7 +9,6 @@ settings:
   label: 'Footer menu'
   module: system
   label_display: visible
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/profiles/standard/config/block.block.bartik_help.yml b/core/profiles/standard/config/block.block.bartik_help.yml
index 5806db7..33ccc5a 100644
--- a/core/profiles/standard/config/block.block.bartik_help.yml
+++ b/core/profiles/standard/config/block.block.bartik_help.yml
@@ -9,7 +9,6 @@ settings:
   label: 'System Help'
   module: system
   label_display: '0'
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/profiles/standard/config/block.block.bartik_login.yml b/core/profiles/standard/config/block.block.bartik_login.yml
index 7fc17cb..1f96baa 100644
--- a/core/profiles/standard/config/block.block.bartik_login.yml
+++ b/core/profiles/standard/config/block.block.bartik_login.yml
@@ -9,7 +9,6 @@ settings:
   label: 'User login'
   module: user
   label_display: visible
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/profiles/standard/config/block.block.bartik_powered.yml b/core/profiles/standard/config/block.block.bartik_powered.yml
index 0e3ebab..65f24ad 100644
--- a/core/profiles/standard/config/block.block.bartik_powered.yml
+++ b/core/profiles/standard/config/block.block.bartik_powered.yml
@@ -9,7 +9,6 @@ settings:
   label: 'Powered by Drupal'
   module: system
   label_display: '0'
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/profiles/standard/config/block.block.bartik_search.yml b/core/profiles/standard/config/block.block.bartik_search.yml
index f68adbc..46279d7 100644
--- a/core/profiles/standard/config/block.block.bartik_search.yml
+++ b/core/profiles/standard/config/block.block.bartik_search.yml
@@ -9,7 +9,6 @@ settings:
   label: Search
   module: search
   label_display: visible
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/profiles/standard/config/block.block.bartik_tools.yml b/core/profiles/standard/config/block.block.bartik_tools.yml
index 257990d..d845957 100644
--- a/core/profiles/standard/config/block.block.bartik_tools.yml
+++ b/core/profiles/standard/config/block.block.bartik_tools.yml
@@ -9,7 +9,6 @@ settings:
   label: Tools
   module: system
   label_display: visible
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/profiles/standard/config/block.block.seven_breadcrumbs.yml b/core/profiles/standard/config/block.block.seven_breadcrumbs.yml
index b56f633..1a6318c 100644
--- a/core/profiles/standard/config/block.block.seven_breadcrumbs.yml
+++ b/core/profiles/standard/config/block.block.seven_breadcrumbs.yml
@@ -9,7 +9,6 @@ settings:
   label: Breadcrumbs
   module: system
   label_display: '0'
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/profiles/standard/config/block.block.seven_content.yml b/core/profiles/standard/config/block.block.seven_content.yml
index 23a6568..4f60d5e 100644
--- a/core/profiles/standard/config/block.block.seven_content.yml
+++ b/core/profiles/standard/config/block.block.seven_content.yml
@@ -9,7 +9,6 @@ settings:
   label: 'Main page content'
   module: system
   label_display: '0'
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/profiles/standard/config/block.block.seven_help.yml b/core/profiles/standard/config/block.block.seven_help.yml
index 77918e5..dcc87b7 100644
--- a/core/profiles/standard/config/block.block.seven_help.yml
+++ b/core/profiles/standard/config/block.block.seven_help.yml
@@ -9,7 +9,6 @@ settings:
   label: 'System Help'
   module: system
   label_display: '0'
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/profiles/standard/config/block.block.seven_login.yml b/core/profiles/standard/config/block.block.seven_login.yml
index 239bd36..f2602cc 100644
--- a/core/profiles/standard/config/block.block.seven_login.yml
+++ b/core/profiles/standard/config/block.block.seven_login.yml
@@ -9,7 +9,6 @@ settings:
   label: 'User login'
   module: user
   label_display: visible
-  cache: -1
 visibility:
   path:
     visibility: 0
diff --git a/core/tests/Drupal/Tests/Core/Cache/CacheContextsTest.php b/core/tests/Drupal/Tests/Core/Cache/CacheContextsTest.php
new file mode 100644
index 0000000..6e5864d
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Cache/CacheContextsTest.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Cache\CacheContextsTest.
+ */
+
+namespace Drupal\Tests\Core\Cache;
+
+use Drupal\Core\Cache\CacheContexts;
+use Drupal\Core\Cache\CacheContextInterface;
+
+/**
+ * Fake cache context class.
+ */
+class FooCacheContext implements CacheContextInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLabel() {
+    return 'Foo';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext() {
+    return 'bar';
+  }
+
+}
+
+/**
+ * Tests the CacheContexts service.
+ *
+ * @group Cache
+ *
+ * @see \Drupal\Core\Cache\CacheContexts
+ */
+class CacheContextsTest extends \PHPUnit_Framework_TestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'CacheContext test',
+      'description' => 'Tests cache contexts.',
+      'group' => 'Cache',
+    );
+  }
+
+  public function testContextPlaceholdersAreReplaced() {
+    $container = $this->getMockContainer();
+    $container->expects($this->once())
+              ->method("get")
+              ->with("cache_context.foo")
+              ->will($this->returnValue(new FooCacheContext()));
+
+    $cache_contexts = new CacheContexts($container, $this->getContextsFixture());
+
+    $new_keys = $cache_contexts->convertTokensToKeys(
+      array("non-cache-context", "cache_context.foo")
+    );
+
+    $expected = array("non-cache-context", "bar");
+    $this->assertEquals($expected, $new_keys);
+  }
+
+  public function testAvailableContextStrings() {
+    $cache_contexts = new CacheContexts($this->getMockContainer(), $this->getContextsFixture());
+    $contexts = $cache_contexts->getAll();
+    $this->assertEquals(array("cache_context.foo"), $contexts);
+  }
+
+  public function testAvailableContextLabels() {
+    $container = $this->getMockContainer();
+    $container->expects($this->once())
+              ->method("get")
+              ->with("cache_context.foo")
+              ->will($this->returnValue(new FooCacheContext()));
+
+    $cache_contexts = new CacheContexts($container, $this->getContextsFixture());
+    $labels = $cache_contexts->getLabels();
+    $expected = array("cache_context.foo" => "Foo");
+    $this->assertEquals($expected, $labels);
+  }
+
+  protected function getContextsFixture() {
+    return array('cache_context.foo');
+  }
+
+  protected function getMockContainer() {
+    return $this->getMockBuilder('Drupal\Core\DependencyInjection\Container')
+                ->disableOriginalConstructor()
+                ->getMock();
+  }
+}
