diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php
index 37550a4..70e0461 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;
@@ -240,7 +241,7 @@ 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]));
@@ -248,13 +249,25 @@ public static function fromUri($uri, $options = array()) {
     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]));
     }
+    $uri_parts += ['path' => '', 'fragment' => ''];
+    // Extract query parameters and fragment and merge them into $options.
+    $options += ['fragment' => $uri_parts['fragment']];
+    unset($uri_parts['fragment']);
+    if (!empty($uri_parts['query'])) {
+      $uri_query = [];
+      parse_str($uri_parts['query'], $uri_query);
+      $options['query'] += $uri_query;
+      unset($uri_parts['query']);
+    }
 
-    $uri_parts += ['path' => ''];
     if ($uri_parts['scheme'] === 'entity') {
-      $url = static::fromEntityUri($uri);
+      $url = static::fromEntityUri($uri_parts, $options);
     }
     elseif ($uri_parts['scheme'] === 'user-path') {
-      $url = static::fromUserPathUri($uri, $options);
+      $url = static::fromUserPathUri($uri_parts, $options);
+    }
+    elseif ($uri_parts['scheme'] === 'route') {
+      $url = static::fromRouteUri($uri_parts, $options);
     }
     else {
       $url = new static($uri, array(), $options);
@@ -271,8 +284,10 @@ public static function fromUri($uri, $options = array()) {
   /**
    * Create a new Url object for entity URIs.
    *
-   * @param string $uri
-   *   An URI of the form entity:{entity_type}/{entity_id}.
+   * @param array $uri_parts
+   *   Parts from an URI of the form entity:{entity_type}/{entity_id}.
+   * @param array $options
+   *   An array of options, see static::fromUri() for details.
    *
    * @return \Drupal\Core\Url
    *   A new Url object for an entity's canonical route.
@@ -280,38 +295,60 @@ 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) {
     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}.
    * @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:{path}. Where path is the route name
+   *   optionally followed by a ";" followed by query parameters in key=value
+   *   format with & separators.
+   * @param array $options
+   *   An array of options, see static::fromUri() for details.
+   *
+   * @return \Drupal\Core\Url
+   *   A new Url object for a 'route:' URI.
+   */
+  protected static function fromRouteUri(array $uri_parts, array $options) {
+    $route_parts = explode(';', $uri_parts['path'], 2);
+    $route_name = $route_parts[0];
+    $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 +395,25 @@ protected function setUnrouted() {
   }
 
   /**
+   * @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($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/tests/Drupal/Tests/Core/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php
index 9d883d7..9c5efcb 100644
--- a/core/tests/Drupal/Tests/Core/UrlTest.php
+++ b/core/tests/Drupal/Tests/Core/UrlTest.php
@@ -489,6 +489,18 @@ 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());
+    // Ensure the options are parsed out.
+    $url = Url::fromUri('entity:test_entity/2?page=1&foo=bar#bottom');
+    $this->assertSame('entity.test_entity.canonical', $url->getRouteName());
+    $this->assertEquals(['test_entity' => '2'], $url->getRouteParameters());
+    $this->assertEquals($url->getOption('query'), ['page' => '1', 'foo' => 'bar']);
+    $this->assertSame($url->getOption('fragment'), 'bottom');
+    // Ensure the options are parsed out and merged with $options.
+    $url = Url::fromUri('entity:test_entity/2?page=1&foo=bar#bottom', ['fragment' => 'top', 'query' => ['foo' => 'yes', 'focus' => 'no']]);
+    $this->assertSame('entity.test_entity.canonical', $url->getRouteName());
+    $this->assertEquals(['test_entity' => '2'], $url->getRouteParameters());
+    $this->assertEquals($url->getOption('query'), ['page' => '1', 'foo' => 'yes', 'focus' => 'no']);
+    $this->assertSame($url->getOption('fragment'), 'top');
   }
 
   /**
@@ -508,6 +520,55 @@ public function testInvalidEntityUriParameter() {
   }
 
   /**
+   * Tests the toUriString() method with entity: URI.
+   *
+   * @covers ::toUriString
+   */
+  public function testToUriStringForEntity() {
+    $url = Url::fromUri('entity:test_entity/1');
+    $this->assertSame($url->toUriString(), 'route:entity.test_entity.canonical;test_entity=1');
+    $url = Url::fromUri('entity:test_entity/1', ['fragment' => 'top', 'query' => ['page' => '2']]);
+    $this->assertSame($url->toUriString(), 'route:entity.test_entity.canonical;test_entity=1?page=2#top');
+    $url = Url::fromUri('entity:test_entity/1?page=2#top');
+    $this->assertSame($url->toUriString(), 'route:entity.test_entity.canonical;test_entity=1?page=2#top');
+  }
+
+  /**
+   * Tests the toUriString() method with user-path: URI.
+   *
+   * @covers ::toUriString
+   */
+  public function testToUriStringForUserPath() {
+    $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('user-path:test-entity/1');
+    $this->assertSame($url->toUriString(), 'route:entity.test_entity.canonical;test_entity=1');
+    $url = Url::fromUri('user-path:test-entity/1', ['fragment' => 'top']);
+    $this->assertSame($url->toUriString(), 'route:entity.test_entity.canonical;test_entity=1#top');
+    $url = Url::fromUri('user-path:test-entity/1', ['fragment' => 'top', 'query' => ['page' => '2']]);
+    $this->assertSame($url->toUriString(), 'route:entity.test_entity.canonical;test_entity=1?page=2#top');
+    $url = Url::fromUri('user-path:test-entity/1?page=2#top');
+    $this->assertSame($url->toUriString(), 'route:entity.test_entity.canonical;test_entity=1?page=2#top');
+  }
+
+  /**
+   * Tests the toUriString() method with route: URI.
+   *
+   * @covers ::toUriString
+   */
+  public function testToUriStringForRoute() {
+    $url = Url::fromUri('route:entity.test_entity.canonical;test_entity=1');
+    $this->assertSame($url->toUriString(), 'route:entity.test_entity.canonical;test_entity=1');
+    $url = Url::fromUri('route:entity.test_entity.canonical;test_entity=1', ['fragment' => 'top', 'query' => ['page' => '2']]);
+    $this->assertSame($url->toUriString(), 'route:entity.test_entity.canonical;test_entity=1?page=2#top');
+    $url = Url::fromUri('route:entity.test_entity.canonical;test_entity=1?page=2#top');
+    $this->assertSame($url->toUriString(), 'route:entity.test_entity.canonical;test_entity=1?page=2#top');
+  }
+
+  /**
    * Creates a mock access manager for the access tests.
    *
    * @param bool $access
