 core/core.services.yml                             |   1 -
 core/lib/Drupal/Core/Access/RouteProcessorCsrf.php |   9 +-
 .../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       |  11 +-
 .../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      |  20 ++-
 .../Drupal/Core/Routing/UrlGeneratorInterface.php  |   7 +-
 core/lib/Drupal/Core/Template/TwigExtension.php    |  36 ++---
 core/lib/Drupal/Core/Url.php                       |   8 +-
 core/lib/Drupal/Core/Utility/LinkGenerator.php     |   5 +-
 .../Drupal/Core/Utility/LinkGeneratorInterface.php |   3 +-
 .../Drupal/Core/Utility/UnroutedUrlAssembler.php   |  14 +-
 .../Core/Utility/UnroutedUrlAssemblerInterface.php |   3 +-
 .../modules/help_test/src/SuperNovaGenerator.php   |   3 +-
 .../src/HttpKernel/PathProcessorLanguage.php       |   5 +-
 .../LanguageNegotiationSession.php                 |   6 +-
 .../LanguageNegotiation/LanguageNegotiationUrl.php |   9 +-
 .../MenuLinkContentCacheabilityBubblingTest.php    | 149 +++++++++++++++++++++
 .../outbound_processing_test.info.yml              |   4 +
 .../outbound_processing_test.routing.yml           |   5 +
 core/modules/menu_ui/src/MenuForm.php              |   2 +-
 core/modules/shortcut/src/Form/SetCustomize.php    |   6 +-
 .../url_alter_test/src/PathProcessorTest.php       |   6 +-
 .../Tests/Core/Access/RouteProcessorCsrfTest.php   |  22 ++-
 .../Drupal/Tests/Core/Entity/EntityUrlTest.php     |   2 +
 .../Drupal/Tests/Core/Form/FormSubmitterTest.php   |   4 +-
 .../RouteProcessor/RouteProcessorManagerTest.php   |   7 +-
 core/tests/Drupal/Tests/Core/UrlTest.php           |  16 +--
 34 files changed, 314 insertions(+), 91 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index 6579204..4dce309 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1315,7 +1315,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/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/PathProcessor/OutboundPathProcessorInterface.php b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php
index 9e69001..993c1c5 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 metadata.
    *
    * @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..36269f6 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,14 @@ 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');
+      $cacheable_metadata = new CacheableMetadata();
+      $element['#markup'] = $link_generator->generate($element['#title'], $element['#url']->setOptions($options), $cacheable_metadata);
+      $merged = $cacheable_metadata->merge(CacheableMetadata::createFromRenderArray($element));
+      $merged->applyTo($element);
     }
     return $element;
   }
diff --git a/core/lib/Drupal/Core/RouteProcessor/OutboundRouteProcessorInterface.php b/core/lib/Drupal/Core/RouteProcessor/OutboundRouteProcessorInterface.php
index 145c8ee..f7487da 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 path processors' cacheability metadata.
    *
    * @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..933a0fc 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,11 @@ 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(), CacheableMetadata $cacheable_metadata = 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 +289,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, '/');
@@ -349,6 +350,9 @@ public function generateFromRoute($name, $parameters = array(), $options = array
     } elseif ('https' === $scheme && 443 != $this->context->getHttpsPort()) {
       $port = ':' . $this->context->getHttpsPort();
     }
+    if ($cacheable_metadata) {
+      $cacheable_metadata->addCacheContexts(['url.host']);
+    }
     return $scheme . '://' . $host . $port . $base_url . $path . $fragment;
   }
 
