diff --git a/core/modules/comment/comment.entity.inc b/core/modules/comment/comment.entity.inc
new file mode 100644
index 0000000..8db4167
--- /dev/null
+++ b/core/modules/comment/comment.entity.inc
@@ -0,0 +1,280 @@
+<?php
+
+/**
+ * @file
+ * Entity controller and class for comments.
+ */
+
+/**
+ * Defines the comment entity class.
+ */
+class Comment extends Entity {
+
+  /**
+   * The comment ID.
+   *
+   * @var integer
+   */
+  public $cid;
+
+  /**
+   * The parent comment ID if this is a reply to a comment.
+   *
+   * @var integer
+   */
+  public $pid;
+
+  /**
+   * The comment language.
+   *
+   * @var string
+   */
+  public $language = LANGUAGE_NONE;
+
+  /**
+   * The comment title.
+   *
+   * @var string
+   */
+  public $subject;
+
+
+  /**
+   * The comment author ID.
+   *
+   * @var integer
+   */
+  public $uid = 0;
+
+  /**
+   * The comment author's name.
+   *
+   * For anonymous authors, this is the value as typed in the comment form.
+   *
+   * @var string
+   */
+  public $name = '';
+
+  /**
+   * The comment author's e-mail address.
+   *
+   * For anonymous authors, this is the value as typed in the comment form.
+   *
+   * @var string
+   */
+  public $mail;
+
+  /**
+   * The comment author's home page address.
+   *
+   * For anonymous authors, this is the value as typed in the comment form.
+   *
+   * @var string
+   */
+  public $homepage;
+
+}
+
+/**
+ * Defines the controller class for comments.
+ *
+ * This extends the EntityDatabaseStorageController class, adding required
+ * special handling for comment entities.
+ */
+class CommentStorageController extends EntityDatabaseStorageController {
+
+  /**
+   * Overrides EntityDatabaseStorageController::buildQuery().
+   */
+  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
+    $query = parent::buildQuery($ids, $conditions, $revision_id);
+    // Specify additional fields from the user and node tables.
+    $query->innerJoin('node', 'n', 'base.nid = n.nid');
+    $query->addField('n', 'type', 'node_type');
+    $query->innerJoin('users', 'u', 'base.uid = u.uid');
+    $query->addField('u', 'name', 'registered_name');
+    $query->fields('u', array('uid', 'signature', 'signature_format', 'picture'));
+    return $query;
+  }
+
+  /**
+   * Overrides EntityDatabaseStorageController::attachLoad().
+   */
+  protected function attachLoad(&$comments, $revision_id = FALSE) {
+    // Set up standard comment properties.
+    foreach ($comments as $key => $comment) {
+      $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
+      $comment->new = node_mark($comment->nid, $comment->changed);
+      $comment->node_type = 'comment_node_' . $comment->node_type;
+      $comments[$key] = $comment;
+    }
+    parent::attachLoad($comments, $revision_id);
+  }
+
+  /**
+   * Overrides EntityDatabaseStorageController::preSave().
+   *
+   * @see comment_int_to_alphadecimal()
+   * @see comment_increment_alphadecimal()
+   */
+  protected function preSave(EntityInterface $comment) {
+    global $user;
+
+    if (!isset($comment->status)) {
+      $comment->status = user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED;
+    }
+    // Make sure we have a proper bundle name.
+    if (!isset($comment->node_type)) {
+      $node = node_load($comment->nid);
+      $comment->node_type = 'comment_node_' . $node->type;
+    }
+    if (!$comment->cid) {
+      // Add the comment to database. This next section builds the thread field.
+      // Also see the documentation for comment_view().
+      if (!empty($comment->thread)) {
+        // Allow calling code to set thread itself.
+        $thread = $comment->thread;
+      }
+      elseif ($comment->pid == 0) {
+        // This is a comment with no parent comment (depth 0): we start
+        // by retrieving the maximum thread level.
+        $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField();
+        // Strip the "/" from the end of the thread.
+        $max = rtrim($max, '/');
+        // Finally, build the thread field for this new comment.
+        $thread = comment_increment_alphadecimal($max) . '/';
+      }
+      else {
+        // This is a comment with a parent comment, so increase the part of
+        // the thread value at the proper depth.
+
+        // Get the parent comment:
+        $parent = comment_load($comment->pid);
+        // Strip the "/" from the end of the parent thread.
+        $parent->thread = (string) rtrim((string) $parent->thread, '/');
+        // Get the max value in *this* thread.
+        $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array(
+          ':thread' => $parent->thread . '.%',
+          ':nid' => $comment->nid,
+        ))->fetchField();
+
+        if ($max == '') {
+          // First child of this parent.
+          $thread = $parent->thread . '.' . comment_int_to_alphadecimal(0) . '/';
+        }
+        else {
+          // Strip the "/" at the end of the thread.
+          $max = rtrim($max, '/');
+          // Get the value at the correct depth.
+          $parts = explode('.', $max);
+          $parent_depth = count(explode('.', $parent->thread));
+          $last = $parts[$parent_depth];
+          // Finally, build the thread field for this new comment.
+          $thread = $parent->thread . '.' . comment_increment_alphadecimal($last) . '/';
+        }
+      }
+      if (empty($comment->created)) {
+        $comment->created = REQUEST_TIME;
+      }
+      if (empty($comment->changed)) {
+        $comment->changed = $comment->created;
+      }
+      // We test the value with '===' because we need to modify anonymous
+      // users as well.
+      if ($comment->uid === $user->uid && isset($user->name)) {
+        $comment->name = $user->name;
+      }
+      // Add the values which aren't passed into the function.
+      $comment->thread = $thread;
+      $comment->hostname = ip_address();
+    }
+  }
+
+  /**
+   * Overrides EntityDatabaseStorageController::postSave().
+   */
+  protected function postSave(EntityInterface $comment) {
+    // Update the {node_comment_statistics} table prior to executing the hook.
+    $this->updateNodeStatistics($comment->nid);
+    if ($comment->status == COMMENT_PUBLISHED) {
+      module_invoke_all('comment_publish', $comment);
+    }
+  }
+
+  /**
+   * Overrides EntityDatabaseStorageController::postDelete().
+   */
+  protected function postDelete($comments) {
+    // Delete the comments' replies.
+    $query = db_select('comment', 'c')
+      ->fields('c', array('cid'))
+      ->condition('pid', array(array_keys($comments)), 'IN');
+    $child_cids = $query->execute()->fetchCol();
+    comment_delete_multiple($child_cids);
+
+    foreach ($comments as $comment) {
+      $this->updateNodeStatistics($comment->nid);
+    }
+  }
+
+  /**
+   * Updates the comment statistics for a given node.
+   *
+   * The {node_comment_statistics} table has the following fields:
+   * - last_comment_timestamp: The timestamp of the last comment for this node,
+   *   or the node created timestamp if no comments exist for the node.
+   * - last_comment_name: The name of the anonymous poster for the last comment.
+   * - last_comment_uid: The user ID of the poster for the last comment for
+   *   this node, or the node author's user ID if no comments exist for the
+   *   node.
+   * - comment_count: The total number of approved/published comments on this
+   *   node.
+   *
+   * @param $nid
+   *   The node ID.
+   */
+  protected function updateNodeStatistics($nid) {
+    // Allow bulk updates and inserts to temporarily disable the
+    // maintenance of the {node_comment_statistics} table.
+    if (!variable_get('comment_maintain_node_statistics', TRUE)) {
+      return;
+    }
+
+    $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND status = :status', array(
+      ':nid' => $nid,
+      ':status' => COMMENT_PUBLISHED,
+    ))->fetchField();
+
+    if ($count > 0) {
+      // Comments exist.
+      $last_reply = db_query_range('SELECT cid, name, changed, uid FROM {comment} WHERE nid = :nid AND status = :status ORDER BY cid DESC', 0, 1, array(
+        ':nid' => $nid,
+        ':status' => COMMENT_PUBLISHED,
+      ))->fetchObject();
+      db_update('node_comment_statistics')
+        ->fields(array(
+          'cid' => $last_reply->cid,
+          'comment_count' => $count,
+          'last_comment_timestamp' => $last_reply->changed,
+          'last_comment_name' => $last_reply->uid ? '' : $last_reply->name,
+          'last_comment_uid' => $last_reply->uid,
+        ))
+        ->condition('nid', $nid)
+        ->execute();
+    }
+    else {
+      // Comments do not exist.
+      $node = db_query('SELECT uid, created FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject();
+      db_update('node_comment_statistics')
+        ->fields(array(
+          'cid' => 0,
+          'comment_count' => 0,
+          'last_comment_timestamp' => $node->created,
+          'last_comment_name' => '',
+          'last_comment_uid' => $node->uid,
+        ))
+        ->condition('nid', $nid)
+        ->execute();
+    }
+  }
+}
diff --git a/core/modules/comment/comment.info b/core/modules/comment/comment.info
index 949ffc2..606f46d 100644
--- a/core/modules/comment/comment.info
+++ b/core/modules/comment/comment.info
@@ -5,7 +5,7 @@ version = VERSION
 core = 8.x
 dependencies[] = text
 dependencies[] = entity
