diff --git a/core/core.services.yml b/core/core.services.yml
index fee9101701ce33a46a3f68e63eba093b4c48fa28..9d6ad2711a12597e054d81e2139ceaa894aa0bf9 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -465,10 +465,10 @@ services:
     class: Drupal\Core\Path\AliasWhitelist
     tags:
       - { name: needs_destruction }
-    arguments: [path_alias_whitelist, '@cache.bootstrap', '@lock', '@state', '@path.alias_storage']
+    arguments: [path_alias_whitelist, '@cache.bootstrap', '@lock', '@state', '@path.alias_repository']
   path.alias_manager:
     class: Drupal\Core\Path\AliasManager
-    arguments: ['@path.alias_storage', '@path.alias_whitelist', '@language_manager', '@cache.data']
+    arguments: ['@path.alias_repository', '@path.alias_whitelist', '@language_manager', '@cache.data']
   path.current:
     class: Drupal\Core\Path\CurrentPathStack
     arguments: ['@request_stack']
@@ -960,11 +960,17 @@ services:
     arguments: ['@lock', '@plugin.manager.menu.link', '@database', '@database.replica_kill_switch']
     tags:
       - { name: event_subscriber }
+  path.alias_repository:
+    class: Drupal\Core\Path\AliasRepository
+    arguments: ['@database']
+    tags:
+      - { name: backend_overridable }
   path.alias_storage:
     class: Drupal\Core\Path\AliasStorage
     arguments: ['@database', '@module_handler', '@entity_type.manager']
     tags:
       - { name: backend_overridable }
+    deprecated: The "%service_id%" service is deprecated. Use the "path.alias_repository" service instead, or the entity storage handler for the "path_alias" entity type for CRUD methods. See https://www.drupal.org/node/3013865
   path.matcher:
     class: Drupal\Core\Path\PathMatcher
     arguments: ['@config.factory', '@current_route_match']