@@ -457,7 +461,7 @@ public function generateFromPath($path = NULL, $options = array()) {
   /**
    * 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 +473,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 +487,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 path processors' cacheability metadata.
    */
-  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..7f51b8c 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;
 
 /**
@@ -100,7 +101,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,6 +137,8 @@ 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 \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata
+   *   (optional) Object to collect path processors' cacheability metadata.
    *
    * @return string
    *   The generated URL for the given route.
@@ -148,6 +151,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(), CacheableMetadata $cacheable_metadata = NULL);
 
 }
diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php
index bb29cd1..d02fab9 100644
--- a/core/lib/Drupal/Core/Template/TwigExtension.php
+++ b/core/lib/Drupal/Core/Template/TwigExtension.php
@@ -15,8 +15,6 @@
 use Drupal\Component\Utility\SafeMarkup;
 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 +33,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 +63,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() {
@@ -232,14 +210,16 @@ 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..aa71db9 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,18 @@ public function setAbsolute($absolute = TRUE) {
    * http://example.com/node/1 depending on the options array, plus any
    * specified query string or fragment.
    *
+   * @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata
+   *  Optional object to collect cache tags and contexts for the URL.
+   *
    * @return string
    *   A string URL.
    */
-  public function toString() {
+  public function toString(CacheableMetadata $cacheable_metadata = NULL) {
     if ($this->unrouted) {
       return $this->unroutedUrlAssembler()->assemble($this->getUri(), $this->getOptions());
     }
 
-    return $this->urlGenerator()->generateFromRoute($this->getRouteName(), $this->getRouteParameters(), $this->getOptions());
+    return $this->urlGenerator()->generateFromRoute($this->getRouteName(), $this->getRouteParameters(), $this->getOptions(), $cacheable_metadata);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php
index 8794836..c81fb9d 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;
@@ -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, CacheableMetadata $cacheable_metadata = NULL) {
     // Performance: avoid Url::toString() needing to retrieve the URL generator
     // service from the container.
     $url->setUrlGenerator($this->urlGenerator);
@@ -131,7 +132,7 @@ 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());
+    $url = SafeMarkup::checkPlain($url->toString($cacheable_metadata));
 
     // Make sure the link text is sanitized.
     $safe_text = SafeMarkup::escape($variables['text']);
diff --git a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
index 8d2d9fc..92cb681 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;
 
@@ -72,7 +73,7 @@
    *   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, CacheableMetadata $cacheable_metadata = NULL);
 
   /**
    * Renders a link from a link object.
diff --git a/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php b/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
index efc966b..5c69bc9 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,12 +58,12 @@ 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 = [], CacheableMetadata $cacheable_metadata = NULL) {
     // 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, $cacheable_metadata);
     }
     elseif (UrlHelper::isExternal($uri)) {
       // UrlHelper::isExternal() only returns true for safe protocols.
@@ -104,7 +105,7 @@ protected function buildExternalUrl($uri, array $options = []) {
   /**
    * {@inheritdoc}
    */
-  protected function buildLocalUrl($uri, array $options = []) {
+  protected function buildLocalUrl($uri, array $options = [], CacheableMetadata $cacheable_metadata = NULL) {
     $this->addOptionDefaults($options);
     $request = $this->requestStack->getCurrentRequest();
 
@@ -122,7 +123,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 +146,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;
diff --git a/core/lib/Drupal/Core/Utility/UnroutedUrlAssemblerInterface.php b/core/lib/Drupal/Core/Utility/UnroutedUrlAssemblerInterface.php
index 868f0c1..edba5cd 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.
@@ -52,6 +53,6 @@
    * @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(), CacheableMetadata $cacheable_metadata = NULL);
 
 }
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..619603e 100644
--- a/core/modules/help/tests/modules/help_test/src/SuperNovaGenerator.php
+++ b/core/modules/help/tests/modules/help_test/src/SuperNovaGenerator.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\help_test;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Routing\UrlGeneratorInterface;
 use Symfony\Component\Routing\RequestContext;
 
@@ -53,7 +54,7 @@ public function getPathFromRoute($name, $parameters = array()) {
   /**
    * {@inheritdoc}
    */
-  public function generateFromRoute($name, $parameters = array(), $options = array()) {
+  public function generateFromRoute($name, $parameters = array(), $options = array(), CacheableMetadata $cacheable_metadata = NULL) {
     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/menu_link_content/src/Tests/MenuLinkContentCacheabilityBubblingTest.php b/core/modules/menu_link_content/src/Tests/MenuLinkContentCacheabilityBubblingTest.php
new file mode 100644
index 0000000..7980c77
--- /dev/null
+++ b/core/modules/menu_link_content/src/Tests/MenuLinkContentCacheabilityBubblingTest.php
@@ -0,0 +1,149 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_link_content\Tests\MenuLinkContentCacheabilityBubblingTest.
+ */
+
+namespace Drupal\menu_link_content\Tests;
+
+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()),
+      ],
+    ];
+
+    // 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());
+      $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());
+    $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/shortcut/src/Form/SetCustomize.php b/core/modules/shortcut/src/Form/SetCustomize.php
index 4f84888..4058175 100644
--- a/core/modules/shortcut/src/Form/SetCustomize.php
+++ b/core/modules/shortcut/src/Form/SetCustomize.php
@@ -56,11 +56,13 @@ public function form(array $form, FormStateInterface $form_state) {
         continue;
       }
       $form['shortcuts']['links'][$id]['#attributes']['class'][] = 'draggable';
+      // Skip setting #access_callback.
       $form['shortcuts']['links'][$id]['name'] = array(
         '#type' => 'link',
         '#title' => $shortcut->getTitle(),
-      ) + $url->toRenderArray();
-      unset($form['shortcuts']['links'][$id]['name']['#access_callback']);
+        '#url' => $url,
+        '#options' => $url->getOptions(),
+      );
       $form['shortcuts']['links'][$id]['#weight'] = $shortcut->getWeight();
       $form['shortcuts']['links'][$id]['weight'] = array(
         '#type' => 'weight',
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/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..4f5feb1 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),
+          NULL,
           '/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),
+          NULL,
           'http://drupal/entity/test_entity_type/test_entity_id',
         ),
       )));
diff --git a/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php b/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php
index 4e7e85c..463da74 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), NULL, 'test-route'),
+          array('test_route_b', array('key' => 'value'), array('absolute' => TRUE), NULL, 'test-route/value'),
         ))
       );
 
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/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php
index dcc2c66..92b3bb9 100644
--- a/core/tests/Drupal/Tests/Core/UrlTest.php
+++ b/core/tests/Drupal/Tests/Core/UrlTest.php
@@ -73,19 +73,19 @@ 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(), NULL, '/node');
+    $map[] = array('node_view', array('node' => '1'), array(), NULL, '/node/1');
+    $map[] = array('node_edit', array('node' => '2'), array(), NULL, '/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, NULL, '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, NULL, 'node'),
+      array('node/1', NULL, NULL, 'node/1'),
+      array('node/2/edit', NULL, NULL, 'node/2/edit'),
+      array('non-existent', NULL, NULL, 'non-existent'),
     );
 
     $this->urlGenerator = $this->getMock('Drupal\Core\Routing\UrlGeneratorInterface');