-files[] = comment.module
+files[] = comment.entity.inc
 files[] = comment.test
 configure = admin/content/comment
 stylesheets[all][] = comment.css
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index cf49923..298d878 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -98,7 +98,8 @@ function comment_entity_info() {
       'base table' => 'comment',
       'uri callback' => 'comment_uri',
       'fieldable' => TRUE,
-      'controller class' => 'CommentController',
+      'controller class' => 'CommentStorageController',
+      'entity class' => 'Comment',
       'entity keys' => array(
         'id' => 'cid',
         'bundle' => 'node_type',
@@ -738,8 +739,8 @@ function comment_node_page_additions($node) {
 
   // Append comment form if needed.
   if (user_access('post comments') && $node->comment == COMMENT_NODE_OPEN && (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_BELOW)) {
-    $build = drupal_get_form("comment_node_{$node->type}_form", (object) array('nid' => $node->nid));
-    $additions['comment_form'] = $build;
+    $comment = entity_create('comment', array('nid' => $node->nid));
+    $additions['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment);
   }
 
   if ($additions) {
@@ -1436,151 +1437,9 @@ function comment_access($op, $comment) {
  *
  * @param $comment
  *   A comment object.
- *
- * @see comment_int_to_alphadecimal()
  */
 function comment_save($comment) {
-  global $user;
-
-  $transaction = db_transaction();
-  try {
-    $defaults = array(
-      'mail' => '',
-      'homepage' => '',
-      'name' => '',
-      'status' => user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED,
-    );
-    foreach ($defaults as $key => $default) {
-      if (!isset($comment->$key)) {
-        $comment->$key = $default;
-      }
-    }
-    // Make sure we have a bundle name.
-    if (!isset($comment->node_type)) {
-      $node = node_load($comment->nid);
-      $comment->node_type = 'comment_node_' . $node->type;
-    }
-
-    // Load the stored entity, if any.
-    if (!empty($comment->cid) && !isset($comment->original)) {
-      $comment->original = entity_load_unchanged('comment', $comment->cid);
-    }
-
-    field_attach_presave('comment', $comment);
-
-    // Allow modules to alter the comment before saving.
-    module_invoke_all('comment_presave', $comment);
-    module_invoke_all('entity_presave', $comment, 'comment');
-
-    if ($comment->cid) {
-
-      drupal_write_record('comment', $comment, 'cid');
-
-      // Ignore slave server temporarily to give time for the
-      // saved comment to be propagated to the slave.
-      db_ignore_slave();
-
-      // Update the {node_comment_statistics} table prior to executing hooks.
-      _comment_update_node_statistics($comment->nid);
-
-      field_attach_update('comment', $comment);
-      // Allow modules to respond to the updating of a comment.
-      module_invoke_all('comment_update', $comment);
-      module_invoke_all('entity_update', $comment, 'comment');
-    }
-    else {
-      // Add the comment to database. This next section builds the thread field.
-      // Also see the documentation for comment_view().
-      if (!empty($comment->thread)) {
-        // Allow calling code to set thread itself.
-        $thread = $comment->thread;
-      }
-      elseif ($comment->pid == 0) {
-        // This is a comment with no parent comment (depth 0): we start
-        // by retrieving the maximum thread level.
-        $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField();
-        // Strip the "/" from the end of the thread.
-        $max = rtrim($max, '/');
-        // Finally, build the thread field for this new comment.
-        $thread = comment_increment_alphadecimal($max) . '/';
-      }
-      else {
-        // This is a comment with a parent comment, so increase the part of the
-        // thread value at the proper depth.
-
-        // Get the parent comment:
-        $parent = comment_load($comment->pid);
-        // Strip the "/" from the end of the parent thread.
-        $parent->thread = (string) rtrim((string) $parent->thread, '/');
-        // Get the max value in *this* thread.
-        $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array(
-          ':thread' => $parent->thread . '.%',
-          ':nid' => $comment->nid,
-        ))->fetchField();
-
-        if ($max == '') {
-          // First child of this parent.
-          $thread = $parent->thread . '.' . comment_int_to_alphadecimal(0) . '/';
-        }
-        else {
-          // Strip the "/" at the end of the thread.
-          $max = rtrim($max, '/');
-          // Get the value at the correct depth.
-          $parts = explode('.', $max);
-          $parent_depth = count(explode('.', $parent->thread));
-          $last = $parts[$parent_depth];
-          // Finally, build the thread field for this new comment.
-          $thread = $parent->thread . '.' . comment_increment_alphadecimal($last) . '/';
-        }
-      }
-
-      if (empty($comment->created)) {
-        $comment->created = REQUEST_TIME;
-      }
-
-      if (empty($comment->changed)) {
-        $comment->changed = $comment->created;
-      }
-
-      if ($comment->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well.
-        $comment->name = $user->name;
-      }
-
-      // Ensure the parent id (pid) has a value set.
-      if (empty($comment->pid)) {
-        $comment->pid = 0;
-      }
-
-      // Add the values which aren't passed into the function.
-      $comment->thread = $thread;
-      $comment->hostname = ip_address();
-
-      drupal_write_record('comment', $comment);
-
-      // Ignore slave server temporarily to give time for the
-      // created comment to be propagated to the slave.
-      db_ignore_slave();
-
-      // Update the {node_comment_statistics} table prior to executing hooks.
-      _comment_update_node_statistics($comment->nid);
-
-      field_attach_insert('comment', $comment);
-
-      // Tell the other modules a new comment has been submitted.
-      module_invoke_all('comment_insert', $comment);
-      module_invoke_all('entity_insert', $comment, 'comment');
-    }
-    if ($comment->status == COMMENT_PUBLISHED) {
-      module_invoke_all('comment_publish', $comment);
-    }
-    unset($comment->original);
-  }
-  catch (Exception $e) {
-    $transaction->rollback('comment');
-    watchdog_exception('comment', $e);
-    throw $e;
-  }
-
+  $comment->save();
 }
 
 /**
@@ -1600,31 +1459,7 @@ function comment_delete($cid) {
  *   The comment to delete.
  */
 function comment_delete_multiple($cids) {
-  $comments = comment_load_multiple($cids);
-  if ($comments) {
-    $transaction = db_transaction();
-    try {
-      // Delete the comments.
-      db_delete('comment')
-        ->condition('cid', array_keys($comments), 'IN')
-        ->execute();
-      foreach ($comments as $comment) {
-        field_attach_delete('comment', $comment);
-        module_invoke_all('comment_delete', $comment);
-        module_invoke_all('entity_delete', $comment, 'comment');
-
-        // Delete the comment's replies.
-        $child_cids = db_query('SELECT cid FROM {comment} WHERE pid = :cid', array(':cid' => $comment->cid))->fetchCol();
-        comment_delete_multiple($child_cids);
-        _comment_update_node_statistics($comment->nid);
-      }
-    }
-    catch (Exception $e) {
-      $transaction->rollback();
-      watchdog_exception('comment', $e);
-      throw $e;
-    }
-  }
+  entity_delete_multiple('comment', $cids);
 }
 
 /**
@@ -1672,37 +1507,6 @@ function comment_load($cid, $reset = FALSE) {
 }
 
 /**
- * Controller class for comments.
- *
- * This extends the DrupalDefaultEntityController class, adding required
- * special handling for comment objects.
- */
-class CommentController extends DrupalDefaultEntityController {
-
-  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
-    $query = parent::buildQuery($ids, $conditions, $revision_id);
-    // Specify additional fields from the user and node tables.
-    $query->innerJoin('node', 'n', 'base.nid = n.nid');
-    $query->addField('n', 'type', 'node_type');
-    $query->innerJoin('users', 'u', 'base.uid = u.uid');
-    $query->addField('u', 'name', 'registered_name');
-    $query->fields('u', array('uid', 'signature', 'signature_format', 'picture'));
-    return $query;
-  }
-
-  protected function attachLoad(&$comments, $revision_id = FALSE) {
-    // Setup standard comment properties.
-    foreach ($comments as $key => $comment) {
-      $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
-      $comment->new = node_mark($comment->nid, $comment->changed);
-      $comment->node_type = 'comment_node_' . $comment->node_type;
-      $comments[$key] = $comment;
-    }
-    parent::attachLoad($comments, $revision_id);
-  }
-}
-
-/**
  * Get number of new comments for current user and specified node.
  *
  * @param $nid
@@ -1831,22 +1635,6 @@ function comment_form($form, &$form_state, $comment) {
   // use during form building and processing. During a rebuild, use what is in
   // the form state.
   if (!isset($form_state['comment'])) {
-    $defaults = array(
-      'name' => '',
-      'mail' => '',
-      'homepage' => '',
-      'subject' => '',
-      'comment' => '',
-      'cid' => NULL,
-      'pid' => NULL,
-      'language' => LANGUAGE_NONE,
-      'uid' => 0,
-    );
-    foreach ($defaults as $key => $value) {
-      if (!isset($comment->$key)) {
-        $comment->$key = $value;
-      }
-    }
     $form_state['comment'] = $comment;
   }
   else {
@@ -2143,12 +1931,6 @@ function comment_form_validate($form, &$form_state) {
  * Prepare a comment for submission.
  */
 function comment_submit($comment) {
-  // @todo Legacy support. Remove in Drupal 8.
-  if (is_array($comment)) {
-    $comment += array('subject' => '');
-    $comment = (object) $comment;
-  }
-
   if (empty($comment->date)) {
     $comment->date = 'now';
   }
@@ -2399,61 +2181,6 @@ function _comment_per_page() {
 }
 
 /**
- * Updates the comment statistics for a given node. This should be called any
- * time a comment is added, deleted, or updated.
- *
- * The following fields are contained in the node_comment_statistics table.
- * - last_comment_timestamp: the timestamp of the last comment for this node or the node create stamp if no comments exist for the node.
- * - last_comment_name: the name of the anonymous poster for the last comment
- * - last_comment_uid: the uid of the poster for the last comment for this node or the node authors uid if no comments exists for the node.
- * - comment_count: the total number of approved/published comments on this node.
- */
-function _comment_update_node_statistics($nid) {
-  // Allow bulk updates and inserts to temporarily disable the
-  // maintenance of the {node_comment_statistics} table.
-  if (!variable_get('comment_maintain_node_statistics', TRUE)) {
-    return;
-  }
-
-  $count = db_query('SELECT COUNT(cid) FROM {comment} WHERE nid = :nid AND status = :status', array(
-    ':nid' => $nid,
-    ':status' => COMMENT_PUBLISHED,
-  ))->fetchField();
-
-  if ($count > 0) {
-    // Comments exist.
-    $last_reply = db_query_range('SELECT cid, name, changed, uid FROM {comment} WHERE nid = :nid AND status = :status ORDER BY cid DESC', 0, 1, array(
-      ':nid' => $nid,
-      ':status' => COMMENT_PUBLISHED,
-    ))->fetchObject();
-    db_update('node_comment_statistics')
-      ->fields(array(
-        'cid' => $last_reply->cid,
-        'comment_count' => $count,
-        'last_comment_timestamp' => $last_reply->changed,
-        'last_comment_name' => $last_reply->uid ? '' : $last_reply->name,
-        'last_comment_uid' => $last_reply->uid,
-      ))
-      ->condition('nid', $nid)
-      ->execute();
-  }
-  else {
-    // Comments do not exist.
-    $node = db_query('SELECT uid, created FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject();
-    db_update('node_comment_statistics')
-      ->fields(array(
-        'cid' => 0,
-        'comment_count' => 0,
-        'last_comment_timestamp' => $node->created,
-        'last_comment_name' => '',
-        'last_comment_uid' => $node->uid,
-      ))
-      ->condition('nid', $nid)
-      ->execute();
-  }
-}
-
-/**
  * Generate sorting code.
  *
  * Consists of a leading character indicating length, followed by N digits
diff --git a/core/modules/comment/comment.pages.inc b/core/modules/comment/comment.pages.inc
index 7e88bff..de8129d 100644
--- a/core/modules/comment/comment.pages.inc
+++ b/core/modules/comment/comment.pages.inc
@@ -35,7 +35,8 @@ function comment_reply($node, $pid = NULL) {
   // The user is previewing a comment prior to submitting it.
   if ($op == t('Preview')) {
     if (user_access('post comments')) {
-      $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", (object) array('pid' => $pid, 'nid' => $node->nid));
+      $comment = entity_create('comment', array('nid' => $node->nid, 'pid' => $pid));
+      $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment);
     }
     else {
       drupal_set_message(t('You are not authorized to post comments.'), 'error');
@@ -86,8 +87,8 @@ function comment_reply($node, $pid = NULL) {
       drupal_goto("node/$node->nid");
     }
     elseif (user_access('post comments')) {
-      $edit = array('nid' => $node->nid, 'pid' => $pid);
-      $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", (object) $edit);
+      $comment = entity_create('comment', array('nid' => $node->nid, 'pid' => $pid));
+      $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment);
     }
     else {
       drupal_set_message(t('You are not authorized to post comments.'), 'error');
diff --git a/core/modules/comment/comment.test b/core/modules/comment/comment.test
index 2e96ba3..2771bc4 100644
--- a/core/modules/comment/comment.test
+++ b/core/modules/comment/comment.test
@@ -87,7 +87,7 @@ class CommentHelperCase extends DrupalWebTestCase {
     }
 
     if (isset($match[1])) {
-      return (object) array('id' => $match[1], 'subject' => $subject, 'comment' => $comment);
+      return entity_create('comment', array('id' => $match[1], 'subject' => $subject, 'comment' => $comment));
     }
   }
 
@@ -269,7 +269,7 @@ class CommentHelperCase extends DrupalWebTestCase {
 
     // Create a new comment. This helper function may be run with different
     // comment settings so use comment_save() to avoid complex setup.
-    $comment = (object) array(
+    $comment = entity_create('comment', array(
       'cid' => NULL,
       'nid' => $this->node->nid,
       'node_type' => $this->node->type,
@@ -280,7 +280,7 @@ class CommentHelperCase extends DrupalWebTestCase {
       'hostname' => ip_address(),
       'language' => LANGUAGE_NONE,
       'comment_body' => array(LANGUAGE_NONE => array($this->randomName())),
-    );
+    ));
     comment_save($comment);
     $this->drupalLogout();
 
@@ -661,7 +661,7 @@ class CommentInterfaceTest extends CommentHelperCase {
       if ($info['comment count']) {
         // Create a comment via CRUD API functionality, since
         // $this->postComment() relies on actual user permissions.
-        $comment = (object) array(
+        $comment = entity_create('comment', array(
           'cid' => NULL,
           'nid' => $this->node->nid,
           'node_type' => $this->node->type,
@@ -672,7 +672,7 @@ class CommentInterfaceTest extends CommentHelperCase {
           'hostname' => ip_address(),
           'language' => LANGUAGE_NONE,
           'comment_body' => array(LANGUAGE_NONE => array($this->randomName())),
-        );
+        ));
         comment_save($comment);
         $this->comment = $comment;
 
@@ -1463,7 +1463,7 @@ class CommentApprovalTest extends CommentHelperCase {
     // Get unapproved comment id.
     $this->drupalLogin($this->admin_user);
     $anonymous_comment4 = $this->getUnapprovedComment($subject);
-    $anonymous_comment4 = (object) array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body);
+    $anonymous_comment4 = entity_create('comment', array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body));
     $this->drupalLogout();
 
     $this->assertFalse($this->commentExists($anonymous_comment4), t('Anonymous comment was not published.'));
@@ -1527,7 +1527,7 @@ class CommentApprovalTest extends CommentHelperCase {
     // Get unapproved comment id.
     $this->drupalLogin($this->admin_user);
     $anonymous_comment4 = $this->getUnapprovedComment($subject);
-    $anonymous_comment4 = (object) array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body);
+    $anonymous_comment4 = entity_create('comment', array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body));
     $this->drupalLogout();
 
     $this->assertFalse($this->commentExists($anonymous_comment4), t('Anonymous comment was not published.'));
diff --git a/core/modules/entity/entity.class.inc b/core/modules/entity/entity.class.inc
new file mode 100644
index 0000000..1dec8ee
--- /dev/null
+++ b/core/modules/entity/entity.class.inc
@@ -0,0 +1,300 @@
+<?php
+
+/**
+ * @file
+ * Provides an interface and a base class for entities.
+ */
+
+/**
+ * Defines a common interface for all entity objects.
+ */
+interface EntityInterface {
+
+  /**
+   * Constructs a new entity object.
+   *
+   * @param $values
+   *   An array of values to set, keyed by property name. If the entity type
+   *   has bundles, the bundle key has to be specified.
+   * @param $entity_type
+   *   The type of the entity to create.
+   */
+  public function __construct(array $values, $entity_type);
+
+  /**
+   * Returns the entity identifier (the entity's machine name or numeric ID).
+   *
+   * @return
+   *   The identifier of the entity, or NULL if the entity does not yet have
+   *   an identifier.
+   */
+  public function id();
+
+  /**
+   * Returns whether the entity is new.
+   *
+   * @return
+   *   TRUE if the entity is new, or FALSE if the entity has already been saved.
+   */
+  public function isNew();
+
+  /**
+   * Returns the type of the entity.
+   *
+   * @return
+   *   The type of the entity.
+   */
+  public function entityType();
+
+  /**
+   * Returns the bundle of the entity.
+   *
+   * @return
+   *   The bundle of the entity. Defaults to the entity type if the entity type
+   *   does not make use of different bundles.
+   */
+  public function bundle();
+
+  /**
+   * Returns the label of the entity.
+   *
+   * @return
+   *   The label of the entity, or NULL if there is no label defined.
+   */
+  public function label();
+
+  /**
+   * Returns the URI elements of the entity.
+   *
+   * @return
+   *   An array containing the 'path' and 'options' keys used to build the URI
+   *   of the entity, and matching the signature of url(). NULL if the entity
+   *   has no URI of its own.
+   */
+  public function uri();
+
+  /**
+   * Saves an entity permanently.
+   *
+   * @return
+   *   Either SAVED_NEW or SAVED_UPDATED, depending on the operation performed.
+   *
+   * @throws EntityStorageException
+   *   In case of failures an exception is thrown.
+   */
+  public function save();
+
+  /**
+   * Deletes an entity permanently.
+   *
+   * @throws EntityStorageException
+   *   In case of failures an exception is thrown.
+   */
+  public function delete();
+
+  /**
+   * Creates a duplicate of the entity.
+   *
+   * @return EntityInterface
+   *   A clone of the current entity with all identifiers unset, so saving
+   *   it inserts a new entity into the storage system.
+   */
+  public function createDuplicate();
+
+  /**
+   * Returns the info of the type of the entity.
+   *
+   * @see entity_get_info()
+   */
+  public function entityInfo();
+}
+
+/**
+ * Defines a base entity class.
+ *
+ * Default implementation of EntityInterface.
+ *
+ * This class can be used as-is by simple entity types. Entity types requiring
+ * special handling can extend the class.
+ */
+class Entity implements EntityInterface {
+
+  /**
+   * The entity type.
+   *
+   * @var string
+   */
+  protected $entityType;
+
+  /**
+   * Information about the entity's type.
+   *
+   * @var array
+   */
+  protected $entityInfo;
+
+  /**
+   * The entity ID key.
+   *
+   * @var string
+   */
+  protected $idKey;
+
+  /**
+   * The entity bundle key.
+   *
+   * @var string
+   */
+  protected $bundleKey;
+
+  /**
+   * Constructs a new entity object.
+   */
+  public function __construct(array $values = array(), $entity_type) {
+    $this->entityType = $entity_type;
+    $this->setUp();
+    // Set initial values.
+    foreach ($values as $key => $value) {
+      $this->$key = $value;
+    }
+  }
+
+  /**
+   * Sets up the object instance on construction or unserialization.
+   */
+  protected function setUp() {
+    $this->entityInfo = entity_get_info($this->entityType);
+    $this->idKey = $this->entityInfo['entity keys']['id'];
+    $this->bundleKey = isset($this->entityInfo['entity keys']['bundle']) ? $this->entityInfo['entity keys']['bundle'] : NULL;
+  }
+
+  /**
+   * Implements EntityInterface::id().
+   */
+  public function id() {
+    return isset($this->{$this->idKey}) ? $this->{$this->idKey} : NULL;
+  }
+
+  /**
+   * Implements EntityInterface::isNew().
+   */
+  public function isNew() {
+    // We support creating entities with pre-defined IDs to ease migrations.
+    // For that the "is_new" property may be set to TRUE.
+    return !empty($this->is_new) || empty($this->{$this->idKey});
+  }
+
+  /**
+   * Implements EntityInterface::entityType().
+   */
+  public function entityType() {
+    return $this->entityType;
+  }
+
+  /**
+   * Implements EntityInterface::bundle().
+   */
+  public function bundle() {
+    return isset($this->bundleKey) ? $this->{$this->bundleKey} : $this->entityType;
+  }
+
+  /**
+   * Implements EntityInterface::label().
+   *
+   * @see entity_label()
+   */
+  public function label() {
+    $label = FALSE;
+    if (isset($this->entityInfo['label callback']) && function_exists($this->entityInfo['label callback'])) {
+      $label = $this->entityInfo['label callback']($this->entityType, $this);
+    }
+    elseif (!empty($this->entityInfo['entity keys']['label']) && isset($this->{$this->entityInfo['entity keys']['label']})) {
+      $label = $this->{$this->entityInfo['entity keys']['label']};
+    }
+    return $label;
+  }
+
+  /**
+   * Implements EntityInterface::uri().
+   *
+   * @see entity_uri()
+   */
+  public function uri() {
+    $bundle = $this->bundle();
+    // A bundle-specific callback takes precedence over the generic one for the
+    // entity type.
+    if (isset($this->entityInfo['bundles'][$bundle]['uri callback'])) {
+      $uri_callback = $this->entityInfo['bundles'][$bundle]['uri callback'];
+    }
+    elseif (isset($this->entityInfo['uri callback'])) {
+      $uri_callback = $this->entityInfo['uri callback'];
+    }
+    else {
+      return NULL;
+    }
+
+    // Invoke the callback to get the URI. If there is no callback, return NULL.
+    if (isset($uri_callback) && function_exists($uri_callback)) {
+      $uri = $uri_callback($this);
+      // Pass the entity data to url() so that alter functions do not need to
+      // look up this entity again.
+      $uri['options']['entity_type'] = $this->entityType;
+      $uri['options']['entity'] = $this;
+      return $uri;
+    }
+  }
+
+  /**
+   * Implements EntityInterface::save().
+   */
+  public function save() {
+    return entity_get_controller($this->entityType)->save($this);
+  }
+
+  /**
+   * Implements EntityInterface::delete().
+   */
+  public function delete() {
+    if (!$this->isNew()) {
+      entity_get_controller($this->entityType)->delete(array($this->id()));
+    }
+  }
+
+  /**
+   * Implements EntityInterface::createDuplicate().
+   */
+  public function createDuplicate() {
+    $duplicate = clone $this;
+    $duplicate->{$this->idKey} = NULL;
+    return $duplicate;
+  }
+
+  /**
+   * Implements EntityInterface::entityInfo().
+   */
+  public function entityInfo() {
+    return $this->entityInfo;
+  }
+
+  /**
+   * Serializes only what is necessary.
+   *
+   * See @link http://www.php.net/manual/en/language.oop5.magic.php#language.oop5.magic.sleep PHP Magic Methods @endlink.
+   */
+  public function __sleep() {
+    $vars = get_object_vars($this);
+    unset($vars['entityInfo'], $vars['idKey'], $vars['bundleKey']);
+    // Also key the returned array with the variable names so the method may
+    // be easily overridden and customized.
+    return drupal_map_assoc(array_keys($vars));
+  }
+
+  /**
+   * Invokes setUp() on unserialization.
+   *
+   * See @link http://www.php.net/manual/en/language.oop5.magic.php#language.oop5.magic.sleep PHP Magic Methods @endlink
+   */
+  public function __wakeup() {
+    $this->setUp();
+  }
+}
diff --git a/core/modules/entity/entity.controller.inc b/core/modules/entity/entity.controller.inc
index 8a34207..cca1845 100644
--- a/core/modules/entity/entity.controller.inc
+++ b/core/modules/entity/entity.controller.inc
@@ -6,7 +6,7 @@
  */
 
 /**
- * Interface for entity controller classes.
+ * Defines a common interface for entity controller classes.
  *
  * All entity controller classes specified via the 'controller class' key
  * returned by hook_entity_info() or hook_entity_info_alter() have to implement
@@ -19,7 +19,7 @@
 interface DrupalEntityControllerInterface {
 
   /**
-   * Constructor.
+   * Constructs a new DrupalEntityControllerInterface object.
    *
    * @param $entityType
    *   The entity type for which the instance is created.
@@ -50,6 +50,8 @@ interface DrupalEntityControllerInterface {
 }
 
 /**
+ * Defines a base entity controller class.
+ *
  * Default implementation of DrupalEntityControllerInterface.
  *
  * This class can be used as-is by most simple entity types. Entity types
@@ -122,7 +124,9 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
   protected $cache;
 
   /**
-   * Constructor: sets basic variables.
+   * Implements DrupalEntityControllerInterface::__construct().
+   *
+   * Sets basic variables.
    */
   public function __construct($entityType) {
     $this->entityType = $entityType;
@@ -194,11 +198,16 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
     // is set to FALSE (so we load all entities), if there are any ids left to
     // load, if loading a revision, or if $conditions was passed without $ids.
     if ($ids === FALSE || $ids || $revision_id || ($conditions && !$passed_ids)) {
-      // Build the query.
-      $query = $this->buildQuery($ids, $conditions, $revision_id);
-      $queried_entities = $query
-        ->execute()
-        ->fetchAllAssoc($this->idKey);
+      // Build and execute the query.
+      $query_result = $this->buildQuery($ids, $conditions, $revision_id)->execute();
+
+      if (!empty($this->entityInfo['entity class'])) {
+        // We provide the necessary arguments for PDO to create objects of the
+        // specified entity class.
+        // @see EntityInterface::__construct()
+        $query_result->setFetchMode(PDO::FETCH_CLASS, $this->entityInfo['entity class'], array(array(), $this->entityType));
+      }
+      $queried_entities = $query_result->fetchAllAssoc($this->idKey);
     }
 
     // Pass all entities loaded from the database through $this->attachLoad(),
@@ -388,3 +397,169 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
     $this->entityCache += $entities;
   }
 }
+
+/**
+ * Defines a common interface for entity storage controllers.
+ */
+interface EntityStorageControllerInterface extends DrupalEntityControllerInterface {
+
+  /**
+   * Deletes permanently saved entities.
+   *
+   * @param $ids
+   *   An array of entity IDs.
+   *
+   * @throws EntityStorageException
+   *   In case of failures, an exception is thrown.
+   */
+  public function delete($ids);
+
+  /**
+   * Saves the entity permanently.
+   *
+   * @param EntityInterface $entity
+   *   The entity to save.
+   *
+   * @return
+   *   SAVED_NEW or SAVED_UPDATED is returned depending on the operation
+   *   performed.
+   *
+   * @throws EntityStorageException
+   *   In case of failures, an exception is thrown.
+   */
+  public function save(EntityInterface $entity);
+
+}
+
+/**
+ * Defines an exception thrown when storage operations fail.
+ */
+class EntityStorageException extends Exception { }
+
+/**
+ * Implements the entity storage controller interface for the database.
+ */
+class EntityDatabaseStorageController extends DrupalDefaultEntityController implements EntityStorageControllerInterface {
+
+  /**
+   * Implements 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;
+    }
+    $transaction = db_transaction();
+
+    try {
+      $this->preDelete($entities);
+      $ids = array_keys($entities);
+
+      db_delete($this->entityInfo['base table'])
+        ->condition($this->idKey, $ids, 'IN')
+        ->execute();
+      // Reset the cache as soon as the changes have been applied.
+      $this->resetCache($ids);
+
+      $this->postDelete($entities);
+      foreach ($entities as $id => $entity) {
+        $this->invokeHook('delete', $entity);
+      }
+      // Ignore slave server temporarily.
+      db_ignore_slave();
+    }
+    catch (Exception $e) {
+      $transaction->rollback();
+      watchdog_exception($this->entityType, $e);
+      throw new EntityStorageException($e->getMessage, $e->getCode, $e);
+    }
+  }
+
+  /**
+   * Implements EntityStorageControllerInterface::save().
+   */
+  public function save(EntityInterface $entity) {
+    $transaction = db_transaction();
+    try {
+      // Load the stored entity, if any.
+      if (!$entity->isNew() && !isset($entity->original)) {
+        $entity->original = entity_load_unchanged($this->entityType, $entity->id());
+      }
+
+      $this->preSave($entity);
+      $this->invokeHook('presave', $entity);
+
+      if (!$entity->isNew()) {
+        $return = drupal_write_record($this->entityInfo['base table'], $entity, $this->idKey);
+        $this->resetCache(array($entity->{$this->idKey}));
+        $this->postSave($entity);
+        $this->invokeHook('update', $entity);
+      }
+      else {
+        $return = drupal_write_record($this->entityInfo['base table'], $entity);
+        $this->postSave($entity);
+        $this->invokeHook('insert', $entity);
+      }
+
+      // Ignore slave server temporarily.
+      db_ignore_slave();
+      unset($entity->is_new);
+      unset($entity->original);
+
+      return $return;
+    }
+    catch (Exception $e) {
+      $transaction->rollback();
+      watchdog_exception($this->entityType, $e);
+      throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
+    }
+  }
+
+  /**
+   * 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.
+   */
+  protected function postSave(EntityInterface $entity) { }
+
+  /**
+   * 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 $op
+   *   One of 'presave', 'insert', 'update', or 'delete'.
+   * @param $entity
+   *   The entity object.
+   */
+  protected function invokeHook($hook, EntityInterface $entity) {
+    if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $hook)) {
+      $function($this->entityType, $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/entity/entity.info b/core/modules/entity/entity.info
index a26079f..b15fadd 100644
--- a/core/modules/entity/entity.info
+++ b/core/modules/entity/entity.info
@@ -4,7 +4,9 @@ package = Core
 version = VERSION
 core = 8.x
 required = TRUE
+files[] = entity.class.inc
 files[] = entity.query.inc
 files[] = entity.controller.inc
 files[] = tests/entity_crud_hook_test.test
 files[] = tests/entity_query.test
+files[] = tests/entity.test
diff --git a/core/modules/entity/entity.module b/core/modules/entity/entity.module
index 02611e3..2eda87c 100644
--- a/core/modules/entity/entity.module
+++ b/core/modules/entity/entity.module
@@ -173,19 +173,20 @@ function entity_extract_ids($entity_type, $entity) {
  *   2: bundle name of the entity, or NULL if $entity_type has no bundles
  *
  * @return
- *   An entity structure, initialized with the ids provided.
+ *   An entity object, initialized with the IDs provided.
  */
 function entity_create_stub_entity($entity_type, $ids) {
-  $entity = new stdClass();
+  $values = array();
   $info = entity_get_info($entity_type);
-  $entity->{$info['entity keys']['id']} = $ids[0];
+  $values[$info['entity keys']['id']] = $ids[0];
   if (!empty($info['entity keys']['revision']) && isset($ids[1])) {
-    $entity->{$info['entity keys']['revision']} = $ids[1];
+    $values[$info['entity keys']['revision']] = $ids[1];
   }
   if (!empty($info['entity keys']['bundle']) && isset($ids[2])) {
-    $entity->{$info['entity keys']['bundle']} = $ids[2];
+    $values[$info['entity keys']['bundle']] = $ids[2];
   }
-  return $entity;
+  // @todo Once all entities are converted, just rely on entity_create().
+  return isset($info['entity class']) ? entity_create($entity_type, $values) : (object) $values;
 }
 
 /**
@@ -256,6 +257,36 @@ function entity_load_unchanged($entity_type, $id) {
 }
 
 /**
+ * Deletes multiple entities permanently.
+ *
+ * @param $entity_type
+ *   The type of the entity.
+ * @param $ids
+ *   An array of entity IDs of the entities to delete.
+ */
+function entity_delete_multiple($entity_type, $ids) {
+  entity_get_controller($entity_type)->delete($ids);
+}
+
+/**
+ * Constructs a new entity object, without saving it to the database.
+ *
+ * @param $entity_type
+ *   The type of the entity.
+ * @param $values
+ *   An array of values to set, keyed by property name. If the entity type has
+ *   bundles the bundle key has to be specified.
+ *
+ * @return EntityInterface
+ *   A new entity object.
+ */
+function entity_create($entity_type, array $values) {
+  $info = entity_get_info($entity_type) + array('entity class' => 'Entity');
+  $class = $info['entity class'];
+  return new $class($values, $entity_type);
+}
+
+/**
  * Gets the entity controller class for an entity type.
  */
 function entity_get_controller($entity_type) {
@@ -321,6 +352,9 @@ function entity_prepare_view($entity_type, $entities) {
  *   An array containing the 'path' and 'options' keys used to build the uri of
  *   the entity, and matching the signature of url(). NULL if the entity has no
  *   uri of its own.
+ *
+ * @todo
+ *   Remove once all entity types are implementing the EntityInterface.
  */
 function entity_uri($entity_type, $entity) {
   $info = entity_get_info($entity_type);
@@ -362,6 +396,9 @@ function entity_uri($entity_type, $entity) {
  *
  * @return
  *   The entity label, or FALSE if not found.
+ *
+ * @todo
+ *   Remove once all entity types are implementing the EntityInterface.
  */
 function entity_label($entity_type, $entity) {
   $label = FALSE;
@@ -439,4 +476,3 @@ function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_st
  * Exception thrown when a malformed entity is passed.
  */
 class EntityMalformedException extends Exception { }
-
diff --git a/core/modules/entity/tests/entity.test b/core/modules/entity/tests/entity.test
new file mode 100644
index 0000000..e7a4b34
--- /dev/null
+++ b/core/modules/entity/tests/entity.test
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Entity CRUD API tests.
+ */
+
+/**
+ * Tests the basic Entity API.
+ */
+class EntityAPITestCase extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Entity CRUD',
+      'description' => 'Tests basic CRUD functionality.',
+      'group' => 'Entity API',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('entity', 'entity_test');
+  }
+
+  /**
+   * Tests basic CRUD functionality of the Entity API.
+   */
+  function testCRUD() {
+    $user1 = $this->drupalCreateUser();
+
+    // Create some test entities.
+    $entity = entity_create('entity_test', array('name' => 'test', 'uid' => $user1->uid));
+    $entity->save();
+    $entity = entity_create('entity_test', array('name' => 'test2', 'uid' => $user1->uid));
+    $entity->save();
+    $entity = entity_create('entity_test', array('name' => 'test', 'uid' => NULL));
+    $entity->save();
+
+    $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test')));
+
+    $this->assertEqual($entities[0]->name, 'test', 'Created and loaded entity.');
+    $this->assertEqual($entities[1]->name, 'test', 'Created and loaded entity.');
+
+    // Test loading a single entity.
+    $loaded_entity = entity_test_load($entity->id);
+    $this->assertEqual($loaded_entity->id, $entity->id, 'Loaded a single entity by id.');
+
+    // Test deleting an entity.
+    $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test2')));
+    $entities[0]->delete();
+    $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test2')));
+    $this->assertEqual($entities, array(), 'Entity deleted.');
+
+    // Test updating an entity.
+    $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test')));
+    $entities[0]->name = 'test3';
+    $entities[0]->save();
+    $entity = entity_test_load($entities[0]->id);
+    $this->assertEqual($entity->name, 'test3', 'Entity updated.');
+
+    // Try deleting multiple test entities by deleting all.
+    $ids = array_keys(entity_test_load_multiple(FALSE));
+    entity_test_delete_multiple($ids);
+
+    $all = entity_test_load_multiple(FALSE);
+    $this->assertTrue(empty($all), 'Deleted all entities.');
+  }
+}
diff --git a/core/modules/entity/tests/entity_crud_hook_test.test b/core/modules/entity/tests/entity_crud_hook_test.test
index 3f18fc8..782201e 100644
--- a/core/modules/entity/tests/entity_crud_hook_test.test
+++ b/core/modules/entity/tests/entity_crud_hook_test.test
@@ -66,7 +66,7 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
     node_save($node);
     $nid = $node->nid;
 
-    $comment = (object) array(
+    $comment = entity_create('comment', array(
       'cid' => NULL,
       'pid' => 0,
       'nid' => $nid,
@@ -76,7 +76,7 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
       'changed' => REQUEST_TIME,
       'status' => 1,
       'language' => LANGUAGE_NONE,
-    );
+    ));
     $_SESSION['entity_crud_hook_test'] = array();
     comment_save($comment);
 
diff --git a/core/modules/entity/tests/entity_test.info b/core/modules/entity/tests/entity_test.info
new file mode 100644
index 0000000..ae6006b
--- /dev/null
+++ b/core/modules/entity/tests/entity_test.info
@@ -0,0 +1,7 @@
+name = Entity CRUD test module
+description = Provides entity types based upon the CRUD API.
+package = Testing
+version = VERSION
+core = 8.x
+dependencies[] = entity
+hidden = TRUE
diff --git a/core/modules/entity/tests/entity_test.install b/core/modules/entity/tests/entity_test.install
new file mode 100644
index 0000000..ec2e5bd
--- /dev/null
+++ b/core/modules/entity/tests/entity_test.install
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the entity_test module.
+ */
+
+/**
+ * Implements hook_install().
+ */
+function entity_test_install() {
+  // Auto-create a field for testing.
+  $field = array(
+    'field_name' => 'field_test_text',
+    'type' => 'text',
+    'cardinality' => 1,
+    'translatable' => FALSE,
+  );
+  field_create_field($field);
+
+  $instance = array(
+    'entity_type' => 'entity_test',
+    'field_name' => 'field_test_text',
+    'bundle' => 'entity_test',
+    'label' => 'Test text-field',
+    'widget' => array(
+      'type' => 'text_textfield',
+      'weight' => 0,
+    ),
+  );
+  field_create_instance($instance);
+}
+
+/**
+ * Implements hook_schema().
+ */
+function entity_test_schema() {
+  $schema['entity_test'] = array(
+    'description' => 'Stores entity_test items.',
+    'fields' => array(
+      'id' => array(
+        'type' => 'serial',
+        'not null' => TRUE,
+        'description' => 'Primary Key: Unique entity-test item ID.',
+      ),
+      'name' => array(
+        'description' => 'The name of the test entity.',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'uid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => FALSE,
+        'default' => NULL,
+        'description' => "The {users}.uid of the associated user.",
+      ),
+    ),
+    'indexes' => array(
+      'uid' => array('uid'),
+    ),
+    'foreign keys' => array(
+      'uid' => array('users' => 'uid'),
+    ),
+    'primary key' => array('id'),
+  );
+  return $schema;
+}
diff --git a/core/modules/entity/tests/entity_test.module b/core/modules/entity/tests/entity_test.module
new file mode 100644
index 0000000..6034b06
--- /dev/null
+++ b/core/modules/entity/tests/entity_test.module
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Test module for the entity API providing an entity type for testing.
+ */
+
+/**
+ * Implements hook_entity_info().
+ */
+function entity_test_entity_info() {
+  $return = array(
+    'entity_test' => array(
+      'label' => t('Test entity'),
+      'entity class' => 'Entity',
+      'controller class' => 'EntityDatabaseStorageController',
+      'base table' => 'entity_test',
+      'fieldable' => TRUE,
+      'entity keys' => array(
+        'id' => 'id',
+      ),
+    ),
+  );
+  return $return;
+}
+
+/**
+ * Loads a test entity.
+ *
+ * @param $id
+ *   A test entity ID.
+ * @param $reset
+ *   A boolean indicating that the internal cache should be reset.
+ *
+ * @return Entity
+ *   The loaded entity object, or FALSE if the entity cannot be loaded.
+ */
+function entity_test_load($id, $reset = FALSE) {
+  $result = entity_load('entity_test', array($id), array(), $reset);
+  return reset($result);
+}
+
+/**
+ * Loads multiple test entities based on certain conditions.
+ *
+ * @param $ids
+ *   An array of entity IDs.
+ * @param $conditions
+ *   An array of conditions to match against the {entity} table.
+ * @param $reset
+ *   A boolean indicating that the internal cache should be reset.
+ *
+ * @return
+ *   An array of test entity objects, indexed by ID.
+ */
+function entity_test_load_multiple($ids = array(), $conditions = array(), $reset = FALSE) {
+  return entity_load('entity_test', $ids, $conditions, $reset);
+}
+
+/**
+ * Deletes multiple test entities.
+ *
+ * @param $ids
+ *   An array of test entity IDs.
+ */
+function entity_test_delete_multiple(array $ids) {
+  entity_get_controller('entity_test')->delete($ids);
+}
diff --git a/core/modules/user/user.test b/core/modules/user/user.test
index 66c4903..39d90d1 100644
--- a/core/modules/user/user.test
+++ b/core/modules/user/user.test
@@ -732,7 +732,7 @@ class UserCancelTestCase extends DrupalWebTestCase {
     $this->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));
     $this->drupalPost(NULL, array(), t('Save'));
     $this->assertText(t('Your comment has been posted.'));
-    $comments = comment_load_multiple(array(), array('subject' => $edit['subject']));
+    $comments = comment_load_multiple(FALSE, array('subject' => $edit['subject']));
     $comment = reset($comments);
     $this->assertTrue($comment->cid, t('Comment found.'));
 
