diff --git a/core/core.services.yml b/core/core.services.yml
index 9afeed2..4d60d20 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -191,6 +191,9 @@ services:
   plugin.manager.menu.local_task:
     class: Drupal\Core\Menu\LocalTaskManager
     arguments: ['@controller_resolver', '@request', '@router.route_provider', '@module_handler', '@cache.cache', '@language_manager', '@access_manager']
+  plugin.manager.menu.contextual_link:
+    class: Drupal\Core\Menu\ContextualLinkManager
+    arguments: ['@controller_resolver', '@module_handler', '@cache.cache', '@language_manager', '@access_manager']
   request:
     class: Symfony\Component\HttpFoundation\Request
     # @TODO the synthetic setting must be uncommented whenever drupal_session_initialize()
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 1d24dd5..794e8db 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -1692,7 +1692,7 @@ function theme_links($variables) {
       }
 
       // Handle links.
-      if (isset($link['href'])) {
+      if (isset($link['href']) || isset($link['route_name'])) {
         $is_current_path = ($link['href'] == current_path() || ($link['href'] == '<front>' && drupal_is_front_page()));
         $is_current_language = (empty($link['language']) || $link['language']->id == $language_url->id);
         if ($is_current_path && $is_current_language) {
@@ -1713,8 +1713,17 @@ function theme_links($variables) {
           $item = drupal_render($link_element);
         }
         else {
-          // Pass in $link as $options, they share the same keys.
-          $item = l($link['title'], $link['href'], $link);
+          // @todo theme_links() should *really* use the same parameters as l(),
+          //   and just take an array of '#type' => 'link' elements, see
+          //   https://drupal.org/node/2102777.
+          // Pass in $link as $options, as they share the same keys.
+          if (isset($link['href'])) {
+            $item = l($link['title'], $link['href'], $link);
+          }
+          else {
+            $link += array('route_parameters' => array());
+            $item = \Drupal::l($link['title'], $link['route_name'], $link['route_parameters'], $link);
+          }
         }
       }
       // Handle title-only text items.
diff --git a/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php b/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php
new file mode 100644
index 0000000..3ab4c33
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\ContextualLinkDefault.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Core\Plugin\PluginBase;
+
+/**
+ * Provides a common base implementation of a contextual link.
+ */
+class ContextualLinkDefault extends PluginBase implements ContextualLinkInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTitle() {
+    return $this->t($this->pluginDefinition['title']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteName() {
+    return $this->pluginDefinition['route_name'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGroup() {
+    return $this->pluginDefinition['group'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOptions() {
+    return $this->pluginDefinition['options'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWeight() {
+    return $this->pluginDefinition['weight'];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/ContextualLinkInterface.php b/core/lib/Drupal/Core/Menu/ContextualLinkInterface.php
new file mode 100644
index 0000000..e5eecec
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/ContextualLinkInterface.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\ContextualLinkInterface.
+ */
+
+namespace Drupal\Core\Menu;
+
+/**
+ * Defines a contextual link plugin.
+ */
+interface ContextualLinkInterface {
+
+  /**
+   * Returns the localized title to be shown for this contextual link.
+   *
+   * Subclasses may add optional arguments like NodeInterface $node = NULL that
+   * will be supplied by the ControllerResolver.
+   *
+   * @return string
+   *   The title to be shown for this action.
+   *
+   * @see \Drupal\Core\Menu\ContextualLinksManager::getTitle()
+   */
+  public function getTitle();
+
+  /**
+   * Returns the route name of the contextual link.
+   *
+   * @return string
+   *   The name of the route this contextual link links to.
+   */
+  public function getRouteName();
+
+  /**
+   * Returns the group this contextual link should be rendered in.
+   *
+   * A contextual link group is a set of contextual links that are displayed
+   * together on a certain page. For example, the 'block' group displays all
+   * links related to the block, such as the block instance edit link as well as
+   * the views edit link, if it is a view block.
+   *
+   * @return string
+   *   The contextual links group name.
+   */
+  public function getGroup();
+
+  /**
+   * Returns the link options passed to the link generator.
+   *
+   * @return array
+   *   The options as expected by LinkGeneratorInterface::generate()
+   *
+   * @see \Drupal\Core\Utility\LinkGeneratorInterface::generate()
+   */
+  public function getOptions();
+
+  /**
+   * Returns the weight of the contextual link.
+   *
+   * The contextual links in one group are sorted by weight for display.
+   *
+   * @return int
+   *   The weight as positive/negative integer.
+   */
+  public function getWeight();
+
+}
diff --git a/core/lib/Drupal/Core/Menu/ContextualLinkManager.php b/core/lib/Drupal/Core/Menu/ContextualLinkManager.php
new file mode 100644
index 0000000..8b1c159
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/ContextualLinkManager.php
@@ -0,0 +1,154 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\ContextualLinkManager.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Component\Plugin\Exception\PluginException;
+use Drupal\Core\Access\AccessManager;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Controller\ControllerResolverInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Language\LanguageManager;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
+use Drupal\Core\Plugin\Discovery\YamlDiscovery;
+use Drupal\Core\Plugin\Factory\ContainerFactory;
+
+/**
+ * Defines a contextual link plugin manager to deal with contextual links.
+ *
+ * @see \Drupal\Core\Menu\ContextualLinkInterface
+ */
+class ContextualLinkManager extends DefaultPluginManager implements ContextualLinkManagerInterface {
+
+  /**
+   * Provides default values for a contextual link definition.
+   *
+   * @var array
+   */
+  protected $defaults = array(
+    // (required) The name of the route to link to.
+    'route_name' => '',
+    // (required) The contextual links group.
+    'group' => '',
+    // The static title text for the link.
+    'title' => '',
+    // The default link options.
+    'options' => array(),
+    // The weight of the link.
+    'weight' => NULL,
+    // Default class for contextual link implementations.
+    'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
+    // The plugin id. Set by the plugin system based on the top-level YAML key.
+    'id' => '',
+  );
+
+  /**
+   * A controller resolver object.
+   *
+   * @var \Symfony\Component\HttpKernel\Controller\ControllerResolverInterface
+   */
+  protected $controllerResolver;
+
+  /**
+   * The access manager.
+   *
+   * @var \Drupal\Core\Access\AccessManager
+   */
+  protected $accessManager;
+
+  /**
+   * Constructs a new ContextualLinkManager instance.
+   *
+   * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
+   *   The controller resolver.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   The cache backend.
+   * @param \Drupal\Core\Language\LanguageManager $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Access\AccessManager $access_manager
+   *   The access manager.
+   */
+  public function __construct(ControllerResolverInterface $controller_resolver, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManager $language_manager, AccessManager $access_manager) {
+    $this->discovery = new YamlDiscovery('contextual_links', $module_handler->getModuleDirectories());
+    $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
+    $this->factory = new ContainerFactory($this);
+
+    $this->controllerResolver = $controller_resolver;
+    $this->accessManager = $access_manager;
+    $this->alterInfo($module_handler, 'contextual_links_plugins');
+    $this->setCacheBackend($cache_backend, $language_manager, 'contextual_links_plugins');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processDefinition(&$definition, $plugin_id) {
+    parent::processDefinition($definition, $plugin_id);
+
+     // If there is no route name, this is a broken definition.
+    if (empty($definition['route_name'])) {
+      throw new PluginException(sprintf('Contextual link plugin (%s) definition must include "route_name".', $plugin_id));
+    }
+     // If there is no group name, this is a broken definition.
+    if (empty($definition['group'])) {
+      throw new PluginException(sprintf('Contextual link plugin (%s) definition must include "group".', $plugin_id));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContextualLinkPluginsByGroup($group_name) {
+    if ($cache = $this->cacheBackend->get($this->cacheKey . ':' . $group_name)) {
+      $contextual_links = $cache->data;
+    }
+    else {
+      $contextual_links = array();
+      foreach ($this->getDefinitions() as $plugin_id => $plugin_definition) {
+        if ($plugin_definition['group'] == $group_name) {
+          $contextual_links[$plugin_id] = $plugin_definition;
+        }
+      }
+      $this->cacheBackend->set($this->cacheKey . ':' . $group_name, $contextual_links);
+    }
+    return $contextual_links;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContextualLinksArrayByGroup($group_name, array $route_parameters, array $metadata = array()) {
+    $links = array();
+    foreach ($this->getContextualLinkPluginsByGroup($group_name) as $plugin_id => $plugin_definition) {
+      /** @var $plugin \Drupal\Core\Menu\ContextualLinkInterface */
+      $plugin = $this->createInstance($plugin_id);
+      $route_name = $plugin->getRouteName();
+
+      // Check access.
+      if (!$this->accessManager->checkNamedRoute($route_name, $route_parameters)) {
+        continue;
+      }
+
+      $links[$plugin_id] = array(
+        'route_name' => $route_name,
+        'route_parameters' => $route_parameters,
+        'title' => $plugin->getTitle(),
+        'weight' => $plugin->getWeight(),
+        'localized_options' => $plugin->getOptions(),
+        'metadata' => $metadata,
+      );
+    }
+
+    $this->moduleHandler->alter('contextual_links', $links, $group_name, $route_parameters);
+
+    return $links;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/ContextualLinkManagerInterface.php b/core/lib/Drupal/Core/Menu/ContextualLinkManagerInterface.php
new file mode 100644
index 0000000..801990b
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/ContextualLinkManagerInterface.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\ContextualLinkManagerInterface.
+ */
+
+namespace Drupal\Core\Menu;
+
+/**
+ * Provides an object which returns the available contextual links.
+ */
+interface ContextualLinkManagerInterface {
+
+  /**
+   * Gets the contextual link plugins by contextual link group.
+   *
+   * @param string $group_name
+   *   The group name.
+   *
+   * @return array
+   *   A list of contextual links plugin definitions.
+   */
+  public function getContextualLinkPluginsByGroup($group_name);
+
+  /**
+   * Gets the contextual links prepared as expected by theme_links().
+   *
+   * @param string $group_name
+   *   The group name.
+   * @param array $route_parameters
+   *   The incoming route parameters. The route parameters need to have the same
+   *   name on all contextual link routes, e.g. you cannot use 'node' and
+   *   'entity' in parallel.
+   * @param array $metadata
+   *   Additional metadata of contextual links, like the position (optional).
+   *
+   * @return array
+   *   An array of link information, keyed by the plugin ID. Each entry is an
+   *   associative array with the following keys:
+   *     - route_name: The route name to link to.
+   *     - route_parameters: The route parameters for the contextual link.
+   *     - title: The title of the contextual link.
+   *     - weight: The weight of the contextual link.
+   *     - localized_options: The options of the link, which will be passed
+   *       to the link generator.
+   *     - metadata: The array of additional metadata that was passed in.
+   */
+  public function getContextualLinksArrayByGroup($group_name, array $route_parameters, array $metadata = array());
+
+}
diff --git a/core/modules/block/block.contextual_links.yml b/core/modules/block/block.contextual_links.yml
new file mode 100644
index 0000000..5d6ba44
--- /dev/null
+++ b/core/modules/block/block.contextual_links.yml
@@ -0,0 +1,4 @@
+block_configure:
+  title: 'Configure block'
+  route_name: 'block.admin_edit'
+  group: 'block'
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index ce1ecb7..68af2d7 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -116,7 +116,6 @@ function block_menu() {
   $items['admin/structure/block/manage/%block/configure'] = array(
     'title' => 'Configure block',
     'type' => MENU_DEFAULT_LOCAL_TASK,
-    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
   );
   $items['admin/structure/block/add/%/%'] = array(
     'title' => 'Place block',
@@ -298,7 +297,9 @@ function _block_get_renderable_region($list = array()) {
     // to perform contextual actions on the help block, and the links needlessly
     // draw attention on it.
     if (isset($build[$key]) && !in_array($block->get('plugin'), array('system_help_block', 'system_main_block'))) {
-      $build[$key]['#contextual_links']['block'] = array('admin/structure/block/manage', array($key));
+      $build[$key]['#contextual_links']['block'] = array(
+        'route_parameters' => array('block' => $key),
+      );
 
       // If there are any nested contextual links, move them to the top level.
       if (isset($build[$key]['content']['#contextual_links'])) {
diff --git a/core/modules/block/custom_block/custom_block.contextual_links.yml b/core/modules/block/custom_block/custom_block.contextual_links.yml
new file mode 100644
index 0000000..acd117f
--- /dev/null
+++ b/core/modules/block/custom_block/custom_block.contextual_links.yml
@@ -0,0 +1,10 @@
+custom_block.block_edit:
+  title: 'Edit'
+  group: custom_block
+  route_name: 'custom_block.edit'
+
+custom_block.block_delete:
+  title: 'Delete'
+  group: custom_block
+  route_name: 'custom_block.delete'
+  weight: 1
diff --git a/core/modules/block/custom_block/custom_block.module b/core/modules/block/custom_block/custom_block.module
index d55d279..498753d 100644
--- a/core/modules/block/custom_block/custom_block.module
+++ b/core/modules/block/custom_block/custom_block.module
@@ -126,14 +126,6 @@ function custom_block_menu() {
     'title' => 'Edit',
     'weight' => 0,
     'type' => MENU_DEFAULT_LOCAL_TASK,
-    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
-  );
-  $items['block/%custom_block/delete'] = array(
-    'title' => 'Delete',
-    'weight' => 1,
-    'type' => MENU_LOCAL_TASK,
-    'context' => MENU_CONTEXT_INLINE,
-    'route_name' => 'custom_block.delete',
   );
   return $items;
 }
diff --git a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockRenderController.php b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockRenderController.php
index 3457115..76b8b34 100644
--- a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockRenderController.php
+++ b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockRenderController.php
@@ -23,7 +23,9 @@ protected function alterBuild(array &$build, EntityInterface $entity, EntityDisp
     parent::alterBuild($build, $entity, $display, $view_mode, $langcode);
     // Add contextual links for this custom block.
     if (!empty($entity->id->value) && $view_mode == 'full') {
-      $build['#contextual_links']['custom_block'] = array('block', array($entity->id()));
+      $build['#contextual_links']['custom_block'] = array(
+        'route_parameters' => array('custom_block' => $entity->id()),
+      );
     }
   }
 
diff --git a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
index d759c57..9a4680c 100644
--- a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
+++ b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php
@@ -230,7 +230,7 @@ public function testBlockContextualLinks() {
     $block = $this->drupalPlaceBlock('views_block:test_view_block-block_1');
     $this->drupalGet('test-page');
 
-    $id = 'block:admin/structure/block/manage:' . $block->id() . ':|views_ui:admin/structure/views/view:test_view_block:location=block&name=test_view_block&display_id=block_1';
+    $id = 'block:block=' . $block->id() . ':|views_ui_edit:view=test_view_block:location=block&name=test_view_block&display_id=block_1';
     // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder()
     $this->assertRaw('<div data-contextual-id="'. $id . '"></div>', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id)));
 
@@ -240,7 +240,7 @@ public function testBlockContextualLinks() {
     $response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page')));
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
-    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '?destination=test-page">Configure block</a></li><li class="views-ui-edit even last"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1?destination=test-page">Edit view</a></li></ul>');
+    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '?destination=test-page">Configure block</a></li><li class="views-uiedit even last"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1?destination=test-page">Edit view</a></li></ul>');
   }
 
 }
diff --git a/core/modules/content_translation/content_translation.contextual_links.yml b/core/modules/content_translation/content_translation.contextual_links.yml
new file mode 100644
index 0000000..2d2818a
--- /dev/null
+++ b/core/modules/content_translation/content_translation.contextual_links.yml
@@ -0,0 +1,3 @@
+content_translation.contextual_links:
+  derivative: 'Drupal\content_translation\Plugin\Derivative\ContentTranslationContextualLinks'
+  weight: 2
diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module
index 661aac9..1e23f97 100644
--- a/core/modules/content_translation/content_translation.module
+++ b/core/modules/content_translation/content_translation.module
@@ -179,7 +179,7 @@ function content_translation_menu() {
         'title' => 'Translate',
         'route_name' => "content_translation.translation_overview_$entity_type",
         'type' => MENU_LOCAL_TASK,
-        'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+        'context' => MENU_CONTEXT_PAGE,
         'weight' => 2,
       ) + $item;
 
diff --git a/core/modules/content_translation/lib/Drupal/content_translation/Plugin/Derivative/ContentTranslationContextualLinks.php b/core/modules/content_translation/lib/Drupal/content_translation/Plugin/Derivative/ContentTranslationContextualLinks.php
new file mode 100644
index 0000000..c6cc8ea
--- /dev/null
+++ b/core/modules/content_translation/lib/Drupal/content_translation/Plugin/Derivative/ContentTranslationContextualLinks.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\content_translation\Plugin\Derivative\ContentTranslationContextualLinks.
+ */
+
+namespace Drupal\content_translation\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DerivativeBase;
+use Drupal\Core\Entity\EntityManager;
+use Drupal\Core\Plugin\Discovery\ContainerDerivativeInterface;
+use Drupal\Core\Routing\RouteProviderInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides dynamic contextual links for content translation.
+ *
+ * @see \Drupal\content_translation\Plugin\Menu\ContextualLink\ContentTranslationContextualLinks
+ */
+class ContentTranslationContextualLinks extends DerivativeBase implements ContainerDerivativeInterface {
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManager
+   */
+  protected $entityManager;
+
+  /**
+   * Constructs a new ContentTranslationContextualLinks.
+   *
+   * @param \Drupal\Core\Entity\EntityManager $entity_manager
+   *   The entity manager.
+   */
+  public function __construct(EntityManager $entity_manager) {
+    $this->entityManager = $entity_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, $base_plugin_id) {
+    return new static(
+      $container->get('entity.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions(array $base_plugin_definition) {
+    // Create contextual links for all possible entity types.
+    foreach ($this->entityManager->getDefinitions() as $entity_type => $entity_info) {
+      if ($entity_info['translatable'] && isset($entity_info['translation'])) {
+        $this->derivatives[$entity_type]['title'] = t('Translate');
+        $this->derivatives[$entity_type]['route_name'] = "content_translation.translation_overview_$entity_type";
+        $this->derivatives[$entity_type]['group'] = $entity_type;
+      }
+    }
+    return parent::getDerivativeDefinitions($base_plugin_definition);
+  }
+
+}
diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module
index 529fa5c..1d5898d 100644
--- a/core/modules/contextual/contextual.module
+++ b/core/modules/contextual/contextual.module
@@ -1,4 +1,5 @@
 <?php
+use Drupal\Component\Utility\Url;
 
 /**
  * @file
@@ -237,13 +238,20 @@ function contextual_pre_render_placeholder($element) {
  *
  * @param $element
  *   A renderable array containing a #contextual_links property, which is a
- *   keyed array. Each key is the name of the implementing module, and each
- *   value is an array that forms the function arguments for
- *   menu_contextual_links(). For example:
+ *   keyed array. Each key is the name of the group of contextual links to
+ *   render (based on the 'group' key in the *.contextual_links.yml files for
+ *   all enabled modules). The value contains an associative array containing
+ *   the following keys:
+ *   - route_parameters: The route parameters passed to the url generator.
+ *   - metadata: Any additional data needed in order to alter the link.
  *   @code
  *     array('#contextual_links' => array(
- *       'block' => array('admin/structure/block/manage', array('system', 'menu-tools')),
- *       'menu' => array('admin/structure/menu/manage', array('tools')),
+ *       'block' => array(
+ *         'route_parameters' => array('block' => 'system.menu-tools'),
+ *       ),
+ *       'menu' => array(
+ *         'route_parameters' => array('menu' => 'tools'),
+ *       ),
  *     ))
  *   @endcode
  *
@@ -256,8 +264,29 @@ function contextual_pre_render_placeholder($element) {
 function contextual_pre_render_links($element) {
   // Retrieve contextual menu links.
   $items = array();
-  foreach ($element['#contextual_links'] as $module => $args) {
-    $items += menu_contextual_links($module, $args[0], $args[1]);
+
+  /** @var $contextual_links_manager \Drupal\Core\Menu\ContextualLinkManager */
+  $contextual_links_manager = \Drupal::service('plugin.manager.menu.contextual_link');
+  foreach ($element['#contextual_links'] as $group => $args) {
+    $args += array(
+      'route_parameters' => array(),
+      'metadata' => array(),
+    );
+    $contextual_links = $contextual_links_manager->getContextualLinksArrayByGroup($group, $args['route_parameters'], $args['metadata']);
+    if (!empty($contextual_links)) {
+      // @todo Remove once all contextual links are converted.
+      unset($element['#contextual_links'][$group]);
+    }
+    $items += $contextual_links;
+  }
+
+  // @todo Remove once all contextual links are converted.
+  foreach ($element['#contextual_links'] as $group => $args) {
+    $args += array(
+      'route_parameters' => array(),
+    );
+
+    $items += menu_contextual_links($group, $group, array_values($args['route_parameters']));
   }
 
   // Transform contextual links into parameters suitable for theme_links().
@@ -266,9 +295,10 @@ function contextual_pre_render_links($element) {
     $class = drupal_html_class($class);
     $links[$class] = array(
       'title' => $item['title'],
-      'href' => $item['href'],
+      'href' => isset($item['href']) ? $item['href'] : NULL,
+      'route_name' => isset($item['route_name']) ? $item['route_name'] : '',
+      'route_parameters' => isset($item['route_parameters']) ? $item['route_parameters'] : array(),
     );
-    // @todo theme_links() should *really* use the same parameters as l().
     $item['localized_options'] += array('query' => array());
     $item['localized_options']['query'] += drupal_get_destination();
     $links[$class] += $item['localized_options'];
@@ -293,7 +323,7 @@ function contextual_pre_render_links($element) {
  */
 function contextual_contextual_links_view_alter(&$element, $items) {
   if (isset($element['#contextual_links']['contextual'])) {
-    $encoded_links = $element['#contextual_links']['contextual'][2]['contextual-views-field-links'];
+    $encoded_links = $element['#contextual_links']['contextual']['metadata']['contextual-views-field-links'];
     $element['#links'] = drupal_json_decode(rawurldecode($encoded_links));
   }
 }
@@ -302,15 +332,14 @@ function contextual_contextual_links_view_alter(&$element, $items) {
  * Serializes #contextual_links property value array to a string.
  *
  * Examples:
- *  - node:node:1:
- *  - views_ui:admin/structure/views/view:frontpage:location=page&view_name=frontpage&view_display_id=page_1
- *  - menu:admin/structure/menu/manage:tools:|block:admin/structure/block/manage:bartik.tools:
+ *  - node:node=1:
+ *  - views_ui_edit:view=frontpage:location=page&view_name=frontpage&view_display_id=page_1
+ *  - menu:menu=tools:|block:block=bartik.tools:
  *
  * So, expressed in a pattern:
- *  <module name>:<parent path>:<path args>:<options>
+ *  <group>:<route parameters>:<metadata>
  *
- * The (dynamic) path args are joined with slashes. The options are encoded as a
- * query string.
+ * The route parameters and options are encoded as query strings.
  *
  * @param array $contextual_links
  *   The $element['#contextual_links'] value for some render element.
@@ -320,18 +349,13 @@ function contextual_contextual_links_view_alter(&$element, $items) {
  *   use in a data- attribute.
  */
 function _contextual_links_to_id($contextual_links) {
-  $id = '';
-  foreach ($contextual_links as $module => $args) {
-    $parent_path = $args[0];
-    $path_args = implode('/', $args[1]);
-    $metadata = drupal_http_build_query((isset($args[2])) ? $args[2] : array());
-
-    if (drupal_strlen($id) > 0) {
-      $id .= '|';
-    }
-    $id .= $module . ':' . $parent_path . ':' . $path_args . ':' . $metadata;
+  $ids = array();
+  foreach ($contextual_links as $group => $args) {
+    $route_parameters = Url::buildQuery($args['route_parameters']);
+    $metadata = Url::buildQuery((isset($args['metadata'])) ? $args['metadata'] : array());
+    $ids[] = "{$group}:{$route_parameters}:{$metadata}";
   }
-  return $id;
+  return implode('|', $ids);
 }
 
 /**
@@ -349,11 +373,14 @@ function _contextual_id_to_links($id) {
   $contextual_links = array();
   $contexts = explode('|', $id);
   foreach ($contexts as $context) {
-    list($module, $parent_path, $path_args, $metadata_raw) = explode(':', $context);
-    $path_args = explode('/', $path_args);
+    list($group, $route_parameters_raw, $metadata_raw) = explode(':', $context);
+    parse_str($route_parameters_raw, $route_parameters);
     $metadata = array();
     parse_str($metadata_raw, $metadata);
-    $contextual_links[$module] = array($parent_path, $path_args, $metadata);
+    $contextual_links[$group] = array(
+      'route_parameters' => $route_parameters,
+      'metadata' => $metadata,
+    );
   }
   return $contextual_links;
 }
diff --git a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
index 638ce15..4cbbca3 100644
--- a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
+++ b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
@@ -60,10 +60,10 @@ function testDifferentPermissions() {
     // Now, on the front page, all article nodes should have contextual links
     // placeholders, as should the view that contains them.
     $ids = array(
-      'node:node:' . $node1->id() . ':',
-      'node:node:' . $node2->id() . ':',
-      'node:node:' . $node3->id() . ':',
-      'views_ui:admin/structure/views/view:frontpage:location=page&name=frontpage&display_id=page_1',
+      'node:node=' . $node1->id() . ':',
+      'node:node=' . $node2->id() . ':',
+      'node:node=' . $node3->id() . ':',
+      'views_ui_edit:view=frontpage:location=page&name=frontpage&display_id=page_1',
     );
 
     // Editor user: can access contextual links and can edit articles.
@@ -77,9 +77,9 @@ function testDifferentPermissions() {
     $response = $this->renderContextualLinks($ids, 'node');
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
-    $this->assertIdentical($json[$ids[0]], '<ul class="contextual-links"><li class="node-edit odd first last"><a href="' . base_path() . 'node/1/edit?destination=node">Edit</a></li></ul>');
+    $this->assertIdentical($json[$ids[0]], '<ul class="contextual-links"><li class="nodepage-edit odd first last"><a href="' . base_path() . 'node/1/edit?destination=node">Edit</a></li></ul>');
     $this->assertIdentical($json[$ids[1]], '');
-    $this->assertIdentical($json[$ids[2]], '<ul class="contextual-links"><li class="node-edit odd first last"><a href="' . base_path() . 'node/3/edit?destination=node">Edit</a></li></ul>');
+    $this->assertIdentical($json[$ids[2]], '<ul class="contextual-links"><li class="nodepage-edit odd first last"><a href="' . base_path() . 'node/3/edit?destination=node">Edit</a></li></ul>');
     $this->assertIdentical($json[$ids[3]], '');
 
     // Authenticated user: can access contextual links, cannot edit articles.
diff --git a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php
index d2018ac..738f047 100644
--- a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php
+++ b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php
@@ -26,75 +26,83 @@ public static function getInfo() {
    */
   function _contextual_links_id_testcases() {
     // Test branch conditions:
-    // - one module.
+    // - one group.
     // - one dynamic path argument.
     // - no metadata.
     $tests[] = array(
       'links' => array(
         'node' => array(
-          'node',
-          array('14031991'),
-          array()
+          'route_parameters' => array(
+            'node' => '14031991',
+          ),
+          'metadata' => array()
         ),
       ),
-      'id' => 'node:node:14031991:',
+      'id' => 'node:node=14031991:',
     );
 
     // Test branch conditions:
-    // - one module.
+    // - one group.
     // - multiple dynamic path arguments.
     // - no metadata.
     $tests[] = array(
       'links' => array(
         'foo' => array(
-          'baz/in/ga',
-          array('bar', 'baz', 'qux'),
-          array()
+          'route_parameters'=> array(
+            'bar',
+            'key' => 'baz',
+            'qux',
+          ),
+          'metadata' => array(),
         ),
       ),
-      'id' => 'foo:baz/in/ga:bar/baz/qux:',
+      'id' => 'foo:0=bar&key=baz&1=qux:',
     );
 
     // Test branch conditions:
-    // - one module.
+    // - one group.
     // - one dynamic path argument.
     // - metadata.
     $tests[] = array(
       'links' => array(
-        'views_ui' => array(
-          'admin/structure/views/view',
-          array('frontpage'),
-          array(
+        'views_ui_edit' => array(
+          'route_parameters' => array(
+            'view' => 'frontpage'
+          ),
+          'metadata' => array(
             'location' => 'page',
             'display' => 'page_1',
-          )
+          ),
         ),
       ),
-      'id' => 'views_ui:admin/structure/views/view:frontpage:location=page&display=page_1',
+      'id' => 'views_ui_edit:view=frontpage:location=page&display=page_1',
     );
 
     // Test branch conditions:
-    // - multiple modules.
+    // - multiple groups.
     // - multiple dynamic path arguments.
     $tests[] = array(
       'links' => array(
         'node' => array(
-          'node',
-          array('14031991'),
-          array()
+          'route_parameters' => array(
+            'node' => '14031991',
+          ),
+          'metadata' => array(),
         ),
         'foo' => array(
-          'baz/in/ga',
-          array('bar', 'baz', 'qux'),
-          array()
+          'route_parameters' => array(
+            'bar',
+            'key' => 'baz',
+            'qux',
+          ),
+          'metadata' => array(),
         ),
         'edge' => array(
-          'edge',
-          array('20011988'),
-          array()
+          'route_parameters' => array('20011988'),
+          'metadata' => array(),
         ),
       ),
-      'id' => 'node:node:14031991:|foo:baz/in/ga:bar/baz/qux:|edge:edge:20011988:',
+      'id' => 'node:node=14031991:|foo:0=bar&key=baz&1=qux:|edge:0=20011988:',
     );
 
     return $tests;
diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
index 8316c8b..fdcb63c 100644
--- a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
+++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
@@ -408,7 +408,7 @@ public function testBlockContextualLinks() {
     $block = $this->drupalPlaceBlock('system_menu_block:tools', array('label' => 'Tools', 'module' => 'system'));
     $this->drupalGet('test-page');
 
-    $id = 'block:admin/structure/block/manage:' . $block->id() . ':|menu:admin/structure/menu/manage:tools:';
+    $id = 'block:block=' . $block->id() . ':|menu:menu=tools:';
     // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder()
     $this->assertRaw('<div data-contextual-id="'. $id . '"></div>', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id)));
 
diff --git a/core/modules/menu/menu.contextual_links.yml b/core/modules/menu/menu.contextual_links.yml
new file mode 100644
index 0000000..5af427e
--- /dev/null
+++ b/core/modules/menu/menu.contextual_links.yml
@@ -0,0 +1,4 @@
+menu_edit:
+  title: 'Edit menu'
+  route_name: 'menu.menu_edit'
+  group: menu
diff --git a/core/modules/menu/menu.module b/core/modules/menu/menu.module
index fe52570..c48bb46 100644
--- a/core/modules/menu/menu.module
+++ b/core/modules/menu/menu.module
@@ -94,7 +94,7 @@ function menu_menu() {
   $items['admin/structure/menu/manage/%menu/edit'] = array(
     'title' => 'Edit menu',
     'type' => MENU_DEFAULT_LOCAL_TASK,
-    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'context' => MENU_CONTEXT_PAGE,
   );
   $items['admin/structure/menu/item/%menu_link/edit'] = array(
     'title' => 'Edit menu link',
@@ -338,7 +338,9 @@ function menu_block_view_system_menu_block_alter(array &$build, BlockPluginInter
   list(, $menu_name) = explode(':', $block->getPluginId());
   if (isset($menus[$menu_name]) && isset($build['content'])) {
     foreach (element_children($build['content']) as $key) {
-      $build['content']['#contextual_links']['menu'] = array('admin/structure/menu/manage', array($build['content'][$key]['#original_link']['menu_name']));
+      $build['content']['#contextual_links']['menu'] = array(
+        'route_parameters' => array('menu' => $build['content'][$key]['#original_link']['menu_name']),
+      );
     }
   }
 }
diff --git a/core/modules/node/lib/Drupal/node/NodeRenderController.php b/core/modules/node/lib/Drupal/node/NodeRenderController.php
index 433adb3..9765735 100644
--- a/core/modules/node/lib/Drupal/node/NodeRenderController.php
+++ b/core/modules/node/lib/Drupal/node/NodeRenderController.php
@@ -83,7 +83,9 @@ public function buildContent(array $entities, array $displays, $view_mode, $lang
   protected function alterBuild(array &$build, EntityInterface $entity, EntityDisplay $display, $view_mode, $langcode = NULL) {
     parent::alterBuild($build, $entity, $display, $view_mode, $langcode);
     if ($entity->id()) {
-      $build['#contextual_links']['node'] = array('node', array($entity->id()));
+      $build['#contextual_links']['node'] = array(
+        'route_parameters' =>array('node' => $entity->id()),
+      );
     }
 
     // The node 'submitted' info is not rendered in a standard way (renderable
diff --git a/core/modules/node/lib/Drupal/node/Tests/Views/NodeContextualLinksTest.php b/core/modules/node/lib/Drupal/node/Tests/Views/NodeContextualLinksTest.php
index cf24cba..2cd0082 100644
--- a/core/modules/node/lib/Drupal/node/Tests/Views/NodeContextualLinksTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/Views/NodeContextualLinksTest.php
@@ -47,10 +47,10 @@ public function testNodeContextualLinks() {
     $user = $this->drupalCreateUser(array('administer nodes', 'access contextual links'));
     $this->drupalLogin($user);
 
-    $response = $this->renderContextualLinks(array('node:node:1:'), 'node');
+    $response = $this->renderContextualLinks(array('node:node=1:'), 'node');
     $this->assertResponse(200);
     $json = Json::decode($response);
-    $this->drupalSetContent($json['node:node:1:']);
+    $this->drupalSetContent($json['node:node=1:']);
 
     $this->assertLinkByHref('node/1/contextual-links', 0, 'The contextual link to the view was found.');
     $this->assertLink('Test contextual link', 0, 'The contextual link to the view was found.');
diff --git a/core/modules/node/node.contextual_links.yml b/core/modules/node/node.contextual_links.yml
new file mode 100644
index 0000000..91159bd
--- /dev/null
+++ b/core/modules/node/node.contextual_links.yml
@@ -0,0 +1,10 @@
+node.page_edit:
+  route_name: node.page_edit
+  group: node
+  title: Edit
+
+node.delete_confirm:
+  route_name: node.delete_confirm
+  group: node
+  title: Delete
+  weight: 10
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 511a494..ab6af6b 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -989,14 +989,7 @@ function node_menu() {
     'title' => 'Edit',
     'route_name' => 'node.page_edit',
     'type' => MENU_LOCAL_TASK,
-    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
-  );
-  $items['node/%node/delete'] = array(
-    'title' => 'Delete',
-    'route_name' => 'node.delete_confirm',
-    'weight' => 10,
-    'type' => MENU_LOCAL_TASK,
-    'context' => MENU_CONTEXT_INLINE,
+    'context' => MENU_CONTEXT_PAGE,
   );
   $items['node/%node/revisions'] = array(
     'title' => 'Revisions',
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
index f341681..4b69e98 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
@@ -296,10 +296,6 @@ protected function doTestMenuHidden() {
     $depth = $parent['depth'] + 1;
     $plid = $parent['mlid'];
 
-    $link = $links['menu-test/hidden/block/manage/%/%/configure'];
-    $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth)));
-    $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid)));
-
     $link = $links['menu-test/hidden/block/manage/%/%/delete'];
     $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth)));
     $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid)));
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 0d9b5be..18fa5e9 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -719,6 +719,64 @@ function hook_menu_contextual_links_alter(&$links, $router_item, $root_path) {
 }
 
 /**
+ * Alter contextual links before they are rendered.
+ *
+ * This hook is invoked by
+ * \Drupal\Core\Menu\ContextualLinkManager::getContextualLinkPluginsByGroup().
+ * The system-determined contextual links are passed in by reference. Additional
+ * links may be added and existing links can be altered.
+ *
+ * Each contextual link contains the following entries:
+ * - title: The localized title of the link.
+ * - route_name: The route name of the link.
+ * - route_parameters: The route parameters of the link.
+ * - localized_options: An array of options to pass to url().
+ * - (optional) weight: The weight of the link, which is used to sort the links.
+ *
+ *
+ * @param array $links
+ *   An associative array containing contextual links for the given $group,
+ *   as described above. The array keys are used to build CSS class names for
+ *   contextual links and must therefore be unique for each set of contextual
+ *   links.
+ * @param string $group
+ *   The group of contextual links being rendered.
+ * @param array $route_parameters.
+ *   The route parameters passed to each route_name of the contextual links.
+ *   For example:
+ *   @code
+ *   array('node' => $node->id())
+ *   @endcode
+ *
+ * @see \Drupal\Core\Menu\ContextualLinkManager
+ */
+function hook_contextual_links_alter(array &$links, $group, array $route_parameters) {
+  if ($group == 'menu') {
+    // Dynamically use the menu name for the title of the menu_edit contextual
+    // link.
+    $menu = \Drupal::entityManager()->getStorageController('menu')->load($route_parameters['menu']);
+    $links['menu_edit']['title'] = t('Edit menu: !label', array('!label' => $menu->label()));
+  }
+}
+
+/**
+ * Alter the plugin definition of contextual links.
+ *
+ * @param array $contextual_links
+ *   An array of contextual_links plugin definitions, keyed by contextual link
+ *   ID. Each entry contains the following keys:
+ *     - title: The displayed title of the link
+ *     - route_name: The route_name of the contextual link to be displayed
+ *     - group: The group under which the contextual links should be added to.
+ *       Possible values are e.g. 'node' or 'menu'.
+ *
+ * @see \Drupal\Core\Menu\ContextualLinkManager
+ */
+function hook_contextual_links_plugins_alter(array &$contextual_links) {
+  $contextual_links['menu_edit']['title'] = 'Edit the menu';
+}
+
+/**
  * Perform alterations before a page is rendered.
  *
  * Use this hook when you want to remove or alter elements at the page
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.contextual_links.yml b/core/modules/system/tests/modules/menu_test/menu_test.contextual_links.yml
new file mode 100644
index 0000000..b2d8e47
--- /dev/null
+++ b/core/modules/system/tests/modules/menu_test/menu_test.contextual_links.yml
@@ -0,0 +1,14 @@
+menu_test.hidden_manage:
+  title: 'List links'
+  group: menu_test_menu
+  route_name: menu_test.hidden_manage
+
+menu_test.hidden_manage_edit:
+  title: 'Edit menu'
+  group: menu_test_menu
+  route_name: menu_test.hidden_manage_edit
+
+menu_test.hidden_block_configure:
+  title: 'Configure block'
+  group: menu_test_block
+  route_name: menu_test.hidden_block_configure
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module
index 566a620..c42e705 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.module
+++ b/core/modules/system/tests/modules/menu_test/menu_test.module
@@ -126,7 +126,7 @@ function menu_test_menu() {
   $items['menu-test/hidden/menu/manage/%menu/list'] = array(
     'title' => 'List links',
     'type' => MENU_DEFAULT_LOCAL_TASK,
-    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'context' => MENU_CONTEXT_PAGE,
   );
   $items['menu-test/hidden/menu/manage/%menu/add'] = array(
     'title' => 'Add link',
@@ -137,7 +137,7 @@ function menu_test_menu() {
     'title' => 'Edit menu',
     'route_name' => 'menu_test.hidden_manage_edit',
     'type' => MENU_LOCAL_TASK,
-    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'context' => MENU_CONTEXT_PAGE,
   );
   $items['menu-test/hidden/menu/manage/%menu/delete'] = array(
     'title' => 'Delete menu',
@@ -162,11 +162,6 @@ function menu_test_menu() {
     'title' => 'Configure block',
     'route_name' => 'menu_test.hidden_block_configure',
   );
-  $items['menu-test/hidden/block/manage/%/%/configure'] = array(
-    'title' => 'Configure block',
-    'type' => MENU_DEFAULT_LOCAL_TASK,
-    'context' => MENU_CONTEXT_INLINE,
-  );
   $items['menu-test/hidden/block/manage/%/%/delete'] = array(
     'title' => 'Delete block',
     'route_name' => 'menu_test.hidden_block_delete',
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TermRenderController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermRenderController.php
index cf319d0..0df4dc4 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/TermRenderController.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermRenderController.php
@@ -54,7 +54,9 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco
   protected function alterBuild(array &$build, EntityInterface $entity, EntityDisplay $display, $view_mode, $langcode = NULL) {
     parent::alterBuild($build, $entity, $display, $view_mode, $langcode);
     $build['#attached']['css'][] = drupal_get_path('module', 'taxonomy') . '/css/taxonomy.module.css';
-    $build['#contextual_links']['taxonomy'] = array('taxonomy/term', array($entity->id()));
+    $build['#contextual_links']['taxonomy_term'] = array(
+      'route_parameters' => array('taxonomy_term' => $entity->id()),
+    );
   }
 
 }
diff --git a/core/modules/taxonomy/taxonomy.contextual_links.yml b/core/modules/taxonomy/taxonomy.contextual_links.yml
new file mode 100644
index 0000000..3fafba7
--- /dev/null
+++ b/core/modules/taxonomy/taxonomy.contextual_links.yml
@@ -0,0 +1,17 @@
+taxonomy.term_edit:
+  title: Edit
+  group: taxonomy_term
+  route_name: taxonomy.term_edit
+  weight: 10
+
+taxonomy.term_delete:
+  title: Delete
+  group: taxonomy_term
+  route_name: taxonomy.term_delete
+  weight: 20
+
+taxonomy.vocabulary_delete:
+  title: Delete
+  group: taxonomy_vocabulary
+  route_name: taxonomy.vocabulary_delete
+  weight: 20
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index e8dd686..b2dbd35 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -263,16 +263,9 @@ function taxonomy_menu() {
     'title' => 'Edit',
     'route_name' => 'taxonomy.term_edit',
     'type' => MENU_LOCAL_TASK,
-    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'context' => MENU_CONTEXT_PAGE,
     'weight' => 10,
   );
-  $items['taxonomy/term/%taxonomy_term/delete'] = array(
-    'title' => 'Delete',
-    'type' => MENU_LOCAL_TASK,
-    'context' => MENU_CONTEXT_INLINE,
-    'weight' => 20,
-    'route_name' => 'taxonomy.term_delete',
-  );
   $items['taxonomy/term/%taxonomy_term/feed'] = array(
     'title' => 'Taxonomy term',
     'title callback' => 'taxonomy_term_title',
@@ -295,13 +288,6 @@ function taxonomy_menu() {
     'route_name' => 'taxonomy.vocabulary_edit',
     'type' => MENU_LOCAL_TASK,
   );
-  $items['admin/structure/taxonomy/%taxonomy_vocabulary/delete'] = array(
-    'title' => 'Delete',
-    'type' => MENU_LOCAL_TASK,
-    'context' => MENU_CONTEXT_INLINE,
-    'weight' => 20,
-    'route_name' => 'taxonomy.vocabulary_delete',
-  );
 
   $items['admin/structure/taxonomy/manage/%taxonomy_vocabulary/add'] = array(
     'title' => 'Add term',
diff --git a/core/modules/user/user.contextual_links.yml b/core/modules/user/user.contextual_links.yml
new file mode 100644
index 0000000..1e0df52
--- /dev/null
+++ b/core/modules/user/user.contextual_links.yml
@@ -0,0 +1,5 @@
+user.role_delete:
+  title: 'Delete role'
+  group: role
+  weight: 10
+  route_name: 'user.role_delete'
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 41dbae9..85bb9b4 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -807,12 +807,6 @@ function user_menu() {
     'title' => 'Edit',
     'type' => MENU_DEFAULT_LOCAL_TASK,
   );
-  $items['admin/people/roles/manage/%user_role/delete'] = array(
-    'title' => 'Delete role',
-    'route_name' => 'user.role_delete',
-    'weight' => 10,
-    'context' => MENU_CONTEXT_INLINE,
-  );
 
   // Administration pages.
   $items['admin/config/people'] = array(
diff --git a/core/modules/views/views.module b/core/modules/views/views.module
index f8f9ff5..fd79b85 100644
--- a/core/modules/views/views.module
+++ b/core/modules/views/views.module
@@ -603,11 +603,11 @@ function views_add_contextual_links(&$render_element, $location, ViewExecutable
     $plugin['contextual_links_locations'][] = 'exposed_filter';
     $has_links = !empty($plugin['contextual links']) && !empty($plugin['contextual_links_locations']);
     if ($has_links && in_array($location, $plugin['contextual_links_locations'])) {
-      foreach ($plugin['contextual links'] as $module => $link) {
+      foreach ($plugin['contextual links'] as $group => $link) {
         $args = array();
         $valid = TRUE;
-        if (!empty($link['argument properties'])) {
-          foreach ($link['argument properties'] as $property) {
+        if (!empty($link['route_parameters_names'])) {
+          foreach ($link['route_parameters_names'] as $parameter_name => $property) {
             // If the plugin is trying to create an invalid contextual link
             // (for example, "path/to/{$view->storage->property}", where
             // $view->storage->{property} does not exist), we cannot construct
@@ -617,7 +617,7 @@ function views_add_contextual_links(&$render_element, $location, ViewExecutable
               break;
             }
             else {
-              $args[] = $view->storage->{$property};
+              $args[$parameter_name] = $view->storage->{$property};
             }
           }
         }
@@ -625,14 +625,13 @@ function views_add_contextual_links(&$render_element, $location, ViewExecutable
         // array.
         if ($valid) {
           $render_element['#views_contextual_links'] = TRUE;
-          $render_element['#contextual_links'][$module] = array(
-            $link['parent path'],
-            $args,
-            array(
+          $render_element['#contextual_links'][$group] = array(
+            'route_parameters' => $args,
+            'metadata' => array(
               'location' => $location,
               'name' => $view->storage->id(),
               'display_id' => $display_id,
-            )
+            ),
           );
         }
       }
diff --git a/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php b/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php
index 4f700cf..905095a 100644
--- a/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php
+++ b/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php
@@ -219,13 +219,13 @@ public function testDisplayPluginsAlter() {
     $definitions = Views::pluginManager('display')->getDefinitions();
 
     $expected = array(
-      'parent path' => 'admin/structure/views/view',
-      'argument properties' => array('id'),
+      'route_name' => 'views_ui.edit',
+      'route_parameters_names' => array('view' => 'id'),
     );
 
     // Test the expected views_ui array exists on each definition.
     foreach ($definitions as $definition) {
-      $this->assertIdentical($definition['contextual links']['views_ui'], $expected, 'Expected views_ui array found in plugin definition.');
+      $this->assertIdentical($definition['contextual links']['views_ui_edit'], $expected, 'Expected views_ui array found in plugin definition.');
     }
   }
 
@@ -288,7 +288,7 @@ public function testPageContextualLinks() {
     $view->enable()->save();
 
     $this->drupalGet('test-display');
-    $id = 'views_ui:admin/structure/views/view:test_display:location=page&name=test_display&display_id=page_1';
+    $id = 'views_ui_edit:view=test_display:location=page&name=test_display&display_id=page_1';
     // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder()
     $this->assertRaw('<div data-contextual-id="'. $id . '"></div>', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id)));
 
@@ -298,7 +298,7 @@ public function testPageContextualLinks() {
     $response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-display')));
     $this->assertResponse(200);
     $json = drupal_json_decode($response);
-    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="views-ui-edit odd first last"><a href="' . base_path() . 'admin/structure/views/view/test_display/edit/page_1?destination=test-display">Edit view</a></li></ul>');
+    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="views-uiedit odd first last"><a href="' . base_path() . 'admin/structure/views/view/test_display/edit/page_1?destination=test-display">Edit view</a></li></ul>');
   }
 
   /**
diff --git a/core/modules/views_ui/views_ui.contextual_links.yml b/core/modules/views_ui/views_ui.contextual_links.yml
new file mode 100644
index 0000000..20e9ead
--- /dev/null
+++ b/core/modules/views_ui/views_ui.contextual_links.yml
@@ -0,0 +1,9 @@
+views_ui.edit:
+  title: 'Edit view'
+  route_name: views_ui.edit
+  group: views_ui_edit
+
+views_ui.preview:
+  title: 'Preview view'
+  route_name: views_ui.preview
+  group: views_ui_preview
diff --git a/core/modules/views_ui/views_ui.module b/core/modules/views_ui/views_ui.module
index 0f76cbd..bb873ed 100644
--- a/core/modules/views_ui/views_ui.module
+++ b/core/modules/views_ui/views_ui.module
@@ -47,11 +47,11 @@ function views_ui_menu() {
   $items['admin/structure/views/view/%/edit'] = array(
     'title' => 'Edit view',
     'type' => MENU_DEFAULT_LOCAL_TASK,
-    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'context' => MENU_CONTEXT_PAGE,
   );
   $items['admin/structure/views/view/%/preview/%'] = array(
     'route_name' => 'views_ui.preview',
-    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+    'context' => MENU_CONTEXT_PAGE,
     'type' => MENU_VISIBLE_IN_BREADCRUMB,
   );
 
@@ -317,9 +317,9 @@ function views_ui_views_plugins_display_alter(&$plugins) {
   // paths underneath "admin/structure/views/view/{$view->id()}" (i.e., paths
   // for editing and performing other contextual actions on the view).
   foreach ($plugins as &$display) {
-    $display['contextual links']['views_ui'] = array(
-      'parent path' => 'admin/structure/views/view',
-      'argument properties' => array('id'),
+    $display['contextual links']['views_ui_edit'] = array(
+      'route_name' => 'views_ui.edit',
+      'route_parameters_names' => array('view' => 'id'),
     );
   }
 }
@@ -336,9 +336,10 @@ function views_ui_contextual_links_view_alter(&$element, $items) {
   // Append the display ID to the Views UI edit links, so that clicking on the
   // contextual link takes you directly to the correct display tab on the edit
   // screen.
-  elseif (!empty($element['#links']['views-ui-edit'])) {
-    $display_id = $element['#contextual_links']['views_ui'][2]['display_id'];
-    $element['#links']['views-ui-edit']['href'] .= '/edit/' . $display_id;
+  elseif (!empty($element['#links']['views-uiedit'])) {
+    $display_id = $items['views_ui.edit']['metadata']['display_id'];
+    $element['#links']['views-uiedit']['route_parameters']['display_id'] = $display_id;
+    $element['#links']['views-uiedit']['route_name'] = 'views_ui.edit_display';
   }
 }
 
diff --git a/core/tests/Drupal/Tests/Core/Menu/ContextualLinkManagerTest.php b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkManagerTest.php
new file mode 100644
index 0000000..229798f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkManagerTest.php
@@ -0,0 +1,398 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Menu\ContextualLinkManagerTest.
+ */
+
+namespace Drupal\Tests\Core\Menu;
+
+use Drupal\Core\Language\Language;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the contextual links manager.
+ *
+ * @see \Drupal\Core\Menu\ContextualLinkManager
+ */
+class ContextualLinkManagerTest extends UnitTestCase {
+
+  /**
+   * The tested contextual link manager.
+   *
+   * @var \Drupal\Core\Menu\ContextualLinkManager
+   */
+  protected $contextualLinkManager;
+
+  /**
+   * The mocked controller resolver.
+   *
+   * @var \Symfony\Component\HttpKernel\Controller\ControllerResolverInterface|\Drupal\Core\\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $controllerResolver;
+
+  /**
+   * The mocked plugin discovery.
+   *
+   * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $pluginDiscovery;
+
+  /**
+   * The plugin factory used in the test.
+   *
+   * @var \Drupal\Component\Plugin\Factory\FactoryInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $factory;
+
+  /**
+   * The cache backend used in the test.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $cacheBackend;
+
+  /**
+   * The mocked module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $moduleHandler;
+
+  /**
+   * The mocked access manager.
+   *
+   * @var \Drupal\Core\Access\AccessManager|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $accessManager;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Contextual links manager.',
+      'description' => 'Tests the contextual links manager.',
+      'group' => 'Menu',
+    );
+  }
+
+  protected function setUp() {
+    $this->contextualLinkManager = $this
+      ->getMockBuilder('Drupal\Core\Menu\ContextualLinkManager')
+      ->disableOriginalConstructor()
+      ->setMethods(NULL)
+      ->getMock();
+
+    $this->controllerResolver = $this->getMock('Symfony\Component\HttpKernel\Controller\ControllerResolverInterface');
+    $this->pluginDiscovery = $this->getMock('Drupal\Component\Plugin\Discovery\DiscoveryInterface');
+    $this->factory = $this->getMock('Drupal\Component\Plugin\Factory\FactoryInterface');
+    $this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
+    $this->accessManager = $this->getMockBuilder('Drupal\Core\Access\AccessManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $property = new \ReflectionProperty('Drupal\Core\Menu\ContextualLinkManager', 'controllerResolver');
+    $property->setAccessible(TRUE);
+    $property->setValue($this->contextualLinkManager, $this->controllerResolver);
+
+    $property = new \ReflectionProperty('Drupal\Core\Menu\ContextualLinkManager', 'discovery');
+    $property->setAccessible(TRUE);
+    $property->setValue($this->contextualLinkManager, $this->pluginDiscovery);
+
+    $property = new \ReflectionProperty('Drupal\Core\Menu\ContextualLinkManager', 'factory');
+    $property->setAccessible(TRUE);
+    $property->setValue($this->contextualLinkManager, $this->factory);
+
+
+    $property = new \ReflectionProperty('Drupal\Core\Menu\ContextualLinkManager', 'accessManager');
+    $property->setAccessible(TRUE);
+    $property->setValue($this->contextualLinkManager, $this->accessManager);
+
+    $language_manager = $this->getMockBuilder('Drupal\Core\Language\LanguageManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $language_manager->expects($this->any())
+      ->method('getLanguage')
+      ->will($this->returnValue(new Language(array('id' => 'en'))));
+
+    $this->moduleHandler = $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface');
+
+    $method = new \ReflectionMethod('Drupal\Core\Menu\ContextualLinkManager', 'alterInfo');
+    $method->setAccessible(TRUE);
+    $method->invoke($this->contextualLinkManager, $this->moduleHandler, 'contextual_links_plugins');
+
+    $this->contextualLinkManager->setCacheBackend($this->cacheBackend, $language_manager, 'contextual_links_plugins');
+  }
+
+  /**
+   * Tests the getContextualLinkPluginsByGroup method.
+   *
+   * @see \Drupal\Core\Menu\ContextualLinkManager::getContextualLinkPluginsByGroup()
+   */
+  public function testGetContextualLinkPluginsByGroup() {
+    $definitions = array(
+      'test_plugin1' => array(
+        'id' => 'test_plugin1',
+        'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
+        'group' => 'group1',
+        'route_name' => 'test_route',
+      ),
+      'test_plugin2' => array(
+        'id' => 'test_plugin2',
+        'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
+        'group' => 'group1',
+        'route_name' => 'test_route2',
+      ),
+      'test_plugin3' => array(
+        'id' => 'test_plugin3',
+        'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
+        'group' => 'group2',
+        'route_name' => 'test_router3',
+      ),
+    );
+    $this->pluginDiscovery->expects($this->once())
+      ->method('getDefinitions')
+      ->will($this->returnValue($definitions));
+
+    // Test with a non existing group.
+    $result = $this->contextualLinkManager->getContextualLinkPluginsByGroup('group_non_existing');
+    $this->assertEmpty($result);
+
+    $result = $this->contextualLinkManager->getContextualLinkPluginsByGroup('group1');
+    $this->assertEquals(array('test_plugin1', 'test_plugin2'), array_keys($result));
+
+    $result = $this->contextualLinkManager->getContextualLinkPluginsByGroup('group2');
+    $this->assertEquals(array('test_plugin3'), array_keys($result));
+  }
+
+  /**
+   * Tests the getContextualLinkPluginsByGroup method with a prefilled cache.
+   */
+  public function testGetContextualLinkPluginsByGroupWithCache() {
+    $definitions = array(
+      'test_plugin1' => array(
+        'id' => 'test_plugin1',
+        'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
+        'group' => 'group1',
+        'route_name' => 'test_route',
+      ),
+      'test_plugin2' => array(
+        'id' => 'test_plugin2',
+        'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
+        'group' => 'group1',
+        'route_name' => 'test_route2',
+      ),
+    );
+
+    $this->cacheBackend->expects($this->once())
+      ->method('get')
+      ->with('contextual_links_plugins:en:group1')
+      ->will($this->returnValue((object) array('data' => $definitions)));
+
+    $result = $this->contextualLinkManager->getContextualLinkPluginsByGroup('group1');
+    $this->assertEquals($definitions, $result);
+  }
+
+  /**
+   * Tests processDefinition() by passing a plugin definition without a route.
+   *
+   * @see \Drupal\Core\Menu\ContextualLinkManager::processDefinition()
+   *
+   * @expectedException \Drupal\Component\Plugin\Exception\PluginException
+   */
+  public function testProcessDefinitionWithoutRoute() {
+    $definition = array(
+      'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
+      'group' => 'example',
+      'id' => 'test_plugin',
+    );
+    $this->contextualLinkManager->processDefinition($definition, 'test_plugin');
+  }
+
+  /**
+   * Tests processDefinition() by passing a plugin definition without a group.
+   *
+   * @see \Drupal\Core\Menu\ContextualLinkManager::processDefinition()
+   *
+   * @expectedException \Drupal\Component\Plugin\Exception\PluginException
+   */
+  public function testProcessDefinitionWithoutGroup() {
+    $definition = array(
+      'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
+      'route_name' => 'example',
+      'id' => 'test_plugin',
+    );
+    $this->contextualLinkManager->processDefinition($definition, 'test_plugin');
+  }
+
+  /**
+   * Tests the getContextualLinksArrayByGroup method.
+   *
+   * @see \Drupal\Core\Menu\ContextualLinkManager::getContextualLinksArrayByGroup()
+   */
+  public function testGetContextualLinksArrayByGroup() {
+    $definitions = array(
+      'test_plugin1' => array(
+        'id' => 'test_plugin1',
+        'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
+        'title' => 'Plugin 1',
+        'weight' => 0,
+        'group' => 'group1',
+        'route_name' => 'test_route',
+        'options' => array(),
+      ),
+      'test_plugin2' => array(
+        'id' => 'test_plugin2',
+        'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
+        'title' => 'Plugin 2',
+        'weight' => 2,
+        'group' => 'group1',
+        'route_name' => 'test_route2',
+        'options' => array('key' => 'value'),
+      ),
+      'test_plugin3' => array(
+        'id' => 'test_plugin3',
+        'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
+        'title' => 'Plugin 3',
+        'weight' => 5,
+        'group' => 'group2',
+        'route_name' => 'test_router3',
+        'options' => array(),
+      ),
+    );
+
+    $this->pluginDiscovery->expects($this->once())
+      ->method('getDefinitions')
+      ->will($this->returnValue($definitions));
+
+    $this->accessManager->expects($this->any())
+      ->method('checkNamedRoute')
+      ->will($this->returnValue(TRUE));
+
+    // Set up mocking of the plugin factory.
+    $map = array();
+    foreach ($definitions as $plugin_id => $definition) {
+      $plugin = $this->getMock('Drupal\Core\Menu\ContextualLinkInterface');
+      $plugin->expects($this->any())
+        ->method('getRouteName')
+        ->will($this->returnValue($definition['route_name']));
+      $plugin->expects($this->any())
+        ->method('getTitle')
+        ->will($this->returnValue($definition['title']));
+      $plugin->expects($this->any())
+        ->method('getWeight')
+        ->will($this->returnValue($definition['weight']));
+      $plugin->expects($this->any())
+        ->method('getOptions')
+        ->will($this->returnValue($definition['options']));
+      $map[] = array($plugin_id, array(), $plugin);
+    }
+    $this->factory->expects($this->any())
+      ->method('createInstance')
+      ->will($this->returnValueMap($map));
+
+    $this->moduleHandler->expects($this->at(1))
+      ->method('alter')
+      ->with($this->equalTo('contextual_links'), new \PHPUnit_Framework_Constraint_Count(2), $this->equalTo('group1'), $this->equalTo(array('key' => 'value')));
+
+    $result = $this->contextualLinkManager->getContextualLinksArrayByGroup('group1', array('key' => 'value'));
+    $this->assertCount(2, $result);
+    foreach (array('test_plugin1', 'test_plugin2') as $plugin_id) {
+      $definition = $definitions[$plugin_id];
+      $this->assertEquals($definition['weight'], $result[$plugin_id]['weight']);
+      $this->assertEquals($definition['title'], $result[$plugin_id]['title']);
+      $this->assertEquals($definition['route_name'], $result[$plugin_id]['route_name']);
+    }
+  }
+
+  /**
+   * Tests the access checking of the getContextualLinksArrayByGroup method.
+   *
+   * @see \Drupal\Core\Menu\ContextualLinkManager::getContextualLinksArrayByGroup()
+   */
+  public function testGetContextualLinksArrayByGroupAccessCheck() {
+    $definitions = array(
+      'test_plugin1' => array(
+        'id' => 'test_plugin1',
+        'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
+        'title' => 'Plugin 1',
+        'weight' => 0,
+        'group' => 'group1',
+        'route_name' => 'test_route',
+        'options' => array(),
+      ),
+      'test_plugin2' => array(
+        'id' => 'test_plugin2',
+        'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
+        'title' => 'Plugin 2',
+        'weight' => 2,
+        'group' => 'group1',
+        'route_name' => 'test_route2',
+        'options' => array('key' => 'value'),
+      ),
+    );
+
+    $this->pluginDiscovery->expects($this->once())
+      ->method('getDefinitions')
+      ->will($this->returnValue($definitions));
+
+    $this->accessManager->expects($this->any())
+      ->method('checkNamedRoute')
+      ->will($this->returnValueMap(array(
+        array('test_route', array('key' => 'value'), NULL, TRUE),
+        array('test_route2', array('key' => 'value'), NULL, FALSE),
+      )));
+
+    // Set up mocking of the plugin factory.
+    $map = array();
+    foreach ($definitions as $plugin_id => $definition) {
+      $plugin = $this->getMock('Drupal\Core\Menu\ContextualLinkInterface');
+      $plugin->expects($this->any())
+        ->method('getRouteName')
+        ->will($this->returnValue($definition['route_name']));
+      $plugin->expects($this->any())
+        ->method('getTitle')
+        ->will($this->returnValue($definition['title']));
+      $plugin->expects($this->any())
+        ->method('getWeight')
+        ->will($this->returnValue($definition['weight']));
+      $plugin->expects($this->any())
+        ->method('getOptions')
+        ->will($this->returnValue($definition['options']));
+      $map[] = array($plugin_id, array(), $plugin);
+    }
+    $this->factory->expects($this->any())
+      ->method('createInstance')
+      ->will($this->returnValueMap($map));
+
+    $result = $this->contextualLinkManager->getContextualLinksArrayByGroup('group1', array('key' => 'value'));
+
+    // Ensure that access checking was respected.
+    $this->assertTrue(isset($result['test_plugin1']));
+    $this->assertFalse(isset($result['test_plugin2']));
+  }
+
+  /**
+   * Tests the plugins alter hook.
+   */
+  public function testPluginDefinitionAlter() {
+    $definitions['test_plugin'] = array(
+      'id' => 'test_plugin',
+      'class' => '\Drupal\Core\Menu\ContextualLinkDefault',
+      'title' => 'Plugin',
+      'weight' => 2,
+      'group' => 'group1',
+      'route_name' => 'test_route',
+      'options' => array('key' => 'value'),
+    );
+
+    $this->pluginDiscovery->expects($this->once())
+      ->method('getDefinitions')
+      ->will($this->returnValue($definitions));
+
+    $this->moduleHandler->expects($this->once())
+      ->method('alter')
+      ->with('contextual_links_plugins', $definitions);
+
+    $this->contextualLinkManager->getDefinition('test_plugin');
+  }
+
+}
