diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php
index 37550a4..b2e3062 100644
--- a/core/lib/Drupal/Core/Url.php
+++ b/core/lib/Drupal/Core/Url.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core;
 
 use Drupal\Component\Utility\String;
+use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Routing\UrlGeneratorInterface;
@@ -213,7 +214,7 @@ public static function fromRouteMatch(RouteMatchInterface $route_match) {
    *   (optional) An associative array of additional URL options, with the
    *   following elements:
    *   - 'query': An array of query key/value-pairs (without any URL-encoding)
-   *     to append to the URL. Merged with the parameters array.
+   *     to append to the URL.
    *   - 'fragment': A fragment identifier (named anchor) to append to the URL.
    *     Do not include the leading '#' character.
    *   - 'absolute': Defaults to FALSE. Whether to force the output to be an
@@ -240,21 +241,37 @@ public static function fromRouteMatch(RouteMatchInterface $route_match) {
    *
    * @see static::fromRoute()
    */
-  public static function fromUri($uri, $options = array()) {
+  public static function fromUri($uri, $options = []) {
     $uri_parts = parse_url($uri);
     if ($uri_parts === FALSE) {
       throw new \InvalidArgumentException(String::format('The URI "@uri" is malformed.', ['@uri' => $uri]));
     }
     if (empty($uri_parts['scheme'])) {
-      throw new \InvalidArgumentException(String::format('The URI "@uri" is invalid. You must use a valid URI scheme. Use base: for items like a static file that needs the base path. Use user-path: for user input without a scheme. Use entity: for referencing the canonical route of a content entity.', ['@uri' => $uri]));
+      throw new \InvalidArgumentException(String::format('The URI "@uri" is invalid. You must use a valid URI scheme. Use base: for items like a static file that needs the base path. Use user-path: for user input without a scheme. Use entity: for referencing the canonical route of a content entity. Use route: for directly representing a route name and parameters.', ['@uri' => $uri]));
     }
-
     $uri_parts += ['path' => ''];
+    // Extract query parameters and fragment and merge them into $uri_options,
+    // but preserve the original $options for the fallback case.
+    $uri_options = $options;
+    if (!empty($uri_parts['fragment'])) {
+      $uri_options += ['fragment' => $uri_parts['fragment']];
+    }
+    unset($uri_parts['fragment']);
+    if (!empty($uri_parts['query'])) {
+      $uri_query = [];
+      parse_str($uri_parts['query'], $uri_query);
+      $uri_options['query'] = isset($uri_options['query']) ? $uri_options['query'] + $uri_query : $uri_query;
+      unset($uri_parts['query']);
+    }
+
     if ($uri_parts['scheme'] === 'entity') {
-      $url = static::fromEntityUri($uri);
+      $url = static::fromEntityUri($uri_parts, $uri_options, $uri);
     }
     elseif ($uri_parts['scheme'] === 'user-path') {
-      $url = static::fromUserPathUri($uri, $options);
+      $url = static::fromUserPathUri($uri_parts, $uri_options);
+    }
+    elseif ($uri_parts['scheme'] === 'route') {
+      $url = static::fromRouteUri($uri_parts, $uri_options, $uri);
     }
     else {
       $url = new static($uri, array(), $options);
@@ -271,8 +288,13 @@ public static function fromUri($uri, $options = array()) {
   /**
    * Create a new Url object for entity URIs.
    *
+   * @param array $uri_parts
+   *   Parts from an URI of the form entity:{entity_type}/{entity_id} as from
+   *   parse_url().
+   * @param array $options
+   *   An array of options, see static::fromUri() for details.
    * @param string $uri
-   *   An URI of the form entity:{entity_type}/{entity_id}.
+   *   The original entered URI.
    *
    * @return \Drupal\Core\Url
    *   A new Url object for an entity's canonical route.
@@ -280,38 +302,68 @@ public static function fromUri($uri, $options = array()) {
    * @throws \InvalidArgumentException
    *   Thrown if the entity URI is invalid.
    */
-  protected static function fromEntityUri($uri) {
-    $uri_parts = parse_url($uri);
+  protected static function fromEntityUri(array $uri_parts, $options, $uri) {
     list($entity_type_id, $entity_id) = explode('/', $uri_parts['path'], 2);
     if ($uri_parts['scheme'] != 'entity' || $entity_id === '') {
       throw new \InvalidArgumentException(String::format('The entity URI "@uri" is invalid. You must specify the entity id in the URL. e.g., entity:node/1 for loading the canonical path to node entity with id 1.', ['@uri' => $uri]));
     }
 
-    return new static("entity.$entity_type_id.canonical", [$entity_type_id => $entity_id]);
+    return new static("entity.$entity_type_id.canonical", [$entity_type_id => $entity_id], $options);
   }
 
   /**
    * Creates a new Url object for 'user-path:' URIs.
    *
-   * @param string $uri
-   *   An URI of the form user-path:{path}.
+   * @param array $uri_parts
+   *   Parts from an URI of the form user-path:{path} as from parse_url().
    * @param array $options
    *   An array of options, see static::fromUri() for details.
    *
    * @return \Drupal\Core\Url
    *   A new Url object for a 'user-path:' URI.
    */
-  protected static function fromUserPathUri($uri, array $options) {
-    $uri_reference = explode(':', $uri, 2)[1];
+  protected static function fromUserPathUri(array $uri_parts, array $options) {
     $url = \Drupal::pathValidator()
-      ->getUrlIfValidWithoutAccessCheck($uri_reference) ?: static::fromUri('base:' . $uri_reference);
-    // Allow to specify additional or override options from the path.
+      ->getUrlIfValidWithoutAccessCheck($uri_parts['path']) ?: static::fromUri('base:' . $uri_parts['path'], $options);
+    // Allow specifying additional options.
     $url->setOptions($options + $url->getOptions());
 
     return $url;
   }
 
   /**
+   * Creates a new Url object for 'route:' URIs.
+   *
+   * @param array $uri_parts
+   *   Parts from an URI of the form route:{route_name};{route_parameters} as
+   *   from parse_url(), where the path is the route name optionally followed by
+   *   a ";" followed by route parameters in key=value format with & separators.
+   * @param array $options
+   *   An array of options, see static::fromUri() for details.
+   * @param string $uri
+   *   The original passed in URI.
+   *
+   * @return \Drupal\Core\Url
+   *   A new Url object for a 'route:' URI.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when the route URI does not have a route name.
+   */
+  protected static function fromRouteUri(array $uri_parts, array $options, $uri) {
+    $route_parts = explode(';', $uri_parts['path'], 2);
+    $route_name = $route_parts[0];
+    if ($route_name === '') {
+      throw new \InvalidArgumentException(String::format('The route URI "@uri" is invalid. You must have a route name in the URI. e.g., route:system.admin', ['@uri' => $uri]));
+    }
+    $route_parameters = [];
+    if (!empty($route_parts[1])) {
+      parse_str($route_parts[1], $route_parameters);
+    }
+
+    return new static($route_name, $route_parameters, $options);
+  }
+
+  /**
    * Returns the Url object matching a request.
    *
    * SECURITY NOTE: The request path is not checked to be valid and accessible
@@ -358,6 +410,32 @@ protected function setUnrouted() {
   }
 
   /**
+   * Return a URI string that represents tha data in the Url object.
+   *
+   * The URI will typically have the scheme of route: even if the object was
+   * constructed using an entity: or user-path: scheme.  A user-path: URI
+   * that does not match a Drupal route with be returned here with the base:
+   * scheme, and external URLs will be returned in their original form.
+   *
+   * @return string
+   *   A URI representation of the Url object data.
+   */
+  public function toUriString() {
+    if ($this->isRouted()) {
+      $uri = 'route:' . $this->routeName;
+      if ($this->routeParameters) {
+        $uri .= ';' . UrlHelper::buildQuery($this->routeParameters);
+      }
+    }
+    else {
+      $uri = $this->uri;
+    }
+    $query = !empty($this->options['query']) ? ('?' . UrlHelper::buildQuery($this->options['query'])) : '';
+    $fragment = !empty($this->options['fragment']) ? '#' . $this->options['fragment'] : '';
+    return $uri . $query . $fragment;
+  }
+
+  /**
    * Indicates if this Url is external.
    *
    * @return bool
diff --git a/core/modules/system/src/Tests/Common/UrlTest.php b/core/modules/system/src/Tests/Common/UrlTest.php
index 45ae2d3..0b8a223 100644
--- a/core/modules/system/src/Tests/Common/UrlTest.php
+++ b/core/modules/system/src/Tests/Common/UrlTest.php
@@ -265,7 +265,7 @@ function testExternalUrls() {
     // Verify external URL can contain a query string.
     $url = $test_url . '?drupal=awesome';
     $result = Url::fromUri($url)->toString();
-    $this->assertEqual($url, $result, 'External URL with query string works without a query string in $options.');
+    $this->assertEqual($url, $result);
 
     // Verify external URL can be extended with a query string.
     $url = $test_url;
@@ -277,6 +277,6 @@ function testExternalUrls() {
     $url = $test_url . '?drupal=awesome';
     $query = array($this->randomMachineName(5) => $this->randomMachineName(5));
     $result = Url::fromUri($url, array('query' => $query))->toString();
-    $this->assertEqual($url . '&' . http_build_query($query, '', '&'), $result, 'External URL query string can be extended with a custom query string in $options.');
+    $this->assertEqual($url . '&' . http_build_query($query, '', '&'), $result);
   }
 }
diff --git a/core/tests/Drupal/Tests/Core/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php
index 9d883d7..b26feb8 100644
--- a/core/tests/Drupal/Tests/Core/UrlTest.php
+++ b/core/tests/Drupal/Tests/Core/UrlTest.php
@@ -9,7 +9,6 @@
 
 use Drupal\Core\Access\AccessManagerInterface;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
-use Drupal\Core\Entity\EntityType;
 use Drupal\Core\Routing\RouteMatch;
 use Drupal\Core\Url;
 use Drupal\Tests\UnitTestCase;
@@ -18,7 +17,6 @@
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\Routing\Exception\InvalidParameterException;
 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
-use Symfony\Component\Routing\Exception\RouteNotFoundException;
 use Symfony\Component\Routing\Route;
 
 /**
@@ -481,14 +479,50 @@ public function testFromRouteMatch() {
   }
 
   /**
+   * Data provider for testing entity URIs
+   */
+  public function providerTestEntityUris() {
+    return [
+      [
+        'entity:test_entity/1',
+        [],
+        'entity.test_entity.canonical',
+        ['test_entity' => '1'],
+        NULL,
+        NULL,
+      ],
+      [
+        'entity:test_entity/2?page=1&foo=bar#bottom',
+        [], 'entity.test_entity.canonical',
+        ['test_entity' => '2'],
+        ['page' => '1', 'foo' => 'bar'],
+        'bottom',
+      ],
+      [
+        'entity:test_entity/2?page=1&foo=bar#bottom',
+        ['fragment' => 'top', 'query' => ['foo' => 'yes', 'focus' => 'no']],
+        'entity.test_entity.canonical',
+        ['test_entity' => '2'],
+        ['page' => '1', 'foo' => 'yes', 'focus' => 'no'],
+        'top',
+      ],
+
+    ];
+  }
+
+  /**
    * Tests the fromUri() method with an entity: URI.
    *
    * @covers ::fromUri
+   *
+   * @dataProvider providerTestEntityUris
    */
-  public function testEntityUris() {
-    $url = Url::fromUri('entity:test_entity/1');
-    $this->assertSame('entity.test_entity.canonical', $url->getRouteName());
-    $this->assertEquals(['test_entity' => '1'], $url->getRouteParameters());
+  public function testEntityUris($uri, $options, $route_name, $route_parameters, $query, $fragment) {
+    $url = Url::fromUri($uri, $options);
+    $this->assertSame($route_name, $url->getRouteName());
+    $this->assertEquals($route_parameters, $url->getRouteParameters());
+    $this->assertEquals($url->getOption('query'), $query);
+    $this->assertSame($url->getOption('fragment'), $fragment);
   }
 
   /**
@@ -508,6 +542,89 @@ public function testInvalidEntityUriParameter() {
   }
 
   /**
+   * Tests the toUriString() method with entity: URIs.
+   *
+   * @covers ::toUriString
+   *
+   * @dataProvider providerTestToUriStringForEntity
+   */
+  public function testToUriStringForEntity($uri, $options, $uri_string) {
+    $url = Url::fromUri($uri, $options);
+    $this->assertSame($url->toUriString(), $uri_string);
+  }
+
+  /**
+   * Data provider for testing string entity URIs
+   */
+  public function providerTestToUriStringForEntity() {
+    return [
+      ['entity:test_entity/1', [], 'route:entity.test_entity.canonical;test_entity=1'],
+      ['entity:test_entity/1', ['fragment' => 'top', 'query' => ['page' => '2']], 'route:entity.test_entity.canonical;test_entity=1?page=2#top'],
+      ['entity:test_entity/1?page=2#top', [], 'route:entity.test_entity.canonical;test_entity=1?page=2#top'],
+    ];
+  }
+
+  /**
+   * Tests the toUriString() method with user-path: URIs.
+   *
+   * @covers ::toUriString
+   *
+   * @dataProvider providerTestToUriStringForUserPath
+   */
+  public function testToUriStringForUserPath($uri, $options, $uri_string) {
+    $url = Url::fromRoute('entity.test_entity.canonical', ['test_entity' => '1']);
+    $this->pathValidator->expects($this->any())
+      ->method('getUrlIfValidWithoutAccessCheck')
+      ->with('test-entity/1')
+      ->willReturn($url);
+    $url = Url::fromUri($uri, $options);
+    $this->assertSame($url->toUriString(), $uri_string);
+  }
+
+  /**
+   * Data provider for testing user-path URIs
+   */
+  public function providerTestToUriStringForUserPath() {
+    return [
+      ['user-path:test-entity/1', [], 'route:entity.test_entity.canonical;test_entity=1'],
+      ['user-path:test-entity/1', ['fragment' => 'top'], 'route:entity.test_entity.canonical;test_entity=1#top'],
+      ['user-path:test-entity/1', ['fragment' => 'top', 'query' => ['page' => '2']], 'route:entity.test_entity.canonical;test_entity=1?page=2#top'],
+      ['user-path:test-entity/1?page=2#top', [], 'route:entity.test_entity.canonical;test_entity=1?page=2#top'],
+    ];
+  }
+
+  /**
+   * Tests the toUriString() method with route: URIs.
+   *
+   * @covers ::toUriString
+   *
+   * @dataProvider providerTestToUriStringForRoute
+   */
+  public function testToUriStringForRoute($uri, $options, $uri_string) {
+    $url = Url::fromUri($uri, $options);
+    $this->assertSame($url->toUriString(), $uri_string);
+  }
+
+  /**
+   * Data provider for testing user-path URIs
+   */
+  public function providerTestToUriStringForRoute() {
+    return [
+      ['route:entity.test_entity.canonical;test_entity=1', [], 'route:entity.test_entity.canonical;test_entity=1'],
+      ['route:entity.test_entity.canonical;test_entity=1', ['fragment' => 'top', 'query' => ['page' => '2']], 'route:entity.test_entity.canonical;test_entity=1?page=2#top'],
+      ['route:entity.test_entity.canonical;test_entity=1?page=2#top', [], 'route:entity.test_entity.canonical;test_entity=1?page=2#top'],
+    ];
+  }
+
+  /**
+   * @expectedException \InvalidArgumentException
+   * @expectedExceptionMessage The route URI "route:" is invalid.
+   */
+  public function testFromRouteUriWithMissingRouteName() {
+    Url::fromUri('route:');
+  }
+
+  /**
    * Creates a mock access manager for the access tests.
    *
    * @param bool $access