diff --git a/core/lib/Drupal/Core/Path/AliasManager.php b/core/lib/Drupal/Core/Path/AliasManager.php
index 39472797ad64d40a4d18c0c21901f7f9464ed13e..59694bb2f85217dbbf5345bc2fdfe2222c34727f 100644
--- a/core/lib/Drupal/Core/Path/AliasManager.php
+++ b/core/lib/Drupal/Core/Path/AliasManager.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\CacheDecorator\CacheDecoratorInterface;
+use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 
@@ -12,12 +13,19 @@
  */
 class AliasManager implements AliasManagerInterface, CacheDecoratorInterface {
 
+  use DeprecatedServicePropertyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $deprecatedProperties = ['storage' => 'path.alias_storage'];
+
   /**
-   * The alias storage service.
+   * The path alias repository.
    *
-   * @var \Drupal\Core\Path\AliasStorageInterface
+   * @var \Drupal\Core\Path\AliasRepositoryInterface
    */
-  protected $storage;
+  protected $pathAliasRepository;
 
   /**
    * Cache backend service.
@@ -95,8 +103,8 @@ class AliasManager implements AliasManagerInterface, CacheDecoratorInterface {
   /**
    * Constructs an AliasManager.
    *
-   * @param \Drupal\Core\Path\AliasStorageInterface $storage
-   *   The alias storage service.
+   * @param \Drupal\Core\Path\AliasRepositoryInterface $alias_repository
+   *   The path alias repository.
    * @param \Drupal\Core\Path\AliasWhitelistInterface $whitelist
    *   The whitelist implementation to use.
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
@@ -104,8 +112,12 @@ class AliasManager implements AliasManagerInterface, CacheDecoratorInterface {
    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
    *   Cache backend.
    */
-  public function __construct(AliasStorageInterface $storage, AliasWhitelistInterface $whitelist, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
-    $this->storage = $storage;
+  public function __construct($alias_repository, AliasWhitelistInterface $whitelist, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
+    if (!$alias_repository instanceof AliasRepositoryInterface) {
+      @trigger_error('Passing the path.alias_storage service to AliasManager::__construct() is deprecated in drupal:8.8.0 and will be removed before drupal:9.0.0. Pass the new dependencies instead. See https://www.drupal.org/node/3013865.', E_USER_DEPRECATED);
+      $alias_repository = \Drupal::service('path.alias_repository');
+    }
+    $this->pathAliasRepository = $alias_repository;
     $this->languageManager = $language_manager;
     $this->whitelist = $whitelist;
     $this->cache = $cache;
@@ -166,9 +178,9 @@ public function getPathByAlias($alias, $langcode = NULL) {
     }
 
     // Look for path in storage.
-    if ($path = $this->storage->lookupPathSource($alias, $langcode)) {
-      $this->lookupMap[$langcode][$path] = $alias;
-      return $path;
+    if ($path_alias = $this->pathAliasRepository->lookupByAlias($alias, $langcode)) {
+      $this->lookupMap[$langcode][$path_alias['path']] = $alias;
+      return $path_alias['path'];
     }
 
     // We can't record anything into $this->lookupMap because we didn't find any
@@ -220,7 +232,7 @@ public function getAliasByPath($path, $langcode = NULL) {
 
       // Load paths from cache.
       if (!empty($this->preloadedPathLookups[$langcode])) {
-        $this->lookupMap[$langcode] = $this->storage->preloadPathAlias($this->preloadedPathLookups[$langcode], $langcode);
+        $this->lookupMap[$langcode] = $this->pathAliasRepository->preloadPathAlias($this->preloadedPathLookups[$langcode], $langcode);
         // Keep a record of paths with no alias to avoid querying twice.
         $this->noAlias[$langcode] = array_flip(array_diff_key($this->preloadedPathLookups[$langcode], array_keys($this->lookupMap[$langcode])));
       }
@@ -237,9 +249,9 @@ public function getAliasByPath($path, $langcode = NULL) {
     }
 
     // Try to load alias from storage.
-    if ($alias = $this->storage->lookupPathAlias($path, $langcode)) {
-      $this->lookupMap[$langcode][$path] = $alias;
-      return $alias;
+    if ($path_alias = $this->pathAliasRepository->lookupBySystemPath($path, $langcode)) {
+      $this->lookupMap[$langcode][$path] = $path_alias['alias'];
+      return $path_alias['alias'];
     }
 
     // We can't record anything into $this->lookupMap because we didn't find any
diff --git a/core/lib/Drupal/Core/Path/AliasManagerInterface.php b/core/lib/Drupal/Core/Path/AliasManagerInterface.php
index a3096fc64fb37580a9a23359bcf215b96404af69..6ede000e4196a964e0771c68d3c2ecdf4c2d0654 100644
--- a/core/lib/Drupal/Core/Path/AliasManagerInterface.php
+++ b/core/lib/Drupal/Core/Path/AliasManagerInterface.php
@@ -4,8 +4,6 @@
 
 /**
  * Find an alias for a path and vice versa.
- *
- * @see \Drupal\Core\Path\AliasStorageInterface
  */
 interface AliasManagerInterface {
 
diff --git a/core/lib/Drupal/Core/Path/AliasRepository.php b/core/lib/Drupal/Core/Path/AliasRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..dbb14d800ca27f86fd7ca6f719a916ef0514789d
--- /dev/null
+++ b/core/lib/Drupal/Core/Path/AliasRepository.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace Drupal\Core\Path;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\Query\Condition;
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\Language\LanguageInterface;
+
+/**
+ * Provides the default path alias lookup operations.
+ */
+class AliasRepository implements AliasRepositoryInterface {
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * Constructs an AliasRepository object.
+   *
+   * @param \Drupal\Core\Database\Connection $connection
+   *   A database connection for reading and writing path aliases.
+   */
+  public function __construct(Connection $connection) {
+    $this->connection = $connection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preloadPathAlias($preloaded, $langcode) {
+    $select = $this->getBaseQuery()
+      ->fields('base_table', ['path', 'alias']);
+
+    if (!empty($preloaded)) {
+      $conditions = new Condition('OR');
+      foreach ($preloaded as $preloaded_item) {
+        $conditions->condition('base_table.path', $this->connection->escapeLike($preloaded_item), 'LIKE');
+      }
+      $select->condition($conditions);
+    }
+
+    $this->addLanguageFallback($select, $langcode);
+
+    // We order by ID ASC so that fetchAllKeyed() returns the most recently
+    // created alias for each source. Subsequent queries using fetchField() must
+    // use ID DESC to have the same effect.
+    $select->orderBy('base_table.id', 'ASC');
+
+    return $select->execute()->fetchAllKeyed();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function lookupBySystemPath($path, $langcode) {
+    // See the queries above. Use LIKE for case-insensitive matching.
+    $select = $this->getBaseQuery()
+      ->fields('base_table', ['id', 'path', 'alias', 'langcode'])
+      ->condition('base_table.path', $this->connection->escapeLike($path), 'LIKE');
+
+    $this->addLanguageFallback($select, $langcode);
+
+    $select->orderBy('base_table.id', 'DESC');
+
+    return $select->execute()->fetchAssoc() ?: NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function lookupByAlias($alias, $langcode) {
+    // See the queries above. Use LIKE for case-insensitive matching.
+    $select = $this->getBaseQuery()
+      ->fields('base_table', ['id', 'path', 'alias', 'langcode'])
+      ->condition('base_table.alias', $this->connection->escapeLike($alias), 'LIKE');
+
+    $this->addLanguageFallback($select, $langcode);
+
+    $select->orderBy('base_table.id', 'DESC');
+
+    return $select->execute()->fetchAssoc() ?: NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function pathHasMatchingAlias($initial_substring) {
+    $query = $this->getBaseQuery();
+    $query->addExpression(1);
+
+    return (bool) $query
+      ->condition('base_table.path', $this->connection->escapeLike($initial_substring) . '%', 'LIKE')
+      ->range(0, 1)
+      ->execute()
+      ->fetchField();
+  }
+
+  /**
+   * Returns a SELECT query for the path_alias base table.
+   *
+   * @return \Drupal\Core\Database\Query\SelectInterface
+   *   A Select query object.
+   */
+  protected function getBaseQuery() {
+    $query = $this->connection->select('path_alias', 'base_table');
+    $query->condition('base_table.status', 1);
+
+    return $query;
+  }
+
+  /**
+   * Adds path alias language fallback conditions to a select query object.
+   *
+   * @param \Drupal\Core\Database\Query\SelectInterface $query
+   *   A Select query object.
+   * @param string $langcode
+   *   Language code to search the path with. If there's no path defined for
+   *   that language it will search paths without language.
+   */
+  protected function addLanguageFallback(SelectInterface $query, $langcode) {
+    // Always get the language-specific alias before the language-neutral one.
+    // For example 'de' is less than 'und' so the order needs to be ASC, while
+    // 'xx-lolspeak' is more than 'und' so the order needs to be DESC.
+    $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
+    if ($langcode === LanguageInterface::LANGCODE_NOT_SPECIFIED) {
+      array_pop($langcode_list);
+    }
+    elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) {
+      $query->orderBy('base_table.langcode', 'DESC');
+    }
+    else {
+      $query->orderBy('base_table.langcode', 'ASC');
+    }
+    $query->condition('base_table.langcode', $langcode_list, 'IN');
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Path/AliasRepositoryInterface.php b/core/lib/Drupal/Core/Path/AliasRepositoryInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..15febd495b143f5d7f00fb25cd2580d1b0e49b3b
--- /dev/null
+++ b/core/lib/Drupal/Core/Path/AliasRepositoryInterface.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Drupal\Core\Path;
+
+/**
+ * Provides an interface for path alias lookup operations.
+ *
+ * The path alias repository service is only used internally in order to
+ * optimize alias lookup queries needed in the critical path of each request.
+ * However, it is not marked as an internal service because alternative storage
+ * backends still need to override it if they provide a different storage class
+ * for the PathAlias entity type.
+ *
+ * Whenever you need to determine whether an alias exists for a system path, or
+ * whether a system path has an alias, the 'path.alias_manager' service should
+ * be used instead.
+ */
+interface AliasRepositoryInterface {
+
+  /**
+   * Pre-loads path alias information for a given list of system paths.
+   *
+   * @param array $preloaded
+   *   System paths that need preloading of aliases.
+   * @param string $langcode
+   *   Language code to search the path with. If there's no path defined for
+   *   that language it will search paths without language.
+   *
+   * @return string[]
+   *   System paths (keys) to alias (values) mapping.
+   */
+  public function preloadPathAlias($preloaded, $langcode);
+
+  /**
+   * Searches a path alias for a given Drupal system path.
+   *
+   * The default implementation performs case-insensitive matching on the
+   * 'path' and 'alias' strings.
+   *
+   * @param string $path
+   *   The system path to investigate for corresponding path aliases.
+   * @param string $langcode
+   *   Language code to search the path with. If there's no path defined for
+   *   that language it will search paths without language.
+   *
+   * @return array|null
+   *   An array containing the 'id', 'path', 'alias' and 'langcode' properties
+   *   of a path alias, or NULL if none was found.
+   */
+  public function lookupBySystemPath($path, $langcode);
+
+  /**
+   * Searches a path alias for a given alias.
+   *
+   * The default implementation performs case-insensitive matching on the
+   * 'path' and 'alias' strings.
+   *
+   * @param string $alias
+   *   The alias to investigate for corresponding system paths.
+   * @param string $langcode
+   *   Language code to search the alias with. If there's no alias defined for
+   *   that language it will search aliases without language.
+   *
+   * @return array|null
+   *   An array containing the 'id', 'path', 'alias' and 'langcode' properties
+   *   of a path alias, or NULL if none was found.
+   */
+  public function lookupByAlias($alias, $langcode);
+
+  /**
+   * Check if any alias exists starting with $initial_substring.
+   *
+   * @param string $initial_substring
+   *   Initial system path substring to test against.
+   *
+   * @return bool
+   *   TRUE if any alias exists, FALSE otherwise.
+   */
+  public function pathHasMatchingAlias($initial_substring);
+
+}
diff --git a/core/lib/Drupal/Core/Path/AliasStorage.php b/core/lib/Drupal/Core/Path/AliasStorage.php
index 5781359732c783630c6b4cb810c39e7823ff7d96..b0dfb5bc9b66e97f955106030f64e2d88ae1c1ba 100644
--- a/core/lib/Drupal/Core/Path/AliasStorage.php
+++ b/core/lib/Drupal/Core/Path/AliasStorage.php
@@ -9,12 +9,21 @@
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Language\LanguageInterface;
 
+@trigger_error('\Drupal\Core\Path\AliasStorage is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use the "path.alias_repository" service instead, or the entity storage handler for the "path_alias" entity type for CRUD methods. See https://www.drupal.org/node/3013865.', E_USER_DEPRECATED);
+
 /**
  * Provides a class for CRUD operations on path aliases.
  *
  * All queries perform case-insensitive matching on the 'source' and 'alias'
  * fields, so the aliases '/test-alias' and '/test-Alias' are considered to be
  * the same, and will both refer to the same internal system path.
+ *
+ * @deprecated \Drupal\Core\Path\AliasStorage is deprecated in drupal:8.8.0 and
+ *   is removed from drupal:9.0.0. Use the "path.alias_repository" service
+ *   instead, or the entity storage handler for the "path_alias" entity type
+ *   for CRUD methods.
+ *
+ * @see https://www.drupal.org/node/3013865
  */
 class AliasStorage implements AliasStorageInterface {
 
diff --git a/core/lib/Drupal/Core/Path/AliasStorageInterface.php b/core/lib/Drupal/Core/Path/AliasStorageInterface.php
index 398ce07c6537382e05212df323af15ebf9e82351..24a7c627b14af57709ad9d21d75315833403b4f2 100644
--- a/core/lib/Drupal/Core/Path/AliasStorageInterface.php
+++ b/core/lib/Drupal/Core/Path/AliasStorageInterface.php
@@ -6,6 +6,13 @@
 
 /**
  * Provides a class for CRUD operations on path aliases.
+ *
+ * @deprecated \Drupal\Core\Path\AliasStorage is deprecated in drupal:8.8.0 and
+ *   is removed from drupal:9.0.0. Use the "path.alias_repository" service
+ *   instead, or the entity storage handler for the "path_alias" entity type
+ *   for CRUD methods.
+ *
+ * @see https://www.drupal.org/node/3013865
  */
 interface AliasStorageInterface {
 
diff --git a/core/lib/Drupal/Core/Path/AliasWhitelist.php b/core/lib/Drupal/Core/Path/AliasWhitelist.php
index 9286c955b8cf6acef9ce3376f210a74421ff8c81..48b736280c50db39a4015573a7b72b8e01120229 100644
--- a/core/lib/Drupal/Core/Path/AliasWhitelist.php
+++ b/core/lib/Drupal/Core/Path/AliasWhitelist.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Cache\CacheCollector;
+use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
 use Drupal\Core\State\StateInterface;
 use Drupal\Core\Lock\LockBackendInterface;
 
@@ -12,6 +13,13 @@
  */
 class AliasWhitelist extends CacheCollector implements AliasWhitelistInterface {
 
+  use DeprecatedServicePropertyTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $deprecatedProperties = ['aliasStorage' => 'path.alias_storage'];
+
   /**
    * The Key/Value Store to use for state.
    *
@@ -20,11 +28,11 @@ class AliasWhitelist extends CacheCollector implements AliasWhitelistInterface {
   protected $state;
 
   /**
-   * The Path CRUD service.
+   * The path alias repository.
    *
-   * @var \Drupal\Core\Path\AliasStorageInterface
+   * @var \Drupal\Core\Path\AliasRepositoryInterface
    */
-  protected $aliasStorage;
+  protected $pathAliasRepository;
 
   /**
    * Constructs an AliasWhitelist object.
@@ -37,13 +45,18 @@ class AliasWhitelist extends CacheCollector implements AliasWhitelistInterface {
    *   The lock backend.
    * @param \Drupal\Core\State\StateInterface $state
    *   The state keyvalue store.
-   * @param \Drupal\Core\Path\AliasStorageInterface $alias_storage
-   *   The alias storage service.
+   * @param \Drupal\Core\Path\AliasRepositoryInterface $alias_repository
+   *   The path alias repository.
    */
-  public function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, StateInterface $state, AliasStorageInterface $alias_storage) {
+  public function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, StateInterface $state, $alias_repository) {
     parent::__construct($cid, $cache, $lock);
     $this->state = $state;
-    $this->aliasStorage = $alias_storage;
+
+    if (!$alias_repository instanceof AliasRepositoryInterface) {
+      @trigger_error('Passing the path.alias_storage service to AliasWhitelist::__construct() is deprecated in drupal:8.8.0 and will be removed before drupal:9.0.0. Pass the new dependencies instead. See https://www.drupal.org/node/3013865.', E_USER_DEPRECATED);
+      $alias_repository = \Drupal::service('path.alias_repository');
+    }
+    $this->pathAliasRepository = $alias_repository;
   }
 
   /**
@@ -101,7 +114,7 @@ public function get($offset) {
    * {@inheritdoc}
    */
   public function resolveCacheMiss($root) {
-    $exists = $this->aliasStorage->pathHasMatchingAlias('/' . $root);
+    $exists = $this->pathAliasRepository->pathHasMatchingAlias('/' . $root);
     $this->storage[$root] = $exists;
     $this->persist($root);
     if ($exists) {
diff --git a/core/modules/link/tests/src/Functional/LinkFieldTest.php b/core/modules/link/tests/src/Functional/LinkFieldTest.php
index e5aede5c9c65c448a125e925a2174a81e309c9c7..fb3680af66fc24ff1ecb2510812f3b7a4ee1e395 100644
--- a/core/modules/link/tests/src/Functional/LinkFieldTest.php
+++ b/core/modules/link/tests/src/Functional/LinkFieldTest.php
@@ -13,6 +13,7 @@
 use Drupal\node\NodeInterface;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 
 /**
  * Tests link field widgets and formatters.
@@ -21,6 +22,8 @@
  */
 class LinkFieldTest extends BrowserTestBase {
 
+  use PathAliasTestTrait;
+
   /**
    * Modules to enable.
    *
@@ -100,7 +103,7 @@ public function testURLValidation() {
     $this->assertRaw('placeholder="http://example.com"');
 
     // Create a path alias.
-    \Drupal::service('path.alias_storage')->save('/admin', '/a/path/alias');
+    $this->createPathAlias('/admin', '/a/path/alias');
 
     // Create a node to test the link widget.
     $node = $this->drupalCreateNode();
diff --git a/core/modules/locale/tests/src/Functional/LocalePathTest.php b/core/modules/locale/tests/src/Functional/LocalePathTest.php
index 074a82c47d1c65c62b9f84e51378dc14c9ee73c0..588488eb0b2ee65d4b831c62ccc959fbb23224a9 100644
--- a/core/modules/locale/tests/src/Functional/LocalePathTest.php
+++ b/core/modules/locale/tests/src/Functional/LocalePathTest.php
@@ -5,14 +5,18 @@
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Url;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 
 /**
  * Tests you can configure a language for individual URL aliases.
  *
  * @group locale
+ * @group path
  */
 class LocalePathTest extends BrowserTestBase {
 
+  use PathAliasTestTrait;
+
   /**
    * Modules to enable.
    *
@@ -96,44 +100,24 @@ public function testPathLanguageConfiguration() {
     $custom_path = $this->randomMachineName(8);
 
     // Check priority of language for alias by source path.
-    $edit = [
-      'path[0][value]' => '/node/' . $node->id(),
-      'alias[0][value]' => '/' . $custom_path,
-      'langcode[0][value]' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
-    ];
-    $this->container->get('path.alias_storage')->save($edit['path[0][value]'], $edit['alias[0][value]'], $edit['langcode[0][value]']);
+    $path_alias = $this->createPathAlias('/node/' . $node->id(), '/' . $custom_path, LanguageInterface::LANGCODE_NOT_SPECIFIED);
     $lookup_path = $this->container->get('path.alias_manager')->getAliasByPath('/node/' . $node->id(), 'en');
     $this->assertEqual('/' . $english_path, $lookup_path, 'English language alias has priority.');
     // Same check for language 'xx'.
     $lookup_path = $this->container->get('path.alias_manager')->getAliasByPath('/node/' . $node->id(), $prefix);
     $this->assertEqual('/' . $custom_language_path, $lookup_path, 'Custom language alias has priority.');
-    $path_alias = [
-      'path' => $edit['path[0][value]'],
-      'alias' => $edit['alias[0][value]'],
-      'langcode' => $edit['langcode[0][value]'],
-    ];
-    $this->container->get('path.alias_storage')->delete($path_alias);
+    $path_alias->delete();
 
     // Create language nodes to check priority of aliases.
     $first_node = $this->drupalCreateNode(['type' => 'page', 'promote' => 1, 'langcode' => 'en']);
     $second_node = $this->drupalCreateNode(['type' => 'page', 'promote' => 1, 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED]);
 
     // Assign a custom path alias to the first node with the English language.
-    $edit = [
-      'path[0][value]' => '/node/' . $first_node->id(),
-      'alias[0][value]' => '/' . $custom_path,
-      'langcode[0][value]' => $first_node->language()->getId(),
-    ];
-    $this->container->get('path.alias_storage')->save($edit['path[0][value]'], $edit['alias[0][value]'], $edit['langcode[0][value]']);
+    $this->createPathAlias('/node/' . $first_node->id(), '/' . $custom_path, $first_node->language()->getId());
 
     // Assign a custom path alias to second node with
     // LanguageInterface::LANGCODE_NOT_SPECIFIED.
-    $edit = [
-      'path[0][value]' => '/node/' . $second_node->id(),
-      'alias[0][value]' => '/' . $custom_path,
-      'langcode[0][value]' => $second_node->language()->getId(),
-    ];
-    $this->container->get('path.alias_storage')->save($edit['path[0][value]'], $edit['alias[0][value]'], $edit['langcode[0][value]']);
+    $this->createPathAlias('/node/' . $second_node->id(), '/' . $custom_path, $second_node->language()->getId());
 
     // Test that both node titles link to our path alias.
     $this->drupalGet('admin/content');
diff --git a/core/modules/menu_link_content/tests/src/Kernel/PathAliasMenuLinkContentTest.php b/core/modules/menu_link_content/tests/src/Kernel/PathAliasMenuLinkContentTest.php
index c0cdba31b80273c21a13c5e31c3797cab7063bbe..a00bbd183811d9937812c5e194fe20fa78c8ab07 100644
--- a/core/modules/menu_link_content/tests/src/Kernel/PathAliasMenuLinkContentTest.php
+++ b/core/modules/menu_link_content/tests/src/Kernel/PathAliasMenuLinkContentTest.php
@@ -3,18 +3,21 @@
 namespace Drupal\Tests\menu_link_content\Kernel;
 
 use Drupal\Core\DependencyInjection\ContainerBuilder;
-use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Menu\MenuTreeParameters;
 use Drupal\menu_link_content\Entity\MenuLinkContent;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 
 /**
  * Ensures that the menu tree adapts to path alias changes.
  *
  * @group menu_link_content
+ * @group path
  */
 class PathAliasMenuLinkContentTest extends KernelTestBase {
 
+  use PathAliasTestTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -52,11 +55,7 @@ public function register(ContainerBuilder $container) {
   public function testPathAliasChange() {
     \Drupal::service('router.builder')->rebuild();
 
-    /** @var \Drupal\Core\Path\AliasStorageInterface $path_alias_storage */
-    $path_alias_storage = \Drupal::service('path.alias_storage');
-    $alias = $path_alias_storage->save('/test-page', '/my-blog');
-    $pid = $alias['pid'];
-
+    $path_alias = $this->createPathAlias('/test-page', '/my-blog');
     $menu_link_content = MenuLinkContent::create([
       'title' => 'Menu title',
       'link' => ['uri' => 'internal:/my-blog'],
@@ -68,13 +67,15 @@ public function testPathAliasChange() {
     $this->assertEqual('test_page_test.test_page', $tree[$menu_link_content->getPluginId()]->link->getPluginDefinition()['route_name']);
 
     // Saving an alias should clear the alias manager cache.
-    $path_alias_storage->save('/test-render-title', '/my-blog', LanguageInterface::LANGCODE_NOT_SPECIFIED, $pid);
+    $path_alias->setPath('/test-render-title');
+    $path_alias->setAlias('/my-blog');
+    $path_alias->save();
 
     $tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters());
     $this->assertEqual('test_page_test.render_title', $tree[$menu_link_content->getPluginId()]->link->getPluginDefinition()['route_name']);
 
     // Delete the alias.
-    $path_alias_storage->delete(['pid' => $pid]);
+    $path_alias->delete();
     $tree = \Drupal::menuTree()->load('tools', new MenuTreeParameters());
     $this->assertTrue(isset($tree[$menu_link_content->getPluginId()]));
     $this->assertEqual('', $tree[$menu_link_content->getPluginId()]->link->getRouteName());
diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php
index 1985499dee5278a4db62b9921fb6a31b025bfb4c..bdc20e60aa49bb8216ae63b97278b02f6b19af01 100644
--- a/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php
+++ b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php
@@ -4,7 +4,6 @@
 
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Field\FieldItemList;
-use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\TypedData\ComputedItemListTrait;
 
@@ -27,21 +26,15 @@ protected function computeValue() {
 
     $entity = $this->getEntity();
     if (!$entity->isNew()) {
-      $conditions = [
-        'source' => '/' . $entity->toUrl()->getInternalPath(),
-        'langcode' => $this->getLangcode(),
-      ];
-      $alias = \Drupal::service('path.alias_storage')->load($conditions);
-      if ($alias === FALSE) {
-        // Fall back to non-specific language.
-        if ($this->getLangcode() !== LanguageInterface::LANGCODE_NOT_SPECIFIED) {
-          $conditions['langcode'] = LanguageInterface::LANGCODE_NOT_SPECIFIED;
-          $alias = \Drupal::service('path.alias_storage')->load($conditions);
-        }
-      }
+      /** @var \Drupal\Core\Path\AliasRepositoryInterface $path_alias_repository */
+      $path_alias_repository = \Drupal::service('path.alias_repository');
 
-      if ($alias) {
-        $value = $alias;
+      if ($path_alias = $path_alias_repository->lookupBySystemPath('/' . $entity->toUrl()->getInternalPath(), $this->getLangcode())) {
+        $value = [
+          'alias' => $path_alias['alias'],
+          'pid' => $path_alias['id'],
+          'langcode' => $path_alias['langcode'],
+        ];
       }
     }
 
@@ -64,11 +57,12 @@ public function defaultAccess($operation = 'view', AccountInterface $account = N
   public function delete() {
     // Delete all aliases associated with this entity in the current language.
     $entity = $this->getEntity();
-    $conditions = [
-      'source' => '/' . $entity->toUrl()->getInternalPath(),
+    $path_alias_storage = \Drupal::entityTypeManager()->getStorage('path_alias');
+    $entities = $path_alias_storage->loadByProperties([
+      'path' => '/' . $entity->toUrl()->getInternalPath(),
       'langcode' => $entity->language()->getId(),
-    ];
-    \Drupal::service('path.alias_storage')->delete($conditions);
+    ]);
+    $path_alias_storage->delete($entities);
   }
 
 }
diff --git a/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php b/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php
index 090edd4a9d8f86f6a70b123699706c8167c839ef..3962175b261a1a740e1c5bac91fb44fc9018ee62 100644
--- a/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php
+++ b/core/modules/path/src/Plugin/Field/FieldWidget/PathWidget.php
@@ -87,15 +87,22 @@ public static function validateFormElement(array &$element, FormStateInterface $
     if (!empty($alias)) {
       $form_state->setValueForElement($element['alias'], $alias);
 
-      // Validate that the submitted alias does not exist yet.
-      $is_exists = \Drupal::service('path.alias_storage')->aliasExists($alias, $element['langcode']['#value'], $element['source']['#value']);
-      if ($is_exists) {
-        $form_state->setError($element['alias'], t('The alias is already in use.'));
-      }
-    }
+      /** @var \Drupal\Core\Path\PathAliasInterface $path_alias */
+      $path_alias = \Drupal::entityTypeManager()->getStorage('path_alias')->create([
+        'path' => $element['source']['#value'],
+        'alias' => $alias,
+        'langcode' => $element['langcode']['#value'],
+      ]);
+      $violations = $path_alias->validate();
 
-    if ($alias && $alias[0] !== '/') {
-      $form_state->setError($element['alias'], t('The alias needs to start with a slash.'));
+      foreach ($violations as $violation) {
+        // Newly created entities do not have a system path yet, so we need to
+        // disregard some violations.
+        if (!$path_alias->getPath() && $violation->getPropertyPath() === 'path') {
+          continue;
+        }
+        $form_state->setError($element['alias'], $violation->getMessage());
+      }
     }
   }
 
diff --git a/core/modules/path/src/Plugin/migrate/source/UrlAliasBase.php b/core/modules/path/src/Plugin/migrate/source/UrlAliasBase.php
index 7714cbd1c4fd2e6e11c708a1bedce4e4d4ab9d3d..b2e7e0d758a253414ca47437268e2816c1998fa1 100644
--- a/core/modules/path/src/Plugin/migrate/source/UrlAliasBase.php
+++ b/core/modules/path/src/Plugin/migrate/source/UrlAliasBase.php
@@ -14,7 +14,7 @@ abstract class UrlAliasBase extends DrupalSqlBase {
    */
   public function query() {
     // The order of the migration is significant since
-    // \Drupal\Core\Path\AliasStorage::lookupPathAlias() orders by pid before
+    // \Drupal\Core\Path\AliasRepository::lookupPathAlias() orders by pid before
     // returning a result. Postgres does not automatically order by primary key
     // therefore we need to add a specific order by.
     return $this->select('url_alias', 'ua')->fields('ua')->orderBy('pid');
diff --git a/core/modules/path/tests/src/Functional/PathAliasTest.php b/core/modules/path/tests/src/Functional/PathAliasTest.php
index 80ce4c385bfea90ca4c53962ae8be352f2068211..71e5783b9a4f34902f2be83a12f3c692a09d53a6 100644
--- a/core/modules/path/tests/src/Functional/PathAliasTest.php
+++ b/core/modules/path/tests/src/Functional/PathAliasTest.php
@@ -283,7 +283,7 @@ public function testNodeAlias() {
     $this->drupalPostForm('node/' . $node2->id() . '/edit', $edit, t('Save'));
 
     // Confirm that the alias didn't make a duplicate.
-    $this->assertText(t('The alias is already in use.'), 'Attempt to moved alias was rejected.');
+    $this->assertSession()->pageTextContains("The alias {$edit['path[0][alias]']} is already in use in this language.");
 
     // Delete alias.
     $this->drupalPostForm('node/' . $node1->id() . '/edit', ['path[0][alias]' => ''], t('Save'));
@@ -326,7 +326,7 @@ public function testNodeAlias() {
 
     // Delete the node and check that the path alias is also deleted.
     $node5->delete();
-    $path_alias = \Drupal::service('path.alias_storage')->lookupPathAlias('/node/' . $node5->id(), $node5->language()->getId());
+    $path_alias = \Drupal::service('path.alias_repository')->lookUpBySystemPath('/node/' . $node5->id(), $node5->language()->getId());
     $this->assertFalse($path_alias, 'Alias was successfully deleted when the referenced node was deleted.');
 
     // Create sixth test node.
@@ -388,7 +388,7 @@ public function testDuplicateNodeAlias() {
     // Now create another node and try to set the same alias.
     $node_two = $this->drupalCreateNode();
     $this->drupalPostForm('node/' . $node_two->id() . '/edit', $edit, t('Save'));
-    $this->assertText(t('The alias is already in use.'));
+    $this->assertSession()->pageTextContains("The alias {$edit['path[0][alias]']} is already in use in this language.");
     $this->assertFieldByXPath("//input[@name='path[0][alias]' and contains(@class, 'error')]", $edit['path[0][alias]'], 'Textfield exists and has the error class.');
 
     // Behavior here differs with the inline_form_errors module enabled.
@@ -399,7 +399,7 @@ public function testDuplicateNodeAlias() {
     // Attempt to edit the second node again, as before.
     $this->drupalPostForm('node/' . $node_two->id() . '/edit', $edit, t('Preview'));
     // This error should still be present next to the field.
-    $this->assertSession()->pageTextContains(t('The alias is already in use.'), 'Field error found with expected text.');
+    $this->assertSession()->pageTextContains("The alias {$edit['path[0][alias]']} is already in use in this language.");
     // The validation error set for the page should include this text.
     $this->assertSession()->pageTextContains(t('1 error has been found: URL alias'), 'Form error found with expected text.');
     // The text 'URL alias' should be a link.
diff --git a/core/modules/path/tests/src/Functional/PathLanguageTest.php b/core/modules/path/tests/src/Functional/PathLanguageTest.php
index 3d7a2d6199e8d3d00b971d592e7719a5ec145586..ddb512fd799be22df55291e8e10ad0c5683874ae 100644
--- a/core/modules/path/tests/src/Functional/PathLanguageTest.php
+++ b/core/modules/path/tests/src/Functional/PathLanguageTest.php
@@ -184,11 +184,11 @@ public function testAliasTranslation() {
     // Confirm that the alias is removed if the translation is deleted.
     $english_node->removeTranslation('fr');
     $english_node->save();
-    $this->assertFalse($this->container->get('path.alias_storage')->aliasExists('/' . $french_alias, 'fr'), 'Alias for French translation is removed when translation is deleted.');
+    $this->assertPathAliasNotExists('/' . $french_alias, 'fr', NULL, 'Alias for French translation is removed when translation is deleted.');
 
     // Check that the English alias still works.
     $this->drupalGet($english_alias);
-    $this->assertTrue($this->container->get('path.alias_storage')->aliasExists('/' . $english_alias, 'en'), 'English alias is not deleted when French translation is removed.');
+    $this->assertPathAliasExists('/' . $english_alias, 'en', NULL, 'English alias is not deleted when French translation is removed.');
     $this->assertText($english_node->body->value, 'English alias still works');
   }
 
diff --git a/core/modules/path/tests/src/Functional/PathTestBase.php b/core/modules/path/tests/src/Functional/PathTestBase.php
index d4b8826d58d59be0272bad48c217007c841abd59..ca6f03c9016754118af777f3baa0b7e82854f07f 100644
--- a/core/modules/path/tests/src/Functional/PathTestBase.php
+++ b/core/modules/path/tests/src/Functional/PathTestBase.php
@@ -3,12 +3,15 @@
 namespace Drupal\Tests\path\Functional;
 
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 
 /**
  * Provides a base class for testing the Path module.
  */
 abstract class PathTestBase extends BrowserTestBase {
 
+  use PathAliasTestTrait;
+
   /**
    * Modules to enable.
    *
diff --git a/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php b/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php
index db63c41104eb1795ca70cd6d5feec72b2cf6a46b..c38f3d1f89acd25bb15c48f0f0cdf47d4a499a75 100644
--- a/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php
+++ b/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php
@@ -2,9 +2,11 @@
 
 namespace Drupal\Tests\path\Kernel\Migrate\d6;
 
+use Drupal\Core\Path\PathAliasInterface;
 use Drupal\migrate\Plugin\MigrateIdMapInterface;
 use Drupal\Core\Database\Database;
 use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 
 /**
  * URL alias migration.
@@ -13,6 +15,8 @@
  */
 class MigrateUrlAliasTest extends MigrateDrupal6TestBase {
 
+  use PathAliasTestTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -47,20 +51,20 @@ protected function setUp() {
   }
 
   /**
-   * Assert a path.
+   * Asserts that a path alias matches a set of conditions.
    *
-   * @param string $pid
-   *   The path id.
+   * @param int $pid
+   *   The path alias ID.
    * @param array $conditions
    *   The path conditions.
-   * @param array $path
-   *   The path.
+   * @param \Drupal\Core\Path\PathAliasInterface $path_alias
+   *   The path alias.
    */
-  private function assertPath($pid, $conditions, $path) {
-    $this->assertTrue($path, "Path alias for " . $conditions['source'] . " successfully loaded.");
-    $this->assertIdentical($conditions['alias'], $path['alias']);
-    $this->assertIdentical($conditions['langcode'], $path['langcode']);
-    $this->assertIdentical($conditions['source'], $path['source']);
+  private function assertPath($pid, $conditions, PathAliasInterface $path_alias) {
+    $this->assertSame($pid, (int) $path_alias->id());
+    $this->assertSame($conditions['alias'], $path_alias->getAlias());
+    $this->assertSame($conditions['langcode'], $path_alias->get('langcode')->value);
+    $this->assertSame($conditions['path'], $path_alias->getPath());
   }
 
   /**
@@ -70,21 +74,21 @@ public function testUrlAlias() {
     $id_map = $this->getMigration('d6_url_alias')->getIdMap();
     // Test that the field exists.
     $conditions = [
-      'source' => '/node/1',
+      'path' => '/node/1',
       'alias' => '/alias-one',
       'langcode' => 'af',
     ];
-    $path = \Drupal::service('path.alias_storage')->load($conditions);
-    $this->assertPath('1', $conditions, $path);
-    $this->assertIdentical($id_map->lookupDestinationIds([$path['pid']]), [['1']], "Test IdMap");
+    $path_alias = $this->loadPathAliasByConditions($conditions);
+    $this->assertPath(1, $conditions, $path_alias);
+    $this->assertSame([['1']], $id_map->lookupDestinationIds([$path_alias->id()]), "Test IdMap");
 
     $conditions = [
-      'source' => '/node/2',
+      'path' => '/node/2',
       'alias' => '/alias-two',
       'langcode' => 'en',
     ];
-    $path = \Drupal::service('path.alias_storage')->load($conditions);
-    $this->assertPath('2', $conditions, $path);
+    $path_alias = $this->loadPathAliasByConditions($conditions);
+    $this->assertPath(2, $conditions, $path_alias);
 
     // Test that we can re-import using the UrlAlias destination.
     Database::getConnection('default', 'migrate')
@@ -100,54 +104,53 @@ public function testUrlAlias() {
     $migration = $this->getMigration('d6_url_alias');
     $this->executeMigration($migration);
 
-    $path = \Drupal::service('path.alias_storage')->load(['pid' => $path['pid']]);
+    $path_alias = $this->loadPathAliasByConditions(['id' => $path_alias->id()]);
     $conditions['alias'] = '/new-url-alias';
-    $this->assertPath('2', $conditions, $path);
+    $this->assertPath(2, $conditions, $path_alias);
 
     $conditions = [
-      'source' => '/node/3',
+      'path' => '/node/3',
       'alias' => '/alias-three',
       'langcode' => 'und',
     ];
-    $path = \Drupal::service('path.alias_storage')->load($conditions);
-    $this->assertPath('3', $conditions, $path);
+    $path_alias = $this->loadPathAliasByConditions($conditions);
+    $this->assertPath(3, $conditions, $path_alias);
 
-    $path = \Drupal::service('path.alias_storage')->load(['alias' => '/source-noslash']);
+    $path_alias = $this->loadPathAliasByConditions(['alias' => '/source-noslash']);
     $conditions = [
-      'source' => '/admin',
+      'path' => '/admin',
       'alias' => '/source-noslash',
       'langcode' => 'und',
     ];
-    $this->assertPath('2', $conditions, $path);
+    $this->assertPath(8, $conditions, $path_alias);
   }
 
   /**
    * Test the URL alias migration with translated nodes.
    */
   public function testUrlAliasWithTranslatedNodes() {
-    $alias_storage = $this->container->get('path.alias_storage');
-
     // Alias for the 'The Real McCoy' node in English.
-    $path = $alias_storage->load(['alias' => '/the-real-mccoy']);
-    $this->assertSame('/node/10', $path['source']);
-    $this->assertSame('en', $path['langcode']);
+
+    $path_alias = $this->loadPathAliasByConditions(['alias' => '/the-real-mccoy']);
+    $this->assertSame('/node/10', $path_alias->getPath());
+    $this->assertSame('en', $path_alias->get('langcode')->value);
 
     // Alias for the 'The Real McCoy' French translation,
     // which should now point to node/10 instead of node/11.
-    $path = $alias_storage->load(['alias' => '/le-vrai-mccoy']);
-    $this->assertSame('/node/10', $path['source']);
-    $this->assertSame('fr', $path['langcode']);
+    $path_alias = $this->loadPathAliasByConditions(['alias' => '/le-vrai-mccoy']);
+    $this->assertSame('/node/10', $path_alias->getPath());
+    $this->assertSame('fr', $path_alias->get('langcode')->value);
 
     // Alias for the 'Abantu zulu' node in Zulu.
-    $path = $alias_storage->load(['alias' => '/abantu-zulu']);
-    $this->assertSame('/node/12', $path['source']);
-    $this->assertSame('zu', $path['langcode']);
+    $path_alias = $this->loadPathAliasByConditions(['alias' => '/abantu-zulu']);
+    $this->assertSame('/node/12', $path_alias->getPath());
+    $this->assertSame('zu', $path_alias->get('langcode')->value);
 
     // Alias for the 'Abantu zulu' English translation,
     // which should now point to node/12 instead of node/13.
-    $path = $alias_storage->load(['alias' => '/the-zulu-people']);
-    $this->assertSame('/node/12', $path['source']);
-    $this->assertSame('en', $path['langcode']);
+    $path_alias = $this->loadPathAliasByConditions(['alias' => '/the-zulu-people']);
+    $this->assertSame('/node/12', $path_alias->getPath());
+    $this->assertSame('en', $path_alias->get('langcode')->value);
   }
 
 }
diff --git a/core/modules/path/tests/src/Kernel/Migrate/d7/MigrateUrlAliasTest.php b/core/modules/path/tests/src/Kernel/Migrate/d7/MigrateUrlAliasTest.php
index e48996e5186cdadf1955e51cf4bd772047359e25..307e5172f6c878a772b78611a34598c84fcdf338 100644
--- a/core/modules/path/tests/src/Kernel/Migrate/d7/MigrateUrlAliasTest.php
+++ b/core/modules/path/tests/src/Kernel/Migrate/d7/MigrateUrlAliasTest.php
@@ -32,29 +32,27 @@ protected function setUp() {
    * Test the URL alias migration with translated nodes.
    */
   public function testUrlAliasWithTranslatedNodes() {
-    $alias_storage = $this->container->get('path.alias_storage');
-
     // Alias for the 'The thing about Deep Space 9' node in English.
-    $path = $alias_storage->load(['alias' => '/deep-space-9']);
-    $this->assertSame('/node/2', $path['source']);
-    $this->assertSame('en', $path['langcode']);
+    $path_alias = $this->loadPathAliasByConditions(['alias' => '/deep-space-9']);
+    $this->assertSame('/node/2', $path_alias->getPath());
+    $this->assertSame('en', $path_alias->get('langcode')->value);
 
     // Alias for the 'The thing about Deep Space 9' Icelandic translation,
     // which should now point to node/2 instead of node/3.
-    $path = $alias_storage->load(['alias' => '/deep-space-9-is']);
-    $this->assertSame('/node/2', $path['source']);
-    $this->assertSame('is', $path['langcode']);
+    $path_alias = $this->loadPathAliasByConditions(['alias' => '/deep-space-9-is']);
+    $this->assertSame('/node/2', $path_alias->getPath());
+    $this->assertSame('is', $path_alias->get('langcode')->value);
 
     // Alias for the 'The thing about Firefly' node in Icelandic.
-    $path = $alias_storage->load(['alias' => '/firefly-is']);
-    $this->assertSame('/node/4', $path['source']);
-    $this->assertSame('is', $path['langcode']);
+    $path_alias = $this->loadPathAliasByConditions(['alias' => '/firefly-is']);
+    $this->assertSame('/node/4', $path_alias->getPath());
+    $this->assertSame('is', $path_alias->get('langcode')->value);
 
     // Alias for the 'The thing about Firefly' English translation,
     // which should now point to node/4 instead of node/5.
-    $path = $alias_storage->load(['alias' => '/firefly']);
-    $this->assertSame('/node/4', $path['source']);
-    $this->assertSame('en', $path['langcode']);
+    $path_alias = $this->loadPathAliasByConditions(['alias' => '/firefly']);
+    $this->assertSame('/node/4', $path_alias->getPath());
+    $this->assertSame('en', $path_alias->get('langcode')->value);
   }
 
 }
diff --git a/core/modules/path/tests/src/Kernel/Migrate/d7/MigrateUrlAliasTestBase.php b/core/modules/path/tests/src/Kernel/Migrate/d7/MigrateUrlAliasTestBase.php
index 315063ea2884b7724784160fd4f792b9b38cca40..5d9ca426b56a0776dff49d2544a724a26106fc9d 100644
--- a/core/modules/path/tests/src/Kernel/Migrate/d7/MigrateUrlAliasTestBase.php
+++ b/core/modules/path/tests/src/Kernel/Migrate/d7/MigrateUrlAliasTestBase.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\path\Kernel\Migrate\d7;
 
 use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 
 /**
  * Tests URL alias migration.
@@ -11,6 +12,8 @@
  */
 abstract class MigrateUrlAliasTestBase extends MigrateDrupal7TestBase {
 
+  use PathAliasTestTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -45,21 +48,19 @@ protected function setUp() {
    * Test the URL alias migration.
    */
   public function testUrlAlias() {
-    $alias_storage = $this->container->get('path.alias_storage');
-
-    $path = $alias_storage->load([
-      'source' => '/taxonomy/term/4',
+    $path_alias = $this->loadPathAliasByConditions([
+      'path' => '/taxonomy/term/4',
       'alias' => '/term33',
       'langcode' => 'und',
     ]);
-    $this->assertIdentical('/taxonomy/term/4', $path['source']);
-    $this->assertIdentical('/term33', $path['alias']);
-    $this->assertIdentical('und', $path['langcode']);
+    $this->assertIdentical('/taxonomy/term/4', $path_alias->getPath());
+    $this->assertIdentical('/term33', $path_alias->getAlias());
+    $this->assertIdentical('und', $path_alias->language()->getId());
 
     // Alias with no slash.
-    $path = $alias_storage->load(['alias' => '/source-noslash']);
-    $this->assertSame('/admin', $path['source']);
-    $this->assertSame('und', $path['langcode']);
+    $path_alias = $this->loadPathAliasByConditions(['alias' => '/source-noslash']);
+    $this->assertSame('/admin', $path_alias->getPath());
+    $this->assertSame('und', $path_alias->language()->getId());
   }
 
 }
diff --git a/core/modules/path/tests/src/Kernel/PathItemTest.php b/core/modules/path/tests/src/Kernel/PathItemTest.php
index f001dc6bcfe7cba6a24d83cfc4df4f4a41e009df..5a39d698b3f1e4286d0a94dd19013ef1bfca834f 100644
--- a/core/modules/path/tests/src/Kernel/PathItemTest.php
+++ b/core/modules/path/tests/src/Kernel/PathItemTest.php
@@ -44,9 +44,8 @@ protected function setUp() {
    * Test creating, loading, updating and deleting aliases through PathItem.
    */
   public function testPathItem() {
-
-    /** @var \Drupal\Core\Path\AliasStorageInterface $alias_storage */
-    $alias_storage = \Drupal::service('path.alias_storage');
+    /** @var \Drupal\Core\Path\AliasRepositoryInterface $alias_repository */
+    $alias_repository = \Drupal::service('path.alias_repository');
 
     $node_storage = \Drupal::entityTypeManager()->getStorage('node');
 
@@ -62,8 +61,8 @@ public function testPathItem() {
     $this->assertFalse($node->get('path')->isEmpty());
     $this->assertEquals('/foo', $node->get('path')->alias);
 
-    $stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
-    $this->assertEquals('/foo', $stored_alias);
+    $stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
+    $this->assertEquals('/foo', $stored_alias['alias']);
 
     $node_storage->resetCache();
 
@@ -97,10 +96,10 @@ public function testPathItem() {
     $translation = $loaded_node->getTranslation('de');
     $this->assertEquals('/furchtbar', $translation->path->alias);
 
-    $stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
-    $this->assertEquals('/foo', $stored_alias);
-    $stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $translation->language()->getId());
-    $this->assertEquals('/furchtbar', $stored_alias);
+    $stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
+    $this->assertEquals('/foo', $stored_alias['alias']);
+    $stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $translation->language()->getId());
+    $this->assertEquals('/furchtbar', $stored_alias['alias']);
 
     $loaded_node->get('path')->alias = '/bar';
     $this->assertFalse($loaded_node->get('path')->isEmpty());
@@ -123,11 +122,11 @@ public function testPathItem() {
     $this->assertFalse($loaded_node->get('path')->isEmpty());
     $this->assertEquals('/bar', $loaded_node->get('path')->alias);
 
-    $stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
-    $this->assertEquals('/bar', $stored_alias);
+    $stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
+    $this->assertEquals('/bar', $stored_alias['alias']);
 
-    $old_alias = $alias_storage->lookupPathSource('/foo', $node->language()->getId());
-    $this->assertFalse($old_alias);
+    $old_alias = $alias_repository->lookupByAlias('/foo', $node->language()->getId());
+    $this->assertNull($old_alias);
 
     // Reload the node to make sure that it is possible to set a value
     // immediately after loading.
@@ -140,19 +139,19 @@ public function testPathItem() {
     $loaded_node = $node_storage->load($node->id());
     $this->assertFalse($loaded_node->get('path')->isEmpty());
     $this->assertEquals('/foobar', $loaded_node->get('path')->alias);
-    $stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
-    $this->assertEquals('/foobar', $stored_alias);
+    $stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
+    $this->assertEquals('/foobar', $stored_alias['alias']);
 
-    $old_alias = $alias_storage->lookupPathSource('/bar', $node->language()->getId());
-    $this->assertFalse($old_alias);
+    $old_alias = $alias_repository->lookupByAlias('/bar', $node->language()->getId());
+    $this->assertNull($old_alias);
 
     $loaded_node->get('path')->alias = '';
     $this->assertEquals('', $loaded_node->get('path')->alias);
 
     $loaded_node->save();
 
-    $stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
-    $this->assertFalse($stored_alias);
+    $stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
+    $this->assertNull($stored_alias);
 
     // Check that reading, updating and reading the computed alias again in the
     // same request works without clearing any caches in between.
@@ -162,16 +161,16 @@ public function testPathItem() {
 
     $this->assertFalse($loaded_node->get('path')->isEmpty());
     $this->assertEquals('/foo', $loaded_node->get('path')->alias);
-    $stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
-    $this->assertEquals('/foo', $stored_alias);
+    $stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
+    $this->assertEquals('/foo', $stored_alias['alias']);
 
     $loaded_node->get('path')->alias = '/foobar';
     $loaded_node->save();
 
     $this->assertFalse($loaded_node->get('path')->isEmpty());
     $this->assertEquals('/foobar', $loaded_node->get('path')->alias);
-    $stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
-    $this->assertEquals('/foobar', $stored_alias);
+    $stored_alias = $alias_repository->lookupBySystemPath('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
+    $this->assertEquals('/foobar', $stored_alias['alias']);
 
     // Check that \Drupal\Core\Field\FieldItemList::equals() for the path field
     // type.
diff --git a/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php b/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php
index f14109b539e5ecec04e3c26c8db296de82e08cd2..aa6a5432b1ef414a403fd79ad7ece58fe395b8ad 100644
--- a/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php
+++ b/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php
@@ -8,6 +8,7 @@
 use Drupal\shortcut\Entity\Shortcut;
 use Drupal\shortcut\Entity\ShortcutSet;
 use Drupal\Tests\block\Functional\AssertBlockAppearsTrait;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 use Drupal\views\Entity\View;
 
 /**
@@ -18,6 +19,7 @@
 class ShortcutLinksTest extends ShortcutTestBase {
 
   use AssertBlockAppearsTrait;
+  use PathAliasTestTrait;
 
   /**
    * Modules to enable.
@@ -42,11 +44,7 @@ public function testShortcutLinkAdd() {
     $set = $this->set;
 
     // Create an alias for the node so we can test aliases.
-    $path = [
-      'source' => '/node/' . $this->node->id(),
-      'alias' => '/' . $this->randomMachineName(8),
-    ];
-    $this->container->get('path.alias_storage')->save($path['source'], $path['alias']);
+    $path_alias = $this->createPathAlias('/node/' . $this->node->id(), '/' . $this->randomMachineName(8));
 
     // Create some paths to test.
     $test_cases = [
@@ -54,7 +52,7 @@ public function testShortcutLinkAdd() {
       '/admin',
       '/admin/config/system/site-information',
       '/node/' . $this->node->id() . '/edit',
-      $path['alias'],
+      $path_alias->getAlias(),
       '/router_test/test2',
       '/router_test/test3/value',
     ];
diff --git a/core/modules/system/tests/src/Functional/Path/UrlAlterFunctionalTest.php b/core/modules/system/tests/src/Functional/Path/UrlAlterFunctionalTest.php
index 8a79fd7c4b454d5986a838826ede0926408bf708..6dc35713362e52f071041614f391e8c07200b467 100644
--- a/core/modules/system/tests/src/Functional/Path/UrlAlterFunctionalTest.php
+++ b/core/modules/system/tests/src/Functional/Path/UrlAlterFunctionalTest.php
@@ -7,14 +7,17 @@
 use Drupal\Core\Url;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\taxonomy\Entity\Term;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 
 /**
  * Tests altering the inbound path and the outbound path.
  *
- * @group Path
+ * @group path
  */
 class UrlAlterFunctionalTest extends BrowserTestBase {
 
+  use PathAliasTestTrait;
+
   /**
    * Modules to enable.
    *
@@ -43,8 +46,7 @@ public function testUrlAlter() {
     $this->assertUrlOutboundAlter("/user/$uid", "/user/$name");
 
     // Test that a path always uses its alias.
-    $path = ['source' => "/user/$uid/test1", 'alias' => '/alias/test1'];
-    $this->container->get('path.alias_storage')->save($path['source'], $path['alias']);
+    $this->createPathAlias("/user/$uid/test1", '/alias/test1');
     $this->rebuildContainer();
     $this->assertUrlInboundAlter('/alias/test1', "/user/$uid/test1");
     $this->assertUrlOutboundAlter("/user/$uid/test1", '/alias/test1');
diff --git a/core/modules/system/tests/src/Kernel/PathHooksTest.php b/core/modules/system/tests/src/Kernel/PathHooksTest.php
index 0c4638cbad2adf87834228b56c5b141359cf6d30..cef1211928de4ff379aa0ca4c583333d31435710 100644
--- a/core/modules/system/tests/src/Kernel/PathHooksTest.php
+++ b/core/modules/system/tests/src/Kernel/PathHooksTest.php
@@ -2,13 +2,15 @@
 
 namespace Drupal\Tests\system\Kernel;
 
-use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Path\AliasManagerInterface;
+use Drupal\Core\Path\Entity\PathAlias;
 use Drupal\KernelTests\KernelTestBase;
 use Prophecy\Argument;
 
 /**
- * @group Drupal
+ * @coversDefaultClass \Drupal\Core\Path\Entity\PathAlias
+ *
+ * @group path
  */
 class PathHooksTest extends KernelTestBase {
 
@@ -27,38 +29,43 @@ protected function setUp() {
   }
 
   /**
-   * Test system_path_alias_*() correctly clears caches.
+   * Tests that the PathAlias entity clears caches correctly.
+   *
+   * @covers ::postSave
+   * @covers ::postDelete
    */
   public function testPathHooks() {
-    $source = '/' . $this->randomMachineName();
-    $alias = '/' . $this->randomMachineName();
+    $path_alias = PathAlias::create([
+      'path' => '/' . $this->randomMachineName(),
+      'alias' => '/' . $this->randomMachineName(),
+    ]);
 
-    // Check system_path_alias_insert();
+    // Check \Drupal\Core\Path\Entity\PathAlias::postSave() for new path alias
+    // entities.
     $alias_manager = $this->prophesize(AliasManagerInterface::class);
     $alias_manager->cacheClear(Argument::any())->shouldBeCalledTimes(1);
-    $alias_manager->cacheClear($source)->shouldBeCalledTimes(1);
+    $alias_manager->cacheClear($path_alias->getPath())->shouldBeCalledTimes(1);
     \Drupal::getContainer()->set('path.alias_manager', $alias_manager->reveal());
-    $alias_storage = \Drupal::service('path.alias_storage');
-    $alias_storage->save($source, $alias);
+    $path_alias->save();
 
     $new_source = '/' . $this->randomMachineName();
-    $path = $alias_storage->load(['source' => $source]);
 
-    // Check system_path_alias_update();
+    // Check \Drupal\Core\Path\Entity\PathAlias::postSave() for existing path
+    // alias entities.
     $alias_manager = $this->prophesize(AliasManagerInterface::class);
     $alias_manager->cacheClear(Argument::any())->shouldBeCalledTimes(2);
-    $alias_manager->cacheClear($source)->shouldBeCalledTimes(1);
+    $alias_manager->cacheClear($path_alias->getPath())->shouldBeCalledTimes(1);
     $alias_manager->cacheClear($new_source)->shouldBeCalledTimes(1);
     \Drupal::getContainer()->set('path.alias_manager', $alias_manager->reveal());
-    $alias_storage->save($new_source, $alias, LanguageInterface::LANGCODE_NOT_SPECIFIED, $path['pid']);
+    $path_alias->setPath($new_source);
+    $path_alias->save();
 
-    // Check system_path_alias_delete();
+    // Check \Drupal\Core\Path\Entity\PathAlias::postDelete().
     $alias_manager = $this->prophesize(AliasManagerInterface::class);
     $alias_manager->cacheClear(Argument::any())->shouldBeCalledTimes(1);
     $alias_manager->cacheClear($new_source)->shouldBeCalledTimes(1);
     \Drupal::getContainer()->set('path.alias_manager', $alias_manager->reveal());
-    $alias_storage->delete(['pid' => $path['pid']]);
-
+    $path_alias->delete();
   }
 
 }
diff --git a/core/modules/workspaces/src/AliasStorage.php b/core/modules/workspaces/src/WorkspacesAliasRepository.php
similarity index 58%
rename from core/modules/workspaces/src/AliasStorage.php
rename to core/modules/workspaces/src/WorkspacesAliasRepository.php
index 219eb7f4160fa4b7f7e932025fa518800caf9795..e1a1e828341f9dbe8f7a59fec88e5ed42170d275 100644
--- a/core/modules/workspaces/src/AliasStorage.php
+++ b/core/modules/workspaces/src/WorkspacesAliasRepository.php
@@ -2,15 +2,12 @@
 
 namespace Drupal\workspaces;
 
-use Drupal\Core\Database\Connection;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Path\AliasStorage as CoreAliasStorage;
+use Drupal\Core\Path\AliasRepository;
 
 /**
  * Provides workspace-specific path alias lookup queries.
  */
-class AliasStorage extends CoreAliasStorage {
+class WorkspacesAliasRepository extends AliasRepository {
 
   /**
    * The workspace manager.
@@ -20,20 +17,16 @@ class AliasStorage extends CoreAliasStorage {
   protected $workspaceManager;
 
   /**
-   * AliasStorage constructor.
+   * Sets the workspace manager.
    *
-   * @param \Drupal\Core\Database\Connection $connection
-   *   A database connection for reading and writing path aliases.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *   The module handler.
-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
-   *   The entity type manager.
    * @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
    *   The workspace manager service.
+   *
+   * @return $this
    */
-  public function __construct(Connection $connection, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager) {
-    parent::__construct($connection, $module_handler, $entity_type_manager);
+  public function setWorkspacesManager(WorkspaceManagerInterface $workspace_manager) {
     $this->workspaceManager = $workspace_manager;
+    return $this;
   }
 
   /**
diff --git a/core/modules/workspaces/src/WorkspacesServiceProvider.php b/core/modules/workspaces/src/WorkspacesServiceProvider.php
index 598ae742d7c3d756f28d2984abcfe0f2250e3630..1ed61747aaabe5f5f551d325eeb2af9f5d928405 100644
--- a/core/modules/workspaces/src/WorkspacesServiceProvider.php
+++ b/core/modules/workspaces/src/WorkspacesServiceProvider.php
@@ -20,10 +20,10 @@ public function alter(ContainerBuilder $container) {
     $renderer_config['required_cache_contexts'][] = 'workspace';
     $container->setParameter('renderer.config', $renderer_config);
 
-    // Replace the class of the 'path.alias_storage' service.
-    $container->getDefinition('path.alias_storage')
-      ->setClass(AliasStorage::class)
-      ->addArgument(new Reference('workspaces.manager'));
+    // Replace the class of the 'path.alias_repository' service.
+    $container->getDefinition('path.alias_repository')
+      ->setClass(WorkspacesAliasRepository::class)
+      ->addMethodCall('setWorkspacesManager', [new Reference('workspaces.manager')]);
   }
 
 }
diff --git a/core/tests/Drupal/FunctionalTests/Installer/DistributionProfileTest.php b/core/tests/Drupal/FunctionalTests/Installer/DistributionProfileTest.php
index 51e702f40b0a644a746e61460bc01ff0a2325824..dce7ef51178b73c92a814e1180394418f47db395 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/DistributionProfileTest.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/DistributionProfileTest.php
@@ -36,7 +36,7 @@ protected function prepareEnvironment() {
     $path = $this->siteDirectory . '/profiles/mydistro';
     mkdir($path, 0777, TRUE);
     file_put_contents("$path/mydistro.info.yml", Yaml::encode($this->info));
-    file_put_contents("$path/mydistro.install", "<?php function mydistro_install() {\Drupal::service('path.alias_storage')->save('/user/1', '/myrootuser');}");
+    file_put_contents("$path/mydistro.install", "<?php function mydistro_install() {\Drupal::entityTypeManager()->getStorage('path_alias')->create(['path' => '/user/1', 'alias' => '/myrootuser'])->save();}");
   }
 
   /**
diff --git a/core/tests/Drupal/FunctionalTests/Routing/PathEncodedTest.php b/core/tests/Drupal/FunctionalTests/Routing/PathEncodedTest.php
index a8aee89711f418234beeb6d0412ab19237316f32..bf33b18f309061779e9dd83009a66888f4ec6b25 100644
--- a/core/tests/Drupal/FunctionalTests/Routing/PathEncodedTest.php
+++ b/core/tests/Drupal/FunctionalTests/Routing/PathEncodedTest.php
@@ -4,14 +4,18 @@
 
 use Drupal\Core\Url;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 
 /**
  * Tests url generation and routing for route paths with encoded characters.
  *
+ * @group path
  * @group routing
  */
 class PathEncodedTest extends BrowserTestBase {
 
+  use PathAliasTestTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -35,12 +39,10 @@ public function testAliasToEncoded() {
       'path_encoded_test.atsign' => '/bloggy/@Dries',
       'path_encoded_test.parens' => '/cat(box)',
     ];
-    /** @var \Drupal\Core\Path\AliasStorageInterface $alias_storage */
-    $alias_storage = $this->container->get('path.alias_storage');
     $aliases = [];
     foreach ($route_paths as $route_name => $path) {
       $aliases[$route_name] = $this->randomMachineName();
-      $alias_storage->save($path, '/' . $aliases[$route_name]);
+      $this->createPathAlias($path, '/' . $aliases[$route_name]);
     }
     foreach ($route_paths as $route_name => $path) {
       // The alias may be only a suffix of the generated path when the test is
diff --git a/core/tests/Drupal/FunctionalTests/Routing/RouteCachingNonPathLanguageNegotiationTest.php b/core/tests/Drupal/FunctionalTests/Routing/RouteCachingNonPathLanguageNegotiationTest.php
index 07286121b3c5fd7ccb0910ead04ee6edb6e40040..37a07c69fe563dee454f0095e552ae26b50e435e 100644
--- a/core/tests/Drupal/FunctionalTests/Routing/RouteCachingNonPathLanguageNegotiationTest.php
+++ b/core/tests/Drupal/FunctionalTests/Routing/RouteCachingNonPathLanguageNegotiationTest.php
@@ -5,6 +5,7 @@
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 
 /**
  * Tests the route cache when the language is not in the path.
@@ -13,6 +14,8 @@
  */
 class RouteCachingNonPathLanguageNegotiationTest extends BrowserTestBase {
 
+  use PathAliasTestTrait;
+
   /**
    * Modules to enable.
    *
@@ -71,7 +74,7 @@ public function testAliases() {
     // on the french page exist in english, no matter which language is
     // checked first. Create the alias after visiting frontpage to make sure
     // there is no existing cache entry for this that affects the tests.
-    \Drupal::service('path.alias_storage')->save('/user/' . $this->adminUser->id(), '/user-page', 'en');
+    $this->createPathAlias('/user/' . $this->adminUser->id(), '/user-page', 'en');
 
     $this->clickLink('French');
     $this->drupalGet('user-page');
diff --git a/core/tests/Drupal/KernelTests/Core/Command/DbDumpTest.php b/core/tests/Drupal/KernelTests/Core/Command/DbDumpTest.php
index 47fa613ab265c6f65a49e94f596e9c577ecb6050..f0f430e2c8cc5499aff06e52db2cec026a31ccd8 100644
--- a/core/tests/Drupal/KernelTests/Core/Command/DbDumpTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Command/DbDumpTest.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Database\Database;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 use Drupal\user\Entity\User;
 use Symfony\Component\Console\Tester\CommandTester;
 use Symfony\Component\DependencyInjection\Reference;
@@ -19,6 +20,8 @@
  */
 class DbDumpTest extends KernelTestBase {
 
+  use PathAliasTestTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -109,7 +112,7 @@ protected function setUp() {
     $account->save();
 
     // Create a path alias.
-    $this->container->get('path.alias_storage')->save('/user/' . $account->id(), '/user/example');
+    $this->createPathAlias('/user/' . $account->id(), '/user/example');
 
     // Create a cache table (this will create 'cache_discovery').
     \Drupal::cache('discovery')->set('test', $this->data);
diff --git a/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php b/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php
deleted file mode 100644
index 2e986d18b3e845904f0c5074f9c9163e53fdf5c1..0000000000000000000000000000000000000000
--- a/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-
-namespace Drupal\KernelTests\Core\Path;
-
-use Drupal\Core\Language\LanguageInterface;
-use Drupal\KernelTests\KernelTestBase;
-
-/**
- * @coversDefaultClass \Drupal\Core\Path\AliasStorage
- * @group path
- */
-class AliasStorageTest extends KernelTestBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public static $modules = ['system'];
-
-  /**
-   * @var \Drupal\Core\Path\AliasStorage
-   */
-  protected $storage;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUp() {
-    parent::setUp();
-
-    $this->installEntitySchema('path_alias');
-    $this->storage = $this->container->get('path.alias_storage');
-  }
-
-  /**
-   * @covers ::load
-   */
-  public function testLoad() {
-    $this->storage->save('/test-source-Case', '/test-alias-Case');
-
-    $expected = [
-      'pid' => 1,
-      'alias' => '/test-alias-Case',
-      'source' => '/test-source-Case',
-      'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
-    ];
-
-    $this->assertEquals($expected, $this->storage->load(['alias' => '/test-alias-Case']));
-    $this->assertEquals($expected, $this->storage->load(['alias' => '/test-alias-case']));
-    $this->assertEquals($expected, $this->storage->load(['source' => '/test-source-Case']));
-    $this->assertEquals($expected, $this->storage->load(['source' => '/test-source-case']));
-  }
-
-  /**
-   * @covers ::lookupPathAlias
-   */
-  public function testLookupPathAlias() {
-    $this->storage->save('/test-source-Case', '/test-alias');
-
-    $this->assertEquals('/test-alias', $this->storage->lookupPathAlias('/test-source-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
-    $this->assertEquals('/test-alias', $this->storage->lookupPathAlias('/test-source-case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
-  }
-
-  /**
-   * @covers ::lookupPathSource
-   */
-  public function testLookupPathSource() {
-    $this->storage->save('/test-source', '/test-alias-Case');
-
-    $this->assertEquals('/test-source', $this->storage->lookupPathSource('/test-alias-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
-    $this->assertEquals('/test-source', $this->storage->lookupPathSource('/test-alias-case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
-  }
-
-  /**
-   * @covers ::aliasExists
-   */
-  public function testAliasExists() {
-    $this->storage->save('/test-source-Case', '/test-alias-Case');
-
-    $this->assertTrue($this->storage->aliasExists('/test-alias-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
-    $this->assertTrue($this->storage->aliasExists('/test-alias-case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
-  }
-
-}
diff --git a/core/tests/Drupal/KernelTests/Core/Path/AliasTest.php b/core/tests/Drupal/KernelTests/Core/Path/AliasTest.php
index bd26e1c6e0f6797d99a428cbc10bc9bc05823c9c..f37ba98b1eb3e3d662135d2f681ac25a38c4d66d 100644
--- a/core/tests/Drupal/KernelTests/Core/Path/AliasTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Path/AliasTest.php
@@ -2,20 +2,24 @@
 
 namespace Drupal\KernelTests\Core\Path;
 
-use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Cache\MemoryCounterBackend;
-use Drupal\Core\Database\Database;
+use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Path\AliasManager;
 use Drupal\Core\Path\AliasWhitelist;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 
 /**
  * Tests path alias CRUD and lookup functionality.
  *
- * @group Path
+ * @coversDefaultClass \Drupal\Core\Path\AliasRepository
+ *
+ * @group path
  */
 class AliasTest extends KernelTestBase {
 
+  use PathAliasTestTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -29,166 +33,76 @@ protected function setUp() {
     $this->installEntitySchema('path_alias');
   }
 
-  public function testCRUD() {
-    // Prepare database table.
-    $connection = Database::getConnection();
-
-    // Create Path object.
-    $aliasStorage = $this->container->get('path.alias_storage');
-
-    $aliases = $this->sampleUrlAliases();
-
-    // Create a few aliases
-    foreach ($aliases as $idx => $alias) {
-      $aliasStorage->save($alias['source'], $alias['alias'], $alias['langcode']);
-
-      $result = $connection->query('SELECT * FROM {path_alias} WHERE path = :path AND alias= :alias AND langcode = :langcode', [':path' => $alias['source'], ':alias' => $alias['alias'], ':langcode' => $alias['langcode']]);
-      $rows = $result->fetchAll();
-
-      $this->assertEqual(count($rows), 1, new FormattableMarkup('Created an entry for %alias.', ['%alias' => $alias['alias']]));
-
-      // Cache the pid for further tests.
-      $aliases[$idx]['pid'] = $rows[0]->id;
-    }
-
-    // Load a few aliases
-    foreach ($aliases as $alias) {
-      $pid = $alias['pid'];
-      $loadedAlias = $aliasStorage->load(['pid' => $pid]);
-      $this->assertEqual($loadedAlias, $alias, new FormattableMarkup('Loaded the expected path with pid %pid.', ['%pid' => $pid]));
-    }
-
-    // Load alias by source path.
-    $loadedAlias = $aliasStorage->load(['source' => '/node/1']);
-    $this->assertEqual($loadedAlias['alias'], '/alias_for_node_1_und', 'The last created alias loaded by default.');
-
-    // Update a few aliases
-    foreach ($aliases as $alias) {
-      $fields = $aliasStorage->save($alias['source'], $alias['alias'] . '_updated', $alias['langcode'], $alias['pid']);
-
-      $this->assertEqual($alias['alias'], $fields['original']['alias']);
-
-      $result = $connection->query('SELECT id FROM {path_alias} WHERE path = :path AND alias= :alias AND langcode = :langcode', [':path' => $alias['source'], ':alias' => $alias['alias'] . '_updated', ':langcode' => $alias['langcode']]);
-      $pid = $result->fetchField();
-
-      $this->assertEqual($pid, $alias['pid'], new FormattableMarkup('Updated entry for pid %pid.', ['%pid' => $pid]));
-    }
-
-    // Delete a few aliases
-    foreach ($aliases as $alias) {
-      $pid = $alias['pid'];
-      $aliasStorage->delete(['pid' => $pid]);
-
-      $result = $connection->query('SELECT * FROM {path_alias} WHERE id = :id', [':id' => $pid]);
-      $rows = $result->fetchAll();
+  /**
+   * @covers ::lookupBySystemPath
+   */
+  public function testLookupBySystemPath() {
+    $this->createPathAlias('/test-source-Case', '/test-alias');
 
-      $this->assertEqual(count($rows), 0, new FormattableMarkup('Deleted entry with pid %pid.', ['%pid' => $pid]));
-    }
+    $path_alias_repository = $this->container->get('path.alias_repository');
+    $this->assertEquals('/test-alias', $path_alias_repository->lookupBySystemPath('/test-source-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED)['alias']);
+    $this->assertEquals('/test-alias', $path_alias_repository->lookupBySystemPath('/test-source-case', LanguageInterface::LANGCODE_NOT_SPECIFIED)['alias']);
   }
 
   /**
-   * Returns an array of URL aliases for testing.
-   *
-   * @return array of URL alias definitions.
+   * @covers ::lookupByAlias
    */
-  protected function sampleUrlAliases() {
-    return [
-      [
-        'source' => '/node/1',
-        'alias' => '/alias_for_node_1_en',
-        'langcode' => 'en',
-      ],
-      [
-        'source' => '/node/2',
-        'alias' => '/alias_for_node_2_en',
-        'langcode' => 'en',
-      ],
-      [
-        'source' => '/node/1',
-        'alias' => '/alias_for_node_1_fr',
-        'langcode' => 'fr',
-      ],
-      [
-        'source' => '/node/1',
-        'alias' => '/alias_for_node_1_und',
-        'langcode' => 'und',
-      ],
-    ];
+  public function testLookupByAlias() {
+    $this->createPathAlias('/test-source', '/test-alias-Case');
+
+    $path_alias_repository = $this->container->get('path.alias_repository');
+    $this->assertEquals('/test-source', $path_alias_repository->lookupByAlias('/test-alias-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED)['path']);
+    $this->assertEquals('/test-source', $path_alias_repository->lookupByAlias('/test-alias-case', LanguageInterface::LANGCODE_NOT_SPECIFIED)['path']);
   }
 
+  /**
+   * @covers \Drupal\Core\Path\AliasManager::getPathByAlias
+   * @covers \Drupal\Core\Path\AliasManager::getAliasByPath
+   */
   public function testLookupPath() {
     // Create AliasManager and Path object.
     $aliasManager = $this->container->get('path.alias_manager');
-    $aliasStorage = $this->container->get('path.alias_storage');
 
     // Test the situation where the source is the same for multiple aliases.
     // Start with a language-neutral alias, which we will override.
-    $path = [
-      'source' => "/user/1",
-      'alias' => '/foo',
-    ];
-
-    $aliasStorage->save($path['source'], $path['alias']);
-    $this->assertEqual($aliasManager->getAliasByPath($path['source']), $path['alias'], 'Basic alias lookup works.');
-    $this->assertEqual($aliasManager->getPathByAlias($path['alias']), $path['source'], 'Basic source lookup works.');
+    $path_alias = $this->createPathAlias('/user/1', '/foo');
+    $this->assertEquals($path_alias->getAlias(), $aliasManager->getAliasByPath($path_alias->getPath()), 'Basic alias lookup works.');
+    $this->assertEquals($path_alias->getPath(), $aliasManager->getPathByAlias($path_alias->getAlias()), 'Basic source lookup works.');
 
     // Create a language specific alias for the default language (English).
-    $path = [
-      'source' => "/user/1",
-      'alias' => "/users/Dries",
-      'langcode' => 'en',
-    ];
-    $aliasStorage->save($path['source'], $path['alias'], $path['langcode']);
-    // Hook that clears cache is not executed with unit tests.
-    \Drupal::service('path.alias_manager')->cacheClear();
-    $this->assertEqual($aliasManager->getAliasByPath($path['source']), $path['alias'], 'English alias overrides language-neutral alias.');
-    $this->assertEqual($aliasManager->getPathByAlias($path['alias']), $path['source'], 'English source overrides language-neutral source.');
+    $path_alias = $this->createPathAlias('/user/1', '/users/Dries', 'en');
+
+    $this->assertEquals($path_alias->getAlias(), $aliasManager->getAliasByPath($path_alias->getPath()), 'English alias overrides language-neutral alias.');
+    $this->assertEquals($path_alias->getPath(), $aliasManager->getPathByAlias($path_alias->getAlias()), 'English source overrides language-neutral source.');
 
     // Create a language-neutral alias for the same path, again.
-    $path = [
-      'source' => "/user/1",
-      'alias' => '/bar',
-    ];
-    $aliasStorage->save($path['source'], $path['alias']);
-    $this->assertEqual($aliasManager->getAliasByPath($path['source']), "/users/Dries", 'English alias still returned after entering a language-neutral alias.');
+    $path_alias = $this->createPathAlias('/user/1', '/bar');
+    $this->assertEquals("/users/Dries", $aliasManager->getAliasByPath($path_alias->getPath()), 'English alias still returned after entering a language-neutral alias.');
 
     // Create a language-specific (xx-lolspeak) alias for the same path.
-    $path = [
-      'source' => "/user/1",
-      'alias' => '/LOL',
-      'langcode' => 'xx-lolspeak',
-    ];
-    $aliasStorage->save($path['source'], $path['alias'], $path['langcode']);
-    $this->assertEqual($aliasManager->getAliasByPath($path['source']), "/users/Dries", 'English alias still returned after entering a LOLspeak alias.');
+    $path_alias = $this->createPathAlias('/user/1', '/LOL', 'xx-lolspeak');
+    $this->assertEquals("/users/Dries", $aliasManager->getAliasByPath($path_alias->getPath()), 'English alias still returned after entering a LOLspeak alias.');
     // The LOLspeak alias should be returned if we really want LOLspeak.
-    $this->assertEqual($aliasManager->getAliasByPath($path['source'], 'xx-lolspeak'), '/LOL', 'LOLspeak alias returned if we specify xx-lolspeak to the alias manager.');
+    $this->assertEquals('/LOL', $aliasManager->getAliasByPath($path_alias->getPath(), 'xx-lolspeak'), 'LOLspeak alias returned if we specify xx-lolspeak to the alias manager.');
 
     // Create a new alias for this path in English, which should override the
     // previous alias for "user/1".
-    $path = [
-      'source' => "/user/1",
-      'alias' => '/users/my-new-path',
-      'langcode' => 'en',
-    ];
-    $aliasStorage->save($path['source'], $path['alias'], $path['langcode']);
-    // Hook that clears cache is not executed with unit tests.
-    $aliasManager->cacheClear();
-    $this->assertEqual($aliasManager->getAliasByPath($path['source']), $path['alias'], 'Recently created English alias returned.');
-    $this->assertEqual($aliasManager->getPathByAlias($path['alias']), $path['source'], 'Recently created English source returned.');
+    $path_alias = $this->createPathAlias('/user/1', '/users/my-new-path', 'en');
+    $this->assertEquals($path_alias->getAlias(), $aliasManager->getAliasByPath($path_alias->getPath()), 'Recently created English alias returned.');
+    $this->assertEquals($path_alias->getPath(), $aliasManager->getPathByAlias($path_alias->getAlias()), 'Recently created English source returned.');
 
     // Remove the English aliases, which should cause a fallback to the most
     // recently created language-neutral alias, 'bar'.
-    $aliasStorage->delete(['langcode' => 'en']);
-    // Hook that clears cache is not executed with unit tests.
-    $aliasManager->cacheClear();
-    $this->assertEqual($aliasManager->getAliasByPath($path['source']), '/bar', 'Path lookup falls back to recently created language-neutral alias.');
+    $path_alias_storage = $this->container->get('entity_type.manager')->getStorage('path_alias');
+    $entities = $path_alias_storage->loadByProperties(['langcode' => 'en']);
+    $path_alias_storage->delete($entities);
+    $this->assertEquals('/bar', $aliasManager->getAliasByPath($path_alias->getPath()), 'Path lookup falls back to recently created language-neutral alias.');
 
     // Test the situation where the alias and language are the same, but
     // the source differs. The newer alias record should be returned.
-    $aliasStorage->save('/user/2', '/bar');
-    // Hook that clears cache is not executed with unit tests.
+    $this->createPathAlias('/user/2', '/bar');
     $aliasManager->cacheClear();
-    $this->assertEqual($aliasManager->getPathByAlias('/bar'), '/user/2', 'Newer alias record is returned when comparing two LanguageInterface::LANGCODE_NOT_SPECIFIED paths with the same alias.');
+    $this->assertEquals('/user/2', $aliasManager->getPathByAlias('/bar'), 'Newer alias record is returned when comparing two LanguageInterface::LANGCODE_NOT_SPECIFIED paths with the same alias.');
   }
 
   /**
@@ -198,9 +112,8 @@ public function testWhitelist() {
     $memoryCounterBackend = new MemoryCounterBackend();
 
     // Create AliasManager and Path object.
-    $aliasStorage = $this->container->get('path.alias_storage');
-    $whitelist = new AliasWhitelist('path_alias_whitelist', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $aliasStorage);
-    $aliasManager = new AliasManager($aliasStorage, $whitelist, $this->container->get('language_manager'), $memoryCounterBackend);
+    $whitelist = new AliasWhitelist('path_alias_whitelist', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $this->container->get('path.alias_repository'));
+    $aliasManager = new AliasManager($this->container->get('path.alias_repository'), $whitelist, $this->container->get('language_manager'), $memoryCounterBackend);
 
     // No alias for user and admin yet, so should be NULL.
     $this->assertNull($whitelist->get('user'));
@@ -211,21 +124,23 @@ public function testWhitelist() {
     $this->assertNull($whitelist->get($this->randomMachineName()));
 
     // Add an alias for user/1, user should get whitelisted now.
-    $aliasStorage->save('/user/1', '/' . $this->randomMachineName());
+    $this->createPathAlias('/user/1', '/' . $this->randomMachineName());
     $aliasManager->cacheClear();
     $this->assertTrue($whitelist->get('user'));
     $this->assertNull($whitelist->get('admin'));
     $this->assertNull($whitelist->get($this->randomMachineName()));
 
     // Add an alias for admin, both should get whitelisted now.
-    $aliasStorage->save('/admin/something', '/' . $this->randomMachineName());
+    $this->createPathAlias('/admin/something', '/' . $this->randomMachineName());
     $aliasManager->cacheClear();
     $this->assertTrue($whitelist->get('user'));
     $this->assertTrue($whitelist->get('admin'));
     $this->assertNull($whitelist->get($this->randomMachineName()));
 
     // Remove the user alias again, whitelist entry should be removed.
-    $aliasStorage->delete(['source' => '/user/1']);
+    $path_alias_storage = $this->container->get('entity_type.manager')->getStorage('path_alias');
+    $entities = $path_alias_storage->loadByProperties(['path' => '/user/1']);
+    $path_alias_storage->delete($entities);
     $aliasManager->cacheClear();
     $this->assertNull($whitelist->get('user'));
     $this->assertTrue($whitelist->get('admin'));
@@ -238,7 +153,7 @@ public function testWhitelist() {
 
     // Re-initialize the whitelist using the same cache backend, should load
     // from cache.
-    $whitelist = new AliasWhitelist('path_alias_whitelist', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $aliasStorage);
+    $whitelist = new AliasWhitelist('path_alias_whitelist', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $this->container->get('path.alias_repository'));
     $this->assertNull($whitelist->get('user'));
     $this->assertTrue($whitelist->get('admin'));
     $this->assertNull($whitelist->get($this->randomMachineName()));
@@ -258,17 +173,15 @@ public function testWhitelistCacheDeletionMidRequest() {
     $memoryCounterBackend = new MemoryCounterBackend();
 
     // Create AliasManager and Path object.
-    $aliasStorage = $this->container->get('path.alias_storage');
-    $whitelist = new AliasWhitelist('path_alias_whitelist', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $aliasStorage);
-    $aliasManager = new AliasManager($aliasStorage, $whitelist, $this->container->get('language_manager'), $memoryCounterBackend);
+    $whitelist = new AliasWhitelist('path_alias_whitelist', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $this->container->get('path.alias_repository'));
+    $aliasManager = new AliasManager($this->container->get('path.alias_repository'), $whitelist, $this->container->get('language_manager'), $memoryCounterBackend);
 
     // Whitelist cache should not exist at all yet.
     $this->assertFalse($memoryCounterBackend->get('path_alias_whitelist'));
 
     // Add some aliases for both menu routes we have.
-    $aliasStorage->save('/admin/something', '/' . $this->randomMachineName());
-    $aliasStorage->save('/user/something', '/' . $this->randomMachineName());
-    $aliasManager->cacheClear();
+    $this->createPathAlias('/admin/something', '/' . $this->randomMachineName());
+    $this->createPathAlias('/user/something', '/' . $this->randomMachineName());
 
     // Lookup admin path in whitelist. It will query the DB and figure out
     // that it indeed has an alias, and add it to the internal whitelist and
@@ -288,7 +201,7 @@ public function testWhitelistCacheDeletionMidRequest() {
     // Whitelist should load data from its cache, see that it hasn't done a
     // check for 'user' yet, perform the check, then mark the result to be
     // persisted to cache.
-    $whitelist = new AliasWhitelist('path_alias_whitelist', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $aliasStorage);
+    $whitelist = new AliasWhitelist('path_alias_whitelist', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $this->container->get('path.alias_repository'));
     $this->assertTrue($whitelist->get('user'));
 
     // Delete the whitelist cache. This could happen from an outside process,
diff --git a/core/tests/Drupal/KernelTests/Core/Path/LegacyAliasStorageTest.php b/core/tests/Drupal/KernelTests/Core/Path/LegacyAliasStorageTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..dc15422796f69e738125b587940c755a43b2bd6b
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Path/LegacyAliasStorageTest.php
@@ -0,0 +1,238 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Path;
+
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Path\AliasStorage
+ * @group path
+ * @group legacy
+ */
+class LegacyAliasStorageTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['system'];
+
+  /**
+   * @var \Drupal\Core\Path\AliasStorage
+   */
+  protected $storage;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('path_alias');
+    $this->storage = $this->container->get('path.alias_storage');
+  }
+
+  /**
+   * @covers ::load
+   * @expectedDeprecation \Drupal\Core\Path\AliasStorage is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use the "path.alias_repository" service instead, or the entity storage handler for the "path_alias" entity type for CRUD methods. See https://www.drupal.org/node/3013865.
+   */
+  public function testLoad() {
+    $this->storage->save('/test-source-Case', '/test-alias-Case');
+
+    $expected = [
+      'pid' => 1,
+      'alias' => '/test-alias-Case',
+      'source' => '/test-source-Case',
+      'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+    ];
+
+    $this->assertEquals($expected, $this->storage->load(['alias' => '/test-alias-Case']));
+    $this->assertEquals($expected, $this->storage->load(['alias' => '/test-alias-case']));
+    $this->assertEquals($expected, $this->storage->load(['source' => '/test-source-Case']));
+    $this->assertEquals($expected, $this->storage->load(['source' => '/test-source-case']));
+  }
+
+  /**
+   * @covers ::load
+   * @covers ::save
+   * @covers ::delete
+   * @expectedDeprecation \Drupal\Core\Path\AliasStorage is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use the "path.alias_repository" service instead, or the entity storage handler for the "path_alias" entity type for CRUD methods. See https://www.drupal.org/node/3013865.
+   */
+  public function testCRUD() {
+    $entity_storage = \Drupal::entityTypeManager()->getStorage('path_alias');
+    $aliases = $this->sampleUrlAliases();
+
+    // Create a few aliases
+    foreach ($aliases as $idx => $alias) {
+      $this->storage->save($alias['source'], $alias['alias'], $alias['langcode']);
+
+      $result = $entity_storage->getQuery()
+        ->condition('path', $alias['source'])
+        ->condition('alias', $alias['alias'])
+        ->condition('langcode', $alias['langcode'])
+        ->execute();
+
+      $this->assertCount(1, $result, "Created an entry for {$alias['alias']}.");
+
+      // Cache the pid for further tests.
+      $aliases[$idx]['pid'] = reset($result);
+    }
+
+    // Load a few aliases
+    foreach ($aliases as $alias) {
+      $pid = $alias['pid'];
+      $loadedAlias = $this->storage->load(['pid' => $pid]);
+      $this->assertEquals($alias, $loadedAlias, "Loaded the expected path with pid $pid.");
+    }
+
+    // Load alias by source path.
+    $loadedAlias = $this->storage->load(['source' => '/node/1']);
+    $this->assertEquals('/alias_for_node_1_und', $loadedAlias['alias'], 'The last created alias loaded by default.');
+
+    // Update a few aliases
+    foreach ($aliases as $alias) {
+      $fields = $this->storage->save($alias['source'], $alias['alias'] . '_updated', $alias['langcode'], $alias['pid']);
+
+      $this->assertEquals($alias['alias'], $fields['original']['alias']);
+
+      $result = $entity_storage->getQuery()
+        ->condition('path', $alias['source'])
+        ->condition('alias', $alias['alias'] . '_updated')
+        ->condition('langcode', $alias['langcode'])
+        ->execute();
+      $pid = reset($result);
+
+      $this->assertEquals($alias['pid'], $pid, "Updated entry for pid $pid.");
+    }
+
+    // Delete a few aliases
+    foreach ($aliases as $alias) {
+      $pid = $alias['pid'];
+      $this->storage->delete(['pid' => $pid]);
+
+      $result = $entity_storage->getQuery()->condition('id', $pid)->execute();
+
+      $this->assertCount(0, $result, "Deleted entry with pid $pid.");
+    }
+  }
+
+  /**
+   * Returns an array of URL aliases for testing.
+   *
+   * @return array of URL alias definitions.
+   */
+  protected function sampleUrlAliases() {
+    return [
+      [
+        'source' => '/node/1',
+        'alias' => '/alias_for_node_1_en',
+        'langcode' => 'en',
+      ],
+      [
+        'source' => '/node/2',
+        'alias' => '/alias_for_node_2_en',
+        'langcode' => 'en',
+      ],
+      [
+        'source' => '/node/1',
+        'alias' => '/alias_for_node_1_fr',
+        'langcode' => 'fr',
+      ],
+      [
+        'source' => '/node/1',
+        'alias' => '/alias_for_node_1_und',
+        'langcode' => 'und',
+      ],
+    ];
+  }
+
+  /**
+   * @covers ::preloadPathAlias
+   * @expectedDeprecation \Drupal\Core\Path\AliasStorage is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use the "path.alias_repository" service instead, or the entity storage handler for the "path_alias" entity type for CRUD methods. See https://www.drupal.org/node/3013865.
+   */
+  public function testPreLoadPathAlias() {
+    $this->storage->save('/test-source-Case', '/test-alias');
+
+    $this->assertEquals(['/test-source-Case' => '/test-alias'], $this->storage->preloadPathAlias(['/test-source-Case'], LanguageInterface::LANGCODE_NOT_SPECIFIED));
+  }
+
+  /**
+   * @covers ::lookupPathAlias
+   * @expectedDeprecation \Drupal\Core\Path\AliasStorage is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use the "path.alias_repository" service instead, or the entity storage handler for the "path_alias" entity type for CRUD methods. See https://www.drupal.org/node/3013865.
+   */
+  public function testLookupPathAlias() {
+    $this->storage->save('/test-source-Case', '/test-alias');
+
+    $this->assertEquals('/test-alias', $this->storage->lookupPathAlias('/test-source-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+    $this->assertEquals('/test-alias', $this->storage->lookupPathAlias('/test-source-case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+  }
+
+  /**
+   * @covers ::lookupPathSource
+   * @expectedDeprecation \Drupal\Core\Path\AliasStorage is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use the "path.alias_repository" service instead, or the entity storage handler for the "path_alias" entity type for CRUD methods. See https://www.drupal.org/node/3013865.
+   */
+  public function testLookupPathSource() {
+    $this->storage->save('/test-source', '/test-alias-Case');
+
+    $this->assertEquals('/test-source', $this->storage->lookupPathSource('/test-alias-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+    $this->assertEquals('/test-source', $this->storage->lookupPathSource('/test-alias-case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+  }
+
+  /**
+   * @covers ::aliasExists
+   * @expectedDeprecation \Drupal\Core\Path\AliasStorage is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use the "path.alias_repository" service instead, or the entity storage handler for the "path_alias" entity type for CRUD methods. See https://www.drupal.org/node/3013865.
+   */
+  public function testAliasExists() {
+    $this->storage->save('/test-source-Case', '/test-alias-Case');
+
+    $this->assertTrue($this->storage->aliasExists('/test-alias-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+    $this->assertTrue($this->storage->aliasExists('/test-alias-case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+  }
+
+  /**
+   * @covers ::languageAliasExists
+   * @expectedDeprecation \Drupal\Core\Path\AliasStorage is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use the "path.alias_repository" service instead, or the entity storage handler for the "path_alias" entity type for CRUD methods. See https://www.drupal.org/node/3013865.
+   */
+  public function testLanguageAliasExists() {
+    $this->assertFalse($this->storage->languageAliasExists());
+
+    $this->storage->save('/test-source-Case', '/test-alias-Case', 'en');
+    $this->assertTrue($this->storage->languageAliasExists());
+  }
+
+  /**
+   * @covers ::getAliasesForAdminListing
+   * @expectedDeprecation \Drupal\Core\Path\AliasStorage is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use the "path.alias_repository" service instead, or the entity storage handler for the "path_alias" entity type for CRUD methods. See https://www.drupal.org/node/3013865.
+   */
+  public function testGetAliasesForAdminListing() {
+    $this->storage->save('/test-source-Case', '/test-alias-Case');
+    $this->storage->save('/another-test', '/another-test-alias');
+
+    $expected_alias_1 = new \stdClass();
+    $expected_alias_1->pid = '2';
+    $expected_alias_1->source = '/another-test';
+    $expected_alias_1->alias = '/another-test-alias';
+    $expected_alias_1->langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
+
+    $expected_alias_2 = new \stdClass();
+    $expected_alias_2->pid = '1';
+    $expected_alias_2->source = '/test-source-Case';
+    $expected_alias_2->alias = '/test-alias-Case';
+    $expected_alias_2->langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
+
+    $header = [['field' => 'alias', 'sort' => 'asc']];
+    $this->assertEquals([$expected_alias_1, $expected_alias_2], $this->storage->getAliasesForAdminListing($header));
+  }
+
+  /**
+   * @covers ::pathHasMatchingAlias
+   * @expectedDeprecation \Drupal\Core\Path\AliasStorage is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use the "path.alias_repository" service instead, or the entity storage handler for the "path_alias" entity type for CRUD methods. See https://www.drupal.org/node/3013865.
+   */
+  public function testPathHasMatchingAlias() {
+    $this->storage->save('/test-source-Case', '/test-alias-Case');
+
+    $this->assertTrue($this->storage->pathHasMatchingAlias('/test'));
+    $this->assertFalse($this->storage->pathHasMatchingAlias('/another'));
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Routing/ContentNegotiationRoutingTest.php b/core/tests/Drupal/KernelTests/Core/Routing/ContentNegotiationRoutingTest.php
index ea23b6f8e65b6722532fb42a7bfc580ab2b3f833..a393a1ceeb20f5ee3c809b66e4e3e4f464290f36 100644
--- a/core/tests/Drupal/KernelTests/Core/Routing/ContentNegotiationRoutingTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Routing/ContentNegotiationRoutingTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 
@@ -14,6 +15,8 @@
  */
 class ContentNegotiationRoutingTest extends KernelTestBase {
 
+  use PathAliasTestTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -46,13 +49,11 @@ public function register(ContainerBuilder $container) {
    * Tests the content negotiation aspect of routing.
    */
   public function testContentRouting() {
-    /** @var \Drupal\Core\Path\AliasStorageInterface $path_alias_storage */
-    $path_alias_storage = $this->container->get('path.alias_storage');
     // Alias with extension pointing to no extension/constant content-type.
-    $path_alias_storage->save('/conneg/html', '/alias.html');
+    $this->createPathAlias('/conneg/html', '/alias.html');
 
     // Alias with extension pointing to dynamic extension/linked content-type.
-    $path_alias_storage->save('/conneg/html?_format=json', '/alias.json');
+    $this->createPathAlias('/conneg/html?_format=json', '/alias.json');
 
     $tests = [
       // ['path', 'accept', 'content-type'],
diff --git a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php
index f439164c4e257b91844e7cb17dd11508dd119e28..bb1380faa733a2c5722e9256a717b344dbbc8a87 100644
--- a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php
@@ -18,6 +18,7 @@
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\Tests\Core\Routing\RoutingFixtures;
+use Drupal\Tests\Traits\Core\PathAliasTestTrait;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
@@ -32,6 +33,8 @@
  */
 class RouteProviderTest extends KernelTestBase {
 
+  use PathAliasTestTrait;
+
   /**
    * Modules to enable.
    */
@@ -572,9 +575,7 @@ public function testRouteCaching() {
     $this->assertEqual(2, count($cache->data['routes']));
 
     // A path with a path alias.
-    /** @var \Drupal\Core\Path\AliasStorageInterface $path_storage */
-    $path_storage = \Drupal::service('path.alias_storage');
-    $path_storage->save('/path/add/one', '/path/add-one');
+    $this->createPathAlias('/path/add/one', '/path/add-one');
     /** @var \Drupal\Core\Path\AliasManagerInterface $alias_manager */
     $alias_manager = \Drupal::service('path.alias_manager');
     $alias_manager->cacheClear();
diff --git a/core/tests/Drupal/Tests/Core/Path/AliasManagerTest.php b/core/tests/Drupal/Tests/Core/Path/AliasManagerTest.php
index 644af818d78cf39ef11d5f53069964ef139212b8..40d982c7291a0ddf4bc8682ecdc2252fbcb9f9fc 100644
--- a/core/tests/Drupal/Tests/Core/Path/AliasManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Path/AliasManagerTest.php
@@ -5,11 +5,12 @@
 use Drupal\Core\Language\Language;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Path\AliasManager;
+use Drupal\Core\Path\AliasRepositoryInterface;
 use Drupal\Tests\UnitTestCase;
 
 /**
  * @coversDefaultClass \Drupal\Core\Path\AliasManager
- * @group Path
+ * @group path
  */
 class AliasManagerTest extends UnitTestCase {
 
@@ -27,6 +28,13 @@ class AliasManagerTest extends UnitTestCase {
    */
   protected $aliasStorage;
 
+  /**
+   * Alias repository.
+   *
+   * @var \Drupal\Core\Path\AliasRepositoryInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $aliasRepository;
+
   /**
    * Alias whitelist.
    *
@@ -68,12 +76,12 @@ class AliasManagerTest extends UnitTestCase {
   protected function setUp() {
     parent::setUp();
 
-    $this->aliasStorage = $this->createMock('Drupal\Core\Path\AliasStorageInterface');
+    $this->aliasRepository = $this->createMock(AliasRepositoryInterface::class);
     $this->aliasWhitelist = $this->createMock('Drupal\Core\Path\AliasWhitelistInterface');
     $this->languageManager = $this->createMock('Drupal\Core\Language\LanguageManagerInterface');
     $this->cache = $this->createMock('Drupal\Core\Cache\CacheBackendInterface');
 
-    $this->aliasManager = new AliasManager($this->aliasStorage, $this->aliasWhitelist, $this->languageManager, $this->cache);
+    $this->aliasManager = new AliasManager($this->aliasRepository, $this->aliasWhitelist, $this->languageManager, $this->cache);
 
   }
 
@@ -92,8 +100,8 @@ public function testGetPathByAliasNoMatch() {
       ->with(LanguageInterface::TYPE_URL)
       ->will($this->returnValue($language));
 
-    $this->aliasStorage->expects($this->once())
-      ->method('lookupPathSource')
+    $this->aliasRepository->expects($this->once())
+      ->method('lookupByAlias')
       ->with($alias, $language->getId())
       ->will($this->returnValue(NULL));
 
@@ -113,10 +121,10 @@ public function testGetPathByAliasNatch() {
 
     $language = $this->setUpCurrentLanguage();
 
-    $this->aliasStorage->expects($this->once())
-      ->method('lookupPathSource')
+    $this->aliasRepository->expects($this->once())
+      ->method('lookupByAlias')
       ->with($alias, $language->getId())
-      ->will($this->returnValue($path));
+      ->will($this->returnValue(['path' => $path]));
 
     $this->assertEquals($path, $this->aliasManager->getPathByAlias($alias));
     // Call it twice to test the static cache.
@@ -135,10 +143,10 @@ public function testGetPathByAliasLangcode() {
     $this->languageManager->expects($this->never())
       ->method('getCurrentLanguage');
 
-    $this->aliasStorage->expects($this->once())
-      ->method('lookupPathSource')
+    $this->aliasRepository->expects($this->once())
+      ->method('lookupByAlias')
       ->with($alias, 'de')
-      ->will($this->returnValue($path));
+      ->will($this->returnValue(['path' => $path]));
 
     $this->assertEquals($path, $this->aliasManager->getPathByAlias($alias, 'de'));
     // Call it twice to test the static cache.
@@ -164,8 +172,8 @@ public function testGetAliasByPathWhitelist() {
 
     // The whitelist returns FALSE for that path part, so the storage should
     // never be called.
-    $this->aliasStorage->expects($this->never())
-      ->method('lookupPathAlias');
+    $this->aliasRepository->expects($this->never())
+      ->method('lookupBySystemPath');
 
     $this->assertEquals($path, $this->aliasManager->getAliasByPath($path));
   }
@@ -189,8 +197,8 @@ public function testGetAliasByPathNoMatch() {
       ->with($path_part1)
       ->will($this->returnValue(TRUE));
 
-    $this->aliasStorage->expects($this->once())
-      ->method('lookupPathAlias')
+    $this->aliasRepository->expects($this->once())
+      ->method('lookupBySystemPath')
       ->with($path, $language->getId())
       ->will($this->returnValue(NULL));
 
@@ -227,10 +235,10 @@ public function testGetAliasByPathMatch() {
       ->with($path_part1)
       ->will($this->returnValue(TRUE));
 
-    $this->aliasStorage->expects($this->once())
-      ->method('lookupPathAlias')
+    $this->aliasRepository->expects($this->once())
+      ->method('lookupBySystemPath')
       ->with($path, $language->getId())
-      ->will($this->returnValue($alias));
+      ->will($this->returnValue(['alias' => $alias]));
 
     $this->assertEquals($alias, $this->aliasManager->getAliasByPath($path));
     // Call it twice to test the static cache.
@@ -272,14 +280,14 @@ public function testGetAliasByPathCachedMatch() {
       ->with($path_part1)
       ->will($this->returnValue(TRUE));
 
-    $this->aliasStorage->expects($this->once())
+    $this->aliasRepository->expects($this->once())
       ->method('preloadPathAlias')
       ->with($cached_paths[$language->getId()], $language->getId())
       ->will($this->returnValue([$path => $alias]));
 
     // LookupPathAlias should not be called.
-    $this->aliasStorage->expects($this->never())
-      ->method('lookupPathAlias');
+    $this->aliasRepository->expects($this->never())
+      ->method('lookupBySystemPath');
 
     $this->assertEquals($alias, $this->aliasManager->getAliasByPath($path));
     // Call it twice to test the static cache.
@@ -322,12 +330,12 @@ public function testGetAliasByPathCachedMissLanguage() {
 
     // The requested language is different than the cached, so this will
     // need to load.
-    $this->aliasStorage->expects($this->never())
+    $this->aliasRepository->expects($this->never())
       ->method('preloadPathAlias');
-    $this->aliasStorage->expects($this->once())
-      ->method('lookupPathAlias')
+    $this->aliasRepository->expects($this->once())
+      ->method('lookupBySystemPath')
       ->with($path, $language->getId())
-      ->will($this->returnValue($alias));
+      ->will($this->returnValue(['alias' => $alias]));
 
     $this->assertEquals($alias, $this->aliasManager->getAliasByPath($path));
     // Call it twice to test the static cache.
@@ -369,14 +377,14 @@ public function testGetAliasByPathCachedMissNoAlias() {
       ->with($path_part1)
       ->will($this->returnValue(TRUE));
 
-    $this->aliasStorage->expects($this->once())
+    $this->aliasRepository->expects($this->once())
       ->method('preloadPathAlias')
       ->with($cached_paths[$language->getId()], $language->getId())
       ->will($this->returnValue([$cached_path => $cached_alias]));
 
     // LookupPathAlias() should not be called.
-    $this->aliasStorage->expects($this->never())
-      ->method('lookupPathAlias');
+    $this->aliasRepository->expects($this->never())
+      ->method('lookupBySystemPath');
 
     $this->assertEquals($path, $this->aliasManager->getAliasByPath($path));
     // Call it twice to test the static cache.
@@ -417,13 +425,13 @@ public function testGetAliasByPathUncachedMissNoAlias() {
       ->with($path_part1)
       ->will($this->returnValue(TRUE));
 
-    $this->aliasStorage->expects($this->once())
+    $this->aliasRepository->expects($this->once())
       ->method('preloadPathAlias')
       ->with($cached_paths[$language->getId()], $language->getId())
       ->will($this->returnValue([$cached_path => $cached_alias]));
 
-    $this->aliasStorage->expects($this->once())
-      ->method('lookupPathAlias')
+    $this->aliasRepository->expects($this->once())
+      ->method('lookupBySystemPath')
       ->with($path, $language->getId())
       ->will($this->returnValue(NULL));
 
@@ -445,10 +453,10 @@ public function testCacheClear() {
     $path = '/path';
     $alias = '/alias';
     $language = $this->setUpCurrentLanguage();
-    $this->aliasStorage->expects($this->exactly(2))
-      ->method('lookupPathAlias')
+    $this->aliasRepository->expects($this->exactly(2))
+      ->method('lookupBySystemPath')
       ->with($path, $language->getId())
-      ->willReturn($alias);
+      ->willReturn(['alias' => $alias]);
     $this->aliasWhitelist->expects($this->any())
       ->method('get')
       ->willReturn(TRUE);
@@ -457,9 +465,8 @@ public function testCacheClear() {
     $this->assertEquals($alias, $this->aliasManager->getAliasByPath($path, $language->getId()));
 
     // Check that the cache is populated.
-    $original_storage = clone $this->aliasStorage;
-    $this->aliasStorage->expects($this->never())
-      ->method('lookupPathSource');
+    $this->aliasRepository->expects($this->never())
+      ->method('lookupByAlias');
     $this->assertEquals($path, $this->aliasManager->getPathByAlias($alias, $language->getId()));
 
     // Clear specific source.
@@ -506,15 +513,15 @@ public function testGetAliasByPathUncachedMissWithAlias() {
       ->with($path_part1)
       ->will($this->returnValue(TRUE));
 
-    $this->aliasStorage->expects($this->once())
+    $this->aliasRepository->expects($this->once())
       ->method('preloadPathAlias')
       ->with($cached_paths[$language->getId()], $language->getId())
       ->will($this->returnValue([$cached_path => $cached_alias]));
 
-    $this->aliasStorage->expects($this->once())
-      ->method('lookupPathAlias')
+    $this->aliasRepository->expects($this->once())
+      ->method('lookupBySystemPath')
       ->with($path, $language->getId())
-      ->will($this->returnValue($new_alias));
+      ->will($this->returnValue(['alias' => $new_alias]));
 
     $this->assertEquals($new_alias, $this->aliasManager->getAliasByPath($path));
     // Call it twice to test the static cache.
diff --git a/core/tests/Drupal/Tests/Traits/Core/PathAliasTestTrait.php b/core/tests/Drupal/Tests/Traits/Core/PathAliasTestTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..2f12e4c399ce0a1b7e2d48ba99501c44ac4baac5
--- /dev/null
+++ b/core/tests/Drupal/Tests/Traits/Core/PathAliasTestTrait.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Drupal\Tests\Traits\Core;
+
+use Drupal\Core\Language\LanguageInterface;
+
+/**
+ * Provides methods to create and assert path_alias entities.
+ *
+ * This trait is meant to be used only by test classes.
+ */
+trait PathAliasTestTrait {
+
+  /**
+   * Creates a new path alias.
+   *
+   * @param string $path
+   *   The system path.
+   * @param string $alias
+   *   The alias for the system path.
+   * @param string $langcode
+   *   (optional) A language code for the path alias. Defaults to
+   *   \Drupal\Core\Language\LanguageInterface::LANGCODE_NOT_SPECIFIED.
+   *
+   * @return \Drupal\Core\Path\PathAliasInterface
+   *   A path alias entity.
+   */
+  protected function createPathAlias($path, $alias, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED) {
+    /** @var \Drupal\Core\Path\PathAliasInterface $path_alias */
+    $path_alias = \Drupal::entityTypeManager()->getStorage('path_alias')->create([
+      'path' => $path,
+      'alias' => $alias,
+      'langcode' => $langcode,
+    ]);
+    $path_alias->save();
+
+    return $path_alias;
+  }
+
+  /**
+   * Gets the first result from a 'load by properties' storage call.
+   *
+   * @param array $conditions
+   *   An array of query conditions.
+   *
+   * @return \Drupal\Core\Path\PathAliasInterface|null
+   *   A path alias entity or NULL.
+   */
+  protected function loadPathAliasByConditions($conditions) {
+    $storage = \Drupal::entityTypeManager()->getStorage('path_alias');
+    $query = $storage->getQuery();
+    foreach ($conditions as $field => $value) {
+      $query->condition($field, $value);
+    }
+    $entities = $storage->loadMultiple($query->execute());
+
+    return $entities ? reset($entities) : NULL;
+  }
+
+  /**
+   * Asserts that a path alias exists in the storage.
+   *
+   * @param string $alias
+   *   The path alias.
+   * @param string|null $langcode
+   *   (optional) The language code of the path alias.
+   * @param string|null $path
+   *   (optional) The system path of the path alias.
+   * @param string|null $message
+   *   (optional) A message to display with the assertion.
+   */
+  protected function assertPathAliasExists($alias, $langcode = NULL, $path = NULL, $message = NULL) {
+    $query = \Drupal::entityTypeManager()->getStorage('path_alias')->getQuery();
+    $query->condition('alias', $alias, '=');
+    if ($langcode) {
+      $query->condition('langcode', $langcode, '=');
+    }
+    if ($path) {
+      $query->condition('path', $path, '=');
+    }
+    $query->count();
+
+    $this->assertTrue((bool) $query->execute(), $message);
+  }
+
+  /**
+   * Asserts that a path alias does not exist in the storage.
+   *
+   * @param string $alias
+   *   The path alias.
+   * @param string|null $langcode
+   *   (optional) The language code of the path alias.
+   * @param string|null $path
+   *   (optional) The system path of the path alias.
+   * @param string|null $message
+   *   (optional) A message to display with the assertion.
+   */
+  protected function assertPathAliasNotExists($alias, $langcode = NULL, $path = NULL, $message = NULL) {
+    $query = \Drupal::entityTypeManager()->getStorage('path_alias')->getQuery();
+    $query->condition('alias', $alias, '=');
+    if ($langcode) {
+      $query->condition('langcode', $langcode, '=');
+    }
+    if ($path) {
+      $query->condition('path', $path, '=');
+    }
+    $query->count();
+
+    $this->assertFalse((bool) $query->execute(), $message);
+  }
+
+}