diff --git a/core/includes/form.inc b/core/includes/form.inc
index 362d0d5..120e4c9 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -3542,6 +3542,7 @@ function form_process_machine_name($element, &$form_state) {
   // 'source' only) would leave all other properties undefined, if the defaults
   // were defined in hook_element_info(). Therefore, we apply the defaults here.
   $element['#machine_name'] += array(
+    // @todo Use 'label' by default.
     'source' => array('name'),
     'target' => '#' . $element['#id'],
     'label' => t('Machine name'),
diff --git a/core/lib/Drupal/Core/Config/Config.php b/core/lib/Drupal/Core/Config/Config.php
index 7fd6967..538ab6d 100644
--- a/core/lib/Drupal/Core/Config/Config.php
+++ b/core/lib/Drupal/Core/Config/Config.php
@@ -354,6 +354,7 @@ class Config {
    * Deletes the configuration object.
    */
   public function delete() {
+    // @todo Consider to remove the pruning of data for Config::delete().
     $this->data = array();
     $this->storage->delete($this->name);
     $this->isNew = TRUE;
diff --git a/core/modules/config/config.api.php b/core/modules/config/config.api.php
index f0c3afa..6aef002 100644
--- a/core/modules/config/config.api.php
+++ b/core/modules/config/config.api.php
@@ -31,12 +31,12 @@
  *   A configuration object containing the old configuration data.
  */
 function MODULE_config_import_create($name, $new_config, $old_config) {
-  // Only configurable thingies require custom handling. Any other module
+  // Only configurable entities require custom handling. Any other module
   // settings can be synchronized directly.
   if (strpos($name, 'config_test.dynamic.') !== 0) {
     return FALSE;
   }
-  $config_test = new ConfigTest($new_config);
+  $config_test = entity_create('config_test', $new_config->get());
   $config_test->save();
   return TRUE;
 }
@@ -60,13 +60,25 @@ function MODULE_config_import_create($name, $new_config, $old_config) {
  *   A configuration object containing the old configuration data.
  */
 function MODULE_config_import_change($name, $new_config, $old_config) {
-  // Only configurable thingies require custom handling. Any other module
+  // Only configurable entities require custom handling. Any other module
   // settings can be synchronized directly.
   if (strpos($name, 'config_test.dynamic.') !== 0) {
     return FALSE;
   }
-  $config_test = new ConfigTest($new_config);
-  $config_test->setOriginal($old_config);
+
+  // @todo Make this less ugly.
+  $id = substr($name, strlen(ConfigTest::getConfigPrefix()) + 1);
+  $config_test = entity_load('config_test', $id);
+
+  $config_test->original = clone $config_test;
+  foreach ($old_config->get() as $property => $value) {
+    $config_test->original->$property = $value;
+  }
+
+  foreach ($new_config->get() as $property => $value) {
+    $config_test->$property = $value;
+  }
+
   $config_test->save();
   return TRUE;
 }
@@ -90,7 +102,7 @@ function MODULE_config_import_change($name, $new_config, $old_config) {
  *   A configuration object containing the old configuration data.
  */
 function MODULE_config_import_delete($name, $new_config, $old_config) {
-  // Only configurable thingies require custom handling. Any other module
+  // Only configurable entities require custom handling. Any other module
   // settings can be synchronized directly.
   if (strpos($name, 'config_test.dynamic.') !== 0) {
     return FALSE;
@@ -100,8 +112,8 @@ function MODULE_config_import_delete($name, $new_config, $old_config) {
   //   But that is impossible currently, since the config system only knows
   //   about deleted and added changes. Introduce an 'old_ID' key within
   //   config objects as a standard?
-  $config_test = new ConfigTest($old_config);
-  $config_test->delete();
+  $id = substr($name, strlen(ConfigTest::getConfigPrefix()) + 1);
+  config_test_delete($id);
   return TRUE;
 }
 
diff --git a/core/modules/config/lib/Drupal/config/ConfigStorageController.php b/core/modules/config/lib/Drupal/config/ConfigStorageController.php
new file mode 100644
index 0000000..7b5ef0e
--- /dev/null
+++ b/core/modules/config/lib/Drupal/config/ConfigStorageController.php
@@ -0,0 +1,346 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\config\ConfigStorageController.
+ */
+
+namespace Drupal\config;
+
+use Drupal\Component\Uuid\Uuid;
+use Drupal\entity\EntityInterface;
+use Drupal\entity\EntityStorageControllerInterface;
+
+/**
+ * Defines the storage controller class for configurable entities.
+ */
+class ConfigStorageController implements EntityStorageControllerInterface {
+
+  /**
+   * Entity type for this controller instance.
+   *
+   * @var string
+   */
+  protected $entityType;
+
+  /**
+   * Array of information about the entity.
+   *
+   * @var array
+   *
+   * @see entity_get_info()
+   */
+  protected $entityInfo;
+
+  /**
+   * Additional arguments to pass to hook_TYPE_load().
+   *
+   * Set before calling Drupal\config\ConfigStorageController::attachLoad().
+   *
+   * @var array
+   */
+  protected $hookLoadArguments;
+
+  /**
+   * Name of the entity's ID field in the entity database table.
+   *
+   * @var string
+   */
+  protected $idKey;
+
+  /**
+   * Implements Drupal\entity\EntityStorageControllerInterface::__construct().
+   *
+   * Sets basic variables.
+   */
+  public function __construct($entityType) {
+    $this->entityType = $entityType;
+    $this->entityInfo = entity_get_info($entityType);
+    $this->hookLoadArguments = array();
+    $this->idKey = $this->entityInfo['entity keys']['id'];
+
+    // The UUID key and property is hard-coded for all configurables.
+    $this->uuidKey = 'uuid';
+  }
+
+  /**
+   * Implements Drupal\entity\EntityStorageControllerInterface::resetCache().
+   */
+  public function resetCache(array $ids = NULL) {
+    // The configuration system is fast enough and/or implements its own
+    // (advanced) caching mechanism already.
+  }
+
+  /**
+   * Implements Drupal\entity\EntityStorageControllerInterface::load().
+   */
+  public function load($ids = array(), $conditions = array()) {
+    $entities = array();
+
+    // Create a new variable which is either a prepared version of the $ids
+    // array for later comparison with the entity cache, or FALSE if no $ids
+    // were passed.
+    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
+
+    // Load any remaining entities. This is the case if $ids
+    // is set to FALSE (so we load all entities),
+    // or if $conditions was passed without $ids.
+    if ($ids === FALSE || $ids || ($conditions && !$passed_ids)) {
+      $queried_entities = $this->buildQuery($ids, $conditions);
+    }
+
+    // Pass all entities loaded from the database through $this->attachLoad(),
+    // which calls the
+    // entity type specific load callback, for example hook_node_type_load().
+    if (!empty($queried_entities)) {
+      $this->attachLoad($queried_entities);
+      $entities += $queried_entities;
+    }
+
+    // Ensure that the returned array is ordered the same as the original
+    // $ids array if this was passed in and remove any invalid ids.
+    if ($passed_ids) {
+      // Remove any invalid ids from the array.
+      $passed_ids = array_intersect_key($passed_ids, $entities);
+      foreach ($entities as $entity) {
+        $passed_ids[$entity->{$this->idKey}] = $entity;
+      }
+      $entities = $passed_ids;
+    }
+
+    return $entities;
+  }
+
+  /**
+   * Builds the query to load the entity.
+   *
+   * This has full revision support. For entities requiring special queries,
+   * the class can be extended, and the default query can be constructed by
+   * calling parent::buildQuery(). This is usually necessary when the object
+   * being loaded needs to be augmented with additional data from another
+   * table, such as loading node type into comments or vocabulary machine name
+   * into terms, however it can also support $conditions on different tables.
+   * See Drupal\comment\CommentStorageController::buildQuery() or
+   * Drupal\taxonomy\TermStorageController::buildQuery() for examples.
+   *
+   * @param $ids
+   *   An array of entity IDs, or FALSE to load all entities.
+   * @param $conditions
+   *   An array of conditions in the form 'field' => $value.
+   * @param $revision_id
+   *   The ID of the revision to load, or FALSE if this query is asking for the
+   *   most current revision(s).
+   *
+   * @return SelectQuery
+   *   A SelectQuery object for loading the entity.
+   */
+  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
+    $config_class = $this->entityInfo['entity class'];
+    $prefix = $config_class::getConfigPrefix() . '.';
+
+    // @todo Handle $conditions?
+    if ($ids === FALSE) {
+      $names = drupal_container()->get('config.storage')->listAll($prefix);
+      $result = array();
+      foreach ($names as $name) {
+        $config = config($name);
+        $result[$config->get($this->idKey)] = new $config_class($config->get(), $this->entityType);
+      }
+      return $result;
+    }
+    else {
+      $result = array();
+      foreach ($ids as $id) {
+        $config = config($prefix . $id);
+        if (!$config->isNew()) {
+          $result[$id] = new $config_class($config->get(), $this->entityType);
+        }
+      }
+      return $result;
+    }
+  }
+
+  /**
+   * Attaches data to entities upon loading.
+   *
+   * This will attach fields, if the entity is fieldable. It calls
+   * hook_entity_load() for modules which need to add data to all entities.
+   * It also calls hook_TYPE_load() on the loaded entities. For example
+   * hook_node_load() or hook_user_load(). If your hook_TYPE_load()
+   * expects special parameters apart from the queried entities, you can set
+   * $this->hookLoadArguments prior to calling the method.
+   * See Drupal\node\NodeStorageController::attachLoad() for an example.
+   *
+   * @param $queried_entities
+   *   Associative array of query results, keyed on the entity ID.
+   * @param $revision_id
+   *   ID of the revision that was loaded, or FALSE if the most current revision
+   *   was loaded.
+   */
+  protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
+    // Call hook_entity_load().
+    foreach (module_implements('entity_load') as $module) {
+      $function = $module . '_entity_load';
+      $function($queried_entities, $this->entityType);
+    }
+    // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are
+    // always the queried entities, followed by additional arguments set in
+    // $this->hookLoadArguments.
+    $args = array_merge(array($queried_entities), $this->hookLoadArguments);
+    foreach (module_implements($this->entityType . '_load') as $module) {
+      call_user_func_array($module . '_' . $this->entityType . '_load', $args);
+    }
+  }
+
+  /**
+   * Implements Drupal\entity\EntityStorageControllerInterface::create().
+   */
+  public function create(array $values) {
+    $class = isset($this->entityInfo['entity class']) ? $this->entityInfo['entity class'] : 'Drupal\entity\Entity';
+
+    $entity = new $class($values, $this->entityType);
+
+    // Assign a new UUID if there is none yet.
+    if (!isset($entity->{$this->uuidKey})) {
+      $uuid = new Uuid();
+      $entity->{$this->uuidKey} = $uuid->generate();
+    }
+
+    return $entity;
+  }
+
+  /**
+   * Implements Drupal\entity\EntityStorageControllerInterface::delete().
+   */
+  public function delete($ids) {
+    $entities = $ids ? $this->load($ids) : FALSE;
+    if (!$entities) {
+      // If no IDs or invalid IDs were passed, do nothing.
+      return;
+    }
+
+    $this->preDelete($entities);
+    foreach ($entities as $id => $entity) {
+      $this->invokeHook('predelete', $entity);
+    }
+
+    foreach ($entities as $id => $entity) {
+      $config = config($entity::getConfigPrefix() . '.' . $entity->id());
+      $config->delete();
+    }
+
+    $this->postDelete($entities);
+    foreach ($entities as $id => $entity) {
+      $this->invokeHook('delete', $entity);
+    }
+  }
+
+  /**
+   * Implements Drupal\entity\EntityStorageControllerInterface::save().
+   */
+  public function save(EntityInterface $entity) {
+    $prefix = $entity::getConfigPrefix() . '.';
+
+    // Load the stored entity, if any.
+    if ($entity->getOriginalID()) {
+      $id = $entity->getOriginalID();
+    }
+    else {
+      $id = $entity->id();
+    }
+    $config = config($prefix . $id);
+    $config->setName($prefix . $entity->id());
+
+    if (!$config->isNew() && !isset($entity->original)) {
+      $entity->original = entity_load_unchanged($this->entityType, $id);
+    }
+
+    $this->preSave($entity);
+    $this->invokeHook('presave', $entity);
+
+    // Configuration objects do not have a schema. Extract all key names from
+    // class properties.
+    $class_info = new \ReflectionClass($entity);
+    foreach ($class_info->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
+      $name = $property->getName();
+      $config->set($name, $entity->$name);
+    }
+
+    if (!$config->isNew()) {
+      $return = SAVED_NEW;
+      $config->save();
+      $this->postSave($entity, TRUE);
+      $this->invokeHook('update', $entity);
+    }
+    else {
+      $return = SAVED_UPDATED;
+      $config->save();
+      $entity->enforceIsNew(FALSE);
+      $this->postSave($entity, FALSE);
+      $this->invokeHook('insert', $entity);
+    }
+
+    unset($entity->original);
+
+    return $return;
+  }
+
+  /**
+   * Acts on an entity before the presave hook is invoked.
+   *
+   * Used before the entity is saved and before invoking the presave hook.
+   */
+  protected function preSave(EntityInterface $entity) {
+  }
+
+  /**
+   * Acts on a saved entity before the insert or update hook is invoked.
+   *
+   * Used after the entity is saved, but before invoking the insert or update
+   * hook.
+   *
+   * @param $update
+   *   (bool) TRUE if the entity has been updated, or FALSE if it has been
+   *   inserted.
+   */
+  protected function postSave(EntityInterface $entity, $update) {
+    // Delete the original configurable entity, in case the entity ID was
+    // renamed.
+    if ($update && !empty($entity->original) && $entity->{$this->idKey} !== $entity->original->{$this->idKey}) {
+      // @todo This should just delete the original config object without going
+      //   through the API, no?
+      $entity->original->delete();
+    }
+  }
+
+  /**
+   * Acts on entities before they are deleted.
+   *
+   * Used before the entities are deleted and before invoking the delete hook.
+   */
+  protected function preDelete($entities) {
+  }
+
+  /**
+   * Acts on deleted entities before the delete hook is invoked.
+   *
+   * Used after the entities are deleted but before invoking the delete hook.
+   */
+  protected function postDelete($entities) {
+  }
+
+  /**
+   * Invokes a hook on behalf of the entity.
+   *
+   * @param $hook
+   *   One of 'presave', 'insert', 'update', 'predelete', or 'delete'.
+   * @param $entity
+   *   The entity object.
+   */
+  protected function invokeHook($hook, EntityInterface $entity) {
+    // Invoke the hook.
+    module_invoke_all($this->entityType . '_' . $hook, $entity);
+    // Invoke the respective entity-level hook.
+    module_invoke_all('entity_' . $hook, $entity, $this->entityType);
+  }
+}
diff --git a/core/modules/config/lib/Drupal/config/ConfigurableBase.php b/core/modules/config/lib/Drupal/config/ConfigurableBase.php
new file mode 100644
index 0000000..f53be7b
--- /dev/null
+++ b/core/modules/config/lib/Drupal/config/ConfigurableBase.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\config\ConfigurableBase.
+ */
+
+namespace Drupal\config;
+
+use Drupal\config\ConfigurableInterface;
+use Drupal\entity\Entity;
+
+/**
+ * Defines a base configurable entity class.
+ */
+abstract class ConfigurableBase extends Entity implements ConfigurableInterface {
+
+  /**
+   * The original ID of the configurable entity.
+   *
+   * The ID of a configurable entity is a unique string (machine name). When a
+   * configurable entity is updated and its machine name is renamed, the
+   * original ID needs to be known.
+   *
+   * @var string
+   */
+  protected $originalID;
+
+  /**
+   * Overrides Entity::__construct().
+   */
+  public function __construct(array $values = array(), $entity_type) {
+    parent::__construct($values, $entity_type);
+
+    // Backup the original ID, if any.
+    if ($original_id = $this->id()) {
+      $this->originalID = $original_id;
+    }
+  }
+
+  /**
+   * Implements ConfigurableInterface::getOriginalID().
+   */
+  public function getOriginalID() {
+    return $this->originalID;
+  }
+
+  /**
+   * Overrides Entity::isNew().
+   *
+   * EntityInterface::enforceIsNew() is not supported by configurable entities,
+   * since each Configurable is unique.
+   */
+  final public function isNew() {
+    return !$this->id();
+  }
+
+  /**
+   * Overrides Entity::bundle().
+   *
+   * EntityInterface::bundle() is not supported by configurable entities, since
+   * a Configurable is a bundle.
+   */
+  final public function bundle() {
+    return $this->entityType;
+  }
+
+  /**
+   * Overrides Entity::get().
+   *
+   * EntityInterface::get() implements support for fieldable entities, but
+   * configurable entities are not fieldable.
+   */
+  public function get($property_name, $langcode = NULL) {
+    // @todo: Add support for translatable properties being not fields.
+    return isset($this->{$property_name}) ? $this->{$property_name} : NULL;
+  }
+
+  /**
+   * Overrides Entity::set().
+   *
+   * EntityInterface::set() implements support for fieldable entities, but
+   * configurable entities are not fieldable.
+   */
+  public function set($property_name, $value, $langcode = NULL) {
+    // @todo: Add support for translatable properties being not fields.
+    $this->{$property_name} = $value;
+  }
+
+  /**
+   * Helper callback for uasort() to sort Configurable entities by weight and label.
+   */
+  public static function sort($a, $b) {
+    $a_weight = isset($a->weight) ? $a->weight : 0;
+    $b_weight = isset($b->weight) ? $b->weight : 0;
+    if ($a_weight == $b_weight) {
+      $a_label = $a->label();
+      $b_label = $b->label();
+      return strnatcasecmp($a_label, $b_label);
+    }
+    return ($a_weight < $b_weight) ? -1 : 1;
+  }
+}
diff --git a/core/modules/config/lib/Drupal/config/ConfigurableInterface.php b/core/modules/config/lib/Drupal/config/ConfigurableInterface.php
new file mode 100644
index 0000000..7d6075b
--- /dev/null
+++ b/core/modules/config/lib/Drupal/config/ConfigurableInterface.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\config\ConfigurableInterface.
+ */
+
+namespace Drupal\config;
+
+/**
+ * Defines the interface common for all configurable entities.
+ */
+interface ConfigurableInterface {
+
+  /**
+   * Returns the original ID.
+   *
+   * @return string|null
+   *   The original ID, if any.
+   */
+  public function getOriginalID();
+
+  /**
+   * Returns the configuration object name prefix of the configurable entity.
+   *
+   * @return string
+   *   The configuration object name prefix; e.g., for a 'node.type.article'
+   *   configurable entity, this returns 'node.type'.
+   */
+  public static function getConfigPrefix();
+
+}
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigConfigurableTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigConfigurableTest.php
new file mode 100644
index 0000000..0867812
--- /dev/null
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigConfigurableTest.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\config\Tests\ConfigConfigurableTest.
+ */
+
+namespace Drupal\config\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests configurable entities.
+ */
+class ConfigConfigurableTest extends WebTestBase {
+
+  public static $modules = array('config_test');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Configurable entities',
+      'description' => 'Tests configurable entities.',
+      'group' => 'Configuration',
+    );
+  }
+
+  /**
+   * Tests basic CRUD operations through the UI.
+   */
+  function testCRUD() {
+    // Create a configurable entity.
+    $id = 'thingie';
+    $edit = array(
+      'id' => $id,
+      'label' => 'Thingie',
+    );
+    $this->drupalPost('admin/structure/config_test/add', $edit, 'Save');
+    $this->assertResponse(200);
+    $this->assertText('Thingie');
+
+    // Update the configurable entity.
+    $this->assertLinkByHref('admin/structure/config_test/manage/' . $id);
+    $edit = array(
+      'label' => 'Thongie',
+    );
+    $this->drupalPost('admin/structure/config_test/manage/' . $id, $edit, 'Save');
+    $this->assertResponse(200);
+    $this->assertNoText('Thingie');
+    $this->assertText('Thongie');
+
+    // Delete the configurable entity.
+    $this->assertLinkByHref('admin/structure/config_test/manage/' . $id . '/delete');
+    $this->drupalPost('admin/structure/config_test/manage/' . $id . '/delete', array(), 'Delete');
+    $this->assertResponse(200);
+    $this->assertNoText('Thingie');
+    $this->assertNoText('Thongie');
+
+    // Re-create a configurable entity.
+    $edit = array(
+      'id' => $id,
+      'label' => 'Thingie',
+    );
+    $this->drupalPost('admin/structure/config_test/add', $edit, 'Save');
+    $this->assertResponse(200);
+    $this->assertText('Thingie');
+
+    // Rename the configurable entity's ID/machine name.
+    $this->assertLinkByHref('admin/structure/config_test/manage/' . $id);
+    $new_id = 'zingie';
+    $edit = array(
+      'id' => $new_id,
+      'label' => 'Zingie',
+    );
+    $this->drupalPost('admin/structure/config_test/manage/' . $id, $edit, 'Save');
+    $this->assertResponse(200);
+    $this->assertNoText('Thingie');
+    $this->assertText('Zingie');
+  }
+}
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php
index ec92fdf..7265abaa 100644
--- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php
@@ -31,6 +31,33 @@ class ConfigImportTest extends WebTestBase {
     );
   }
 
+  function setUp() {
+    parent::setUp();
+
+    // Clear out any possibly existing hook invocation records.
+    unset($GLOBALS['hook_config_test']);
+  }
+
+  /**
+   * Tests omission of module APIs for bare configuration operations.
+   */
+  function testNoImport() {
+    $dynamic_name = 'config_test.dynamic.default';
+
+    // Verify the default configuration values exist.
+    $config = config($dynamic_name);
+    $this->assertIdentical($config->get('id'), 'default');
+
+    // Verify that a bare config() does not involve module APIs.
+    $this->assertFalse(isset($GLOBALS['hook_config_test']));
+
+    // Export.
+    config_export();
+
+    // Verify that config_export() does not involve module APIs.
+    $this->assertFalse(isset($GLOBALS['hook_config_test']));
+  }
+
   /**
    * Tests deletion of configuration during import.
    */
@@ -64,6 +91,14 @@ class ConfigImportTest extends WebTestBase {
     $this->assertIdentical($config->get('foo'), NULL);
     $config = config($dynamic_name);
     $this->assertIdentical($config->get('id'), NULL);
+
+    // Verify that appropriate module API hooks have been invoked.
+    $this->assertTrue(isset($GLOBALS['hook_config_test']['load']));
+    $this->assertFalse(isset($GLOBALS['hook_config_test']['presave']));
+    $this->assertFalse(isset($GLOBALS['hook_config_test']['insert']));
+    $this->assertFalse(isset($GLOBALS['hook_config_test']['update']));
+    $this->assertTrue(isset($GLOBALS['hook_config_test']['predelete']));
+    $this->assertTrue(isset($GLOBALS['hook_config_test']['delete']));
   }
 
   /**
@@ -100,6 +135,14 @@ class ConfigImportTest extends WebTestBase {
     $this->assertIdentical($config->get('add_me'), 'new value');
     $config = config($dynamic_name);
     $this->assertIdentical($config->get('label'), 'New');
+
+    // Verify that appropriate module API hooks have been invoked.
+    $this->assertFalse(isset($GLOBALS['hook_config_test']['load']));
+    $this->assertTrue(isset($GLOBALS['hook_config_test']['presave']));
+    $this->assertTrue(isset($GLOBALS['hook_config_test']['insert']));
+    $this->assertFalse(isset($GLOBALS['hook_config_test']['update']));
+    $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete']));
+    $this->assertFalse(isset($GLOBALS['hook_config_test']['delete']));
   }
 
   /**
@@ -138,5 +181,13 @@ class ConfigImportTest extends WebTestBase {
     $this->assertIdentical($config->get('foo'), 'beer');
     $config = config($dynamic_name);
     $this->assertIdentical($config->get('label'), 'Updated');
+
+    // Verify that appropriate module API hooks have been invoked.
+    $this->assertTrue(isset($GLOBALS['hook_config_test']['load']));
+    $this->assertTrue(isset($GLOBALS['hook_config_test']['presave']));
+    $this->assertFalse(isset($GLOBALS['hook_config_test']['insert']));
+    $this->assertTrue(isset($GLOBALS['hook_config_test']['update']));
+    $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete']));
+    $this->assertFalse(isset($GLOBALS['hook_config_test']['delete']));
   }
 }
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php
index 7ec6d8e..d730554 100644
--- a/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php
@@ -26,12 +26,12 @@ class ConfigInstallTest extends WebTestBase {
    */
   function testModuleInstallation() {
     $default_config = 'config_test.system';
-    $default_thingie = 'config_test.dynamic.default';
+    $default_configurable = 'config_test.dynamic.default';
 
     // Verify that default module config does not exist before installation yet.
     $config = config($default_config);
     $this->assertIdentical($config->isNew(), TRUE);
-    $config = config($default_thingie);
+    $config = config($default_configurable);
     $this->assertIdentical($config->isNew(), TRUE);
 
     // Install the test module.
@@ -40,11 +40,20 @@ class ConfigInstallTest extends WebTestBase {
     // Verify that default module config exists.
     $config = config($default_config);
     $this->assertIdentical($config->isNew(), FALSE);
-    $config = config($default_thingie);
+    $config = config($default_configurable);
     $this->assertIdentical($config->isNew(), FALSE);
 
     // Verify that configuration import callback was invoked for the dynamic
-    // thingie.
+    // configurable entity.
     $this->assertTrue($GLOBALS['hook_config_import']);
+
+    // Verify that config_test API hooks were invoked for the dynamic default
+    // configurable entity.
+    $this->assertFalse(isset($GLOBALS['hook_config_test']['load']));
+    $this->assertTrue(isset($GLOBALS['hook_config_test']['presave']));
+    $this->assertTrue(isset($GLOBALS['hook_config_test']['insert']));
+    $this->assertFalse(isset($GLOBALS['hook_config_test']['update']));
+    $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete']));
+    $this->assertFalse(isset($GLOBALS['hook_config_test']['delete']));
   }
 }
diff --git a/core/modules/config/tests/config_test/config_test.hooks.inc b/core/modules/config/tests/config_test/config_test.hooks.inc
new file mode 100644
index 0000000..80f3381
--- /dev/null
+++ b/core/modules/config/tests/config_test/config_test.hooks.inc
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * Fake third-party hook implementations for ConfigTest entities.
+ *
+ * Testing the module/hook system is not the purpose of this test helper module.
+ * Therefore, this file implements hooks on behalf of config_test module for
+ * config_test entity hooks themselves.
+ */
+
+/**
+ * Implements hook_config_test_load().
+ */
+function config_test_config_test_load() {
+  $GLOBALS['hook_config_test']['load'] = __FUNCTION__;
+}
+
+/**
+ * Implements hook_config_test_presave().
+ */
+function config_test_config_test_presave() {
+  $GLOBALS['hook_config_test']['presave'] = __FUNCTION__;
+}
+
+/**
+ * Implements hook_config_test_insert().
+ */
+function config_test_config_test_insert() {
+  $GLOBALS['hook_config_test']['insert'] = __FUNCTION__;
+}
+
+/**
+ * Implements hook_config_test_update().
+ */
+function config_test_config_test_update() {
+  $GLOBALS['hook_config_test']['update'] = __FUNCTION__;
+}
+
+/**
+ * Implements hook_config_test_predelete().
+ */
+function config_test_config_test_predelete() {
+  $GLOBALS['hook_config_test']['predelete'] = __FUNCTION__;
+}
+
+/**
+ * Implements hook_config_test_delete().
+ */
+function config_test_config_test_delete() {
+  $GLOBALS['hook_config_test']['delete'] = __FUNCTION__;
+}
diff --git a/core/modules/config/tests/config_test/config_test.module b/core/modules/config/tests/config_test/config_test.module
index 6fd84a2..c4c01b8 100644
--- a/core/modules/config/tests/config_test/config_test.module
+++ b/core/modules/config/tests/config_test/config_test.module
@@ -1,18 +1,21 @@
 <?php
 
+use Drupal\config_test\ConfigTest;
+
+require_once dirname(__FILE__) . '/config_test.hooks.inc';
+
 /**
  * Implements MODULE_config_import_create().
  */
 function config_test_config_import_create($name, $new_config, $old_config) {
-  // Only configurable thingies require custom handling. Any other module
-  // settings can be synchronized directly.
   if (strpos($name, 'config_test.dynamic.') !== 0) {
     return FALSE;
   }
   // Set a global value we can check in test code.
   $GLOBALS['hook_config_import'] = __FUNCTION__;
 
-  $new_config->save();
+  $config_test = entity_create('config_test', $new_config->get());
+  $config_test->save();
   return TRUE;
 }
 
@@ -20,15 +23,26 @@ function config_test_config_import_create($name, $new_config, $old_config) {
  * Implements MODULE_config_import_change().
  */
 function config_test_config_import_change($name, $new_config, $old_config) {
-  // Only configurable thingies require custom handling. Any other module
-  // settings can be synchronized directly.
   if (strpos($name, 'config_test.dynamic.') !== 0) {
     return FALSE;
   }
   // Set a global value we can check in test code.
   $GLOBALS['hook_config_import'] = __FUNCTION__;
 
-  $new_config->save();
+  // @todo Make this less ugly.
+  $id = substr($name, strlen(ConfigTest::getConfigPrefix()) + 1);
+  $config_test = entity_load('config_test', $id);
+
+  $config_test->original = clone $config_test;
+  foreach ($old_config->get() as $property => $value) {
+    $config_test->original->$property = $value;
+  }
+
+  foreach ($new_config->get() as $property => $value) {
+    $config_test->$property = $value;
+  }
+
+  $config_test->save();
   return TRUE;
 }
 
@@ -36,15 +50,248 @@ function config_test_config_import_change($name, $new_config, $old_config) {
  * Implements MODULE_config_import_delete().
  */
 function config_test_config_import_delete($name, $new_config, $old_config) {
-  // Only configurable thingies require custom handling. Any other module
-  // settings can be synchronized directly.
   if (strpos($name, 'config_test.dynamic.') !== 0) {
     return FALSE;
   }
   // Set a global value we can check in test code.
   $GLOBALS['hook_config_import'] = __FUNCTION__;
 
-  $old_config->delete();
+  $id = substr($name, strlen(ConfigTest::getConfigPrefix()) + 1);
+  config_test_delete($id);
   return TRUE;
 }
 
+/**
+ * Implements hook_entity_info().
+ */
+function config_test_entity_info() {
+  $types['config_test'] = array(
+    'label' => 'Test configuration',
+    'controller class' => 'Drupal\config\ConfigStorageController',
+    'entity class' => 'Drupal\config_test\ConfigTest',
+    'uri callback' => 'config_test_uri',
+    'entity keys' => array(
+      'id' => 'id',
+      'label' => 'label',
+      'uuid' => 'uuid',
+    ),
+  );
+  return $types;
+}
+
+/**
+ * Entity uri callback.
+ *
+ * @param Drupal\config_test\ConfigTest $config_test
+ *   A ConfigTest entity.
+ */
+function config_test_uri(ConfigTest $config_test) {
+  return array(
+    'path' => 'admin/structure/config_test/manage/' . $config_test->id(),
+  );
+}
+
+/**
+ * Implements hook_menu().
+ */
+function config_test_menu() {
+  $items['admin/structure/config_test'] = array(
+    'title' => 'Test configuration',
+    'page callback' => 'config_test_list_page',
+    'access callback' => TRUE,
+  );
+  $items['admin/structure/config_test/add'] = array(
+    'title' => 'Add test configuration',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('config_test_form'),
+    'access callback' => TRUE,
+    'type' => MENU_LOCAL_ACTION,
+  );
+  $items['admin/structure/config_test/manage/%config_test'] = array(
+    'title' => 'Edit test configuration',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('config_test_form', 4),
+    'access callback' => TRUE,
+  );
+  $items['admin/structure/config_test/manage/%config_test/edit'] = array(
+    'title' => 'Edit',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+  $items['admin/structure/config_test/manage/%config_test/delete'] = array(
+    'title' => 'Delete',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('config_test_delete_form', 4),
+    'access callback' => TRUE,
+    'type' => MENU_LOCAL_TASK,
+  );
+  return $items;
+}
+
+/**
+ * Loads a ConfigTest object.
+ *
+ * @param string $id
+ *   The ID of the ConfigTest object to load.
+ */
+function config_test_load($id) {
+  return entity_load('config_test', $id);
+}
+
+/**
+ * Saves a ConfigTest object.
+ *
+ * @param Drupal\config_test\ConfigTest $config_test
+ *   The ConfigTest object to save.
+ */
+function config_test_save(ConfigTest $config_test) {
+  return $config_test->save();
+}
+
+/**
+ * Deletes a ConfigTest object.
+ *
+ * @param string $id
+ *   The ID of the ConfigTest object to delete.
+ */
+function config_test_delete($id) {
+  entity_delete_multiple('config_test', array($id));
+}
+
+/**
+ * Page callback; Lists available ConfigTest objects.
+ */
+function config_test_list_page() {
+  $entities = entity_load_multiple('config_test', FALSE);
+  uasort($entities, 'Drupal\config\ConfigurableBase::sort');
+
+  $rows = array();
+  foreach ($entities as $config_test) {
+    $uri = $config_test->uri();
+    $row = array();
+    $row['name']['data'] = array(
+      '#type' => 'link',
+      '#title' => $config_test->label(),
+      '#href' => $uri['path'],
+      '#options' => $uri['options'],
+    );
+    $row['delete']['data'] = array(
+      '#type' => 'link',
+      '#title' => t('Delete'),
+      '#href' => $uri['path'] . '/delete',
+      '#options' => $uri['options'],
+    );
+    $rows[] = $row;
+  }
+  $build = array(
+    '#theme' => 'table',
+    '#header' => array('Name', 'Operations'),
+    '#rows' => $rows,
+    '#empty' => format_string('No test configuration defined. <a href="@add-url">Add some</a>', array(
+      '@add-url' => url('admin/structure/config_test/add'),
+    )),
+  );
+  return $build;
+}
+
+/**
+ * Form constructor to add or edit a ConfigTest object.
+ *
+ * @param Drupal\config_test\ConfigTest $config_test
+ *   (optional) An existing ConfigTest object to edit. If omitted, the form
+ *   creates a new ConfigTest.
+ */
+function config_test_form($form, &$form_state, ConfigTest $config_test = NULL) {
+  // Standard procedure for handling the entity argument in entity forms, taking
+  // potential form caching and rebuilds properly into account.
+  // @see http://drupal.org/node/1499596
+  if (!isset($form_state['config_test'])) {
+    if (!isset($config_test)) {
+      $config_test = entity_create('config_test', array());
+    }
+    $form_state['config_test'] = $config_test;
+  }
+  else {
+    $config_test = $form_state['config_test'];
+  }
+
+  $form['label'] = array(
+    '#type' => 'textfield',
+    '#title' => 'Label',
+    '#default_value' => $config_test->label(),
+    '#required' => TRUE,
+  );
+  $form['id'] = array(
+    '#type' => 'machine_name',
+    '#default_value' => $config_test->id(),
+    '#required' => TRUE,
+    '#machine_name' => array(
+      'exists' => 'config_test_load',
+      // @todo Update form_process_machine_name() to use 'label' by default.
+      'source' => array('label'),
+    ),
+  );
+  $form['style'] = array(
+    '#type' => 'select',
+    '#title' => 'Image style',
+    '#options' => array(),
+    '#default_value' => $config_test->get('style'),
+    '#access' => FALSE,
+  );
+  if (module_exists('image')) {
+    $form['style']['#access'] = TRUE;
+    $form['style']['#options'] = image_style_options();
+  }
+
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => 'Save');
+
+  return $form;
+}
+
+/**
+ * Form submission handler for config_test_form().
+ */
+function config_test_form_submit($form, &$form_state) {
+  form_state_values_clean($form_state);
+
+  $config_test = $form_state['config_test'];
+  entity_form_submit_build_entity('config_test', $config_test, $form, $form_state);
+
+  $status = $config_test->save();
+
+  if ($status == SAVED_UPDATED) {
+    drupal_set_message(format_string('%label configuration has been updated.', array('%label' => $config_test->label())));
+  }
+  else {
+    drupal_set_message(format_string('%label configuration has been created.', array('%label' => $config_test->label())));
+  }
+
+  $form_state['redirect'] = 'admin/structure/config_test';
+}
+
+/**
+ * Form constructor to delete a ConfigTest object.
+ *
+ * @param Drupal\config_test\ConfigTest $config_test
+ *   The ConfigTest object to delete.
+ */
+function config_test_delete_form($form, &$form_state, ConfigTest $config_test) {
+  $form_state['config_test'] = $config_test;
+
+  $form['id'] = array('#type' => 'value', '#value' => $config_test->id());
+  return confirm_form($form,
+    format_string('Are you sure you want to delete %label', array('%label' => $config_test->label())),
+    'admin/structure/config_test',
+    NULL,
+    'Delete'
+  );
+}
+
+/**
+ * Form submission handler for config_test_delete_form().
+ */
+function config_test_delete_form_submit($form, &$form_state) {
+  $form_state['config_test']->delete();
+  $form_state['redirect'] = 'admin/structure/config_test';
+}
diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php
new file mode 100644
index 0000000..104c11d
--- /dev/null
+++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\config_test\ConfigTest.
+ */
+
+namespace Drupal\config_test;
+
+use Drupal\config\ConfigurableBase;
+
+/**
+ * Defines the ConfigTest configurable entity.
+ */
+class ConfigTest extends ConfigurableBase {
+
+  public $id;
+  public $uuid;
+  public $label;
+
+  /**
+   * The image style to use.
+   *
+   * @var string
+   */
+  public $style;
+
+  /**
+   * Implements Drupal\Core\Configurable\ConfigurableInterface::getConfigPrefix().
+   */
+  public static function getConfigPrefix() {
+    return 'config_test.dynamic';
+  }
+}
diff --git a/core/modules/contact/config/contact.category.feedback.yml b/core/modules/contact/config/contact.category.feedback.yml
new file mode 100644
index 0000000..7d3c686
--- /dev/null
+++ b/core/modules/contact/config/contact.category.feedback.yml
@@ -0,0 +1,5 @@
+id: feedback
+label: Website feedback
+recipients: []
+reply: ''
+weight: '0'
diff --git a/core/modules/contact/contact.admin.inc b/core/modules/contact/contact.admin.inc
index f6835f9..8519714 100644
--- a/core/modules/contact/contact.admin.inc
+++ b/core/modules/contact/contact.admin.inc
@@ -5,6 +5,8 @@
  * Admin page callbacks for the Contact module.
  */
 
+use Drupal\contact\Category;
+
 /**
  * Page callback: Lists contact categories.
  *
@@ -19,23 +21,18 @@ function contact_category_list() {
   );
   $rows = array();
 
-  // Get all the contact categories from the database.
-  $categories = db_select('contact', 'c')
-    ->addTag('translatable')
-    ->fields('c', array('cid', 'category', 'recipients', 'selected'))
-    ->orderBy('weight')
-    ->orderBy('category')
-    ->execute()
-    ->fetchAll();
+  $categories = entity_load_multiple('contact_category', FALSE);
+  uasort($categories, 'Drupal\config\ConfigurableBase::sort');
+  $default_category = config('contact.settings')->get('default_category');
 
   // Loop through the categories and add them to the table.
   foreach ($categories as $category) {
     $rows[] = array(
-      check_plain($category->category),
-      check_plain($category->recipients),
-      ($category->selected ? t('Yes') : t('No')),
-      l(t('Edit'), 'admin/structure/contact/edit/' . $category->cid),
-      l(t('Delete'), 'admin/structure/contact/delete/' . $category->cid),
+      check_plain($category->label()),
+      check_plain(implode(', ', $category->recipients)),
+      ($default_category == $category->id() ? t('Yes') : t('No')),
+      l(t('Edit'), 'admin/structure/contact/manage/' . $category->id()),
+      l(t('Delete'), 'admin/structure/contact/manage/' . $category->id() . '/delete'),
     );
   }
 
@@ -57,68 +54,70 @@ function contact_category_list() {
 /**
  * Form constructor for the category edit form.
  *
- * @param $category
- *   An array describing the category to be edited. May be empty for new
- *   categories. Recognized array keys are:
- *   - category: The name of the category.
- *   - recipients: A comma-separated list of recipients.
- *   - reply: (optional) The body of the auto-reply message.
- *   - weight: The weight of the category.
- *   - selected: Boolean indicating whether the category should be selected by
- *     default.
- *   - cid: The category ID for which the form is to be displayed.
+ * @param Drupal\contact\Category $category
+ *   (optional) The contact category to be edited.
  *
  * @see contact_menu()
  * @see contact_category_edit_form_validate()
  * @see contact_category_edit_form_submit()
  * @ingroup forms
  */
-function contact_category_edit_form($form, &$form_state, array $category = array()) {
-  // If this is a new category, add the default values.
-  $category += array(
-    'category' => '',
-    'recipients' => '',
-    'reply' => '',
-    'weight' => 0,
-    'selected' => 0,
-    'cid' => NULL,
-  );
+function contact_category_edit_form($form, &$form_state, Category $category = NULL) {
+  // During initial form build, add the entity to the form state for use
+  // during form building and processing. During a rebuild, use what is in the
+  // form state.
+  if (!isset($form_state['contact_category'])) {
+    if (!isset($category)) {
+      $category = entity_create('contact_category', array());
+    }
+    $form_state['contact_category'] = $category;
+  }
+  else {
+    $category = $form_state['contact_category'];
+  }
 
-  $form['category'] = array(
+  $default_category = config('contact.settings')->get('default_category');
+
+  $form['label'] = array(
     '#type' => 'textfield',
-    '#title' => t('Category'),
+    '#title' => t('Label'),
     '#maxlength' => 255,
-    '#default_value' => $category['category'],
+    '#default_value' => $category->label(),
     '#description' => t("Example: 'website feedback' or 'product information'."),
     '#required' => TRUE,
   );
+  $form['id'] = array(
+    '#type' => 'machine_name',
+    '#default_value' => $category->id(),
+    '#machine_name' => array(
+      'exists' => 'contact_category_load',
+      'source' => array('label'),
+    ),
+    '#disabled' => (bool) $category->id(),
+  );
   $form['recipients'] = array(
     '#type' => 'textarea',
     '#title' => t('Recipients'),
-    '#default_value' => $category['recipients'],
+    '#default_value' => implode(', ', $category->recipients),
     '#description' => t("Example: 'webmaster@example.com' or 'sales@example.com,support@example.com' . To specify multiple recipients, separate each e-mail address with a comma."),
     '#required' => TRUE,
   );
   $form['reply'] = array(
     '#type' => 'textarea',
     '#title' => t('Auto-reply'),
-    '#default_value' => $category['reply'],
+    '#default_value' => $category->reply,
     '#description' => t('Optional auto-reply. Leave empty if you do not want to send the user an auto-reply message.'),
   );
   $form['weight'] = array(
     '#type' => 'weight',
     '#title' => t('Weight'),
-    '#default_value' => $category['weight'],
+    '#default_value' => $category->weight,
     '#description' => t('When listing categories, those with lighter (smaller) weights get listed before categories with heavier (larger) weights. Categories with equal weights are sorted alphabetically.'),
   );
   $form['selected'] = array(
     '#type' => 'checkbox',
     '#title' => t('Make this the default category.'),
-    '#default_value' => $category['selected'],
-  );
-  $form['cid'] = array(
-    '#type' => 'value',
-    '#value' => $category['cid'],
+    '#default_value' => $default_category === $category->id(),
   );
   $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array(
@@ -138,24 +137,13 @@ function contact_category_edit_form_validate($form, &$form_state) {
   // Validate and each e-mail recipient.
   $recipients = explode(',', $form_state['values']['recipients']);
 
-  // When creating a new contact form, or renaming the category on an existing
-  // contact form, make sure that the given category is unique.
-  $category = $form_state['values']['category'];
-  $query = db_select('contact', 'c')->condition('c.category', $category, '=');
-  if (!empty($form_state['values']['cid'])) {
-    $query->condition('c.cid', $form_state['values']['cid'], '<>');
-  }
-  if ($query->countQuery()->execute()->fetchField()) {
-    form_set_error('category', t('A contact form with category %category already exists.', array('%category' => $category)));
-  }
-
   foreach ($recipients as &$recipient) {
     $recipient = trim($recipient);
     if (!valid_email_address($recipient)) {
       form_set_error('recipients', t('%recipient is an invalid e-mail address.', array('%recipient' => $recipient)));
     }
   }
-  $form_state['values']['recipients'] = implode(',', $recipients);
+  $form_state['values']['recipients'] = $recipients;
 }
 
 /**
@@ -164,48 +152,57 @@ function contact_category_edit_form_validate($form, &$form_state) {
  * @see contact_category_edit_form_validate()
  */
 function contact_category_edit_form_submit($form, &$form_state) {
+  // Update the default category.
+  $contact_config = config('contact.settings');
   if ($form_state['values']['selected']) {
-    // Unselect all other contact categories.
-    db_update('contact')
-      ->fields(array('selected' => '0'))
-      ->execute();
+    $contact_config
+      ->set('default_category', $form_state['values']['id'])
+      ->save();
   }
-
-  if (empty($form_state['values']['cid'])) {
-    drupal_write_record('contact', $form_state['values']);
-  }
-  else {
-    drupal_write_record('contact', $form_state['values'], array('cid'));
+  // If it was the default category, empty out the setting.
+  elseif ($contact_config->get('default_category') == $form_state['values']['id']) {
+    $contact_config
+      ->clear('default_category')
+      ->save();
   }
 
-  drupal_set_message(t('Category %category has been saved.', array('%category' => $form_state['values']['category'])));
-  watchdog('contact', 'Category %category has been saved.', array('%category' => $form_state['values']['category']), WATCHDOG_NOTICE, l(t('Edit'), 'admin/structure/contact/edit/' . $form_state['values']['cid']));
+  // Remove the 'selected' value, which is not part of the Category.
+  unset($form_state['values']['selected']);
+  form_state_values_clean($form_state);
+
+  $category = $form_state['contact_category'];
+  entity_form_submit_build_entity('contact_category', $category, $form, $form_state);
+
+  $category->save();
+
+  drupal_set_message(t('Category %label has been saved.', array('%label' => $category->label())));
+  watchdog('contact', 'Category %label has been saved.', array('%label' => $category->label()), WATCHDOG_NOTICE, l(t('Edit'), 'admin/structure/contact/manage/' . $category->id()));
+
   $form_state['redirect'] = 'admin/structure/contact';
 }
 
 /**
  * Form constructor for the contact category deletion form.
  *
- * @param $contact
- *   Array describing the contact category to be deleted. See the documentation
- *   of contact_category_edit_form() for the recognized keys.
+ * @param Drupal\contact\Category $category
+ *   The contact category to be deleted.
  *
  * @see contact_menu()
  * @see contact_category_delete_form_submit()
  */
-function contact_category_delete_form($form, &$form_state, array $contact) {
-  $form['contact'] = array(
+function contact_category_delete_form($form, &$form_state, Category $category) {
+  $form_state['contact_category'] = $category;
+  $form['id'] = array(
     '#type' => 'value',
-    '#value' => $contact,
+    '#value' => $category->id(),
   );
 
   return confirm_form(
     $form,
-    t('Are you sure you want to delete %category?', array('%category' => $contact['category'])),
+    t('Are you sure you want to delete %label?', array('%label' => $category->label())),
     'admin/structure/contact',
     t('This action cannot be undone.'),
-    t('Delete'),
-    t('Cancel')
+    t('Delete')
   );
 }
 
@@ -213,14 +210,11 @@ function contact_category_delete_form($form, &$form_state, array $contact) {
  * Form submission handler for contact_category_delete_form().
  */
 function contact_category_delete_form_submit($form, &$form_state) {
-  $contact = $form['contact']['#value'];
-
-  db_delete('contact')
-    ->condition('cid', $contact['cid'])
-    ->execute();
+  $category = $form_state['contact_category'];
+  $category->delete();
 
-  drupal_set_message(t('Category %category has been deleted.', array('%category' => $contact['category'])));
-  watchdog('contact', 'Category %category has been deleted.', array('%category' => $contact['category']), WATCHDOG_NOTICE);
+  drupal_set_message(t('Category %label has been deleted.', array('%label' => $category->label())));
+  watchdog('contact', 'Category %label has been deleted.', array('%label' => $category->label()), WATCHDOG_NOTICE);
 
   $form_state['redirect'] = 'admin/structure/contact';
 }
diff --git a/core/modules/contact/contact.info b/core/modules/contact/contact.info
index eff6d33..b04cd1d 100644
--- a/core/modules/contact/contact.info
+++ b/core/modules/contact/contact.info
@@ -4,3 +4,4 @@ package = Core
 version = VERSION
 core = 8.x
 configure = admin/structure/contact
+dependencies[] = config
diff --git a/core/modules/contact/contact.install b/core/modules/contact/contact.install
index f956242..ee83410 100644
--- a/core/modules/contact/contact.install
+++ b/core/modules/contact/contact.install
@@ -6,65 +6,6 @@
  */
 
 /**
- * Implements hook_schema().
- */
-function contact_schema() {
-  $schema['contact'] = array(
-    'description' => 'Contact form category settings.',
-    'fields' => array(
-      'cid' => array(
-        'type' => 'serial',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => 'Primary Key: Unique category ID.',
-      ),
-      'category' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => 'Category name.',
-        'translatable' => TRUE,
-      ),
-      'recipients' => array(
-        'type' => 'text',
-        'not null' => TRUE,
-        'size' => 'big',
-        'description' => 'Comma-separated list of recipient e-mail addresses.',
-      ),
-      'reply' => array(
-        'type' => 'text',
-        'not null' => TRUE,
-        'size' => 'big',
-        'description' => 'Text of the auto-reply message.',
-      ),
-      'weight' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'description' => "The category's weight.",
-      ),
-      'selected' => array(
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'size' => 'tiny',
-        'description' => 'Flag to indicate whether or not category is selected by default. (1 = Yes, 0 = No)',
-      ),
-    ),
-    'primary key' => array('cid'),
-    'unique keys' => array(
-      'category' => array('category'),
-    ),
-    'indexes' => array(
-      'list' => array('weight', 'category'),
-    ),
-  );
-
-  return $schema;
-}
-
-/**
  * Implements hook_install().
  */
 function contact_install() {
@@ -72,18 +13,15 @@ function contact_install() {
   if (empty($site_mail)) {
     $site_mail = ini_get('sendmail_from');
   }
-  // Insert a default contact category.
-  db_insert('contact')
-    ->fields(array(
-      'category' => 'Website feedback',
-      'recipients' => $site_mail,
-      'selected' => 1,
-      'reply' => '',
-    ))
-    ->execute();
+  config('contact.category.feedback')->set('recipients', array($site_mail))->save();
 }
 
 /**
+ * @addtogroup updates-7.x-to-8.x
+ * @{
+ */
+
+/**
  * Moves contact setting from variable to config.
  *
  * @ingroup config_upgrade
@@ -95,3 +33,49 @@ function contact_update_8000() {
     'contact_threshold_window' => 'flood.interval',
   ));
 }
+
+/**
+ * Migrate contact categories into configuration.
+ *
+ * @ingroup config_upgrade
+ */
+function contact_update_8001() {
+  $result = db_query('SELECT * FROM {contact}');
+  foreach ($result as $category) {
+    // Generate machine readable names.
+    if ($category->category == 'Website feedback') {
+      // Use default id for unchanged category.
+      $category->id = 'feedback';
+    }
+    else {
+      $category->id = drupal_strtolower($category->category);
+      $category->id = preg_replace('@[^a-z0-9]@', '_', $category->id);
+    }
+    // Save default category setting.
+    if ($category->selected) {
+      config('contact.settings')
+        ->set('default_category', $category->id)
+        ->save();
+    }
+    // Save the config object.
+    config('contact.category.' . $category->id)
+      ->set('id', $category->id)
+      ->set('label', $category->category)
+      ->set('recipients', explode(',', $category->recipients))
+      ->set('reply', $category->reply)
+      ->set('weight', $category->weight)
+      ->save();
+  }
+}
+
+/**
+ * Drop the {contact} table.
+ */
+function contact_update_8002() {
+  db_drop_table('contact');
+}
+
+/**
+ * @} End of "defgroup updates-7.x-to-8.x".
+ * The next series of updates should start at 9000.
+ */
diff --git a/core/modules/contact/contact.module b/core/modules/contact/contact.module
index 1ba70b9..d8901bc 100644
--- a/core/modules/contact/contact.module
+++ b/core/modules/contact/contact.module
@@ -5,6 +5,8 @@
  * Enables the use of personal and site-wide contact forms.
  */
 
+use Drupal\contact\Category;
+
 /**
  * Implements hook_help().
  */
@@ -71,15 +73,15 @@ function contact_menu() {
     'weight' => 1,
     'file' => 'contact.admin.inc',
   );
-  $items['admin/structure/contact/edit/%contact'] = array(
+  $items['admin/structure/contact/manage/%contact_category'] = array(
     'title' => 'Edit contact category',
     'page callback' => 'drupal_get_form',
     'page arguments' => array('contact_category_edit_form', 4),
     'access arguments' => array('administer contact forms'),
     'file' => 'contact.admin.inc',
   );
-  $items['admin/structure/contact/delete/%contact'] = array(
-    'title' => 'Delete contact',
+  $items['admin/structure/contact/manage/%contact_category/delete'] = array(
+    'title' => 'Delete contact category',
     'page callback' => 'drupal_get_form',
     'page arguments' => array('contact_category_delete_form', 4),
     'access arguments' => array('administer contact forms'),
@@ -147,21 +149,97 @@ function _contact_personal_tab_access($account) {
 }
 
 /**
+ * Implements MODULE_config_import_create().
+ */
+function contact_config_import_create($name, $new_config, $old_config) {
+  if (strpos($name, 'contact.category.') !== 0) {
+    return FALSE;
+  }
+
+  $category = entity_create('contact_category', $new_config->get());
+  $category->save();
+  return TRUE;
+}
+
+/**
+ * Implements MODULE_config_import_change().
+ */
+function contact_config_import_change($name, $new_config, $old_config) {
+  if (strpos($name, 'contact.category.') !== 0) {
+    return FALSE;
+  }
+
+  // @todo Make this less ugly.
+  $id = substr($name, strlen(Category::getConfigPrefix()) + 1);
+  $category = entity_load('contact_category', $id);
+
+  $category->original = clone $category;
+  foreach ($old_config->get() as $property => $value) {
+    $category->original->$property = $value;
+  }
+
+  foreach ($new_config->get() as $property => $value) {
+    $category->$property = $value;
+  }
+
+  $category->save();
+  return TRUE;
+}
+
+/**
+ * Implements MODULE_config_import_delete().
+ */
+function contact_config_import_delete($name, $new_config, $old_config) {
+  if (strpos($name, 'contact.category.') !== 0) {
+    return FALSE;
+  }
+
+  $id = substr($name, strlen(ConfigTest::getConfigPrefix()) + 1);
+  entity_delete_multiple('contact_category', array($id));
+  return TRUE;
+}
+
+/**
+ * Implements hook_entity_info().
+ */
+function contact_entity_info() {
+  $types['contact_category'] = array(
+    'label' => 'Category',
+    'controller class' => 'Drupal\config\ConfigStorageController',
+    'entity class' => 'Drupal\contact\Category',
+    'uri callback' => 'contact_category_uri',
+    'entity keys' => array(
+      'id' => 'id',
+      'label' => 'label',
+      'uuid' => 'uuid',
+    ),
+  );
+  return $types;
+}
+
+/**
+ * Entity uri callback.
+ *
+ * @param Drupal\contact\Category $category
+ *   A Category entity.
+ */
+function contact_category_uri(Category $category) {
+  return array(
+    'path' => 'admin/structure/contact/manage/' . $category->id(),
+  );
+}
+
+/**
  * Loads a contact category.
  *
- * @param $cid
- *   The contact category ID.
+ * @param $id
+ *   The ID of the contact category to load.
  *
- * @return
- *   An array with the contact category's data.
+ * @return Drupal\contact\Category|false
+ *   A Category object or FALSE if the requested $id does not exist.
  */
-function contact_load($cid) {
-  return db_select('contact', 'c')
-    ->addTag('translatable')
-    ->fields('c')
-    ->condition('cid', $cid)
-    ->execute()
-    ->fetchAssoc();
+function contact_category_load($id) {
+  return entity_load('contact_category', $id);
 }
 
 /**
@@ -172,7 +250,7 @@ function contact_mail($key, &$message, $params) {
   $variables = array(
     '!site-name' => config('system.site')->get('name'),
     '!subject' => $params['subject'],
-    '!category' => isset($params['category']['category']) ? $params['category']['category'] : '',
+    '!category' => isset($params['category']) ? $params['category']->label() : '',
     '!form-url' => url(current_path(), array('absolute' => TRUE, 'language' => $language)),
     '!sender-name' => user_format_name($params['sender']),
     '!sender-url' => $params['sender']->uid ? url('user/' . $params['sender']->uid, array('absolute' => TRUE, 'language' => $language)) : $params['sender']->mail,
@@ -188,7 +266,7 @@ function contact_mail($key, &$message, $params) {
 
     case 'page_autoreply':
       $message['subject'] .= t('[!category] !subject', $variables, array('langcode' => $language->langcode));
-      $message['body'][] = $params['category']['reply'];
+      $message['body'][] = $params['category']->reply;
       break;
 
     case 'user_mail':
diff --git a/core/modules/contact/contact.pages.inc b/core/modules/contact/contact.pages.inc
index 92d73bc..9e93f30 100644
--- a/core/modules/contact/contact.pages.inc
+++ b/core/modules/contact/contact.pages.inc
@@ -12,7 +12,6 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  * Form constructor for the site-wide contact form.
  *
  * @see contact_menu()
- * @see contact_site_form_validate()
  * @see contact_site_form_submit()
  * @ingroup forms
  */
@@ -29,14 +28,7 @@ function contact_site_form($form, &$form_state) {
   }
 
   // Get an array of the categories and the current default category.
-  $categories = db_select('contact', 'c')
-    ->addTag('translatable')
-    ->fields('c', array('cid', 'category'))
-    ->orderBy('weight')
-    ->orderBy('category')
-    ->execute()
-    ->fetchAllKeyed();
-  $default_category = db_query("SELECT cid FROM {contact} WHERE selected = 1")->fetchField();
+  $categories = entity_load_multiple('contact_category', FALSE);
 
   // If there are no categories, do not display the form.
   if (!$categories) {
@@ -48,15 +40,18 @@ function contact_site_form($form, &$form_state) {
     }
   }
 
+  // Prepare array for select options.
+  uasort($categories, 'Drupal\config\ConfigurableBase::sort');
+  $options = array();
+  foreach ($categories as $category) {
+    $options[$category->id()] = $category->label();
+  }
+
   // If there is more than one category available and no default category has
   // been selected, prepend a default placeholder value.
+  $default_category = $config->get('default_category');
   if (!$default_category) {
-    if (count($categories) > 1) {
-      $categories = array(0 => t('- Please choose -')) + $categories;
-    }
-    else {
-      $default_category = key($categories);
-    }
+    $default_category = !empty($categories) ? key($categories) : NULL;
   }
 
   if (!$user->uid) {
@@ -103,11 +98,12 @@ function contact_site_form($form, &$form_state) {
     '#maxlength' => 255,
     '#required' => TRUE,
   );
-  $form['cid'] = array(
+  $form['category'] = array(
     '#type' => 'select',
     '#title' => t('Category'),
     '#default_value' => $default_category,
-    '#options' => $categories,
+    '#options' => $options,
+    '#empty_value' => 0,
     '#required' => TRUE,
     '#access' => count($categories) > 1,
   );
@@ -133,17 +129,6 @@ function contact_site_form($form, &$form_state) {
 }
 
 /**
- * Form validation handler for contact_site_form().
- *
- * @see contact_site_form_submit()
- */
-function contact_site_form_validate($form, &$form_state) {
-  if (!$form_state['values']['cid']) {
-    form_set_error('cid', t('You must select a valid category.'));
-  }
-}
-
-/**
  * Form submission handler for contact_site_form().
  *
  * @see contact_site_form_validate()
@@ -156,7 +141,7 @@ function contact_site_form_submit($form, &$form_state) {
   $values['sender'] = $user;
   $values['sender']->name = $values['name'];
   $values['sender']->mail = $values['mail'];
-  $values['category'] = contact_load($values['cid']);
+  $values['category'] = contact_category_load($values['category']);
 
   // Save the anonymous user information to a cookie for reuse.
   if (!$user->uid) {
@@ -165,7 +150,7 @@ function contact_site_form_submit($form, &$form_state) {
   }
 
   // Get the to and from e-mail addresses.
-  $to = $values['category']['recipients'];
+  $to = implode(', ', $values['category']->recipients);
   $from = $values['sender']->mail;
 
   // Send the e-mail to the recipients using the site default language.
@@ -177,12 +162,12 @@ function contact_site_form_submit($form, &$form_state) {
   }
 
   // Send an auto-reply if necessary using the current language.
-  if ($values['category']['reply']) {
+  if ($values['category']->reply) {
     drupal_mail('contact', 'page_autoreply', $from, $language_interface, $values, $to);
   }
 
   flood_register_event('contact', config('contact.settings')->get('flood.interval'));
-  watchdog('mail', '%sender-name (@sender-from) sent an e-mail regarding %category.', array('%sender-name' => $values['name'], '@sender-from' => $from, '%category' => $values['category']['category']));
+  watchdog('mail', '%sender-name (@sender-from) sent an e-mail regarding %category.', array('%sender-name' => $values['name'], '@sender-from' => $from, '%category' => $values['category']->label()));
 
   // Jump to home page rather than back to contact page to avoid
   // contradictory messages if flood control has been activated.
diff --git a/core/modules/contact/lib/Drupal/contact/Category.php b/core/modules/contact/lib/Drupal/contact/Category.php
new file mode 100644
index 0000000..dac5c33
--- /dev/null
+++ b/core/modules/contact/lib/Drupal/contact/Category.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\contact\Category.
+ */
+
+namespace Drupal\contact;
+
+use Drupal\config\ConfigurableBase;
+
+/**
+ * Defines the contact category entity.
+ */
+class Category extends ConfigurableBase {
+
+  public $id;
+  public $uuid;
+  public $label;
+
+  /**
+   * List of recipient e-mail addresses.
+   *
+   * @var array
+   */
+  public $recipients = array();
+
+  /**
+   * An auto-reply message to send to the message author.
+   *
+   * @var string
+   */
+  public $reply = '';
+
+  /**
+   * Weight of this category (used for sorting).
+   *
+   * @var int
+   */
+  public $weight = 0;
+
+  /**
+   * Implements Drupal\Core\Configurable\ConfigurableInterface::getConfigPrefix().
+   */
+  public static function getConfigPrefix() {
+    return 'contact.category';
+  }
+}
diff --git a/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php b/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php
index 7eae709..76bf23b 100644
--- a/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php
+++ b/core/modules/contact/lib/Drupal/contact/Tests/ContactSitewideTest.php
@@ -66,32 +66,31 @@ class ContactSitewideTest extends WebTestBase {
     // Test invalid recipients.
     $invalid_recipients = array('invalid', 'invalid@', 'invalid@site.', '@site.', '@site.com');
     foreach ($invalid_recipients as $invalid_recipient) {
-      $this->addCategory($this->randomName(16), $invalid_recipient, '', FALSE);
+      $this->addCategory($this->randomName(16), $this->randomName(16), $invalid_recipient, '', FALSE);
       $this->assertRaw(t('%recipient is an invalid e-mail address.', array('%recipient' => $invalid_recipient)), t('Caught invalid recipient (' . $invalid_recipient . ').'));
     }
 
     // Test validation of empty category and recipients fields.
-    $this->addCategory($category = '', '', '', TRUE);
-    $this->assertText(t('Category field is required.'), t('Caught empty category field'));
+    $this->addCategory('', '', '', '', TRUE);
+    $this->assertText(t('Label field is required.'), t('Caught empty category field'));
     $this->assertText(t('Recipients field is required.'), t('Caught empty recipients field.'));
 
     // Create first valid category.
     $recipients = array('simpletest@example.com', 'simpletest2@example.com', 'simpletest3@example.com');
-    $this->addCategory($category = $this->randomName(16), implode(',', array($recipients[0])), '', TRUE);
-    $this->assertRaw(t('Category %category has been saved.', array('%category' => $category)), t('Category successfully saved.'));
+    $this->addCategory($id = drupal_strtolower($this->randomName(16)), $label = $this->randomName(16), implode(',', array($recipients[0])), '', TRUE);
+    $this->assertRaw(t('Category %label has been saved.', array('%label' => $label)), t('Category successfully saved.'));
 
     // Make sure the newly created category is included in the list of categories.
-    $this->assertNoUniqueText($category, t('New category included in categories list.'));
+    $this->assertNoUniqueText($label, t('New category included in categories list.'));
 
     // Test update contact form category.
-    $categories = $this->getCategories();
-    $category_id = $this->updateCategory($categories, $category = $this->randomName(16), $recipients_str = implode(',', array($recipients[0], $recipients[1])), $reply = $this->randomName(30), FALSE);
-    $category_array = db_query("SELECT category, recipients, reply, selected FROM {contact} WHERE cid = :cid", array(':cid' => $category_id))->fetchAssoc();
-    $this->assertEqual($category_array['category'], $category);
-    $this->assertEqual($category_array['recipients'], $recipients_str);
-    $this->assertEqual($category_array['reply'], $reply);
-    $this->assertFalse($category_array['selected']);
-    $this->assertRaw(t('Category %category has been saved.', array('%category' => $category)), t('Category successfully saved.'));
+    $this->updateCategory($id, $label = $this->randomName(16), $recipients_str = implode(',', array($recipients[0], $recipients[1])), $reply = $this->randomName(30), FALSE);
+    $config = config('contact.category.' . $id)->get();
+    $this->assertEqual($config['label'], $label);
+    $this->assertEqual($config['recipients'], array($recipients[0], $recipients[1]));
+    $this->assertEqual($config['reply'], $reply);
+    $this->assertNotEqual($id, config('contact.settings')->get('default_category'));
+    $this->assertRaw(t('Category %label has been saved.', array('%label' => $label)), t('Category successfully saved.'));
 
     // Ensure that the contact form is shown without a category selection input.
     user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access site-wide contact form'));
@@ -102,16 +101,16 @@ class ContactSitewideTest extends WebTestBase {
     $this->drupalLogin($admin_user);
 
     // Add more categories.
-    $this->addCategory($category = $this->randomName(16), implode(',', array($recipients[0], $recipients[1])), '', FALSE);
-    $this->assertRaw(t('Category %category has been saved.', array('%category' => $category)), t('Category successfully saved.'));
+    $this->addCategory(drupal_strtolower($this->randomName(16)), $label = $this->randomName(16), implode(',', array($recipients[0], $recipients[1])), '', FALSE);
+    $this->assertRaw(t('Category %label has been saved.', array('%label' => $label)), t('Category successfully saved.'));
 
-    $this->addCategory($category = $this->randomName(16), implode(',', array($recipients[0], $recipients[1], $recipients[2])), '', FALSE);
-    $this->assertRaw(t('Category %category has been saved.', array('%category' => $category)), t('Category successfully saved.'));
+    $this->addCategory($name = drupal_strtolower($this->randomName(16)), $label = $this->randomName(16), implode(',', array($recipients[0], $recipients[1], $recipients[2])), '', FALSE);
+    $this->assertRaw(t('Category %label has been saved.', array('%label' => $label)), t('Category successfully saved.'));
 
     // Try adding a category that already exists.
-    $this->addCategory($category, '', '', FALSE);
-    $this->assertNoRaw(t('Category %category has been saved.', array('%category' => $category)), t('Category not saved.'));
-    $this->assertRaw(t('A contact form with category %category already exists.', array('%category' => $category)), t('Duplicate category error found.'));
+    $this->addCategory($name, $label, '', '', FALSE);
+    $this->assertNoRaw(t('Category %label has been saved.', array('%label' => $label)), t('Category not saved.'));
+    $this->assertRaw(t('The machine-readable name is already in use. It must be unique.'), t('Duplicate category error found.'));
 
     // Clear flood table in preparation for flood test and allow other checks to complete.
     db_delete('flood')->execute();
@@ -130,36 +129,39 @@ class ContactSitewideTest extends WebTestBase {
     $this->assertResponse(200, t('Access granted to anonymous user with permission.'));
 
     // Submit contact form with invalid values.
-    $this->submitContact('', $recipients[0], $this->randomName(16), $categories[0], $this->randomName(64));
+    $categories = entity_load_multiple('contact_category', FALSE);
+    $id = key($categories);
+
+    $this->submitContact('', $recipients[0], $this->randomName(16), $id, $this->randomName(64));
     $this->assertText(t('Your name field is required.'), t('Name required.'));
 
-    $this->submitContact($this->randomName(16), '', $this->randomName(16), $categories[0], $this->randomName(64));
+    $this->submitContact($this->randomName(16), '', $this->randomName(16), $id, $this->randomName(64));
     $this->assertText(t('Your e-mail address field is required.'), t('E-mail required.'));
 
-    $this->submitContact($this->randomName(16), $invalid_recipients[0], $this->randomName(16), $categories[0], $this->randomName(64));
+    $this->submitContact($this->randomName(16), $invalid_recipients[0], $this->randomName(16), $id, $this->randomName(64));
     $this->assertRaw(t('The e-mail address %mail is not valid.', array('%mail' => 'invalid')), 'Valid e-mail required.');
 
-    $this->submitContact($this->randomName(16), $recipients[0], '', $categories[0], $this->randomName(64));
+    $this->submitContact($this->randomName(16), $recipients[0], '', $id, $this->randomName(64));
     $this->assertText(t('Subject field is required.'), t('Subject required.'));
 
-    $this->submitContact($this->randomName(16), $recipients[0], $this->randomName(16), $categories[0], '');
+    $this->submitContact($this->randomName(16), $recipients[0], $this->randomName(16), $id, '');
     $this->assertText(t('Message field is required.'), t('Message required.'));
 
     // Test contact form with no default category selected.
-    db_update('contact')
-      ->fields(array('selected' => 0))
-      ->execute();
+    config('contact.settings')
+      ->set('default_category', '')
+      ->save();
     $this->drupalGet('contact');
-    $this->assertRaw(t('- Please choose -'), t('Without selected categories the visitor is asked to chose a category.'));
+    $this->assertRaw(t('- Select -'), t('Without selected categories the visitor is asked to chose a category.'));
 
     // Submit contact form with invalid category id (cid 0).
     $this->submitContact($this->randomName(16), $recipients[0], $this->randomName(16), 0, '');
-    $this->assertText(t('You must select a valid category.'), t('Valid category required.'));
+    $this->assertText('Category field is required.');
 
     // Submit contact form with correct values and check flood interval.
     for ($i = 0; $i < $flood_limit; $i++) {
-      $this->submitContact($this->randomName(16), $recipients[0], $this->randomName(16), $categories[0], $this->randomName(64));
-      $this->assertText(t('Your message has been sent.'), t('Message sent.'));
+      $this->submitContact($this->randomName(16), $recipients[0], $this->randomName(16), $id, $this->randomName(64));
+      $this->assertText(t('Your message has been sent.'));
     }
     // Submit contact form one over limit.
     $this->drupalGet('contact');
@@ -182,9 +184,9 @@ class ContactSitewideTest extends WebTestBase {
     // Set up three categories, 2 with an auto-reply and one without.
     $foo_autoreply = $this->randomName(40);
     $bar_autoreply = $this->randomName(40);
-    $this->addCategory('foo', 'foo@example.com', $foo_autoreply, FALSE);
-    $this->addCategory('bar', 'bar@example.com', $bar_autoreply, FALSE);
-    $this->addCategory('no_autoreply', 'bar@example.com', '', FALSE);
+    $this->addCategory('foo', 'foo', 'foo@example.com', $foo_autoreply, FALSE);
+    $this->addCategory('bar', 'bar', 'bar@example.com', $bar_autoreply, FALSE);
+    $this->addCategory('no_autoreply', 'no_autoreply', 'bar@example.com', '', FALSE);
 
     // Log the current user out in order to test the name and e-mail fields.
     $this->drupalLogout();
@@ -193,34 +195,36 @@ class ContactSitewideTest extends WebTestBase {
     // Test the auto-reply for category 'foo'.
     $email = $this->randomName(32) . '@example.com';
     $subject = $this->randomName(64);
-    $this->submitContact($this->randomName(16), $email, $subject, 2, $this->randomString(128));
+    $this->submitContact($this->randomName(16), $email, $subject, 'foo', $this->randomString(128));
 
     // We are testing the auto-reply, so there should be one e-mail going to the sender.
     $captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email, 'from' => 'foo@example.com'));
-    $this->assertEqual(count($captured_emails), 1, t('Auto-reply e-mail was sent to the sender for category "foo".'), t('Contact'));
-    $this->assertEqual($captured_emails[0]['body'], drupal_html_to_text($foo_autoreply), t('Auto-reply e-mail body is correct for category "foo".'), t('Contact'));
+    $this->assertEqual(count($captured_emails), 1);
+    $this->assertEqual($captured_emails[0]['body'], drupal_html_to_text($foo_autoreply));
 
     // Test the auto-reply for category 'bar'.
     $email = $this->randomName(32) . '@example.com';
-    $this->submitContact($this->randomName(16), $email, $this->randomString(64), 3, $this->randomString(128));
+    $this->submitContact($this->randomName(16), $email, $this->randomString(64), 'bar', $this->randomString(128));
 
     // Auto-reply for category 'bar' should result in one auto-reply e-mail to the sender.
     $captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email, 'from' => 'bar@example.com'));
-    $this->assertEqual(count($captured_emails), 1, t('Auto-reply e-mail was sent to the sender for category "bar".'), t('Contact'));
-    $this->assertEqual($captured_emails[0]['body'], drupal_html_to_text($bar_autoreply), t('Auto-reply e-mail body is correct for category "bar".'), t('Contact'));
+    $this->assertEqual(count($captured_emails), 1);
+    $this->assertEqual($captured_emails[0]['body'], drupal_html_to_text($bar_autoreply));
 
     // Verify that no auto-reply is sent when the auto-reply field is left blank.
     $email = $this->randomName(32) . '@example.com';
-    $this->submitContact($this->randomName(16), $email, $this->randomString(64), 4, $this->randomString(128));
+    $this->submitContact($this->randomName(16), $email, $this->randomString(64), 'no_autoreply', $this->randomString(128));
     $captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email, 'from' => 'no_autoreply@example.com'));
-    $this->assertEqual(count($captured_emails), 0, t('No auto-reply e-mail was sent to the sender for category "no-autoreply".'), t('Contact'));
+    $this->assertEqual(count($captured_emails), 0);
   }
 
   /**
    * Adds a category.
    *
-   * @param string $category
-   *   The category name.
+   * @param string $id
+   *   The category machine name.
+   * @param string $label
+   *   The category label.
    * @param string $recipients
    *   The list of recipient e-mail addresses.
    * @param string $reply
@@ -229,9 +233,10 @@ class ContactSitewideTest extends WebTestBase {
    * @param boolean $selected
    *   Boolean indicating whether the category should be selected by default.
    */
-  function addCategory($category, $recipients, $reply, $selected) {
+  function addCategory($id, $label, $recipients, $reply, $selected) {
     $edit = array();
-    $edit['category'] = $category;
+    $edit['label'] = $label;
+    $edit['id'] = $id;
     $edit['recipients'] = $recipients;
     $edit['reply'] = $reply;
     $edit['selected'] = ($selected ? TRUE : FALSE);
@@ -241,8 +246,10 @@ class ContactSitewideTest extends WebTestBase {
   /**
    * Updates a category.
    *
-   * @param string $category
-   *   The category name.
+   * @param string $id
+   *   The category machine name.
+   * @param string $label
+   *   The category label.
    * @param string $recipients
    *   The list of recipient e-mail addresses.
    * @param string $reply
@@ -251,15 +258,13 @@ class ContactSitewideTest extends WebTestBase {
    * @param boolean $selected
    *   Boolean indicating whether the category should be selected by default.
    */
-  function updateCategory($categories, $category, $recipients, $reply, $selected) {
-    $category_id = $categories[array_rand($categories)];
+  function updateCategory($id, $label, $recipients, $reply, $selected) {
     $edit = array();
-    $edit['category'] = $category;
+    $edit['label'] = $label;
     $edit['recipients'] = $recipients;
     $edit['reply'] = $reply;
     $edit['selected'] = ($selected ? TRUE : FALSE);
-    $this->drupalPost('admin/structure/contact/edit/' . $category_id, $edit, t('Save'));
-    return ($category_id);
+    $this->drupalPost('admin/structure/contact/manage/' . $id, $edit, t('Save'));
   }
 
   /**
@@ -271,17 +276,17 @@ class ContactSitewideTest extends WebTestBase {
    *   The e-mail address of the sender.
    * @param string $subject
    *   The subject of the message.
-   * @param integer $cid
+   * @param string $id
    *   The category ID of the message.
    * @param string $message
    *   The message body.
    */
-  function submitContact($name, $mail, $subject, $cid, $message) {
+  function submitContact($name, $mail, $subject, $id, $message) {
     $edit = array();
     $edit['name'] = $name;
     $edit['mail'] = $mail;
     $edit['subject'] = $subject;
-    $edit['cid'] = $cid;
+    $edit['category'] = $id;
     $edit['message'] = $message;
     $this->drupalPost('contact', $edit, t('Send message'));
   }
@@ -290,22 +295,11 @@ class ContactSitewideTest extends WebTestBase {
    * Deletes all categories.
    */
   function deleteCategories() {
-    $categories = $this->getCategories();
-    foreach ($categories as $category) {
-      $category_name = db_query("SELECT category FROM {contact} WHERE cid = :cid", array(':cid' => $category))->fetchField();
-      $this->drupalPost('admin/structure/contact/delete/' . $category, array(), t('Delete'));
-      $this->assertRaw(t('Category %category has been deleted.', array('%category' => $category_name)), t('Category deleted successfully.'));
+    $categories = entity_load_multiple('contact_category', FALSE);
+    foreach ($categories as $id => $category) {
+      $this->drupalPost('admin/structure/contact/manage/' . $id . '/delete', array(), t('Delete'));
+      $this->assertRaw(t('Category %label has been deleted.', array('%label' => $category->label())), t('Category deleted successfully.'));
     }
   }
 
-  /**
-   * Gets a list of all category IDs.
-   *
-   * @return array
-   *   A list of the category IDs.
-   */
-  function getCategories() {
-    $categories = db_query('SELECT cid FROM {contact}')->fetchCol();
-    return $categories;
-  }
 }
diff --git a/core/modules/entity/lib/Drupal/entity/Entity.php b/core/modules/entity/lib/Drupal/entity/Entity.php
index 2055336..dae1760 100644
--- a/core/modules/entity/lib/Drupal/entity/Entity.php
+++ b/core/modules/entity/lib/Drupal/entity/Entity.php
@@ -45,7 +45,7 @@ class Entity implements EntityInterface {
    *
    * @var bool
    */
-  public $isCurrentRevision = TRUE;
+  protected $isCurrentRevision = TRUE;
 
   /**
    * Constructs a new entity object.
@@ -280,7 +280,11 @@ class Entity implements EntityInterface {
   /**
    * Implements Drupal\entity\EntityInterface::isCurrentRevision().
    */
-  public function isCurrentRevision() {
-    return $this->isCurrentRevision;
+  public function isCurrentRevision($new_value = NULL) {
+    $return = $this->isCurrentRevision;
+    if (isset($new_value)) {
+      $this->isCurrentRevision = (bool) $new_value;
+    }
+    return $return;
   }
 }
diff --git a/core/modules/entity/lib/Drupal/entity/EntityInterface.php b/core/modules/entity/lib/Drupal/entity/EntityInterface.php
index ef95cc2..4fcf483 100644
--- a/core/modules/entity/lib/Drupal/entity/EntityInterface.php
+++ b/core/modules/entity/lib/Drupal/entity/EntityInterface.php
@@ -211,8 +211,12 @@ interface EntityInterface {
   /**
    * Checks if this entity is the current revision.
    *
+   * @param bool $new_value
+   *   (optional) A Boolean to (re)set the isCurrentRevision flag.
+   *
    * @return bool
-   *   TRUE if the entity is the current revision, FALSE otherwise.
+   *   TRUE if the entity is the current revision, FALSE otherwise. If
+   *   $new_value was passed, the previous value is returned.
    */
   public function isCurrentRevision();
 }
diff --git a/core/modules/node/lib/Drupal/node/NodeStorageController.php b/core/modules/node/lib/Drupal/node/NodeStorageController.php
index d4968a6..898aa71 100644
--- a/core/modules/node/lib/Drupal/node/NodeStorageController.php
+++ b/core/modules/node/lib/Drupal/node/NodeStorageController.php
@@ -151,7 +151,7 @@ class NodeStorageController extends DatabaseStorageController {
     $entity->{$this->revisionKey} = $record->{$this->revisionKey};
 
     // Mark this revision as the current one.
-    $entity->isCurrentRevision = TRUE;
+    $entity->isCurrentRevision(TRUE);
   }
 
   /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/Module/ModuleApiTest.php b/core/modules/system/lib/Drupal/system/Tests/Module/ModuleApiTest.php
index 8468185..8177165 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Module/ModuleApiTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Module/ModuleApiTest.php
@@ -45,6 +45,8 @@ class ModuleApiTest extends WebTestBase {
     // Try to install a new module.
     module_enable(array('contact'));
     $module_list[] = 'contact';
+    // Contact module requires config.
+    $module_list[] = 'config';
     sort($module_list);
     $this->assertModuleList($module_list, t('After adding a module'));
 
