diff --git a/core/core.services.yml b/core/core.services.yml
index c55526383e3945bcd665af7a4c4265769f7e4d27..75198f28dd19958f252df030a8a11a9ba0925a50 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -311,6 +311,11 @@ services:
     parent: container.trait
     tags:
       - { name: plugin_manager_cache_clear }
+  entity_route_subscriber:
+    class: Drupal\Core\EventSubscriber\EntityRouteProviderSubscriber
+    arguments: ['@entity.manager']
+    tags:
+      - { name: event_subscriber }
   entity.definitions.installed:
     class: Drupal\Core\KeyValueStore\KeyValueStoreInterface
     factory_method: get
diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php
index df404d8330468bd128f944a081818d3d03392282..06fb6a357ede587955bba46c47a26fcb7f6f24f6 100644
--- a/core/lib/Drupal/Core/Entity/EntityManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityManager.php
@@ -300,6 +300,20 @@ public function getFormObject($entity_type, $operation) {
     return $this->handlers['form'][$operation][$entity_type];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteProviders($entity_type) {
+    if (!isset($this->handlers['route_provider'][$entity_type])) {
+      $route_provider_classes = $this->getDefinition($entity_type, TRUE)->getRouteProviderClasses();
+
+      foreach ($route_provider_classes as $type => $class) {
+        $this->handlers['route_provider'][$entity_type][$type] = $this->createHandlerInstance($class, $this->getDefinition($entity_type));
+      }
+    }
+    return isset($this->handlers['route_provider'][$entity_type]) ? $this->handlers['route_provider'][$entity_type] : [];
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php
index 13f9862d5099d2a77ee73c6c5cb6fd345a41b112..d60c47d7366d7396330b9cf5f493e59e8f5364fb 100644
--- a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php
@@ -223,6 +223,16 @@ public function getListBuilder($entity_type);
    */
   public function getFormObject($entity_type, $operation);
 
+  /**
+   * Gets all route provider instances.
+   *
+   * @param string $entity_type
+   *   The entity type for this route providers.
+   *
+   * @return \Drupal\Core\Entity\Routing\EntityRouteProviderInterface[]
+   */
+  public function getRouteProviders($entity_type);
+
   /**
    * Checks whether a certain entity type has a certain handler.
    *
diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php
index c5d90ca3ec03eb8a02ef002f67fe7c6f1f5eec67..9c99d0a513711a688bfff4027080703cf0119ec6 100644
--- a/core/lib/Drupal/Core/Entity/EntityType.php
+++ b/core/lib/Drupal/Core/Entity/EntityType.php
@@ -433,6 +433,13 @@ public function hasFormClasses() {
     return !empty($this->handlers['form']);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function hasRouteProviders() {
+    return !empty($this->handlers['route_provider']);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -477,6 +484,13 @@ public function hasViewBuilderClass() {
     return $this->hasHandlerClass('view_builder');
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteProviderClasses() {
+    return !empty($this->handlers['route_provider']) ? $this->handlers['route_provider'] : [];
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php
index e2ba2a9a037d054ea709118da430c0a0c17b58fb..7a87db93ad3eb052350b9cea4a2f2054e84a2c4f 100644
--- a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php
@@ -225,6 +225,10 @@ public function getHandlerClass($handler_type);
    *   - access: The name of the class that is used for access checks. The class
    *     must implement \Drupal\Core\Entity\EntityAccessControlHandlerInterface.
    *     Defaults to \Drupal\Core\Entity\EntityAccessControlHandler.
+   *   - route_provider: (optional) A list of class names, keyed by a group
+   *     string, which will be used to define routes related to this entity
+   *     type. These classes must implement
+   *     \Drupal\Core\Entity\Routing\EntityRouteProviderInterface.
    */
   public function getHandlerClasses();
 
@@ -282,6 +286,22 @@ public function setFormClass($operation, $class);
    */
   public function hasFormClasses();
 
+  /**
+   * Indicates if this entity type has any route provider.
+   *
+   * @return bool
+   */
+  public function hasRouteProviders();
+
+  /**
+   * Gets all the route provide handlers.
+   *
+   * Much like forms you can define multiple route provider handlers.
+   *
+   * @return string[]
+   */
+  public function getRouteProviderClasses();
+
   /**
    * Returns the list class.
    *
diff --git a/core/lib/Drupal/Core/Entity/Routing/EntityRouteProviderInterface.php b/core/lib/Drupal/Core/Entity/Routing/EntityRouteProviderInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..a7daa9463e4f0664e166ec27864c5f1c34f11c62
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Routing/EntityRouteProviderInterface.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\Routing\EntityRouteProviderInterface.
+ */
+
+namespace Drupal\Core\Entity\Routing;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Allows entity types to provide routes.
+ */
+interface EntityRouteProviderInterface {
+
+  /**
+   * Provides routes for entities.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type
+   *
+   * @return \Symfony\Component\Routing\RouteCollection|\Symfony\Component\Routing\Route[]
+   *   Returns a route collection or an array of routes keyed by name, like
+   *   route_callbacks inside 'routing.yml' files.
+   */
+  public function getRoutes(EntityTypeInterface $entity_type);
+
+}
diff --git a/core/lib/Drupal/Core/EventSubscriber/EntityRouteProviderSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/EntityRouteProviderSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..d5c4def45017314c1cb9ce64313734755382bd1c
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/EntityRouteProviderSubscriber.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\EntityRouteProviderSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Routing\RouteBuildEvent;
+use Drupal\Core\Routing\RoutingEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Ensures that routes can be provided by entity types.
+ */
+class EntityRouteProviderSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * Constructs a new EntityRouteProviderSubscriber instance.
+   *
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   */
+  public function __construct(EntityManagerInterface $entity_manager) {
+    $this->entityManager = $entity_manager;
+  }
+
+  /**
+   * Provides routes on route rebuild time.
+   *
+   * @param \Drupal\Core\Routing\RouteBuildEvent $event
+   *   The route build event.
+   */
+  public function onDynamicRouteEvent(RouteBuildEvent $event) {
+    $route_collection = $event->getRouteCollection();
+    foreach ($this->entityManager->getDefinitions() as $entity_type) {
+      if ($entity_type->hasRouteProviders()) {
+        foreach ($this->entityManager->getRouteProviders($entity_type->id()) as $route_provider) {
+          // Allow to both return an array of routes or a route collection,
+          // like route_callbacks in the routing.yml file.
+          $routes = $route_provider->getRoutes($entity_type);
+          if ($routes instanceof RouteCollection) {
+            $route_collection->addCollection($routes);
+          }
+          elseif (is_array($routes)) {
+            foreach ($routes as $route_name => $route) {
+              $route_collection->add($route_name, $route);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[RoutingEvents::DYNAMIC][] = ['onDynamicRouteEvent'];
+    return $events;
+  }
+
+}
+
diff --git a/core/modules/node/node.routing.yml b/core/modules/node/node.routing.yml
index bb431fb4490e43e3dac6c2a7288afff550f9132d..3510669f63eef841abf5104247d4dcb360891ad3 100644
--- a/core/modules/node/node.routing.yml
+++ b/core/modules/node/node.routing.yml
@@ -5,15 +5,6 @@ node.multiple_delete_confirm:
   requirements:
     _permission: 'administer nodes'
 
-entity.node.edit_form:
-  path: '/node/{node}/edit'
-  defaults:
-    _entity_form: 'node.edit'
-  requirements:
-    _entity_access: 'node.update'
-  options:
-    _node_operation_route: TRUE
-
 node.add_page:
   path: '/node/add'
   defaults:
@@ -48,24 +39,6 @@ entity.node.preview:
       node_preview:
         type: 'node_preview'
 
-entity.node.canonical:
-  path: '/node/{node}'
-  defaults:
-    _controller: '\Drupal\node\Controller\NodeViewController::view'
-    _title_callback: '\Drupal\node\Controller\NodeViewController::title'
-  requirements:
-    _entity_access: 'node.view'
-
-entity.node.delete_form:
-  path: '/node/{node}/delete'
-  defaults:
-    _entity_form: 'node.delete'
-    _title: 'Delete'
-  requirements:
-    _entity_access: 'node.delete'
-  options:
-    _node_operation_route: TRUE
-
 entity.node.version_history:
   path: '/node/{node}/revisions'
   defaults:
diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php
index ff4f376681ac162992b784649fc299eb5243bc47..4e7dc84a49a4d797b86e904520b3671ab999bd2a 100644
--- a/core/modules/node/src/Entity/Node.php
+++ b/core/modules/node/src/Entity/Node.php
@@ -34,6 +34,9 @@
  *       "delete" = "Drupal\node\Form\NodeDeleteForm",
  *       "edit" = "Drupal\node\NodeForm"
  *     },
+ *     "route_provider" = {
+ *       "html" = "Drupal\node\Entity\NodeRouteProvider",
+ *     },
  *     "list_builder" = "Drupal\node\NodeListBuilder",
  *     "translation" = "Drupal\node\NodeTranslationHandler"
  *   },
diff --git a/core/modules/node/src/Entity/NodeRouteProvider.php b/core/modules/node/src/Entity/NodeRouteProvider.php
new file mode 100644
index 0000000000000000000000000000000000000000..fa1c02d7fc359e57c8acdd3ebef5eed824d7424d
--- /dev/null
+++ b/core/modules/node/src/Entity/NodeRouteProvider.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Entity\NodeRouteProvider.
+ */
+
+namespace Drupal\node\Entity;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Provides routes for nodes.
+ */
+class NodeRouteProvider implements EntityRouteProviderInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRoutes( EntityTypeInterface $entity_type) {
+    $route_collection = new RouteCollection();
+    $route = (new Route('/node/{node}'))
+      ->addDefaults([
+        '_controller' => '\Drupal\node\Controller\NodeViewController::view',
+        '_title_callback' => '\Drupal\node\Controller\NodeViewController::title',
+      ])
+      ->setRequirement('_entity_access', 'node.view');
+    $route_collection->add('entity.node.canonical', $route);
+
+    $route = (new Route('/node/{node}/delete'))
+      ->addDefaults([
+        '_entity_form' => 'node.delete',
+        '_title' => 'Delete',
+      ])
+      ->setRequirement('_entity_access', 'node.delete')
+      ->setOption('_node_operation_route', TRUE);
+    $route_collection->add('entity.node.delete_form', $route);
+
+    $route = (new Route('/node/{node}/edit'))
+      ->setDefault('_entity_form', 'node.edit')
+      ->setRequirement('_entity_access', 'node.update')
+      ->setOption('_node_operation_route', TRUE);
+    $route_collection->add('entity.node.edit_form', $route);
+
+    return $route_collection;
+  }
+
+}
diff --git a/core/modules/user/src/Entity/User.php b/core/modules/user/src/Entity/User.php
index 2ac5fec2aea2bcfe3749e66a8a7c9cb94e321a10..a32298e64430bb365a40cb7f87b33be2af9a74cf 100644
--- a/core/modules/user/src/Entity/User.php
+++ b/core/modules/user/src/Entity/User.php
@@ -30,6 +30,9 @@
  *     "list_builder" = "Drupal\user\UserListBuilder",
  *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
  *     "views_data" = "Drupal\user\UserViewsData",
+ *     "route_provider" = {
+ *       "html" = "Drupal\user\Entity\UserRouteProvider",
+ *     },
  *     "form" = {
  *       "default" = "Drupal\user\ProfileForm",
  *       "cancel" = "Drupal\user\Form\UserCancelForm",
diff --git a/core/modules/user/src/Entity/UserRouteProvider.php b/core/modules/user/src/Entity/UserRouteProvider.php
new file mode 100644
index 0000000000000000000000000000000000000000..f4cfd792866581e979d6d1f5884517fefdefb14b
--- /dev/null
+++ b/core/modules/user/src/Entity/UserRouteProvider.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Entity\UserRouteProvider.
+ */
+
+namespace Drupal\user\Entity;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Provides routes for the user entity.
+ */
+class UserRouteProvider implements EntityRouteProviderInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRoutes(EntityTypeInterface $entity_type) {
+    $route_collection = new RouteCollection();
+    $route = (new Route('/user/{user}'))
+      ->setDefaults([
+        '_entity_view' => 'user.full',
+        '_title_callback' => 'Drupal\user\Controller\UserController::userTitle',
+      ])
+      ->setRequirement('_entity_access', 'user.view');
+    $route_collection->add('entity.user.canonical', $route);
+
+    $route = (new Route('/user/{user}/edit'))
+      ->setDefaults([
+        '_entity_form' => 'user.default',
+        '_title_callback' => 'Drupal\user\Controller\UserController::userTitle',
+      ])
+      ->setOption('_admin_route', TRUE)
+      ->setRequirement('_entity_access', 'user.update');
+    $route_collection->add('entity.user.edit_form', $route);
+
+    $route = (new Route('/user/{user}/cancel'))
+      ->setDefaults([
+        '_title' => 'Cancel account',
+        '_entity_form' => 'user.cancel',
+      ])
+      ->setOption('_admin_route', TRUE)
+      ->setRequirement('_entity_access', 'user.delete');
+    $route_collection->add('entity.user.cancel_form', $route);
+
+    return $route_collection;
+  }
+
+}
diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml
index 4e1b99df6b43ed967cb5d7739e77a0c558880816..d071113e9ef9a1aab895dddc90071fd9764f50f0 100644
--- a/core/modules/user/user.routing.yml
+++ b/core/modules/user/user.routing.yml
@@ -133,14 +133,6 @@ user.page:
   requirements:
     _user_is_logged_in: 'TRUE'
 
-entity.user.canonical:
-  path: '/user/{user}'
-  defaults:
-    _entity_view: 'user.full'
-    _title_callback: 'Drupal\user\Controller\UserController::userTitle'
-  requirements:
-    _entity_access: 'user.view'
-
 user.login:
   path: '/user/login'
   defaults:
@@ -151,26 +143,6 @@ user.login:
   options:
     _maintenance_access: TRUE
 
-entity.user.edit_form:
-  path: '/user/{user}/edit'
-  defaults:
-    _entity_form: 'user.default'
-    _title_callback: 'Drupal\user\Controller\UserController::userTitle'
-  options:
-    _admin_route: TRUE
-  requirements:
-    _entity_access: 'user.update'
-
-entity.user.cancel_form:
-  path: '/user/{user}/cancel'
-  defaults:
-    _title: 'Cancel account'
-    _entity_form: 'user.cancel'
-  options:
-    _admin_route: TRUE
-  requirements:
-    _entity_access: 'user.delete'
-
 user.cancel_confirm:
   path: '/user/{user}/cancel/confirm/{timestamp}/{hashed_pass}'
   defaults:
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
index ce6cf2798a69120b36b81087f064f9af42974bc6..caea0e566a03b5628afc366d3708828d967596d3 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
@@ -1366,6 +1366,24 @@ public function testGetEntityTypeFromClassAmbiguous() {
     $this->entityManager->getEntityTypeFromClass('\Drupal\apple\Entity\Apple');
   }
 
+  /**
+   * @covers ::getRouteProviders
+   */
+  public function testGetRouteProviders() {
+    $apple = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
+    $apple->expects($this->once())
+      ->method('getRouteProviderClasses')
+      ->willReturn(['default' => 'Drupal\Tests\Core\Entity\TestRouteProvider']);
+    $this->setUpEntityManager(array(
+      'apple' => $apple,
+    ));
+
+    $apple_route_provider = $this->entityManager->getRouteProviders('apple');
+    $this->assertInstanceOf('Drupal\Tests\Core\Entity\TestRouteProvider', $apple_route_provider['default']);
+    $this->assertAttributeInstanceOf('Drupal\Core\Extension\ModuleHandlerInterface', 'moduleHandler', $apple_route_provider['default']);
+    $this->assertAttributeInstanceOf('Drupal\Core\StringTranslation\TranslationInterface', 'stringTranslation', $apple_route_provider['default']);
+  }
+
   /**
    * Gets a mock controller class name.
    *
@@ -1546,6 +1564,14 @@ public static function create(ContainerInterface $container) {
 
 }
 
+/**
+ * Provides a test entity route provider.
+ */
+class TestRouteProvider extends EntityHandlerBase {
+
+}
+
+
 /**
  * Provides a test config entity storage for base field overrides.
  */