 core/core.services.yml                             |   7 +-
 core/lib/Drupal.php                                |  24 ++-
 core/lib/Drupal/Core/Access/RouteProcessorCsrf.php |   9 +-
 .../Core/Menu/DefaultMenuLinkTreeManipulators.php  |  31 ++--
 core/lib/Drupal/Core/Menu/MenuActiveTrail.php      |  57 +++++++-
 core/lib/Drupal/Core/Menu/MenuLinkBase.php         |  29 +++-
 core/lib/Drupal/Core/Menu/MenuLinkInterface.php    |  17 +--
 core/lib/Drupal/Core/Menu/MenuLinkTree.php         | 136 ++++++++---------
 core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php  |   6 +-
 .../OutboundPathProcessorInterface.php             |   8 +-
 .../Core/PathProcessor/PathProcessorAlias.php      |   3 +-
 .../Core/PathProcessor/PathProcessorFront.php      |   5 +-
 .../Core/PathProcessor/PathProcessorManager.php    |   5 +-
 core/lib/Drupal/Core/Render/Element/Link.php       |  10 +-
 .../OutboundRouteProcessorInterface.php            |   5 +-
 .../Core/RouteProcessor/RouteProcessorCurrent.php  |   6 +-
 .../Core/RouteProcessor/RouteProcessorManager.php  |   5 +-
 core/lib/Drupal/Core/Routing/NullGenerator.php     |   5 +-
 core/lib/Drupal/Core/Routing/UrlGenerator.php      |  49 +++++--
 .../Drupal/Core/Routing/UrlGeneratorInterface.php  |  23 ++-
 core/lib/Drupal/Core/Template/TwigExtension.php    |  49 +++----
 core/lib/Drupal/Core/Url.php                       |  16 +-
 core/lib/Drupal/Core/Utility/LinkGenerator.php     |  18 ++-
 .../Drupal/Core/Utility/LinkGeneratorInterface.php |  23 ++-
 .../Drupal/Core/Utility/UnroutedUrlAssembler.php   |  25 +++-
 .../Core/Utility/UnroutedUrlAssemblerInterface.php |  12 +-
 core/modules/block/src/BlockViewBuilder.php        |  15 +-
 .../modules/help_test/src/SuperNovaGenerator.php   |   4 +-
 .../src/HttpKernel/PathProcessorLanguage.php       |   5 +-
 .../LanguageNegotiationSession.php                 |   6 +-
 .../LanguageNegotiation/LanguageNegotiationUrl.php |   9 +-
 .../tests/src/Unit/LanguageNegotiationUrlTest.php  | 141 +++++++++++++++++-
 .../MenuLinkContentCacheabilityBubblingTest.php    | 162 +++++++++++++++++++++
 .../outbound_processing_test.info.yml              |   4 +
 .../outbound_processing_test.routing.yml           |   5 +
 core/modules/menu_ui/src/MenuForm.php              |   2 +-
 .../src/Tests/PageCacheTagsIntegrationTest.php     |   1 -
 .../system/src/Plugin/Block/SystemMenuBlock.php    |   8 +-
 .../system/src/Tests/Block/SystemMenuBlockTest.php |   6 +
 core/modules/system/src/Tests/Common/UrlTest.php   |  30 +++-
 .../src/Tests/RouteProcessor/RouteNoneTest.php     |  29 +++-
 .../RouteProcessorCurrentIntegrationTest.php       |  29 +++-
 .../system/src/Tests/Theme/EngineTwigTest.php      |  15 +-
 .../src/TwigThemeTestController.php                |   2 +-
 .../url_alter_test/src/PathProcessorTest.php       |   6 +-
 core/modules/toolbar/toolbar.module                |   5 +
 .../src/Plugin/views/field/FieldPluginBase.php     |   2 +-
 .../Tests/Core/Access/RouteProcessorCsrfTest.php   |  22 ++-
 .../Drupal/Tests/Core/Entity/EntityUrlTest.php     |   2 +
 .../RedirectResponseSubscriberTest.php             |  10 +-
 .../Drupal/Tests/Core/Form/FormSubmitterTest.php   |   4 +-
 .../Menu/DefaultMenuLinkTreeManipulatorsTest.php   |  64 ++++----
 .../Drupal/Tests/Core/Menu/MenuActiveTrailTest.php |   4 +-
 .../Core/PathProcessor/PathProcessorAliasTest.php  |  29 +++-
 .../RouteProcessor/RouteProcessorManagerTest.php   |   7 +-
 .../Drupal/Tests/Core/Routing/UrlGeneratorTest.php | 111 ++++++++++----
 core/tests/Drupal/Tests/Core/UrlTest.php           |  29 ++--
 .../Tests/Core/Utility/LinkGeneratorTest.php       |  35 +++++
 .../Core/Utility/UnroutedUrlAssemblerTest.php      |  18 ++-
 59 files changed, 1064 insertions(+), 340 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index 5959c44..4ad86ea 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -487,13 +487,15 @@ services:
     arguments: ['@menu.tree_storage', '@menu_link.static.overrides', '@module_handler']
   menu.link_tree:
     class: Drupal\Core\Menu\MenuLinkTree
-    arguments: ['@menu.tree_storage', '@plugin.manager.menu.link', '@router.route_provider', '@menu.active_trail', '@controller_resolver', '@cache.menu', '@current_route_match']
+    arguments: ['@menu.tree_storage', '@plugin.manager.menu.link', '@router.route_provider', '@menu.active_trail', '@controller_resolver']
   menu.default_tree_manipulators:
     class: Drupal\Core\Menu\DefaultMenuLinkTreeManipulators
     arguments: ['@access_manager', '@current_user', '@entity.query']
   menu.active_trail:
     class: Drupal\Core\Menu\MenuActiveTrail
-    arguments: ['@plugin.manager.menu.link', '@current_route_match']
+    arguments: ['@plugin.manager.menu.link', '@current_route_match', '@cache.menu', '@lock']
+    tags:
+      - { name: needs_destruction }
   menu.parent_form_selector:
     class: Drupal\Core\Menu\MenuParentFormSelector
     arguments: ['@menu.link_tree', '@entity.manager', '@string_translation']
@@ -1315,7 +1317,6 @@ services:
       - { name: twig.extension, priority: 100 }
     calls:
       - [setGenerators, ['@url_generator']]
-      - [setLinkGenerator, ['@link_generator']]
   # @todo Figure out what to do about debugging functions.
   # @see http://drupal.org/node/1804998
   twig.extension.debug:
diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php
index f949669..e580628 100644
--- a/core/lib/Drupal.php
+++ b/core/lib/Drupal.php
@@ -504,17 +504,23 @@ public static function urlGenerator() {
    *   (optional) An associative array of parameter names and values.
    * @param array $options
    *   (optional) An associative array of additional options.
+   * @param bool $collect_cacheability_metadata
+   *   (optional) Defaults to FALSE. When TRUE, both the generated URL and its
+   *   associated cacheability metadata are returned.
    *
-   * @return string
+   * @return string|array
    *   The generated URL for the given route.
+   *   When $collect_cacheability_metadata is set to TRUE, an array is returned,
+   *   with the first value being the URL, and the second value being the
+   *   cacheability metadata.
    *
    * @see \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute()
    * @see \Drupal\Core\Url
    * @see \Drupal\Core\Url::fromRoute()
    * @see \Drupal\Core\Url::fromUri()
    */
-  public static function url($route_name, $route_parameters = array(), $options = array()) {
-    return static::getContainer()->get('url_generator')->generateFromRoute($route_name, $route_parameters, $options);
+  public static function url($route_name, $route_parameters = array(), $options = array(), $collect_cacheability_metadata = FALSE) {
+    return static::getContainer()->get('url_generator')->generateFromRoute($route_name, $route_parameters, $options, $collect_cacheability_metadata);
   }
 
   /**
@@ -537,15 +543,21 @@ public static function linkGenerator() {
    *   The link text for the anchor tag.
    * @param \Drupal\Core\Url $url
    *   The URL object used for the link.
+   * @param bool $collect_cacheability_metadata
+   *   (optional) Defaults to FALSE. When TRUE, both the generated URL and its
+   *   associated cacheability metadata are returned.
    *
-   * @return string
+   * @return string|array
    *   An HTML string containing a link to the given route and parameters.
+   *   When $collect_cacheability_metadata is set to TRUE, an array is returned,
+   *   with the first value being the HTML string, and the second value being
+   *   the cacheability metadata.
    *
    * @see \Drupal\Core\Utility\LinkGeneratorInterface::generate()
    * @see \Drupal\Core\Url
    */
-  public static function l($text, Url $url) {
-    return static::getContainer()->get('link_generator')->generate($text, $url);
+  public static function l($text, Url $url, $collect_cacheability_metadata = FALSE) {
+    return static::getContainer()->get('link_generator')->generate($text, $url, $collect_cacheability_metadata);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php
index 49873fa..f00053c 100644
--- a/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php
+++ b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php
@@ -7,9 +7,8 @@
 
 namespace Drupal\Core\Access;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface;
-use Drupal\Core\Access\CsrfTokenGenerator;
-use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\Routing\Route;
 
 /**
@@ -37,7 +36,7 @@ function __construct(CsrfTokenGenerator $csrf_token) {
   /**
    * {@inheritdoc}
    */
-  public function processOutbound($route_name, Route $route, array &$parameters) {
+  public function processOutbound($route_name, Route $route, array &$parameters, CacheableMetadata $cacheable_metadata = NULL) {
     if ($route->hasRequirement('_csrf_token')) {
       $path = ltrim($route->getPath(), '/');
       // Replace the path parameters with values from the parameters array.
@@ -47,6 +46,10 @@ public function processOutbound($route_name, Route $route, array &$parameters) {
       // Adding this to the parameters means it will get merged into the query
       // string when the route is compiled.
       $parameters['token'] = $this->csrfToken->get($path);
+      if ($cacheable_metadata) {
+        // Tokens are per user and per session, so not cacheable.
+        $cacheable_metadata->setCacheMaxAge(0);
+      }
     }
   }
 
diff --git a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php
index d22f538..a545996 100644
--- a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php
+++ b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core\Menu;
 
 use Drupal\Core\Access\AccessManagerInterface;
+use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Entity\Query\QueryFactory;
 use Drupal\Core\Session\AccountInterface;
 
@@ -63,12 +64,13 @@ public function __construct(AccessManagerInterface $access_manager, AccountInter
   /**
    * Performs access checks of a menu tree.
    *
-   * Removes menu links from the given menu tree whose links are inaccessible
-   * for the current user, sets the 'access' property to TRUE on tree elements
-   * that are accessible for the current user.
+   * Sets the 'access' property to AccessResultInterface objects on menu link
+   * tree elements. Descends into subtrees if the root of the subtree is
+   * accessible.
    *
-   * Makes the resulting menu tree impossible to render cache, unless render
-   * caching per user is acceptable.
+   * This is compatible with render caching, because of cache context bubbling:
+   * conditionally defined cache contexts (i.e. subtrees that are only
+   * accessible to some users) will bubble just
    *
    * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
    *   The menu link tree to manipulate.
@@ -83,7 +85,7 @@ public function checkAccess(array $tree) {
       if (!isset($element->access)) {
         $tree[$key]->access = $this->menuLinkCheckAccess($element->link);
       }
-      if ($tree[$key]->access) {
+      if ($tree[$key]->access->isAllowed()) {
         if ($tree[$key]->subtree) {
           $tree[$key]->subtree = $this->checkAccess($tree[$key]->subtree);
         }
@@ -120,17 +122,19 @@ public function checkNodeAccess(array $tree) {
       // query rewrite as well as not checking for the node status. The
       // 'view own unpublished nodes' permission is ignored to not require cache
       // entries per user.
+      $access_result = AccessResult::allowed()->cachePerPermissions();
       if ($this->account->hasPermission('bypass node access')) {
         $query->accessCheck(FALSE);
       }
       else {
+        $access_result->addCacheContexts(['user.node_grants:view']);
         $query->condition('status', NODE_PUBLISHED);
       }
 
       $nids = $query->execute();
       foreach ($nids as $nid) {
         foreach ($node_links[$nid] as $key => $link) {
-          $node_links[$nid][$key]->access = TRUE;
+          $node_links[$nid][$key]->access = $access_result;
         }
       }
     }
@@ -155,7 +159,7 @@ protected function collectNodeLinks(array &$tree, array &$node_links) {
         $nid = $element->link->getRouteParameters()['node'];
         $node_links[$nid][$key] = $element;
         // Deny access by default. checkNodeAccess() will re-add it.
-        $element->access = FALSE;
+        $element->access = AccessResult::neutral();
       }
       if ($element->hasChildren) {
         $this->collectNodeLinks($element->subtree, $node_links);
@@ -169,24 +173,23 @@ protected function collectNodeLinks(array &$tree, array &$node_links) {
    * @param \Drupal\Core\Menu\MenuLinkInterface $instance
    *   The menu link instance.
    *
-   * @return bool
-   *   TRUE if the current user can access the link, FALSE otherwise.
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
    */
   protected function menuLinkCheckAccess(MenuLinkInterface $instance) {
     if ($this->account->hasPermission('link to any page')) {
-      return TRUE;
+      return AccessResult::allowed()->cachePerPermissions();
     }
     // Use the definition here since that's a lot faster than creating a Url
     // object that we don't need.
     $definition = $instance->getPluginDefinition();
     // 'url' should only be populated for external links.
     if (!empty($definition['url']) && empty($definition['route_name'])) {
-      $access = TRUE;
+      return AccessResult::allowed();
     }
     else {
-      $access = $this->accessManager->checkNamedRoute($definition['route_name'], $definition['route_parameters'], $this->account);
+      return $this->accessManager->checkNamedRoute($definition['route_name'], $definition['route_parameters'], $this->account, TRUE);
     }
-    return $access;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Menu/MenuActiveTrail.php b/core/lib/Drupal/Core/Menu/MenuActiveTrail.php
index 6247b29..2acb972 100644
--- a/core/lib/Drupal/Core/Menu/MenuActiveTrail.php
+++ b/core/lib/Drupal/Core/Menu/MenuActiveTrail.php
@@ -7,6 +7,9 @@
 
 namespace Drupal\Core\Menu;
 
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\CacheCollector;
+use Drupal\Core\Lock\LockBackendInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
@@ -15,7 +18,7 @@
  * It uses the current route name and route parameters to compare with the ones
  * of the menu links.
  */
-class MenuActiveTrail implements MenuActiveTrailInterface {
+class MenuActiveTrail extends CacheCollector implements MenuActiveTrailInterface {
 
   /**
    * The menu link plugin manager.
@@ -38,16 +41,66 @@ class MenuActiveTrail implements MenuActiveTrailInterface {
    *   The menu link plugin manager.
    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
    *   A route match object for finding the active link.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The cache backend.
+   * @param \Drupal\Core\Lock\LockBackendInterface $lock
+   *   The lock backend.
    */
-  public function __construct(MenuLinkManagerInterface $menu_link_manager, RouteMatchInterface $route_match) {
+  public function __construct(MenuLinkManagerInterface $menu_link_manager, RouteMatchInterface $route_match, CacheBackendInterface $cache, LockBackendInterface $lock) {
+    parent::__construct(NULL, $cache, $lock);
     $this->menuLinkManager = $menu_link_manager;
     $this->routeMatch = $route_match;
   }
 
   /**
    * {@inheritdoc}
+   *
+   * @see ::getActiveTrailIds()
+   */
+  protected function getCid() {
+    if (!isset($this->cid)) {
+      $route_parameters = $this->routeMatch->getRawParameters()->all();
+      ksort($route_parameters);
+      return 'active-trail:route:' . $this->routeMatch->getRouteName() . ':route_parameters:' . serialize($route_parameters);
+    }
+
+    return $this->cid;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @see ::getActiveTrailIds()
+   */
+  protected function resolveCacheMiss($menu_name) {
+    $this->storage[$menu_name] = $this->doGetActiveTrailIds($menu_name);
+    $this->tags[] = 'config:system.menu.' . $menu_name;
+    $this->persist($menu_name);
+
+    return $this->storage[$menu_name];
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * This implementation caches all active trail IDs per route match for *all*
+   * menus whose active trails are calculated on that page. This ensures 1 cache
+   * get for all active trails per page load, rather than N.
+   *
+   * It uses the cache collector pattern to do this.
+   *
+   * @see ::get()
+   * @see \Drupal\Core\Cache\CacheCollectorInterface
+   * @see \Drupal\Core\Cache\CacheCollector
    */
   public function getActiveTrailIds($menu_name) {
+    return $this->get($menu_name);
+  }
+
+  /**
+   * Helper method for ::getActiveTrailIds().
+   */
+  protected function doGetActiveTrailIds($menu_name) {
     // Parent ids; used both as key and value to ensure uniqueness.
     // We always want all the top-level links with parent == ''.
     $active_trail = array('' => '');
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkBase.php b/core/lib/Drupal/Core/Menu/MenuLinkBase.php
index 6ee3ef4..70774df 100644
--- a/core/lib/Drupal/Core/Menu/MenuLinkBase.php
+++ b/core/lib/Drupal/Core/Menu/MenuLinkBase.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Plugin\Exception\PluginException;
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Plugin\PluginBase;
 use Drupal\Core\Url;
 
@@ -110,13 +111,6 @@ public function getMetaData() {
   /**
    * {@inheritdoc}
    */
-  public function isCacheable() {
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
   public function getRouteName() {
     return isset($this->pluginDefinition['route_name']) ? $this->pluginDefinition['route_name'] : '';
   }
@@ -179,4 +173,25 @@ public function deleteLink() {
     throw new PluginException(SafeMarkup::format('Menu link plugin with ID @id does not support deletion', array('@id' => $this->getPluginId())));
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return Cache::PERMANENT;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return [];
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkInterface.php
index ffef255..da5ca39 100644
--- a/core/lib/Drupal/Core/Menu/MenuLinkInterface.php
+++ b/core/lib/Drupal/Core/Menu/MenuLinkInterface.php
@@ -9,11 +9,12 @@
 
 use Drupal\Component\Plugin\PluginInspectionInterface;
 use Drupal\Component\Plugin\DerivativeInspectionInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 
 /**
  * Defines an interface for classes providing a type of menu link.
  */
-interface MenuLinkInterface extends PluginInspectionInterface, DerivativeInspectionInterface {
+interface MenuLinkInterface extends PluginInspectionInterface, DerivativeInspectionInterface, CacheableDependencyInterface {
 
   /**
    * Returns the weight of the menu link.
@@ -151,20 +152,6 @@ public function getOptions();
   public function getMetaData();
 
   /**
-   * Returns whether the rendered link can be cached.
-   *
-   * The plugin class may make some or all of the data used in the Url object
-   * and build array dynamic. For example, it could include the current user
-   * name in the title, the current time in the description, or a destination
-   * query string. In addition the route parameters may be dynamic so an access
-   * check should be performed for each user.
-   *
-   * @return bool
-   *   TRUE if the link can be cached, FALSE otherwise.
-   */
-  public function isCacheable();
-
-  /**
    * Updates the definition values for a menu link.
    *
    * Depending on the implementation details of the class, not all definition
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTree.php b/core/lib/Drupal/Core/Menu/MenuLinkTree.php
index 40dfbbe..afd0de5 100644
--- a/core/lib/Drupal/Core/Menu/MenuLinkTree.php
+++ b/core/lib/Drupal/Core/Menu/MenuLinkTree.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core\Menu;
 
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Controller\ControllerResolverInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -48,30 +49,6 @@ class MenuLinkTree implements MenuLinkTreeInterface {
   protected $controllerResolver;
 
   /**
-   * The cache backend.
-   *
-   * @var \Drupal\Core\Cache\CacheBackendInterface
-   */
-  protected $cache;
-
-  /**
-   * The current route match.
-   *
-   * @var \Drupal\Core\Routing\RouteMatchInterface
-   */
-  protected $routeMatch;
-
-  /**
-   * Stores the cached current route parameters by menu and current route match.
-   *
-   * @todo Remove this non-static caching in
-   *   https://www.drupal.org/node/1805054.
-   *
-   * @var \Drupal\Core\Menu\MenuTreeParameters[]
-   */
-  protected $cachedCurrentRouteParameters;
-
-  /**
    * Constructs a \Drupal\Core\Menu\MenuLinkTree object.
    *
    * @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage
@@ -84,53 +61,31 @@ class MenuLinkTree implements MenuLinkTreeInterface {
    *   The active menu trail service.
    * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
    *   The controller resolver.
-   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
-   *   The cache backend.
-   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
-   *   The current route match.
    */
-  public function __construct(MenuTreeStorageInterface $tree_storage, MenuLinkManagerInterface $menu_link_manager, RouteProviderInterface $route_provider, MenuActiveTrailInterface $menu_active_trail, ControllerResolverInterface $controller_resolver, CacheBackendInterface $cache, RouteMatchInterface $route_match) {
+  public function __construct(MenuTreeStorageInterface $tree_storage, MenuLinkManagerInterface $menu_link_manager, RouteProviderInterface $route_provider, MenuActiveTrailInterface $menu_active_trail, ControllerResolverInterface $controller_resolver) {
     $this->treeStorage = $tree_storage;
     $this->menuLinkManager = $menu_link_manager;
     $this->routeProvider = $route_provider;
     $this->menuActiveTrail = $menu_active_trail;
     $this->controllerResolver = $controller_resolver;
-    // @todo Remove these two in https://www.drupal.org/node/1805054.
-    $this->cache = $cache;
-    $this->routeMatch = $route_match;
   }
 
   /**
    * {@inheritdoc}
    */
   public function getCurrentRouteMenuTreeParameters($menu_name) {
-    $route_parameters = $this->routeMatch->getRawParameters()->all();
-    ksort($route_parameters);
-    $cid = 'current-route-parameters:' . $menu_name . ':route:' . $this->routeMatch->getRouteName() . ':route_parameters:' . serialize($route_parameters);
+    $active_trail = $this->menuActiveTrail->getActiveTrailIds($menu_name);
 
-    if (!isset($this->cachedCurrentRouteParameters[$cid])) {
-      $cache = $this->cache->get($cid);
-      if ($cache && $cache->data) {
-        $parameters = $cache->data;
-      }
-      else {
-        $active_trail = $this->menuActiveTrail->getActiveTrailIds($menu_name);
+    $parameters = new MenuTreeParameters();
+    $parameters->setActiveTrail($active_trail)
+      // We want links in the active trail to be expanded.
+      ->addExpandedParents($active_trail)
+      // We marked the links in the active trail to be expanded, but we also
+      // want their descendants that have the "expanded" flag enabled to be
+      // expanded.
+      ->addExpandedParents($this->treeStorage->getExpanded($menu_name, $active_trail));
 
-        $parameters = new MenuTreeParameters();
-        $parameters->setActiveTrail($active_trail)
-          // We want links in the active trail to be expanded.
-          ->addExpandedParents($active_trail)
-          // We marked the links in the active trail to be expanded, but we also
-          // want their descendants that have the "expanded" flag enabled to be
-          // expanded.
-          ->addExpandedParents($this->treeStorage->getExpanded($menu_name, $active_trail));
-
-        $this->cache->set($cid, $parameters, CacheBackendInterface::CACHE_PERMANENT, array('config:system.menu.' . $menu_name));
-      }
-      $this->cachedCurrentRouteParameters[$menu_name] = $parameters;
-    }
-
-    return $this->cachedCurrentRouteParameters[$menu_name];
+    return $parameters;
   }
 
   /**
@@ -200,8 +155,27 @@ public function transform(array $tree, array $manipulators) {
    */
   public function build(array $tree, $level = 0) {
     $items = array();
+    $tree_access_cacheability = new CacheableMetadata();
+    $tree_link_cacheability = new CacheableMetadata();
 
     foreach ($tree as $data) {
+      // Gather the access cacheability of every item in the menu link tree,
+      // including inaccessible items. This allows us to render cache the menu
+      // tree, yet still automatically the rendered menu by the same cache
+      // contexts that the access results vary by.
+      $tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($data->access));
+
+      // Gather the cacheability of every item in the menu link tree. Some links
+      // may be dynamic: they may have a dynamic text (e.g. a "Hi, <user>" link
+      // text, which would vary by 'user' cache context), or a dynamic route
+      // name or route parameters.
+      $tree_link_cacheability = $tree_link_cacheability->merge(CacheableMetadata::createFromObject($data->link));
+
+      // Only render accessible links.
+      if (!$data->access->isAllowed()) {
+        continue;
+      }
+
       $class = ['menu-item'];
       /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
       $link = $data->link;
@@ -222,7 +196,8 @@ public function build(array $tree, $level = 0) {
         $class[] = 'menu-item--active-trail';
       }
 
-      // Allow menu-specific theme overrides.
+      // Note: links are rendered in the menu.html.twig template; and they
+      // automatically bubble their associated cacheability metadata.
       $element = array();
       $element['attributes'] = new Attribute();
       $element['attributes']['class'] = $class;
@@ -238,25 +213,36 @@ public function build(array $tree, $level = 0) {
       $items[$link->getPluginId()] = $element;
     }
 
-    if (!$items) {
-      return array();
-    }
-    elseif ($level == 0) {
-      $build = array();
-      // Make sure drupal_render() does not re-order the links.
-      $build['#sorted'] = TRUE;
-      // Get the menu name from the last link.
-      $menu_name = $link->getMenuName();
-      // Add the theme wrapper for outer markup.
-      // Allow menu-specific theme overrides.
-      $build['#theme'] = 'menu__' . strtr($menu_name, '-', '_');
-      $build['#items'] = $items;
-      // Set cache tag.
-      $build['#cache']['tags'][] = 'config:system.menu.' . $menu_name;
-      return $build;
+    // Non-root levels: return $items.
+    if ($level > 0) {
+      return $items;
     }
+    // Root level: return a render array.
     else {
-      return $items;
+      $build = [];
+
+      // Apply the tree-wide gathered access cacheability metadata and link
+      // cacheability metadata to the render array. This ensures that the
+      // rendered menu is varied by the cache contexts that the access results
+      // and (dynamic) links depended upon, and invalidated by the cache tags
+      // that may change the values of the access results and links.
+      $tree_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($tree_link_cacheability));
+      $tree_cacheability->applyTo($build);
+
+      if ($items) {
+        // Make sure drupal_render() does not re-order the links.
+        $build['#sorted'] = TRUE;
+        // Get the menu name from the last link.
+        $menu_name = $link->getMenuName();
+        // Add the theme wrapper for outer markup.
+        // Allow menu-specific theme overrides.
+        $build['#theme'] = 'menu__' . strtr($menu_name, '-', '_');
+        $build['#items'] = $items;
+        // Set cache tag.
+        $build['#cache']['tags'][] = 'config:system.menu.' . $menu_name;
+      }
+
+      return $build;
     }
   }
 
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php b/core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php
index a52b6c2..7358e4f 100644
--- a/core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php
+++ b/core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php
@@ -70,10 +70,10 @@ class MenuLinkTreeElement {
   /**
    * Whether this link is accessible by the current user.
    *
-   * If the value is NULL the access was not determined yet, if Boolean it was
-   * determined already.
+   * If the value is NULL the access was not determined yet, if an access result
+   * object, it was determined already.
    *
-   * @var bool|NULL
+   * @var \Drupal\Core\Access\AccessResultInterface|NULL
    */
   public $access;
 
diff --git a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php
index 9e69001..a1fb57a 100644
--- a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php
+++ b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php
@@ -7,8 +7,8 @@
 
 namespace Drupal\Core\PathProcessor;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\Routing\Route;
 
 /**
  * Defines an interface for classes that process the outbound path.
@@ -20,17 +20,17 @@
    *
    * @param string $path
    *   The path to process.
-   *
    * @param array $options
    *   An array of options such as would be passed to the generator's
    *   generateFromPath() method.
-   *
    * @param \Symfony\Component\HttpFoundation\Request $request
    *   The HttpRequest object representing the current request.
+   * @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata
+   *   (optional) Object to collect path processors' cacheability.
    *
    * @return
    *   The processed path.
    */
-  public function processOutbound($path, &$options = array(), Request $request = NULL);
+  public function processOutbound($path, &$options = array(), Request $request = NULL, CacheableMetadata $cacheable_metadata = NULL);
 
 }
diff --git a/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php
index 67226ab..206e566 100644
--- a/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php
+++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\PathProcessor;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Path\AliasManagerInterface;
 use Symfony\Component\HttpFoundation\Request;
 
@@ -43,7 +44,7 @@ public function processInbound($path, Request $request) {
   /**
    * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound().
    */
-  public function processOutbound($path, &$options = array(), Request $request = NULL) {
+  public function processOutbound($path, &$options = array(), Request $request = NULL, CacheableMetadata $cacheable_metadata = NULL) {
     if (empty($options['alias'])) {
       $langcode = isset($options['language']) ? $options['language']->getId() : NULL;
       $path = $this->aliasManager->getAliasByPath($path, $langcode);
diff --git a/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php
index e8ac5d9..9b6048f7 100644
--- a/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php
+++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php
@@ -7,11 +7,14 @@
 
 namespace Drupal\Core\PathProcessor;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Processes the inbound path by resolving it to the front page if empty.
+ *
+ * @todo - remove ::processOutbound() when we remove UrlGenerator::fromPath().
  */
 class PathProcessorFront implements InboundPathProcessorInterface, OutboundPathProcessorInterface {
 
@@ -45,7 +48,7 @@ public function processInbound($path, Request $request) {
   /**
    * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound().
    */
-  public function processOutbound($path, &$options = array(), Request $request = NULL) {
+  public function processOutbound($path, &$options = array(), Request $request = NULL, CacheableMetadata $cacheable_metadata = NULL) {
     // The special path '<front>' links to the default front page.
     if ($path == '<front>') {
       $path = '';
diff --git a/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php
index 3c8b9be..1bedad8 100644
--- a/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php
+++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\PathProcessor;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Symfony\Component\HttpFoundation\Request;
 
 /**
@@ -107,10 +108,10 @@ public function addOutbound(OutboundPathProcessorInterface $processor, $priority
   /**
    * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound().
    */
-  public function processOutbound($path, &$options = array(), Request $request = NULL) {
+  public function processOutbound($path, &$options = array(), Request $request = NULL, CacheableMetadata $cacheable_metadata = NULL) {
     $processors = $this->getOutbound();
     foreach ($processors as $processor) {
-      $path = $processor->processOutbound($path, $options, $request);
+      $path = $processor->processOutbound($path, $options, $request, $cacheable_metadata);
     }
     return $path;
   }
diff --git a/core/lib/Drupal/Core/Render/Element/Link.php b/core/lib/Drupal/Core/Render/Element/Link.php
index c6714be..c315c78 100644
--- a/core/lib/Drupal/Core/Render/Element/Link.php
+++ b/core/lib/Drupal/Core/Render/Element/Link.php
@@ -9,6 +9,8 @@
 
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\Html as HtmlUtility;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Url as CoreUrl;
 
 /**
  * Provides a link render element.
@@ -75,9 +77,13 @@ public static function preRenderLink($element) {
       $element = static::preRenderAjaxForm($element);
     }
 
-    if (!empty($element['#url'])) {
+    if (!empty($element['#url']) && $element['#url'] instanceof CoreUrl) {
       $options = NestedArray::mergeDeep($element['#url']->getOptions(), $element['#options']);
-      $element['#markup'] = \Drupal::l($element['#title'], $element['#url']->setOptions($options));
+      /** @var \Drupal\Core\Utility\LinkGenerator $link_generator */
+      $link_generator = \Drupal::service('link_generator');
+      list($element['#markup'], $cacheability) = $link_generator->generate($element['#title'], $element['#url']->setOptions($options), TRUE);
+      $cacheability->merge(CacheableMetadata::createFromRenderArray($element))
+        ->applyTo($element);
     }
     return $element;
   }
diff --git a/core/lib/Drupal/Core/RouteProcessor/OutboundRouteProcessorInterface.php b/core/lib/Drupal/Core/RouteProcessor/OutboundRouteProcessorInterface.php
index 145c8ee..c3349ea 100644
--- a/core/lib/Drupal/Core/RouteProcessor/OutboundRouteProcessorInterface.php
+++ b/core/lib/Drupal/Core/RouteProcessor/OutboundRouteProcessorInterface.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\RouteProcessor;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Symfony\Component\Routing\Route;
 
 /**
@@ -24,10 +25,12 @@
    * @param array $parameters
    *   An array of parameters to be passed to the route compiler. Passed by
    *   reference.
+   * @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata
+   *   (optional) Object to collect route processors' cacheability.
    *
    * @return
    *   The processed path.
    */
-  public function processOutbound($route_name, Route $route, array &$parameters);
+  public function processOutbound($route_name, Route $route, array &$parameters, CacheableMetadata $cacheable_metadata = NULL);
 
 }
diff --git a/core/lib/Drupal/Core/RouteProcessor/RouteProcessorCurrent.php b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorCurrent.php
index 1a00b21..17a56b5 100644
--- a/core/lib/Drupal/Core/RouteProcessor/RouteProcessorCurrent.php
+++ b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorCurrent.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\RouteProcessor;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Symfony\Component\Routing\Route;
 
@@ -35,7 +36,7 @@ public function __construct(RouteMatchInterface $route_match) {
   /**
    * {@inheritdoc}
    */
-  public function processOutbound($route_name, Route $route, array &$parameters) {
+  public function processOutbound($route_name, Route $route, array &$parameters, CacheableMetadata $cacheable_metadata = NULL) {
     if ($route_name === '<current>') {
       if ($current_route = $this->routeMatch->getRouteObject()) {
         $route->setPath($current_route->getPath());
@@ -43,6 +44,9 @@ public function processOutbound($route_name, Route $route, array &$parameters) {
         $route->setOptions($current_route->getOptions());
         $route->setDefaults($current_route->getDefaults());
         $parameters = array_merge($parameters, $this->routeMatch->getRawParameters()->all());
+        if ($cacheable_metadata) {
+          $cacheable_metadata->addCacheContexts(['route']);
+        }
       }
       else {
         // If we have no current route match available, point to the frontpage.
diff --git a/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php
index 049fc7c..589af5a 100644
--- a/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php
+++ b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\RouteProcessor;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Symfony\Component\Routing\Route;
 
 /**
@@ -50,10 +51,10 @@ public function addOutbound(OutboundRouteProcessorInterface $processor, $priorit
   /**
    * {@inheritdoc}
    */
-  public function processOutbound($route_name, Route $route, array &$parameters) {
+  public function processOutbound($route_name, Route $route, array &$parameters, CacheableMetadata $cacheable_metadata = NULL) {
     $processors = $this->getOutbound();
     foreach ($processors as $processor) {
-      $processor->processOutbound($route_name, $route, $parameters);
+      $processor->processOutbound($route_name, $route, $parameters, $cacheable_metadata);
     }
   }
 
diff --git a/core/lib/Drupal/Core/Routing/NullGenerator.php b/core/lib/Drupal/Core/Routing/NullGenerator.php
index 74bb41e..986523f 100644
--- a/core/lib/Drupal/Core/Routing/NullGenerator.php
+++ b/core/lib/Drupal/Core/Routing/NullGenerator.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Routing;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\Routing\RequestContext as SymfonyRequestContext;
 use Symfony\Component\Routing\Exception\RouteNotFoundException;
@@ -50,7 +51,7 @@ protected function getRoute($name) {
   /**
    * {@inheritdoc}
    */
-  protected function processRoute($name, Route $route, array &$parameters) {
+  protected function processRoute($name, Route $route, array &$parameters, CacheableMetadata $cacheable_metadata = NULL) {
   }
 
   /**
@@ -75,7 +76,7 @@ public function getContext() {
   /**
    * Overrides Drupal\Core\Routing\UrlGenerator::processPath().
    */
-  protected function processPath($path, &$options = array()) {
+  protected function processPath($path, &$options = array(), CacheableMetadata $cacheable_metadata = NULL) {
     return $path;
   }
 }
diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php
index 97530d7..c05d831 100644
--- a/core/lib/Drupal/Core/Routing/UrlGenerator.php
+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Routing;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\Routing\RequestContext as SymfonyRequestContext;
 use Symfony\Component\Routing\Route as SymfonyRoute;
@@ -275,11 +276,13 @@ public function generate($name, $parameters = array(), $absolute = FALSE) {
   /**
    * {@inheritdoc}
    */
-  public function generateFromRoute($name, $parameters = array(), $options = array()) {
+  public function generateFromRoute($name, $parameters = array(), $options = array(), $collect_cacheability_metadata = FALSE) {
+    $cacheable_metadata = ($collect_cacheability_metadata) ? new CacheableMetadata() : NULL;
+
     $options += array('prefix' => '');
     $route = $this->getRoute($name);
     $name = $this->getRouteDebugMessage($name);
-    $this->processRoute($name, $route, $parameters);
+    $this->processRoute($name, $route, $parameters, $cacheable_metadata);
 
     $query_params = [];
     // Symfony adds any parameters that are not path slugs as query strings.
@@ -288,7 +291,7 @@ public function generateFromRoute($name, $parameters = array(), $options = array
     }
 
     $path = $this->getInternalPathFromRoute($name, $route, $parameters, $query_params);
-    $path = $this->processPath($path, $options);
+    $path = $this->processPath($path, $options, $cacheable_metadata);
 
     if (!empty($options['prefix'])) {
       $path = ltrim($path, '/');
@@ -316,7 +319,8 @@ public function generateFromRoute($name, $parameters = array(), $options = array
         }
       }
 
-      return $base_url . $path . $fragment;
+      $url = $base_url . $path . $fragment;
+      return $collect_cacheability_metadata ? [$url, $cacheable_metadata] : $url;
     }
 
     $base_url = $this->context->getBaseUrl();
@@ -325,10 +329,11 @@ public function generateFromRoute($name, $parameters = array(), $options = array
     if (!$absolute || !$host = $this->context->getHost()) {
 
       if ($route->getOption('_only_fragment')) {
-        return $fragment;
+        return $collect_cacheability_metadata ? [$fragment, $cacheable_metadata] : $fragment;
       }
 
-      return $base_url . $path . $fragment;
+      $url = $base_url . $path . $fragment;
+      return $collect_cacheability_metadata ? [$url, $cacheable_metadata] : $url;
     }
 
     // Prepare an absolute URL by getting the correct scheme, host and port from
@@ -349,13 +354,19 @@ public function generateFromRoute($name, $parameters = array(), $options = array
     } elseif ('https' === $scheme && 443 != $this->context->getHttpsPort()) {
       $port = ':' . $this->context->getHttpsPort();
     }
-    return $scheme . '://' . $host . $port . $base_url . $path . $fragment;
+    if ($cacheable_metadata) {
+      $cacheable_metadata->addCacheContexts(['url.host']);
+    }
+    $url = $scheme . '://' . $host . $port . $base_url . $path . $fragment;
+    return $collect_cacheability_metadata ? [$url, $cacheable_metadata] : $url;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function generateFromPath($path = NULL, $options = array()) {
+  public function generateFromPath($path = NULL, $options = array(), $collect_cacheability_metadata = FALSE) {
+    $cacheable_metadata = ($collect_cacheability_metadata) ? new CacheableMetadata() : NULL;
+
     $request = $this->requestStack->getCurrentRequest();
     $current_base_path = $request->getBasePath() . '/';
     $current_base_url = $request->getSchemeAndHttpHost() . $current_base_path;
@@ -418,10 +429,11 @@ public function generateFromPath($path = NULL, $options = array()) {
         }
       }
       // Reassemble.
-      return $path . $options['fragment'];
+      $url = $path . $options['fragment'];
+      return $collect_cacheability_metadata ? [$url, $cacheable_metadata] : $url;
     }
     else {
-      $path = ltrim($this->processPath($path, $options), '/');
+      $path = ltrim($this->processPath($path, $options, $cacheable_metadata), '/');
     }
 
     if (!isset($options['script'])) {
@@ -449,15 +461,20 @@ public function generateFromPath($path = NULL, $options = array()) {
     $base = $options['absolute'] ? $options['base_url'] : $current_base_path;
     $prefix = empty($path) ? rtrim($options['prefix'], '/') : $options['prefix'];
 
+    if ($options['absolute'] && $cacheable_metadata) {
+      $cacheable_metadata->addCacheContexts(['url.host']);
+    }
+
     $path = str_replace('%2F', '/', rawurlencode($prefix . $path));
     $query = $options['query'] ? ('?' . UrlHelper::buildQuery($options['query'])) : '';
-    return $base . $options['script'] . $path . $query . $options['fragment'];
+    $url = $base . $options['script'] . $path . $query . $options['fragment'];
+    return $collect_cacheability_metadata ? [$url, $cacheable_metadata] : $url;
   }
 
   /**
    * Passes the path to a processor manager to allow alterations.
    */
-  protected function processPath($path, &$options = array()) {
+  protected function processPath($path, &$options = array(), CacheableMetadata $cacheable_metadata = NULL) {
     // Router-based paths may have a querystring on them.
     if ($query_pos = strpos($path, '?')) {
       // We don't need to do a strict check here because position 0 would mean we
@@ -469,7 +486,7 @@ protected function processPath($path, &$options = array()) {
       $actual_path = $path;
       $query_string = '';
     }
-    $path = '/' . $this->pathProcessor->processOutbound(trim($actual_path, '/'), $options, $this->requestStack->getCurrentRequest());
+    $path = '/' . $this->pathProcessor->processOutbound(trim($actual_path, '/'), $options, $this->requestStack->getCurrentRequest(), $cacheable_metadata);
     $path .= $query_string;
     return $path;
   }
@@ -483,9 +500,11 @@ protected function processPath($path, &$options = array()) {
    *   The route object to process.
    * @param array $parameters
    *   An array of parameters to be passed to the route compiler.
+   * @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata
+   *   (optional) Object to collect route processors' cacheability.
    */
-  protected function processRoute($name, SymfonyRoute $route, array &$parameters) {
-    $this->routeProcessor->processOutbound($name, $route, $parameters);
+  protected function processRoute($name, SymfonyRoute $route, array &$parameters, CacheableMetadata $cacheable_metadata = NULL) {
+    $this->routeProcessor->processOutbound($name, $route, $parameters, $cacheable_metadata);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php
index de28531..39f9fff 100644
--- a/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php
+++ b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Routing;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Symfony\Cmf\Component\Routing\VersatileGeneratorInterface;
 
 /**
@@ -70,9 +71,15 @@
    *     set if _url() is invoked by Drupal\Core\Entity\Entity::uri().
    *   - 'entity': The entity object (such as a node) for which the URL is being
    *     generated. Only set if _url() is invoked by Drupal\Core\Entity\Entity::uri().
+   * @param bool $collect_cacheability_metadata
+   *   (optional) Defaults to FALSE. When TRUE, both the generated URL and its
+   *   associated cacheability metadata are returned.
    *
-   * @return
+   * @return string|array
    *   A string containing a URL to the given path.
+   *   When $collect_cacheability_metadata is set to TRUE, an array is returned,
+   *   with the first value being the URL, and the second value being the
+   *   cacheability metadata.
    *
    * @throws \Drupal\Core\Routing\GeneratorNotInitializedException.
    *
@@ -86,7 +93,7 @@
    * @see \Drupal\Core\Utility\UnroutedUrlAssembler
    * @see \Drupal\Core\Url
    */
-  public function generateFromPath($path = NULL, $options = array());
+  public function generateFromPath($path = NULL, $options = array(), $collect_cacheability_metadata = FALSE);
 
   /**
    * Gets the internal path (system path) of a route.
@@ -100,7 +107,7 @@ public function generateFromPath($path = NULL, $options = array());
    * @return string
    *  The internal Drupal path corresponding to the route.
    *
-   * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
+   * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 8.0.0
    *   System paths should not be used - use route names and parameters.
    */
   public function getPathFromRoute($name, $parameters = array());
@@ -136,9 +143,15 @@ public function getPathFromRoute($name, $parameters = array());
    *     modify the base URL when a language dependent URL requires so.
    *   - 'prefix': Only used internally, to modify the path when a language
    *     dependent URL requires so.
+   * @param bool $collect_cacheability_metadata
+   *   (optional) Defaults to FALSE. When TRUE, both the generated URL and its
+   *   associated cacheability metadata are returned.
    *
-   * @return string
+   * @return string|array
    *   The generated URL for the given route.
+   *   When $collect_cacheability_metadata is set to TRUE, an array is returned,
+   *   with the first value being the URL, and the second value being the
+   *   cacheability metadata.
    *
    * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException
    *   Thrown when the named route does not exist.
@@ -148,6 +161,6 @@ public function getPathFromRoute($name, $parameters = array());
    *   Thrown when a parameter value for a placeholder is not correct because it
    *   does not match the requirement.
    */
-  public function generateFromRoute($name, $parameters = array(), $options = array());
+  public function generateFromRoute($name, $parameters = array(), $options = array(), $collect_cacheability_metadata = FALSE);
 
 }
diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php
index bb29cd1..a8cb121 100644
--- a/core/lib/Drupal/Core/Template/TwigExtension.php
+++ b/core/lib/Drupal/Core/Template/TwigExtension.php
@@ -13,10 +13,10 @@
 namespace Drupal\Core\Template;
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Routing\UrlGeneratorInterface;
 use Drupal\Core\Url;
-use Drupal\Core\Utility\LinkGeneratorInterface;
 
 /**
  * A class providing Drupal Twig extensions.
@@ -35,13 +35,6 @@ class TwigExtension extends \Twig_Extension {
   protected $urlGenerator;
 
   /**
-   * The link generator.
-   *
-   * @var \Drupal\Core\Utility\LinkGeneratorInterface
-   */
-  protected $linkGenerator;
-
-  /**
    * The renderer.
    *
    * @var \Drupal\Core\Render\RendererInterface
@@ -72,19 +65,6 @@ public function setGenerators(UrlGeneratorInterface $url_generator) {
   }
 
   /**
-   * Sets the link generator.
-   *
-   * @param \Drupal\Core\Utility\LinkGeneratorInterface $link_generator
-   *   The link generator.
-   *
-   * @return $this
-   */
-  public function setLinkGenerator(LinkGeneratorInterface $link_generator) {
-    $this->linkGenerator = $link_generator;
-    return $this;
-  }
-
-  /**
    * {@inheritdoc}
    */
   public function getFunctions() {
@@ -201,8 +181,14 @@ public function getPath($name, $parameters = array(), $options = array()) {
    * @todo Add an option for scheme-relative URLs.
    */
   public function getUrl($name, $parameters = array(), $options = array()) {
+    // Generate URL.
     $options['absolute'] = TRUE;
-    return $this->urlGenerator->generateFromRoute($name, $parameters, $options);
+    list($url, $cacheability) = $this->urlGenerator->generateFromRoute($name, $parameters, $options, TRUE);
+
+    // Return as render array, so we can bubble the cacheability metadata.
+    $build = ['#markup' => $url];
+    $cacheability->applyTo($build);
+    return $build;
   }
 
   /**
@@ -220,8 +206,14 @@ public function getUrl($name, $parameters = array(), $options = array()) {
    * @deprecated in Drupal 8.0.x-dev and will be removed before Drupal 8.0.0.
    */
   public function getUrlFromPath($path, $options = array()) {
+    // Generate URL.
     $options['absolute'] = TRUE;
-    return $this->urlGenerator->generateFromPath($path, $options);
+    list($url, $cacheability) = $this->urlGenerator->generateFromPath($path, $options, TRUE);
+
+    // Return as render array, so we can bubble the cacheability metadata.
+    $build = ['#markup' => $url];
+    $cacheability->applyTo($build);
+    return $build;
   }
 
   /**
@@ -232,14 +224,19 @@ public function getUrlFromPath($path, $options = array()) {
    * @param \Drupal\Core\Url|string $url
    *   The URL object or string used for the link.
    *
-   * @return string
-   *   An HTML string containing a link to the given url.
+   * @return array
+   *   A render array representing a link to the given URL.
    */
   public function getLink($text, $url) {
     if (!$url instanceof Url) {
       $url = Url::fromUri($url);
     }
-    return $this->linkGenerator->generate($text, $url);
+    $build = [
+      '#type' => 'link',
+      '#title' => $text,
+      '#url' => $url,
+    ];
+    return $build;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php
index c6a5f05..367cdce 100644
--- a/core/lib/Drupal/Core/Url.php
+++ b/core/lib/Drupal/Core/Url.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Routing\UrlGeneratorInterface;
@@ -728,15 +729,22 @@ public function setAbsolute($absolute = TRUE) {
    * http://example.com/node/1 depending on the options array, plus any
    * specified query string or fragment.
    *
-   * @return string
+   * @param bool $collect_cacheability_metadata
+   *   (optional) Defaults to FALSE. When TRUE, both the generated URL and its
+   *   associated cacheability metadata are returned.
+   *
+   * @return string|array
    *   A string URL.
+   *   When $collect_cacheability_metadata is set to TRUE, an array is returned,
+   *   with the first value being the URL, and the second value being the
+   *   cacheability metadata.
    */
-  public function toString() {
+  public function toString($collect_cacheability_metadata = FALSE) {
     if ($this->unrouted) {
-      return $this->unroutedUrlAssembler()->assemble($this->getUri(), $this->getOptions());
+      return $this->unroutedUrlAssembler()->assemble($this->getUri(), $this->getOptions(), $collect_cacheability_metadata);
     }
 
-    return $this->urlGenerator()->generateFromRoute($this->getRouteName(), $this->getRouteParameters(), $this->getOptions());
+    return $this->urlGenerator()->generateFromRoute($this->getRouteName(), $this->getRouteParameters(), $this->getOptions(), $collect_cacheability_metadata);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php
index 8794836..a38dc4f 100644
--- a/core/lib/Drupal/Core/Utility/LinkGenerator.php
+++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Link;
 use Drupal\Core\Path\AliasManagerInterface;
@@ -51,8 +52,8 @@ public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerI
   /**
    * {@inheritdoc}
    */
-  public function generateFromLink(Link $link) {
-    return $this->generate($link->getText(), $link->getUrl());
+  public function generateFromLink(Link $link, $collect_cacheability_metadata = FALSE) {
+    return $this->generate($link->getText(), $link->getUrl(), $collect_cacheability_metadata);
   }
 
   /**
@@ -67,7 +68,7 @@ public function generateFromLink(Link $link) {
    *
    * @see system_page_attachments()
    */
-  public function generate($text, Url $url) {
+  public function generate($text, Url $url, $collect_cacheability_metadata = FALSE) {
     // Performance: avoid Url::toString() needing to retrieve the URL generator
     // service from the container.
     $url->setUrlGenerator($this->urlGenerator);
@@ -131,12 +132,19 @@ public function generate($text, Url $url) {
 
     // The result of the url generator is a plain-text URL. Because we are using
     // it here in an HTML argument context, we need to encode it properly.
-    $url = SafeMarkup::checkPlain($url->toString());
+    if (!$collect_cacheability_metadata) {
+      $url = SafeMarkup::checkPlain($url->toString($collect_cacheability_metadata));
+    }
+    else {
+      list($url, $cacheable_metadata) = $url->toString($collect_cacheability_metadata);
+      $url = SafeMarkup::checkPlain($url);
+    }
 
     // Make sure the link text is sanitized.
     $safe_text = SafeMarkup::escape($variables['text']);
 
-    return SafeMarkup::set('<a href="' . $url . '"' . $attributes . '>' . $safe_text . '</a>');
+    $result = SafeMarkup::set('<a href="' . $url . '"' . $attributes . '>' . $safe_text . '</a>');
+    return $collect_cacheability_metadata ? [$result, $cacheable_metadata] : $result;
   }
 
 }
diff --git a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
index 8d2d9fc..46eaeb8 100644
--- a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
+++ b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Utility;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Link;
 use Drupal\Core\Url;
 
@@ -60,9 +61,15 @@
    *     class will be applied to the link. It is important to use this
    *     sparingly since it is usually unnecessary and requires extra
    *     processing.
+   * @param bool $collect_cacheability_metadata
+   *   (optional) Defaults to FALSE. When TRUE, both the generated URL and its
+   *   associated cacheability metadata are returned.
    *
-   * @return string
+   * @return string|array
    *   An HTML string containing a link to the given route and parameters.
+   *   When $collect_cacheability_metadata is set to TRUE, an array is returned,
+   *   with the first value being the HTML string, and the second value being
+   *   the cacheability metadata.
    *
    * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException
    *   Thrown when the named route doesn't exist.
@@ -72,17 +79,23 @@
    *   Thrown when a parameter value for a placeholder is not correct because it
    *   does not match the requirement.
    */
-  public function generate($text, Url $url);
+  public function generate($text, Url $url, $collect_cacheability_metadata = FALSE);
 
   /**
    * Renders a link from a link object.
    *
    * @param \Drupal\Core\Link $link
    *   A link object to convert to a string.
+   * @param bool $collect_cacheability_metadata
+   *   (optional) Defaults to FALSE. When TRUE, both the generated URL and its
+   *   associated cacheability metadata are returned.
    *
-   * @return string
-   *   An HTML string containing a link to the given link.
+   * @return string|array
+   *   An HTML string containing a link to the given route and parameters.
+   *   When $collect_cacheability_metadata is set to TRUE, an array is returned,
+   *   with the first value being the HTML string, and the second value being
+   *   the cacheability metadata.
    */
-  public function generateFromLink(Link $link);
+  public function generateFromLink(Link $link, $collect_cacheability_metadata = FALSE);
 
 }
diff --git a/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php b/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
index efc966b..949df82 100644
--- a/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
+++ b/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
 use Symfony\Component\HttpFoundation\RequestStack;
@@ -57,16 +58,16 @@ public function __construct(RequestStack $request_stack, ConfigFactoryInterface
    * This is a helper function that calls buildExternalUrl() or buildLocalUrl()
    * based on a check of whether the path is a valid external URL.
    */
-  public function assemble($uri, array $options = []) {
+  public function assemble($uri, array $options = [], $collect_cacheability_metadata = FALSE) {
     // Note that UrlHelper::isExternal will return FALSE if the $uri has a
     // disallowed protocol.  This is later made safe since we always add at
     // least a leading slash.
     if (parse_url($uri, PHP_URL_SCHEME) === 'base') {
-      return $this->buildLocalUrl($uri, $options);
+      return $this->buildLocalUrl($uri, $options, $collect_cacheability_metadata);
     }
     elseif (UrlHelper::isExternal($uri)) {
       // UrlHelper::isExternal() only returns true for safe protocols.
-      return $this->buildExternalUrl($uri, $options);
+      return $this->buildExternalUrl($uri, $options, $collect_cacheability_metadata);
     }
     throw new \InvalidArgumentException(SafeMarkup::format('The URI "@uri" is invalid. You must use a valid URI scheme. Use base: for a path, e.g., to a Drupal file that needs the base path. Do not use this for internal paths controlled by Drupal.', ['@uri' => $uri]));
   }
@@ -74,7 +75,7 @@ public function assemble($uri, array $options = []) {
   /**
    * {@inheritdoc}
    */
-  protected function buildExternalUrl($uri, array $options = []) {
+  protected function buildExternalUrl($uri, array $options = [], $collect_cacheability_metadata = FALSE) {
     $this->addOptionDefaults($options);
     // Split off the fragment.
     if (strpos($uri, '#') !== FALSE) {
@@ -98,13 +99,15 @@ protected function buildExternalUrl($uri, array $options = []) {
       $uri .= (strpos($uri, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($options['query']);
     }
     // Reassemble.
-    return $uri . $options['fragment'];
+    $url = $uri . $options['fragment'];
+    return $collect_cacheability_metadata ? [$url, new CacheableMetadata()] : $url;
   }
 
   /**
    * {@inheritdoc}
    */
-  protected function buildLocalUrl($uri, array $options = []) {
+  protected function buildLocalUrl($uri, array $options = [], $collect_cacheability_metadata = FALSE) {
+    $cacheable_metadata = ($collect_cacheability_metadata) ? new CacheableMetadata() : NULL;
     $this->addOptionDefaults($options);
     $request = $this->requestStack->getCurrentRequest();
 
@@ -122,7 +125,9 @@ protected function buildLocalUrl($uri, array $options = []) {
     // alias overview form:
     // @see \Drupal\path\Controller\PathController::adminOverview().
     if (!empty($options['path_processing'])) {
-      $uri = $this->pathProcessor->processOutbound($uri, $options);
+      // Do not pass the request, since this is a special case and we do not
+      // want to include e.g. the request language in the processing.
+      $uri = $this->pathProcessor->processOutbound($uri, $options, NULL, $cacheable_metadata);
     }
 
     // Add any subdirectory where Drupal is installed.
@@ -143,6 +148,9 @@ protected function buildLocalUrl($uri, array $options = []) {
       else {
         $base = $current_base_url;
       }
+      if ($cacheable_metadata) {
+        $cacheable_metadata->addCacheContexts(['url.host']);
+      }
     }
     else {
       $base = $current_base_path;
@@ -152,7 +160,8 @@ protected function buildLocalUrl($uri, array $options = []) {
 
     $uri = str_replace('%2F', '/', rawurlencode($prefix . $uri));
     $query = $options['query'] ? ('?' . UrlHelper::buildQuery($options['query'])) : '';
-    return $base . $options['script'] . $uri . $query . $options['fragment'];
+    $url = $base . $options['script'] . $uri . $query . $options['fragment'];
+    return $collect_cacheability_metadata ? [$url, $cacheable_metadata] : $url;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Utility/UnroutedUrlAssemblerInterface.php b/core/lib/Drupal/Core/Utility/UnroutedUrlAssemblerInterface.php
index 868f0c1..47a98db 100644
--- a/core/lib/Drupal/Core/Utility/UnroutedUrlAssemblerInterface.php
+++ b/core/lib/Drupal/Core/Utility/UnroutedUrlAssemblerInterface.php
@@ -5,6 +5,7 @@
  */
 
 namespace Drupal\Core\Utility;
+use Drupal\Core\Cache\CacheableMetadata;
 
 /**
  * Provides a way to build external or non Drupal local domain URLs.
@@ -31,7 +32,6 @@
    *     then you can either URL encode the query keys and values yourself and
    *     include them in $uri, or use $options['query'] to let this method
    *     URL encode them.
-   *
    * @param array $options
    *   (optional) An associative array of additional options, with the following
    *   elements:
@@ -45,13 +45,19 @@
    *   - 'https': Whether this URL should point to a secure location. If not
    *     defined, the current scheme is used, so the user stays on HTTP or HTTPS
    *     respectively. TRUE enforces HTTPS and FALSE enforces HTTP.
+   * @param bool $collect_cacheability_metadata
+   *   (optional) Defaults to FALSE. When TRUE, both the generated URL and its
+   *   associated cacheability metadata are returned.
    *
-   * @return
+   * @return string|array
    *   A string containing a relative or absolute URL.
+   *   When $collect_cacheability_metadata is set to TRUE, an array is returned,
+   *   with the first value being the URL, and the second value being the
+   *   cacheability metadata.
    *
    * @throws \InvalidArgumentException
    *   Thrown when the passed in path has no scheme.
    */
-  public function assemble($uri, array $options = array());
+  public function assemble($uri, array $options = array(), $collect_cacheability_metadata = FALSE);
 
 }
diff --git a/core/modules/block/src/BlockViewBuilder.php b/core/modules/block/src/BlockViewBuilder.php
index 216dfa8..5b94d5c 100644
--- a/core/modules/block/src/BlockViewBuilder.php
+++ b/core/modules/block/src/BlockViewBuilder.php
@@ -9,9 +9,11 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityViewBuilder;
 use Drupal\Core\Entity\EntityViewBuilderInterface;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Render\Element;
 
 /**
  * Provides a Block view builder.
@@ -103,7 +105,7 @@ public function buildBlock($build) {
     // Remove the block entity from the render array, to ensure that blocks
     // can be rendered without the block config entity.
     unset($build['#block']);
-    if (!empty($content)) {
+    if (!empty($content) && !(count($content) === 1 && array_keys($content) === ['#cache'])) {
       // 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
@@ -122,6 +124,8 @@ public function buildBlock($build) {
       }
       $build['content'] = $content;
     }
+    // Either the block's content is completely empty, or it consists only of
+    // cacheability metadata.
     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
@@ -131,6 +135,15 @@ public function buildBlock($build) {
         '#markup' => '',
         '#cache' => $build['#cache'],
       );
+      // If $content is not empty, then it contains cacheability metadata, and
+      // we must merge it with the existing cacheability metadata. This allows
+      // blocks to be empty, yet still bubble cacheability metadata, to indicate
+      // *why* they are empty.
+      if (!empty($content)) {
+        CacheableMetadata::createFromRenderArray($build)
+          ->merge(CacheableMetadata::createFromRenderArray($content))
+          ->applyTo($build);
+      }
     }
     return $build;
    }
diff --git a/core/modules/help/tests/modules/help_test/src/SuperNovaGenerator.php b/core/modules/help/tests/modules/help_test/src/SuperNovaGenerator.php
index 9b33f94..2103efc 100644
--- a/core/modules/help/tests/modules/help_test/src/SuperNovaGenerator.php
+++ b/core/modules/help/tests/modules/help_test/src/SuperNovaGenerator.php
@@ -39,7 +39,7 @@ public function generate($name, $parameters = array(), $referenceType = self::AB
   /**
    * {@inheritdoc}
    */
-  public function generateFromPath($path = NULL, $options = array()) {
+  public function generateFromPath($path = NULL, $options = array(), $collect_cacheability_metadata = FALSE) {
     throw new \Exception();
   }
 
@@ -53,7 +53,7 @@ public function getPathFromRoute($name, $parameters = array()) {
   /**
    * {@inheritdoc}
    */
-  public function generateFromRoute($name, $parameters = array(), $options = array()) {
+  public function generateFromRoute($name, $parameters = array(), $options = array(), $collect_cacheability_metadata = FALSE) {
     throw new \Exception();
   }
 
diff --git a/core/modules/language/src/HttpKernel/PathProcessorLanguage.php b/core/modules/language/src/HttpKernel/PathProcessorLanguage.php
index f1402b5..6d9bb71 100644
--- a/core/modules/language/src/HttpKernel/PathProcessorLanguage.php
+++ b/core/modules/language/src/HttpKernel/PathProcessorLanguage.php
@@ -8,6 +8,7 @@
 namespace Drupal\language\HttpKernel;
 
 use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
 use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
@@ -94,7 +95,7 @@ public function processInbound($path, Request $request) {
   /**
    * {@inheritdoc}
    */
-  public function processOutbound($path, &$options = array(), Request $request = NULL) {
+  public function processOutbound($path, &$options = array(), Request $request = NULL, CacheableMetadata $cacheable_metadata = NULL) {
     if (!isset($this->multilingual)) {
       $this->multilingual = $this->languageManager->isMultilingual();
     }
@@ -105,7 +106,7 @@ public function processOutbound($path, &$options = array(), Request $request = N
         $this->initProcessors($scope);
       }
       foreach ($this->processors[$scope] as $instance) {
-        $path = $instance->processOutbound($path, $options, $request);
+        $path = $instance->processOutbound($path, $options, $request, $cacheable_metadata);
       }
       // No language dependent path allowed in this mode.
       if (empty($this->processors[$scope])) {
diff --git a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php
index a1547c0..9e5489c 100644
--- a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php
+++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\language\Plugin\LanguageNegotiation;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
 use Drupal\Core\Url;
@@ -85,7 +86,7 @@ public function persist(LanguageInterface $language) {
   /**
    * {@inheritdoc}
    */
-  public function processOutbound($path, &$options = array(), Request $request = NULL) {
+  public function processOutbound($path, &$options = array(), Request $request = NULL, CacheableMetadata $cacheable_metadata = NULL) {
     if ($request) {
       // The following values are not supposed to change during a single page
       // request processing.
@@ -114,6 +115,9 @@ public function processOutbound($path, &$options = array(), Request $request = N
         if (!isset($options['query'][$this->queryParam])) {
           $options['query'][$this->queryParam] = $this->queryValue;
         }
+        if ($cacheable_metadata) {
+          $cacheable_metadata->addCacheContexts(['languages:' . LanguageInterface::TYPE_URL]);
+        }
       }
     }
     return $path;
diff --git a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php
index b76b1b1..3315517 100644
--- a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php
+++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\language\Plugin\LanguageNegotiation;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
 use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
@@ -122,7 +123,7 @@ public function processInbound($path, Request $request) {
   /**
    * Implements Drupal\Core\PathProcessor\InboundPathProcessorInterface::processOutbound().
    */
-  public function processOutbound($path, &$options = array(), Request $request = NULL) {
+  public function processOutbound($path, &$options = array(), Request $request = NULL, CacheableMetadata $cacheable_metadata = NULL) {
     $url_scheme = 'http';
     $port = 80;
     if ($request) {
@@ -143,6 +144,9 @@ public function processOutbound($path, &$options = array(), Request $request = N
     if ($config['source'] == LanguageNegotiationUrl::CONFIG_PATH_PREFIX) {
       if (is_object($options['language']) && !empty($config['prefixes'][$options['language']->getId()])) {
         $options['prefix'] = $config['prefixes'][$options['language']->getId()] . '/';
+        if ($cacheable_metadata) {
+          $cacheable_metadata->addCacheContexts(['languages:' . LanguageInterface::TYPE_URL]);
+        }
       }
     }
     elseif ($config['source'] ==  LanguageNegotiationUrl::CONFIG_DOMAIN) {
@@ -180,6 +184,9 @@ public function processOutbound($path, &$options = array(), Request $request = N
 
         // Add Drupal's subfolder from the base_path if there is one.
         $options['base_url'] .= rtrim(base_path(), '/');
+        if ($cacheable_metadata) {
+          $cacheable_metadata->addCacheContexts(['languages:' . LanguageInterface::TYPE_URL, 'url.host']);
+        }
       }
     }
     return $path;
diff --git a/core/modules/language/tests/src/Unit/LanguageNegotiationUrlTest.php b/core/modules/language/tests/src/Unit/LanguageNegotiationUrlTest.php
index 7323c14..97fd2ab 100644
--- a/core/modules/language/tests/src/Unit/LanguageNegotiationUrlTest.php
+++ b/core/modules/language/tests/src/Unit/LanguageNegotiationUrlTest.php
@@ -5,11 +5,15 @@
  * Contains \Drupal\Tests\language\Unit\LanguageNegotiationUrlTest.
  */
 
-namespace Drupal\Tests\language\Unit;
+namespace Drupal\Tests\language\Unit {
 
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Session\UserSession;
 use Drupal\Tests\UnitTestCase;
 use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\HttpFoundation\Request;
 
 /**
@@ -39,14 +43,12 @@ protected function setUp() {
       'de' => $language_de,
       'en' => $language_en,
     );
+    $this->languages = $languages;
 
     // Create a language manager stub.
     $language_manager = $this->getMockBuilder('Drupal\language\ConfigurableLanguageManagerInterface')
       ->getMock();
     $language_manager->expects($this->any())
-      ->method('getCurrentLanguage')
-      ->will($this->returnValue($languages['en']));
-    $language_manager->expects($this->any())
       ->method('getLanguages')
       ->will($this->returnValue($languages));
     $this->languageManager = $language_manager;
@@ -54,14 +56,124 @@ protected function setUp() {
     // Create a user stub.
     $this->user = $this->getMockBuilder('Drupal\Core\Session\AccountInterface')
       ->getMock();
+
+    $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $container = new ContainerBuilder();
+    $container->set('cache_contexts_manager', $cache_contexts_manager);
+    \Drupal::setContainer($container);
   }
 
   /**
-   * Test domain language negotiation.
+   * Test path prefix language negotiation and outbound path processing.
+   *
+   * @dataProvider providerTestPathPrefix
+   */
+  public function testPathPrefix($prefix, $prefixes, $expected_langcode) {
+    $this->languageManager->expects($this->any())
+      ->method('getCurrentLanguage')
+      ->will($this->returnValue($this->languages[(in_array($expected_langcode, ['en', 'de'])) ? $expected_langcode : 'en']));
+
+    $config_data = [
+      'source' => LanguageNegotiationUrl::CONFIG_PATH_PREFIX,
+      'prefixes' => $prefixes,
+    ];
+
+    $config_object = $this->getMockBuilder('Drupal\Core\Config\Config')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $config_object->expects($this->any())
+      ->method('get')
+      ->with('url')
+      ->will($this->returnValue($config_data));
+
+    $config = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface');
+    $config->expects($this->any())
+      ->method('get')
+      ->with('language.negotiation')
+      ->will($this->returnValue($config_object));
+
+    $request = Request::create('/' . $prefix . '/foo', 'GET');
+    $method = new LanguageNegotiationUrl();
+    $method->setLanguageManager($this->languageManager);
+    $method->setConfig($config);
+    $method->setCurrentUser($this->user);
+    $this->assertEquals($expected_langcode, $method->getLangcode($request));
+
+    $cacheability = new CacheableMetadata();
+    $options = [];
+    $method->processOutbound('foo', $options, $request, $cacheability);
+    $expected_cacheability = new CacheableMetadata();
+    if ($expected_langcode) {
+      $this->assertSame($prefix . '/', $options['prefix']);
+      $expected_cacheability->setCacheContexts(['languages:' . LanguageInterface::TYPE_URL]);
+    }
+    else {
+      $this->assertFalse(isset($options['prefix']));
+    }
+    $this->assertEquals($expected_cacheability, $cacheability);
+  }
+
+  /**
+   * Provides data for the path prefix test.
+   *
+   * @return array
+   *   An array of data for checking path prefix negotiation.
+   */
+  public function providerTestPathPrefix() {
+    $path_prefix_configuration[] = [
+      'prefix' => 'de',
+      'prefixes' => [
+        'de' => 'de',
+        'en-uk' => 'en',
+      ],
+      'expected_langcode' => 'de',
+    ];
+    $path_prefix_configuration[] = [
+      'prefix' => 'en-uk',
+      'prefixes' => [
+        'de' => 'de',
+        'en' => 'en-uk',
+      ],
+      'expected_langcode' => 'en',
+    ];
+    // No configuration.
+    $path_prefix_configuration[] = [
+      'prefix' => 'de',
+      'prefixes' => array(),
+      'expected_langcode' => FALSE,
+    ];
+    // Non-matching prefix.
+    $path_prefix_configuration[] = [
+      'prefix' => 'de',
+      'prefixes' => [
+        'en-uk' => 'en',
+      ],
+      'expected_langcode' => FALSE,
+    ];
+    // Non-existing language.
+    $path_prefix_configuration[] = [
+      'prefix' => 'it',
+      'prefixes' => [
+        'it' => 'it',
+        'en-uk' => 'en',
+      ],
+      'expected_langcode' => FALSE,
+    ];
+    return $path_prefix_configuration;
+  }
+
+  /**
+   * Test domain language negotiation and outbound path processing.
    *
    * @dataProvider providerTestDomain
    */
   public function testDomain($http_host, $domains, $expected_langcode) {
+    $this->languageManager->expects($this->any())
+      ->method('getCurrentLanguage')
+      ->will($this->returnValue($this->languages['en']));
+
     $config_data = array(
       'source' => LanguageNegotiationUrl::CONFIG_DOMAIN,
       'domains' => $domains,
@@ -87,6 +199,15 @@ public function testDomain($http_host, $domains, $expected_langcode) {
     $method->setConfig($config);
     $method->setCurrentUser($this->user);
     $this->assertEquals($expected_langcode, $method->getLangcode($request));
+
+    $cacheability = new CacheableMetadata();
+    $options = [];
+    $this->assertSame('foo', $method->processOutbound('foo', $options, $request, $cacheability));
+    $expected_cacheability = new CacheableMetadata();
+    if ($expected_langcode !== FALSE && count($domains) > 1) {
+      $expected_cacheability->setCacheMaxAge(Cache::PERMANENT)->setCacheContexts(['languages:' . LanguageInterface::TYPE_URL, 'url.host']);
+    }
+    $this->assertEquals($expected_cacheability, $cacheability);
   }
 
   /**
@@ -154,3 +275,13 @@ public function providerTestDomain() {
     return $domain_configuration;
   }
 }
+
+}
+
+namespace {
+  if (!function_exists('base_path')) {
+    function base_path() {
+      return '/';
+    }
+  }
+}
diff --git a/core/modules/menu_link_content/src/Tests/MenuLinkContentCacheabilityBubblingTest.php b/core/modules/menu_link_content/src/Tests/MenuLinkContentCacheabilityBubblingTest.php
new file mode 100644
index 0000000..3c1e820
--- /dev/null
+++ b/core/modules/menu_link_content/src/Tests/MenuLinkContentCacheabilityBubblingTest.php
@@ -0,0 +1,162 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_link_content\Tests\MenuLinkContentCacheabilityBubblingTest.
+ */
+
+namespace Drupal\menu_link_content\Tests;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Menu\MenuTreeParameters;
+use Drupal\menu_link_content\Entity\MenuLinkContent;
+use Drupal\simpletest\KernelTestBase;
+use Drupal\user\Entity\User;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Ensures that rendered menu links bubble the necessary cacheability metadata
+ * for outbound path/route processing.
+ *
+ * @group menu_link_content
+ */
+class MenuLinkContentCacheabilityBubblingTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['menu_link_content', 'system', 'link', 'outbound_processing_test', 'url_alter_test', 'user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('menu_link_content');
+    $this->installEntitySchema('user');
+    $this->installSchema('system', ['url_alias', 'router']);
+
+    // Ensure that the weight of module_link_content is higher than system.
+    // @see menu_link_content_install()
+    module_set_weight('menu_link_content', 1);
+  }
+
+  /**
+   * Tests bubbling of menu links' outbound route/path processing cacheability.
+   */
+  public function testOutboundPathAndRouteProcessing() {
+    \Drupal::service('router.builder')->rebuild();
+
+    $request_stack = \Drupal::requestStack();
+    /** @var \Symfony\Component\Routing\RequestContext $request_context */
+    $request_context = \Drupal::service('router.request_context');
+
+    $request = Request::create('/');
+    $request->attributes->set(RouteObjectInterface::ROUTE_NAME, '<front>');
+    $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route('/'));
+    $request_stack->push($request);
+    $request_context->fromRequest($request);
+
+    $menu_tree = \Drupal::menuTree();
+    $renderer = \Drupal::service('renderer');
+
+
+    $default_menu_cacheability = (new CacheableMetadata())
+      ->setCacheMaxAge(Cache::PERMANENT)
+      ->setCacheTags(['config:system.menu.tools'])
+      ->setCacheContexts(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme']);
+
+    User::create(['uid' => 1, 'name' => $this->randomString()])->save();
+    User::create(['uid' => 2, 'name' => $this->randomString()])->save();
+
+    // Five test cases, four asserting one outbound path/route processor, and
+    // together covering one of each:
+    // - no cacheability metadata,
+    // - a cache context,
+    // - a cache tag,
+    // - a cache max-age.
+    // Plus an additional test case to verify that multiple links adding
+    // cacheability metadata of the same type is working (two links with cache
+    // tags).
+    $test_cases = [
+      // \Drupal\Core\RouteProcessor\RouteProcessorCurrent: 'route' cache context.
+      [
+        'uri' => 'route:<current>',
+        'cacheability' => (new CacheableMetadata())->setCacheContexts(['route']),
+      ],
+      // \Drupal\Core\Access\RouteProcessorCsrf: max-age = 0.
+      [
+        'uri' => 'route:outbound_processing_test.route.csrf',
+        'cacheability' => (new CacheableMetadata())->setCacheMaxAge(0),
+      ],
+      // \Drupal\Core\PathProcessor\PathProcessorFront: permanently cacheable.
+      [
+        'uri' => 'internal:/',
+        'cacheability' => (new CacheableMetadata()),
+      ],
+      // \Drupal\url_alter_test\PathProcessorTest: user entity's cache tags.
+      [
+        'uri' => 'internal:/user/1',
+        'cacheability' => (new CacheableMetadata())->setCacheTags(User::load(1)->getCacheTags()),
+      ],
+      [
+        'uri' => 'internal:/user/2',
+        'cacheability' => (new CacheableMetadata())->setCacheTags(User::load(2)->getCacheTags()),
+      ],
+    ];
+
+    $access_anything_tree_manipulator = function (array $tree) use (&$access_anything_tree_manipulator) {
+      foreach ($tree as $key => $element) {
+        $tree[$key]->access = AccessResult::allowed();
+        if ($tree[$key]->subtree) {
+          $tree[$key]->subtree = $access_anything_tree_manipulator($tree[$key]->subtree);
+        }
+      }
+      return $tree;
+    };
+
+    // Test each expectation individually.
+    foreach ($test_cases as $expectation) {
+      $menu_link_content = MenuLinkContent::create([
+        'link' => ['uri' => $expectation['uri']],
+        'menu_name' => 'tools',
+      ]);
+      $menu_link_content->save();
+      $tree = $menu_tree->load('tools', new MenuTreeParameters());
+      $tree = $menu_tree->transform($tree, [['callable' => $access_anything_tree_manipulator]]);
+      $build = $menu_tree->build($tree);
+      $renderer->renderRoot($build);
+
+      $expected_cacheability = $default_menu_cacheability->merge($expectation['cacheability']);
+      $this->assertEqual($expected_cacheability, CacheableMetadata::createFromRenderArray($build));
+
+      $menu_link_content->delete();
+    }
+
+    // Now test them all together in one menu: the rendered menu's cacheability
+    // metadata should be the combination of the cacheability of all links, and
+    // thus of all tested outbound path & route processors.
+    $expected_cacheability = new CacheableMetadata();
+    foreach ($test_cases as $expectation) {
+      $menu_link_content = MenuLinkContent::create([
+        'link' => ['uri' => $expectation['uri']],
+        'menu_name' => 'tools',
+      ]);
+      $menu_link_content->save();
+      $expected_cacheability = $expected_cacheability->merge($expectation['cacheability']);
+    }
+    $tree = $menu_tree->load('tools', new MenuTreeParameters());
+    $tree = $menu_tree->transform($tree, [['callable' => $access_anything_tree_manipulator]]);
+    $build = $menu_tree->build($tree);
+    $renderer->renderRoot($build);
+    $expected_cacheability = $expected_cacheability->merge($default_menu_cacheability);
+    $this->assertEqual($expected_cacheability, CacheableMetadata::createFromRenderArray($build));
+  }
+
+}
diff --git a/core/modules/menu_link_content/tests/outbound_processing_test/outbound_processing_test.info.yml b/core/modules/menu_link_content/tests/outbound_processing_test/outbound_processing_test.info.yml
new file mode 100644
index 0000000..6f4e4a9
--- /dev/null
+++ b/core/modules/menu_link_content/tests/outbound_processing_test/outbound_processing_test.info.yml
@@ -0,0 +1,4 @@
+name: 'Outbound route/path processing'
+type: module
+core: 8.x
+hidden: true
diff --git a/core/modules/menu_link_content/tests/outbound_processing_test/outbound_processing_test.routing.yml b/core/modules/menu_link_content/tests/outbound_processing_test/outbound_processing_test.routing.yml
new file mode 100644
index 0000000..79934c6
--- /dev/null
+++ b/core/modules/menu_link_content/tests/outbound_processing_test/outbound_processing_test.routing.yml
@@ -0,0 +1,5 @@
+outbound_processing_test.route.csrf:
+  path: '/outbound_processing_test/route/csrf'
+  requirements:
+    _access: 'TRUE'
+    _csrf_token: 'TRUE'
diff --git a/core/modules/menu_ui/src/MenuForm.php b/core/modules/menu_ui/src/MenuForm.php
index 09f529a..334d496 100644
--- a/core/modules/menu_ui/src/MenuForm.php
+++ b/core/modules/menu_ui/src/MenuForm.php
@@ -344,7 +344,7 @@ protected function buildOverviewTreeForm($tree, $delta) {
         $id = 'menu_plugin_id:' . $link->getPluginId();
         $form[$id]['#item'] = $element;
         $form[$id]['#attributes'] = $link->isEnabled() ? array('class' => array('menu-enabled')) : array('class' => array('menu-disabled'));
-        $form[$id]['title']['#markup'] = $this->linkGenerator->generate($link->getTitle(), $link->getUrlObject(), $link->getOptions());
+        $form[$id]['title']['#markup'] = $this->linkGenerator->generate($link->getTitle(), $link->getUrlObject());
         if (!$link->isEnabled()) {
           $form[$id]['title']['#markup'] .= ' (' . $this->t('disabled') . ')';
         }
diff --git a/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php b/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
index c6af0ac..dff09fe 100644
--- a/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
+++ b/core/modules/page_cache/src/Tests/PageCacheTagsIntegrationTest.php
@@ -78,7 +78,6 @@ function testPageCacheTags() {
       'theme',
       'timezone',
       'user.permissions',
-      'user.roles',
     ];
 
     // Full node page 1.
diff --git a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
index 4a6a2ea..aec9716 100644
--- a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
@@ -187,10 +187,14 @@ public function getCacheTags() {
    * {@inheritdoc}
    */
   public function getCacheContexts() {
-    // Menu blocks must be cached per role and per active trail.
+    // ::build() uses MenuLinkTreeInterface::getCurrentRouteMenuTreeParameters()
+    // to generate menu tree parameters, and those take the active menu trail
+    // into account. Therefore, we must vary the rendered menu by the active
+    // trail of the rendered menu.
+    // Additional cache contexts, e.g. those that determine link text or
+    // accessibility of a menu, will be bubbled automatically.
     $menu_name = $this->getDerivativeId();
     return [
-      'user.roles',
       'route.menu_active_trails:' . $menu_name,
     ];
   }
diff --git a/core/modules/system/src/Tests/Block/SystemMenuBlockTest.php b/core/modules/system/src/Tests/Block/SystemMenuBlockTest.php
index 0361eb7..c053295 100644
--- a/core/modules/system/src/Tests/Block/SystemMenuBlockTest.php
+++ b/core/modules/system/src/Tests/Block/SystemMenuBlockTest.php
@@ -239,6 +239,12 @@ public function testConfigLevelDepth() {
     $request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'example3');
     $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, $route);
     $this->container->get('request_stack')->push($request);
+    // \Drupal\Core\Menu\MenuActiveTrail uses the cache collector pattern, which
+    // includes static caching. Since this second scenario simulates a second
+    // request, we must also simulate it for the MenuActiveTrail service, by
+    // clearing the cache collector's static cache.
+    \Drupal::service('menu.active_trail')->clear();
+
     $active_trail_expectations = [];
     $active_trail_expectations['all'] = [
       'test.example1' => [],
diff --git a/core/modules/system/src/Tests/Common/UrlTest.php b/core/modules/system/src/Tests/Common/UrlTest.php
index 38641c1..5ecb9f3 100644
--- a/core/modules/system/src/Tests/Common/UrlTest.php
+++ b/core/modules/system/src/Tests/Common/UrlTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\system\Tests\Common;
 
 use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Url;
 use Drupal\simpletest\WebTestBase;
@@ -22,7 +23,7 @@
  */
 class UrlTest extends WebTestBase {
 
-  public static $modules = array('common_test');
+  public static $modules = array('common_test', 'url_alter_test');
 
   /**
    * Confirms that invalid URLs are filtered in link generating functions.
@@ -42,6 +43,33 @@ function testLinkXSS() {
   }
 
   /**
+   * Tests that #type=link bubbles outbound route/path processors' cacheability.
+   */
+  function testLinkCacheability() {
+    $cases = [
+      ['Regular link', 'internal:/user', [], ['contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT]],
+      ['Regular link, absolute', 'internal:/user', ['absolute' => TRUE], ['contexts' => ['url.host'], 'tags' => [], 'max-age' => Cache::PERMANENT]],
+      ['Route processor link', 'route:system.run_cron', [], ['contexts' => [], 'tags' => [], 'max-age' => 0]],
+      ['Route processor link, absolute', 'route:system.run_cron', ['absolute' => TRUE], ['contexts' => ['url.host'], 'tags' => [], 'max-age' => 0]],
+      ['Path processor link', 'internal:/user/1', [], ['contexts' => [], 'tags' => ['user:1'], 'max-age' => Cache::PERMANENT]],
+      ['Path processor link, absolute', 'internal:/user/1', ['absolute' => TRUE], ['contexts' => ['url.host'], 'tags' => ['user:1'], 'max-age' => Cache::PERMANENT]],
+    ];
+
+    foreach ($cases as $case) {
+      list($title, $uri, $options, $expected_cacheability) = $case;
+      $link = [
+        '#type' => 'link',
+        '#title' => $title,
+        '#options' => $options,
+        '#url' => Url::fromUri($uri),
+      ];
+      drupal_render($link);
+      $this->pass($title);
+      $this->assertEqual($expected_cacheability, $link['#cache']);
+    }
+  }
+
+  /**
    * Tests that default and custom attributes are handled correctly on links.
    */
   function testLinkAttributes() {
diff --git a/core/modules/system/src/Tests/RouteProcessor/RouteNoneTest.php b/core/modules/system/src/Tests/RouteProcessor/RouteNoneTest.php
index 767203e..2c15bed 100644
--- a/core/modules/system/src/Tests/RouteProcessor/RouteNoneTest.php
+++ b/core/modules/system/src/Tests/RouteProcessor/RouteNoneTest.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\system\Tests\RouteProcessor;
 
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\simpletest\KernelTestBase;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -25,6 +27,13 @@ class RouteNoneTest extends KernelTestBase {
   public static $modules = ['system'];
 
   /**
+   * The URL generator.
+   *
+   * @var \Drupal\Core\Routing\UrlGeneratorInterface
+   */
+  protected $urlGenerator;
+
+  /**
    * {@inheritdoc}
    */
   protected function setUp() {
@@ -32,12 +41,16 @@ protected function setUp() {
 
     $this->installSchema('system', ['router']);
     \Drupal::service('router.builder')->rebuild();
+
+    $this->urlGenerator = \Drupal::urlGenerator();
   }
 
   /**
    * Tests the output process.
    */
   public function testProcessOutbound() {
+    $expected_cacheability = (new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT);
+
     $request_stack = \Drupal::requestStack();
     /** @var \Symfony\Component\Routing\RequestContext $request_context */
     $request_context = \Drupal::service('router.request_context');
@@ -54,8 +67,8 @@ public function testProcessOutbound() {
 
     $request_stack->push($request);
     $request_context->fromRequest($request);
-    $this->assertEqual('', \Drupal::url('<none>'));
-    $this->assertEqual('#test-fragment', \Drupal::url('<none>', [], ['fragment' => 'test-fragment']));
+    $this->assertEqual(['', $expected_cacheability], $this->urlGenerator->generateFromRoute('<none>', [], [], TRUE, TRUE));
+    $this->assertEqual(['#test-fragment', $expected_cacheability], $this->urlGenerator->generateFromRoute('<none>', [], ['fragment' => 'test-fragment'], TRUE));
 
     // Test request with subdir on other page.
     $server = [
@@ -69,8 +82,8 @@ public function testProcessOutbound() {
 
     $request_stack->push($request);
     $request_context->fromRequest($request);
-    $this->assertEqual('', \Drupal::url('<none>'));
-    $this->assertEqual('#test-fragment', \Drupal::url('<none>', [], ['fragment' => 'test-fragment']));
+    $this->assertEqual(['', $expected_cacheability], $this->urlGenerator->generateFromRoute('<none>', [], [], TRUE, TRUE));
+    $this->assertEqual(['#test-fragment', $expected_cacheability], $this->urlGenerator->generateFromRoute('<none>', [], ['fragment' => 'test-fragment'], TRUE));
 
     // Test request without subdir on the homepage.
     $server = [
@@ -84,8 +97,8 @@ public function testProcessOutbound() {
 
     $request_stack->push($request);
     $request_context->fromRequest($request);
-    $this->assertEqual('', \Drupal::url('<none>'));
-    $this->assertEqual('#test-fragment', \Drupal::url('<none>', [], ['fragment' => 'test-fragment']));
+    $this->assertEqual(['', $expected_cacheability], $this->urlGenerator->generateFromRoute('<none>', [], [], TRUE, TRUE));
+    $this->assertEqual(['#test-fragment', $expected_cacheability], $this->urlGenerator->generateFromRoute('<none>', [], ['fragment' => 'test-fragment'], TRUE));
 
     // Test request without subdir on other page.
     $server = [
@@ -99,8 +112,8 @@ public function testProcessOutbound() {
 
     $request_stack->push($request);
     $request_context->fromRequest($request);
-    $this->assertEqual('', \Drupal::url('<none>'));
-    $this->assertEqual('#test-fragment', \Drupal::url('<none>', [], ['fragment' => 'test-fragment']));
+    $this->assertEqual(['', $expected_cacheability], $this->urlGenerator->generateFromRoute('<none>', [], [], TRUE, TRUE));
+    $this->assertEqual(['#test-fragment', $expected_cacheability], $this->urlGenerator->generateFromRoute('<none>', [], ['fragment' => 'test-fragment'], TRUE));
   }
 
 }
diff --git a/core/modules/system/src/Tests/RouteProcessor/RouteProcessorCurrentIntegrationTest.php b/core/modules/system/src/Tests/RouteProcessor/RouteProcessorCurrentIntegrationTest.php
index a7a6649..8e8a357 100644
--- a/core/modules/system/src/Tests/RouteProcessor/RouteProcessorCurrentIntegrationTest.php
+++ b/core/modules/system/src/Tests/RouteProcessor/RouteProcessorCurrentIntegrationTest.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\system\Tests\RouteProcessor;
 
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\simpletest\KernelTestBase;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -24,6 +26,13 @@ class RouteProcessorCurrentIntegrationTest extends KernelTestBase {
   public static $modules = ['system'];
 
   /**
+   * The URL generator.
+   *
+   * @var \Drupal\Core\Routing\UrlGeneratorInterface
+   */
+  protected $urlGenerator;
+
+  /**
    * {@inheritdoc}
    */
   protected function setUp() {
@@ -31,12 +40,18 @@ protected function setUp() {
 
     $this->installSchema('system', ['router']);
     \Drupal::service('router.builder')->rebuild();
+
+    $this->urlGenerator = \Drupal::urlGenerator();
   }
 
   /**
    * Tests the output process.
    */
   public function testProcessOutbound() {
+    $expected_cacheability = (new CacheableMetadata())
+      ->addCacheContexts(['route'])
+      ->setCacheMaxAge(Cache::PERMANENT);
+
     $request_stack = \Drupal::requestStack();
     /** @var \Symfony\Component\Routing\RequestContext $request_context */
     $request_context = \Drupal::service('router.request_context');
@@ -53,7 +68,7 @@ public function testProcessOutbound() {
 
     $request_stack->push($request);
     $request_context->fromRequest($request);
-    $this->assertEqual('/subdir/', \Drupal::url('<current>'));
+    $this->assertEqual(['/subdir/', $expected_cacheability], $this->urlGenerator->generateFromRoute('<current>', [], [], TRUE));
 
     // Test request with subdir on other page.
     $server = [
@@ -67,7 +82,7 @@ public function testProcessOutbound() {
 
     $request_stack->push($request);
     $request_context->fromRequest($request);
-    $this->assertEqual('/subdir/node/add', \Drupal::url('<current>'));
+    $this->assertEqual(['/subdir/node/add', $expected_cacheability], $this->urlGenerator->generateFromRoute('<current>', [], [], TRUE));
 
     // Test request without subdir on the homepage.
     $server = [
@@ -81,7 +96,7 @@ public function testProcessOutbound() {
 
     $request_stack->push($request);
     $request_context->fromRequest($request);
-    $this->assertEqual('/', \Drupal::url('<current>'));
+    $this->assertEqual(['/', $expected_cacheability], $this->urlGenerator->generateFromRoute('<current>', [], [], TRUE));
 
     // Test request without subdir on other page.
     $server = [
@@ -95,7 +110,7 @@ public function testProcessOutbound() {
 
     $request_stack->push($request);
     $request_context->fromRequest($request);
-    $this->assertEqual('/node/add', \Drupal::url('<current>'));
+    $this->assertEqual(['/node/add', $expected_cacheability], $this->urlGenerator->generateFromRoute('<current>', [], [], TRUE));
 
     // Test request without a found route. This happens for example on an
     // not found exception page.
@@ -108,8 +123,10 @@ public function testProcessOutbound() {
 
     $request_stack->push($request);
     $request_context->fromRequest($request);
-    // In case we have no routing, the current route should point to the front.
-    $this->assertEqual('/', \Drupal::url('<current>'));
+    // In case we have no routing, the current route should point to the front,
+    // and the cacheability does not depend on the 'route' cache context, since
+    // no route was involved at all: this is fallback behavior.
+    $this->assertEqual(['/', (new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT)], $this->urlGenerator->generateFromRoute('<current>', [], [], TRUE));
   }
 
 }
diff --git a/core/modules/system/src/Tests/Theme/EngineTwigTest.php b/core/modules/system/src/Tests/Theme/EngineTwigTest.php
index ff84af7..7993d3a 100644
--- a/core/modules/system/src/Tests/Theme/EngineTwigTest.php
+++ b/core/modules/system/src/Tests/Theme/EngineTwigTest.php
@@ -56,6 +56,13 @@ public function testTwigUrlGenerator() {
       'url (as route) absolute despite option: ' . $url_generator->generateFromRoute('user.register', array(), array('absolute' => TRUE)),
       'url (as route) absolute with fragment: ' . $url_generator->generateFromRoute('user.register', array(), array('absolute' => TRUE, 'fragment' => 'bottom')),
     );
+
+    // Verify that url() has the ability to bubble cacheability metadata:
+    // absolute URLs should bubble the 'url.host' cache context. (This only
+    // needs to test that cacheability metadata is bubbled *at all*; detailed
+    // tests for *which* cacheability metadata is bubbled live elsewhere.)
+    $this->assertCacheContext('url.host');
+
     // Make sure we got something.
     $content = $this->getRawContent();
     $this->assertFalse(empty($content), 'Page content is not empty');
@@ -73,9 +80,15 @@ public function testTwigLinkGenerator() {
     $link_generator = $this->container->get('link_generator');
 
     $expected = [
-      'link via the linkgenerator: ' . $link_generator->generate('register', new Url('user.register')),
+      'link via the linkgenerator: ' . $link_generator->generate('register', new Url('user.register', [], ['absolute' => TRUE])),
     ];
 
+    // Verify that link() has the ability to bubble cacheability metadata:
+    // absolute URLs should bubble the 'url.host' cache context. (This only
+    // needs to test that cacheability metadata is bubbled *at all*; detailed
+    // tests for *which* cacheability metadata is bubbled live elsewhere.)
+    $this->assertCacheContext('url.host');
+
     $content = $this->getRawContent();
     $this->assertFalse(empty($content), 'Page content is not empty');
     foreach ($expected as $string) {
diff --git a/core/modules/system/tests/modules/twig_theme_test/src/TwigThemeTestController.php b/core/modules/system/tests/modules/twig_theme_test/src/TwigThemeTestController.php
index 50d6c99..fe25d62 100644
--- a/core/modules/system/tests/modules/twig_theme_test/src/TwigThemeTestController.php
+++ b/core/modules/system/tests/modules/twig_theme_test/src/TwigThemeTestController.php
@@ -45,7 +45,7 @@ public function urlGeneratorRender() {
   public function linkGeneratorRender() {
     return array(
       '#theme' => 'twig_theme_test_link_generator',
-      '#test_url' => new Url('user.register'),
+      '#test_url' => new Url('user.register', [], ['absolute' => TRUE]),
     );
   }
 
diff --git a/core/modules/system/tests/modules/url_alter_test/src/PathProcessorTest.php b/core/modules/system/tests/modules/url_alter_test/src/PathProcessorTest.php
index c4b8017..9af6cab 100644
--- a/core/modules/system/tests/modules/url_alter_test/src/PathProcessorTest.php
+++ b/core/modules/system/tests/modules/url_alter_test/src/PathProcessorTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\url_alter_test;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
 use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -43,12 +44,15 @@ public function processInbound($path, Request $request) {
   /**
    * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound().
    */
-  public function processOutbound($path, &$options = array(), Request $request = NULL) {
+  public function processOutbound($path, &$options = array(), Request $request = NULL, CacheableMetadata $cacheable_metadata = NULL) {
     // Rewrite user/uid to user/username.
     if (preg_match('!^user/([0-9]+)(/.*)?!', $path, $matches)) {
       if ($account = User::load($matches[1])) {
         $matches += array(2 => '');
         $path = 'user/' . $account->getUsername() . $matches[2];
+        if ($cacheable_metadata) {
+          $cacheable_metadata->addCacheTags($account->getCacheTags());
+        }
       }
     }
 
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index e9808d5..4a10761 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -177,6 +177,11 @@ function toolbar_toolbar() {
       '#heading' => t('Administration menu'),
       '#attached' => $subtrees_attached,
       'toolbar_administration' => array(
+        // The cache contexts that must be varied by will bubble up; therefore
+        // setting just cache keys is sufficient.
+        '#cache' => [
+          'keys' => ['toolbar', 'admin_menu_tray'],
+        ],
         '#pre_render' => array(
           'toolbar_prerender_toolbar_administration_tray',
         ),
diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
index a6e388c..ecb5270 100644
--- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
+++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
@@ -1249,7 +1249,7 @@ public function renderText($alter) {
 
         // @todo Views should expect and store a leading /. See:
         //   https://www.drupal.org/node/2423913
-        $more_link = \Drupal::l($more_link_text, CoreUrl::fromUserInput('/' . $more_link_path), array('attributes' => array('class' => array('views-more-link'))));
+        $more_link = \Drupal::l($more_link_text, CoreUrl::fromUserInput('/' . $more_link_path, array('attributes' => array('class' => array('views-more-link')))));
 
         $suffix .= " " . $more_link;
       }
diff --git a/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php b/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php
index 48f77b9..7449536 100644
--- a/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php
+++ b/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\Core\Access;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Tests\UnitTestCase;
 use Drupal\Core\Access\RouteProcessorCsrf;
 use Symfony\Component\Routing\Route;
@@ -49,9 +50,13 @@ public function testProcessOutboundNoRequirement() {
     $route = new Route('/test-path');
     $parameters = array();
 
-    $this->processor->processOutbound('test', $route, $parameters);
+    $cacheable_metadata = new CacheableMetadata();
+    $this->processor->processOutbound('test', $route, $parameters, $cacheable_metadata);
     // No parameters should be added to the parameters array.
     $this->assertEmpty($parameters);
+    // Cacheability of routes without a _csrf_token route requirement is
+    // unaffected.
+    $this->assertEquals((new CacheableMetadata()), $cacheable_metadata);
   }
 
   /**
@@ -67,10 +72,13 @@ public function testProcessOutbound() {
     $route = new Route('/test-path', array(), array('_csrf_token' => 'TRUE'));
     $parameters = array();
 
-    $this->processor->processOutbound('test', $route, $parameters);
+    $cacheable_metadata = new CacheableMetadata();
+    $this->processor->processOutbound('test', $route, $parameters, $cacheable_metadata);
     // 'token' should be added to the parameters array.
     $this->assertArrayHasKey('token', $parameters);
     $this->assertSame($parameters['token'], 'test_token');
+    // Cacheability of routes with a _csrf_token route requirement is max-age=0.
+    $this->assertEquals((new CacheableMetadata())->setCacheMaxAge(0), $cacheable_metadata);
   }
 
   /**
@@ -85,7 +93,10 @@ public function testProcessOutboundDynamicOne() {
     $route = new Route('/test-path/{slug}', array(), array('_csrf_token' => 'TRUE'));
     $parameters = array('slug' => 100);
 
-    $this->assertNull($this->processor->processOutbound('test', $route, $parameters));
+    $cacheable_metadata = new CacheableMetadata();
+    $this->assertNull($this->processor->processOutbound('test', $route, $parameters, $cacheable_metadata));
+    // Cacheability of routes with a _csrf_token route requirement is max-age=0.
+    $this->assertEquals((new CacheableMetadata())->setCacheMaxAge(0), $cacheable_metadata);
   }
 
   /**
@@ -100,7 +111,10 @@ public function testProcessOutboundDynamicTwo() {
     $route = new Route('{slug_1}/test-path/{slug_2}', array(), array('_csrf_token' => 'TRUE'));
     $parameters = array('slug_1' => 100, 'slug_2' => 'test');
 
-    $this->assertNull($this->processor->processOutbound('test', $route, $parameters));
+    $cacheable_metadata = new CacheableMetadata();
+    $this->assertNull($this->processor->processOutbound('test', $route, $parameters, $cacheable_metadata));
+    // Cacheability of routes with a _csrf_token route requirement is max-age=0.
+    $this->assertEquals((new CacheableMetadata())->setCacheMaxAge(0), $cacheable_metadata);
   }
 
 }
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php
index 390eaf7..517f151 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php
@@ -184,12 +184,14 @@ public function testUrl() {
           'entity.test_entity_type.canonical',
           array('test_entity_type' => 'test_entity_id'),
           array('entity_type' => 'test_entity_type', 'entity' => $valid_entity),
+          FALSE,
           '/entity/test_entity_type/test_entity_id',
         ),
         array(
           'entity.test_entity_type.canonical',
           array('test_entity_type' => 'test_entity_id'),
           array('absolute' => TRUE, 'entity_type' => 'test_entity_type', 'entity' => $valid_entity),
+          FALSE,
           'http://drupal/entity/test_entity_type/test_entity_id',
         ),
       )));
diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/RedirectResponseSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/RedirectResponseSubscriberTest.php
index 51f2991..e100205 100644
--- a/core/tests/Drupal/Tests/Core/EventSubscriber/RedirectResponseSubscriberTest.php
+++ b/core/tests/Drupal/Tests/Core/EventSubscriber/RedirectResponseSubscriberTest.php
@@ -49,11 +49,11 @@ public function testDestinationRedirect(Request $request, $expected) {
         ->expects($this->any())
         ->method('generateFromPath')
           ->willReturnMap([
-            ['test', ['query' => [], 'fragment' => '', 'absolute' => TRUE], 'http://example.com/drupal/test'],
-            ['example.com', ['query' => [], 'fragment' => '', 'absolute' => TRUE], 'http://example.com/drupal/example.com'],
-            ['example:com', ['query' => [], 'fragment' => '', 'absolute' => TRUE], 'http://example.com/drupal/example:com'],
-            ['javascript:alert(0)', ['query' => [], 'fragment' => '', 'absolute' => TRUE], 'http://example.com/drupal/javascript:alert(0)'],
-            ['/test', ['query' => [], 'fragment' => '', 'absolute' => TRUE], 'http://example.com/test'],
+            ['test', ['query' => [], 'fragment' => '', 'absolute' => TRUE], FALSE, 'http://example.com/drupal/test'],
+            ['example.com', ['query' => [], 'fragment' => '', 'absolute' => TRUE], FALSE, 'http://example.com/drupal/example.com'],
+            ['example:com', ['query' => [], 'fragment' => '', 'absolute' => TRUE], FALSE, 'http://example.com/drupal/example:com'],
+            ['javascript:alert(0)', ['query' => [], 'fragment' => '', 'absolute' => TRUE], FALSE, 'http://example.com/drupal/javascript:alert(0)'],
+            ['/test', ['query' => [], 'fragment' => '', 'absolute' => TRUE], FALSE, 'http://example.com/test'],
           ]);
     }
 
diff --git a/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php b/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php
index 4e7e85c..6be8a5a 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php
@@ -134,8 +134,8 @@ public function testRedirectWithUrl(Url $redirect_value, $result, $status = 303)
     $this->urlGenerator->expects($this->once())
       ->method('generateFromRoute')
       ->will($this->returnValueMap(array(
-          array('test_route_a', array(), array('absolute' => TRUE), 'test-route'),
-          array('test_route_b', array('key' => 'value'), array('absolute' => TRUE), 'test-route/value'),
+          array('test_route_a', array(), array('absolute' => TRUE), FALSE, 'test-route'),
+          array('test_route_b', array('key' => 'value'), array('absolute' => TRUE), FALSE, 'test-route/value'),
         ))
       );
 
diff --git a/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php b/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php
index cd1a392..588dd87 100644
--- a/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php
+++ b/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\Core\Menu;
 
+use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Menu\DefaultMenuLinkTreeManipulators;
 use Drupal\Core\Menu\MenuLinkTreeElement;
 use Drupal\Tests\UnitTestCase;
@@ -154,15 +155,15 @@ public function testCheckAccess() {
     $this->accessManager->expects($this->exactly(4))
       ->method('checkNamedRoute')
       ->will($this->returnValueMap(array(
-        array('example1', array(), $this->currentUser,  FALSE, FALSE),
-        array('example2', array('foo' => 'bar'), $this->currentUser, FALSE, TRUE),
-        array('example3', array('baz' => 'qux'), $this->currentUser, FALSE, FALSE),
-        array('example5', array(), $this->currentUser, FALSE, TRUE),
+        array('example1', array(), $this->currentUser, TRUE, AccessResult::forbidden()),
+        array('example2', array('foo' => 'bar'), $this->currentUser, TRUE, AccessResult::allowed()->cachePerPermissions()),
+        array('example3', array('baz' => 'qux'), $this->currentUser, TRUE, AccessResult::neutral()),
+        array('example5', array(), $this->currentUser, TRUE, AccessResult::allowed()),
       )));
 
     $this->mockTree();
-    $this->originalTree[5]->subtree[7]->access = TRUE;
-    $this->originalTree[8]->access = FALSE;
+    $this->originalTree[5]->subtree[7]->access = AccessResult::allowed()->cachePerUser();
+    $this->originalTree[8]->access = AccessResult::neutral();
 
     $tree = $this->defaultMenuTreeManipulators->checkAccess($this->originalTree);
 
@@ -170,7 +171,7 @@ public function testCheckAccess() {
     $this->assertFalse(array_key_exists(1, $tree));
     // Menu link 2: route with parameters, access granted.
     $element = $tree[2];
-    $this->assertTrue($element->access);
+    $this->assertEquals(AccessResult::allowed()->cachePerPermissions(), $element->access);
     // Menu link 3: route with parameters, access forbidden, hence removed,
     // including its children.
     $this->assertFalse(array_key_exists(3, $tree[2]->subtree));
@@ -178,14 +179,14 @@ public function testCheckAccess() {
     $this->assertSame(array(), $tree[2]->subtree);
     // Menu link 5: no route name, treated as external, hence access granted.
     $element = $tree[5];
-    $this->assertTrue($element->access);
+    $this->assertEquals(AccessResult::allowed(), $element->access);
     // Menu link 6: external URL, hence access granted.
     $element = $tree[6];
-    $this->assertTrue($element->access);
+    $this->assertEquals(AccessResult::allowed(), $element->access);
     // Menu link 7: 'access' already set.
     $element = $tree[5]->subtree[7];
-    $this->assertTrue($element->access);
-    // Menu link 8: 'access' already set, to FALSE, hence removed.
+    $this->assertEquals(AccessResult::allowed()->cachePerUser(), $element->access);
+    // Menu link 8: 'access' already set: AccessResult::neutral(), hence removed.
     $this->assertFalse(array_key_exists(8, $tree));
   }
 
@@ -205,13 +206,14 @@ public function testCheckAccessWithLinkToAnyPagePermission() {
     $this->mockTree();
     $this->defaultMenuTreeManipulators->checkAccess($this->originalTree);
 
-    $this->assertTrue($this->originalTree[1]->access);
-    $this->assertTrue($this->originalTree[2]->access);
-    $this->assertTrue($this->originalTree[2]->subtree[3]->access);
-    $this->assertTrue($this->originalTree[2]->subtree[3]->subtree[4]->access);
-    $this->assertTrue($this->originalTree[5]->subtree[7]->access);
-    $this->assertTrue($this->originalTree[6]->access);
-    $this->assertTrue($this->originalTree[8]->access);
+    $expected_access_result = AccessResult::allowed()->cachePerPermissions();
+    $this->assertEquals($expected_access_result, $this->originalTree[1]->access);
+    $this->assertEquals($expected_access_result, $this->originalTree[2]->access);
+    $this->assertEquals($expected_access_result, $this->originalTree[2]->subtree[3]->access);
+    $this->assertEquals($expected_access_result, $this->originalTree[2]->subtree[3]->subtree[4]->access);
+    $this->assertEquals($expected_access_result, $this->originalTree[5]->subtree[7]->access);
+    $this->assertEquals($expected_access_result, $this->originalTree[6]->access);
+    $this->assertEquals($expected_access_result, $this->originalTree[8]->access);
   }
 
   /**
@@ -233,7 +235,7 @@ public function testFlatten() {
    * @covers ::collectNodeLinks
    * @covers ::checkAccess
    */
-  public function  testCheckNodeAccess() {
+  public function testCheckNodeAccess() {
     $links = array(
       1 => MenuLinkMock::create(array('id' => 'node.1', 'route_name' => 'entity.node.canonical', 'title' => 'foo', 'parent' => '', 'route_parameters' => array('node' => 1))),
       2 => MenuLinkMock::create(array('id' => 'node.2', 'route_name' => 'entity.node.canonical', 'title' => 'bar', 'parent' => '', 'route_parameters' => array('node' => 2))),
@@ -268,12 +270,14 @@ public function  testCheckNodeAccess() {
       ->with('node')
       ->willReturn($query);
 
+    $node_access_result = AccessResult::allowed()->cachePerPermissions()->addCacheContexts(['user.node_grants:view']);
+
     $tree = $this->defaultMenuTreeManipulators->checkNodeAccess($tree);
-    $this->assertTrue($tree[1]->access);
-    $this->assertTrue($tree[2]->access);
+    $this->assertEquals($node_access_result, $tree[1]->access);
+    $this->assertEquals($node_access_result, $tree[2]->access);
     // Ensure that access denied is set.
-    $this->assertFalse($tree[2]->subtree[3]->access);
-    $this->assertTrue($tree[2]->subtree[3]->subtree[4]->access);
+    $this->assertEquals(AccessResult::neutral(), $tree[2]->subtree[3]->access);
+    $this->assertEquals($node_access_result, $tree[2]->subtree[3]->subtree[4]->access);
     // Ensure that other routes than entity.node.canonical are set as well.
     $this->assertNull($tree[5]->access);
     $this->assertNull($tree[5]->subtree[6]->access);
@@ -284,18 +288,18 @@ public function  testCheckNodeAccess() {
     // Ensure that the access manager is just called for the non-node routes.
     $this->accessManager->expects($this->at(0))
       ->method('checkNamedRoute')
-      ->with('test_route', [], $this->currentUser)
-      ->willReturn(TRUE);
+      ->with('test_route', [], $this->currentUser, TRUE)
+      ->willReturn(AccessResult::allowed());
     $this->accessManager->expects($this->at(1))
       ->method('checkNamedRoute')
-      ->with('test_route', [], $this->currentUser)
-      ->willReturn(FALSE);
+      ->with('test_route', [], $this->currentUser, TRUE)
+      ->willReturn(AccessResult::neutral());
     $tree = $this->defaultMenuTreeManipulators->checkAccess($tree);
 
-    $this->assertTrue($tree[1]->access);
-    $this->assertTrue($tree[2]->access);
+    $this->assertEquals($node_access_result, $tree[1]->access);
+    $this->assertEquals($node_access_result, $tree[2]->access);
     $this->assertFalse(isset($tree[2]->subtree[3]));
-    $this->assertTrue($tree[5]->access);
+    $this->assertEquals(AccessResult::allowed(), $tree[5]->access);
     $this->assertFalse(isset($tree[5]->subtree[6]));
   }
 
diff --git a/core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php b/core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php
index 2ea72ff..16f4b8c 100644
--- a/core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php
+++ b/core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php
@@ -62,8 +62,10 @@ protected function setUp() {
     $this->requestStack = new RequestStack();
     $this->currentRouteMatch = new CurrentRouteMatch($this->requestStack);
     $this->menuLinkManager = $this->getMock('Drupal\Core\Menu\MenuLinkManagerInterface');
+    $cache = $this->getMock('\Drupal\Core\Cache\CacheBackendInterface');
+    $lock = $this->getMock('\Drupal\Core\Lock\LockBackendInterface');
 
-    $this->menuActiveTrail = new MenuActiveTrail($this->menuLinkManager, $this->currentRouteMatch);
+    $this->menuActiveTrail = new MenuActiveTrail($this->menuLinkManager, $this->currentRouteMatch, $cache, $lock);
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorAliasTest.php b/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorAliasTest.php
index de652c0..bf5203e 100644
--- a/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorAliasTest.php
+++ b/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorAliasTest.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Tests\Core\PathProcessor;
 
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\PathProcessor\PathProcessorAlias;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\HttpFoundation\Request;
@@ -56,23 +58,34 @@ public function testProcessInbound() {
   }
 
   /**
-   * Tests the processOutbound method.
+   * @covers ::processOutbound
    *
-   * @see \Drupal\Core\PathProcessor\PathProcessorAlias::processOutbound
+   * @dataProvider providerTestProcessOutbound
    */
-  public function testProcessOutbound() {
-    $this->aliasManager->expects($this->exactly(2))
+  public function testProcessOutbound($path, array $options, $expected_path) {
+    $this->aliasManager->expects($this->any())
       ->method('getAliasByPath')
       ->will($this->returnValueMap(array(
         array('internal-url', NULL, 'urlalias'),
         array('url', NULL, 'url'),
       )));
 
-    $this->assertEquals('urlalias', $this->pathProcessor->processOutbound('internal-url'));
-    $options = array('alias' => TRUE);
-    $this->assertEquals('internal-url', $this->pathProcessor->processOutbound('internal-url', $options));
+    $cacheable_metadata = new CacheableMetadata();
+    $this->assertEquals($expected_path, $this->pathProcessor->processOutbound($path, $options, NULL, $cacheable_metadata));
+    // Cacheability of paths replaced with path aliases is permanent.
+    // @todo https://www.drupal.org/node/2480077
+    $this->assertEquals((new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT), $cacheable_metadata);
+  }
 
-    $this->assertEquals('url', $this->pathProcessor->processOutbound('url'));
+  /**
+   * @return array
+   */
+  public function providerTestProcessOutbound() {
+    return [
+      ['internal-url', [], 'urlalias'],
+      ['internal-url', ['alias' => TRUE], 'internal-url'],
+      ['url', [], 'url'],
+    ];
   }
 
 }
diff --git a/core/tests/Drupal/Tests/Core/RouteProcessor/RouteProcessorManagerTest.php b/core/tests/Drupal/Tests/Core/RouteProcessor/RouteProcessorManagerTest.php
index e002f44..77d31ff 100644
--- a/core/tests/Drupal/Tests/Core/RouteProcessor/RouteProcessorManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/RouteProcessor/RouteProcessorManagerTest.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Tests\Core\RouteProcessor;
 
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\RouteProcessor\RouteProcessorManager;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\Routing\Route;
@@ -47,7 +49,10 @@ public function testRouteProcessorManager() {
       $this->processorManager->addOutbound($processor, $priority);
     }
 
-    $this->processorManager->processOutbound($route_name, $route, $parameters);
+    $cacheable_metadata = new CacheableMetadata();
+    $this->processorManager->processOutbound($route_name, $route, $parameters, $cacheable_metadata);
+    // Default cacheability is: permanently cacheable, no cache tags/contexts.
+    $this->assertEquals((new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT), $cacheable_metadata);
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php
index 52ab79f..6b638ee 100644
--- a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php
+++ b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php
@@ -7,6 +7,9 @@
 
 namespace Drupal\Tests\Core\Routing;
 
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\PathProcessor\PathProcessorAlias;
 use Drupal\Core\PathProcessor\PathProcessorManager;
 use Drupal\Core\Routing\RequestContext;
@@ -52,7 +55,16 @@ class UrlGeneratorTest extends UnitTestCase {
    */
   protected $requestStack;
 
+  /**
+   * {@inheritdoc}
+   */
   protected function setUp() {
+    $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $container = new ContainerBuilder();
+    $container->set('cache_contexts_manager', $cache_contexts_manager);
+    \Drupal::setContainer($container);
 
     $routes = new RouteCollection();
     $first_route = new Route('/test/one');
@@ -164,15 +176,16 @@ public function aliasManagerCallback() {
   public function testAliasGeneration() {
     $url = $this->generator->generate('test_1');
     $this->assertEquals('/hello/world', $url);
+    // No cacheability to test; UrlGenerator::generate() doesn't support
+    // collecting cacheability metadata.
 
-    $this->routeProcessorManager->expects($this->exactly(2))
+    $this->routeProcessorManager->expects($this->exactly(3))
       ->method('processOutbound')
       ->with($this->anything());
 
 
     // Check that the two generate methods return the same result.
-    $url_from_route = $this->generator->generateFromRoute('test_1');
-    $this->assertEquals($url_from_route, $url);
+    $this->assertGenerateFromRoute('test_1', [], [], $url, (new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT));
 
     $path = $this->generator->getPathFromRoute('test_1');
     $this->assertEquals('test/one', $path);
@@ -195,23 +208,22 @@ public function testGetPathFromRouteWithSubdirectory() {
   public function testAliasGenerationWithParameters() {
     $url = $this->generator->generate('test_2', array('narf' => '5'));
     $this->assertEquals('/goodbye/cruel/world', $url);
+    // No cacheability to test; UrlGenerator::generate() doesn't support
+    // collecting cacheability metadata.
 
-    $this->routeProcessorManager->expects($this->exactly(4))
+    $this->routeProcessorManager->expects($this->exactly(7))
       ->method('processOutbound')
       ->with($this->anything());
 
     $options = array('fragment' => 'top');
     // Extra parameters should appear in the query string.
-    $url = $this->generator->generateFromRoute('test_1', array('zoo' => '5'), $options);
-    $this->assertEquals('/hello/world?zoo=5#top', $url);
+    $this->assertGenerateFromRoute('test_1', ['zoo' => 5], $options, '/hello/world?zoo=5#top', (new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT));
 
     $options = array('query' => array('page' => '1'), 'fragment' => 'bottom');
-    $url = $this->generator->generateFromRoute('test_2', array('narf' => '5'), $options);
-    $this->assertEquals('/goodbye/cruel/world?page=1#bottom', $url);
+    $this->assertGenerateFromRoute('test_2', ['narf' => 5], $options, '/goodbye/cruel/world?page=1#bottom', (new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT));
 
     // Changing the parameters, the route still matches but there is no alias.
-    $url = $this->generator->generateFromRoute('test_2', array('narf' => '7'), $options);
-    $this->assertEquals('/test/two/7?page=1#bottom', $url);
+    $this->assertGenerateFromRoute('test_2', ['narf' => 7], $options, '/test/two/7?page=1#bottom', (new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT));
 
     $path = $this->generator->getPathFromRoute('test_2', array('narf' => '5'));
     $this->assertEquals('test/two/5', $path);
@@ -223,8 +235,7 @@ public function testAliasGenerationWithParameters() {
    * @dataProvider providerTestAliasGenerationWithOptions
    */
   public function testAliasGenerationWithOptions($route_name, $route_parameters, $options, $expected) {
-    $url = $this->generator->generateFromRoute($route_name, $route_parameters, $options);
-    $this->assertSame($expected, $url);
+    $this->assertGenerateFromRoute($route_name, $route_parameters, $options, $expected, (new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT));
   }
 
   /**
@@ -279,15 +290,16 @@ public function testGetPathFromRouteTrailing() {
   public function testAbsoluteURLGeneration() {
     $url = $this->generator->generate('test_1', array(), TRUE);
     $this->assertEquals('http://localhost/hello/world', $url);
+    // No cacheability to test; UrlGenerator::generate() doesn't support
+    // collecting cacheability metadata.
 
-    $this->routeProcessorManager->expects($this->once())
+    $this->routeProcessorManager->expects($this->exactly(2))
       ->method('processOutbound')
       ->with($this->anything());
 
     $options = array('absolute' => TRUE, 'fragment' => 'top');
     // Extra parameters should appear in the query string.
-    $url = $this->generator->generateFromRoute('test_1', array('zoo' => '5'), $options);
-    $this->assertEquals('http://localhost/hello/world?zoo=5#top', $url);
+    $this->assertGenerateFromRoute('test_1', ['zoo' => 5], $options, 'http://localhost/hello/world?zoo=5#top', (new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT)->setCacheContexts(['url.host']));
   }
 
   /**
@@ -295,25 +307,21 @@ public function testAbsoluteURLGeneration() {
    */
   public function testBaseURLGeneration() {
     $options = array('base_url' => 'http://www.example.com:8888');
-    $url = $this->generator->generateFromRoute('test_1', array(), $options);
-    $this->assertEquals('http://www.example.com:8888/hello/world', $url);
+    $this->assertGenerateFromRoute('test_1', [], $options, 'http://www.example.com:8888/hello/world', (new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT));
 
     $options = array('base_url' => 'http://www.example.com:8888', 'https' => TRUE);
-    $url = $this->generator->generateFromRoute('test_1', array(), $options);
-    $this->assertEquals('https://www.example.com:8888/hello/world', $url);
+    $this->assertGenerateFromRoute('test_1', [], $options, 'https://www.example.com:8888/hello/world', (new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT));
 
     $options = array('base_url' => 'https://www.example.com:8888', 'https' => FALSE);
-    $url = $this->generator->generateFromRoute('test_1', array(), $options);
-    $this->assertEquals('http://www.example.com:8888/hello/world', $url);
+    $this->assertGenerateFromRoute('test_1', [], $options, 'http://www.example.com:8888/hello/world', (new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT));
 
-    $this->routeProcessorManager->expects($this->once())
+    $this->routeProcessorManager->expects($this->exactly(2))
       ->method('processOutbound')
       ->with($this->anything());
 
     $options = array('base_url' => 'http://www.example.com:8888', 'fragment' => 'top');
     // Extra parameters should appear in the query string.
-    $url = $this->generator->generateFromRoute('test_1', array('zoo' => '5'), $options);
-    $this->assertEquals('http://www.example.com:8888/hello/world?zoo=5#top', $url);
+    $this->assertGenerateFromRoute('test_1', ['zoo' => 5], $options, 'http://www.example.com:8888/hello/world?zoo=5#top', (new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT));
   }
 
   /**
@@ -322,14 +330,15 @@ public function testBaseURLGeneration() {
   public function testUrlGenerationWithHttpsRequirement() {
     $url = $this->generator->generate('test_4', array(), TRUE);
     $this->assertEquals('https://localhost/test/four', $url);
+    // No cacheability to test; UrlGenerator::generate() doesn't support
+    // collecting cacheability metadata.
 
-    $this->routeProcessorManager->expects($this->exactly(1))
+    $this->routeProcessorManager->expects($this->exactly(2))
       ->method('processOutbound')
       ->with($this->anything());
 
     $options = array('absolute' => TRUE, 'https' => TRUE);
-    $url = $this->generator->generateFromRoute('test_1', array(), $options);
-    $this->assertEquals('https://localhost/hello/world', $url);
+    $this->assertGenerateFromRoute('test_1', [], $options, 'https://localhost/hello/world', (new CacheableMetadata())->setCacheMaxAge(Cache::PERMANENT)->setCacheContexts(['url.host']));
   }
 
   /**
@@ -353,33 +362,81 @@ public function testPathBasedURLGeneration() {
         $request->headers->set('host', ['www.example.com']);
         $this->requestStack->push($request);
 
+        // Determine the expected cacheability.
+        $expected_cacheability = (new CacheableMetadata())
+          ->setCacheContexts($absolute ? ['url.host'] : [])
+          ->setCacheMaxAge(Cache::PERMANENT);
+
         // Get the expected start of the path string.
         $base = ($absolute ? $base_url . '/' : $base_path . '/') . $script_path;
         $url = $base . 'node/123';
         $result = $this->generator->generateFromPath('node/123', array('absolute' => $absolute));
         $this->assertEquals($url, $result, "$url == $result");
+        list($result, $cacheability) = $this->generator->generateFromPath('node/123', array('absolute' => $absolute), TRUE);
+        $this->assertEquals($url, $result, "$url == $result");
+        $this->assertEquals($expected_cacheability, $cacheability);
 
         $url = $base . 'node/123#foo';
         $result = $this->generator->generateFromPath('node/123', array('fragment' => 'foo', 'absolute' => $absolute));
         $this->assertEquals($url, $result, "$url == $result");
+        list($result, $cacheability) = $this->generator->generateFromPath('node/123', array('fragment' => 'foo', 'absolute' => $absolute), TRUE);
+        $this->assertEquals($url, $result, "$url == $result");
+        $this->assertEquals($expected_cacheability, $cacheability);
 
         $url = $base . 'node/123?foo';
         $result = $this->generator->generateFromPath('node/123', array('query' => array('foo' => NULL), 'absolute' => $absolute));
         $this->assertEquals($url, $result, "$url == $result");
+        list($result, $cacheability) = $this->generator->generateFromPath('node/123', array('query' => array('foo' => NULL), 'absolute' => $absolute), TRUE);
+        $this->assertEquals($url, $result, "$url == $result");
+        $this->assertEquals($expected_cacheability, $cacheability);
 
         $url = $base . 'node/123?foo=bar&bar=baz';
         $result = $this->generator->generateFromPath('node/123', array('query' => array('foo' => 'bar', 'bar' => 'baz'), 'absolute' => $absolute));
         $this->assertEquals($url, $result, "$url == $result");
+        list($result, $cacheability) = $this->generator->generateFromPath('node/123', array('query' => array('foo' => 'bar', 'bar' => 'baz'), 'absolute' => $absolute), TRUE);
+        $this->assertEquals($url, $result, "$url == $result");
+        $this->assertEquals($expected_cacheability, $cacheability);
 
         $url = $base . 'node/123?foo#bar';
         $result = $this->generator->generateFromPath('node/123', array('query' => array('foo' => NULL), 'fragment' => 'bar', 'absolute' => $absolute));
         $this->assertEquals($url, $result, "$url == $result");
+        list($result, $cacheability) = $this->generator->generateFromPath('node/123', array('query' => array('foo' => NULL), 'fragment' => 'bar', 'absolute' => $absolute), TRUE);
+        $this->assertEquals($url, $result, "$url == $result");
+        $this->assertEquals($expected_cacheability, $cacheability);
 
         $url = $base;
         $result = $this->generator->generateFromPath('<front>', array('absolute' => $absolute));
         $this->assertEquals($url, $result, "$url == $result");
+        list($result, $cacheability) = $this->generator->generateFromPath('<front>', array('absolute' => $absolute), TRUE);
+        $this->assertEquals($url, $result, "$url == $result");
+        $this->assertEquals($expected_cacheability, $cacheability);
       }
     }
   }
 
+  /**
+   * Asserts \Drupal\Core\Routing\UrlGenerator::generateFromRoute()'s output.
+   *
+   * @param $route_name
+   *   The route name to test.
+   * @param array $route_parameters
+   *   The route parameters to test.
+   * @param array $options
+   *   The options to test.
+   * @param $expected_url
+   *   The expected generated URL string.
+   * @param \Drupal\Core\Cache\CacheableMetadata $expected_cacheability
+   *   The expected generated cacheability metadata.
+   */
+  protected function assertGenerateFromRoute($route_name, array $route_parameters, array $options, $expected_url, CacheableMetadata $expected_cacheability) {
+    // First, test with $collect_cacheability_metadata set to the default value.
+    $url = $this->generator->generateFromRoute($route_name, $route_parameters, $options);
+    $this->assertSame($expected_url, $url);
+
+    // Second, test with it set to TRUE.
+    list($url, $cacheability) = $this->generator->generateFromRoute($route_name, $route_parameters, $options, TRUE);
+    $this->assertSame($expected_url, $url);
+    $this->assertEquals($expected_cacheability, $cacheability);
+  }
+
 }
diff --git a/core/tests/Drupal/Tests/Core/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php
index dcc2c66..c41d38d 100644
--- a/core/tests/Drupal/Tests/Core/UrlTest.php
+++ b/core/tests/Drupal/Tests/Core/UrlTest.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Access\AccessManagerInterface;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Routing\RouteMatch;
 use Drupal\Core\Url;
@@ -73,25 +74,32 @@ protected function setUp() {
     parent::setUp();
 
     $map = array();
-    $map[] = array('view.frontpage.page_1', array(), array(), '/node');
-    $map[] = array('node_view', array('node' => '1'), array(), '/node/1');
-    $map[] = array('node_edit', array('node' => '2'), array(), '/node/2/edit');
+    $map[] = array('view.frontpage.page_1', array(), array(), FALSE, '/node');
+    $map[] = array('node_view', array('node' => '1'), array(), FALSE, '/node/1');
+    $map[] = array('node_edit', array('node' => '2'), array(), FALSE, '/node/2/edit');
     $this->map = $map;
 
     $alias_map = array(
       // Set up one proper alias that can be resolved to a system path.
-      array('node-alias-test', NULL, 'node'),
+      array('node-alias-test', NULL, FALSE, 'node'),
       // Passing in anything else should return the same string.
-      array('node', NULL, 'node'),
-      array('node/1', NULL, 'node/1'),
-      array('node/2/edit', NULL, 'node/2/edit'),
-      array('non-existent', NULL, 'non-existent'),
+      array('node', NULL, FALSE, 'node'),
+      array('node/1', NULL, FALSE, 'node/1'),
+      array('node/2/edit', NULL, FALSE, 'node/2/edit'),
+      array('non-existent', NULL, FALSE, 'non-existent'),
     );
 
+    // $this->map has $collect_cacheability_metadata = FALSE; also generate the
+    // $collect_cacheability_metadata = TRUE case for ::generateFromRoute().
+    $generate_from_route_map = [];
+    foreach ($this->map as $values) {
+      $generate_from_route_map[] = $values;
+      $generate_from_route_map[] = [$values[0], $values[1], $values[2], TRUE, [$values[4], new CacheableMetadata()]];
+    }
     $this->urlGenerator = $this->getMock('Drupal\Core\Routing\UrlGeneratorInterface');
     $this->urlGenerator->expects($this->any())
       ->method('generateFromRoute')
-      ->will($this->returnValueMap($this->map));
+      ->will($this->returnValueMap($generate_from_route_map));
 
     $this->pathAliasManager = $this->getMock('Drupal\Core\Path\AliasManagerInterface');
     $this->pathAliasManager->expects($this->any())
@@ -372,6 +380,9 @@ public function testToString($urls) {
     foreach ($urls as $index => $url) {
       $path = array_pop($this->map[$index]);
       $this->assertSame($path, $url->toString());
+      list($url_string, $cacheability) = $url->toString(TRUE);
+      $this->assertSame($path, $url_string);
+      $this->assertInstanceOf('\Drupal\Core\Cache\CacheableMetadata', $cacheability);
     }
   }
 
diff --git a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
index f73b493..91d0ffb 100644
--- a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
+++ b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php
@@ -8,7 +8,9 @@
 namespace Drupal\Tests\Core\Utility {
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Language\Language;
+use Drupal\Core\Link;
 use Drupal\Core\Url;
 use Drupal\Core\Utility\LinkGenerator;
 use Drupal\Tests\UnitTestCase;
@@ -425,6 +427,39 @@ public function testGenerateActive() {
   }
 
   /**
+   * Tests the LinkGenerator's support for collecting cacheability metadata.
+   *
+   * @see \Drupal\Core\Utility\LinkGenerator::generate()
+   * @see \Drupal\Core\Utility\LinkGenerator::generateFromLink()
+   */
+  public function testGenerateCacheability() {
+    $options = ['query' => [], 'language' => NULL, 'set_active_class' => FALSE, 'absolute' => FALSE];
+    $this->urlGenerator->expects($this->any())
+      ->method('generateFromRoute')
+      ->will($this->returnValueMap([
+        ['test_route_1', [], $options, FALSE, '/test-route-1'],
+        ['test_route_1', [], $options, TRUE, ['/test-route-1', new CacheableMetadata()]],
+      ]));
+
+    $url = new Url('test_route_1');
+    $url->setUrlGenerator($this->urlGenerator);
+    $expected_link_markup = '<a href="/test-route-1">Test</a>';
+
+    // Test ::generate().
+    $this->assertSame($expected_link_markup, $this->linkGenerator->generate('Test', $url));
+    list($link_markup, $cacheability) = $this->linkGenerator->generate('Test', $url, TRUE);
+    $this->assertSame($expected_link_markup, $link_markup);
+    $this->assertInstanceOf('\Drupal\Core\Cache\CacheableMetadata', $cacheability);
+
+    // Test ::generateFromLink().
+    $link = new Link('Test', $url);
+    $this->assertSame($expected_link_markup, $this->linkGenerator->generateFromLink($link));
+    list($link_markup, $cacheability) = $this->linkGenerator->generateFromLink($link, TRUE);
+    $this->assertSame($expected_link_markup, $link_markup);
+    $this->assertInstanceOf('\Drupal\Core\Cache\CacheableMetadata', $cacheability);
+  }
+
+  /**
    * Checks that a link with certain properties exists in a given HTML snippet.
    *
    * @param array $properties
diff --git a/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php b/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php
index 6a3642a..0bc57d2 100644
--- a/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php
+++ b/core/tests/Drupal/Tests/Core/Utility/UnroutedUrlAssemblerTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\Core\Utility;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Utility\UnroutedUrlAssembler;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\HttpFoundation\Request;
@@ -83,6 +84,9 @@ public function testAssembleWithLeadingSlash() {
   public function testAssembleWithExternalUrl($uri, array $options, $expected) {
    $this->setupRequestStack(FALSE);
    $this->assertEquals($expected, $this->unroutedUrlAssembler->assemble($uri, $options));
+   list($url, $cacheability) = $this->unroutedUrlAssembler->assemble($uri, $options, TRUE);
+   $this->assertEquals($expected, $url);
+   $this->assertInstanceOf('\Drupal\Core\Cache\CacheableMetadata', $cacheability);
   }
 
   /**
@@ -144,12 +148,20 @@ public function testAssembleWithNotEnabledProcessing() {
    */
   public function testAssembleWithEnabledProcessing() {
     $this->setupRequestStack(FALSE);
-    $this->pathProcessor->expects($this->once())
+    $this->pathProcessor->expects($this->exactly(2))
       ->method('processOutbound')
-      ->with('test-uri', ['path_processing' => TRUE, 'fragment' => NULL, 'query' => [], 'absolute' => NULL, 'prefix' => NULL, 'script' => NULL])
-      ->willReturn('test-other-uri');
+      ->willReturnCallback(function($path, &$options = [], Request $request = NULL, CacheableMetadata $cacheable_metadata = NULL) {
+        if ($cacheable_metadata) {
+          $cacheable_metadata->setCacheContexts(['some-cache-context']);
+        }
+        return 'test-other-uri';
+      });
+
     $result = $this->unroutedUrlAssembler->assemble('base:test-uri', ['path_processing' => TRUE]);
     $this->assertEquals('/test-other-uri', $result);
+
+    $result = $this->unroutedUrlAssembler->assemble('base:test-uri', ['path_processing' => TRUE], TRUE);
+    $this->assertEquals(['/test-other-uri', (new CacheableMetadata())->setCacheContexts(['some-cache-context'])], $result);
   }
 
   /**
