diff --git a/modules/comment/comment.module b/modules/comment/comment.module
index 393318a..a6c51de 100644
--- a/modules/comment/comment.module
+++ b/modules/comment/comment.module
@@ -99,6 +99,7 @@ function comment_entity_info() {
       'uri callback' => 'comment_uri',
       'fieldable' => TRUE,
       'controller class' => 'CommentController',
+      'entity class' => 'Comment',
       'entity keys' => array(
         'id' => 'cid',
         'bundle' => 'node_type',
@@ -735,7 +736,7 @@ 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));
+    $build = drupal_get_form("comment_node_{$node->type}_form", entity_create('comment', array('nid' => $node->nid)));
     $additions['comment_form'] = $build;
   }
 
@@ -1435,147 +1436,7 @@ function comment_access($op, $comment) {
  *   A comment object.
  */
 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 = int2vancode(vancode2int($max) + 1) . '/';
-      }
-      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 . '.' . int2vancode(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 . '.' . int2vancode(vancode2int($last) + 1) . '/';
-        }
-      }
-
-      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();
 }
 
 /**
@@ -1595,31 +1456,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);
 }
 
 /**
@@ -1662,10 +1499,10 @@ function comment_load($cid) {
 /**
  * Controller class for comments.
  *
- * This extends the DrupalDefaultEntityController class, adding required
- * special handling for comment objects.
+ * This extends the EntityAPIController class, adding required pecial handling
+ * for comment objects.
  */
-class CommentController extends DrupalDefaultEntityController {
+class CommentController extends EntityAPIController {
 
   protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
     $query = parent::buildQuery($ids, $conditions, $revision_id);
@@ -1691,6 +1528,128 @@ class CommentController extends DrupalDefaultEntityController {
 }
 
 /**
+ * Entity class for comments.
+ */
+class Comment extends Entity {
+
+  public $cid, $pid;
+  public $uid = 0;
+  public $language = LANGUAGE_NONE;
+  public $mail, $homepage, $subject, $name = '';
+
+
+  public function __construct(array $values = array(), $entityType = NULL) {
+    return parent::__construct($values, 'comment');
+  }
+
+  public function save(DatabaseTransaction $transaction = NULL) {
+    global $user;
+    $transaction = isset($transaction) ? $transaction : db_transaction();
+
+    try {
+      if (!isset($this->status)) {
+        $this->status = user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED;
+      }
+      // Make sure we have a bundle name.
+      if (!isset($this->node_type)) {
+        $node = node_load($this->nid);
+        $this->node_type = 'comment_node_' . $node->type;
+      }
+      if (!$this->cid) {
+        // Add the comment to database. This next section builds the thread field.
+        // Also see the documentation for comment_view().
+        if (!empty($this->thread)) {
+          // Allow calling code to set thread itself.
+          $thread = $this->thread;
+        }
+        elseif ($this->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' => $this->nid))->fetchField();
+          // Strip the "/" from the end of the thread.
+          $max = rtrim($max, '/');
+          // Finally, build the thread field for this new comment.
+          $thread = int2vancode(vancode2int($max) + 1) . '/';
+        }
+        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($this->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' => $this->nid,
+          ))->fetchField();
+
+          if ($max == '') {
+            // First child of this parent.
+            $thread = $parent->thread . '.' . int2vancode(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 . '.' . int2vancode(vancode2int($last) + 1) . '/';
+          }
+        }
+        if (empty($this->created)) {
+          $this->created = REQUEST_TIME;
+        }
+        if (empty($this->changed)) {
+          $this->changed = $this->created;
+        }
+        if ($this->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well.
+          $this->name = $user->name;
+        }
+        // Add the values which aren't passed into the function.
+        $this->thread = $thread;
+        $this->hostname = ip_address();
+      }
+
+      $return = parent::save($transaction);
+
+      if ($this->status == COMMENT_PUBLISHED) {
+        module_invoke_all('comment_publish', $this);
+      }
+      return $return;
+    }
+    catch (Exception $e) {
+      $transaction->rollback('comment');
+      watchdog_exception('comment', $e);
+      throw $e;
+    }
+  }
+
+  public function invokeHookInsert() {
+    // Update the {node_comment_statistics} table prior to executing the hook.
+    _comment_update_node_statistics($this->nid);
+    parent::invokeHookInsert();
+  }
+
+  public function invokeHookUpdate() {
+    // Update the {node_comment_statistics} table prior to executing the hook.
+    _comment_update_node_statistics($this->nid);
+    parent::invokeHookUpdate();
+  }
+
+  public function invokeHookDelete() {
+    parent::invokeHookDelete();
+    // Delete the comment's replies.
+    $child_cids = db_query('SELECT cid FROM {comment} WHERE pid = :cid', array(':cid' => $this->cid))->fetchCol();
+    comment_delete_multiple($child_cids);
+    _comment_update_node_statistics($this->nid);
+  }
+}
+
+/**
  * Get number of new comments for current user and specified node.
  *
  * @param $nid
@@ -1819,22 +1778,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 {
@@ -2132,12 +2075,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';
   }
diff --git a/modules/comment/comment.pages.inc b/modules/comment/comment.pages.inc
index 7e88bff..b176105 100644
--- a/modules/comment/comment.pages.inc
+++ b/modules/comment/comment.pages.inc
@@ -35,7 +35,7 @@ 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));
+      $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", entity_create('comment', array('pid' => $pid, 'nid' => $node->nid)));
     }
     else {
       drupal_set_message(t('You are not authorized to post comments.'), 'error');
@@ -86,8 +86,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/modules/comment/comment.test b/modules/comment/comment.test
index c9478f4..a773da1 100644
--- a/modules/comment/comment.test
+++ b/modules/comment/comment.test
@@ -1,7 +1,7 @@
 <?php
 
 /**
- * @file 
+ * @file
  * Tests for comment.module.
  */
 
@@ -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();
 
@@ -654,7 +654,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,
@@ -665,7 +665,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;
 
@@ -1454,7 +1454,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.'));
@@ -1518,7 +1518,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/modules/entity/entity.class.inc b/modules/entity/entity.class.inc
new file mode 100644
index 0000000..3db3367
--- /dev/null
+++ b/modules/entity/entity.class.inc
@@ -0,0 +1,213 @@
+<?php
+
+/**
+ * @file
+ * Provides a base class for entities.
+ */
+
+/**
+ * A common class for entities.
+ */
+class Entity {
+
+  protected $entityType;
+  protected $entityInfo;
+  protected $idKey;
+
+  /**
+   * Creates a new entity.
+   *
+   * @see entity_create()
+   */
+  public function __construct(array $values = array(), $entityType = NULL) {
+    if (empty($entityType)) {
+      throw new Exception('Cannot created an instance of Entity without a specified entity type.');
+    }
+    $this->entityType = $entityType;
+    $this->setUp();
+    // Set initial values.
+    foreach ($values as $key => $value) {
+      $this->$key = $value;
+    }
+  }
+
+  /**
+   * Set up the object instance on construction or unserializiation.
+   */
+  protected function setUp() {
+    $this->entityInfo = entity_get_info($this->entityType);
+    $this->idKey = $this->entityInfo['entity keys']['id'];
+  }
+
+  /**
+   * Returns the entity identifier.
+   *
+   * @return
+   *   The identifier of the entity.
+   */
+  public function identifier() {
+    return isset($this->{$this->idKey}) ? $this->{$this->idKey} : NULL;
+  }
+
+  /**
+   * Returns the info of the type of the entity.
+   *
+   * @see entity_get_info()
+   */
+  public function entityInfo() {
+    return $this->entityInfo;
+  }
+
+  /**
+   * Returns the type of the entity.
+   */
+  public function entityType() {
+    return $this->entityType;
+  }
+
+  /**
+   * Returns the label of the entity.
+   *
+   * @see entity_label()
+   */
+  public function label() {
+    return entity_label($this->entityType, $this);
+  }
+
+  /**
+   * Returns the uri of the entity just as entity_uri().
+   *
+   * @see entity_uri()
+   */
+  public function uri() {
+    return entity_uri($this->entityType, $this);
+  }
+
+  /**
+   * Saves the entity permanently.
+   *
+   * @see entity_save()
+   */
+  public function save(DatabaseTransaction $transaction = NULL) {
+    return entity_get_controller($this->entityType)->save($this, $transaction);
+  }
+
+  /**
+   * Deletes the entity permanently.
+   *
+   * @see entity_delete()
+   */
+  public function delete() {
+    $id = $this->identifier();
+    if (isset($id)) {
+      entity_get_controller($this->entityType)->delete(array($id));
+    }
+  }
+
+  /**
+   * Generates an array for rendering the entity.
+   *
+   * @see entity_view()
+   */
+  public function view($view_mode = 'full', $langcode = NULL) {
+    return entity_get_controller($this->entityType)->view(array($this), $view_mode, $langcode);
+  }
+
+  /**
+   * Builds a structured array representing the entity's content.
+   *
+   * @see entity_build_content()
+   */
+  public function buildContent($view_mode = 'full', $langcode = NULL, $content = array()) {
+    return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
+  }
+
+  /**
+   * Magic method to only serialize what's necessary.
+   */
+  public function __sleep() {
+    $vars = get_object_vars($this);
+    unset($vars['entityInfo'], $vars['idKey']);
+    // 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));
+  }
+
+  /**
+   * Magic method to invoke setUp() on unserialization.
+   */
+  public function __wakeup() {
+    $this->setUp();
+  }
+
+  /**
+   * Invokes hook_ENTITY_TYPE_presave() on behalf the entity.
+   */
+  public function invokeHookPreSave() {
+    if (!empty($this->entityInfo['fieldable'])) {
+      field_attach_presave($this->entityType, $this);
+    }
+    module_invoke_all($this->entityType . '_presave', $this);
+    module_invoke_all('entity_presave', $this, $this->entityType);
+  }
+
+  /**
+   * Invokes hook_ENTITY_TYPE_insert() on behalf the entity.
+   */
+  public function invokeHookInsert() {
+    if (!empty($this->entityInfo['fieldable'])) {
+      field_attach_insert($this->entityType, $this);
+    }
+    module_invoke_all($this->entityType . '_insert', $this);
+    module_invoke_all('entity_insert', $this, $this->entityType);
+  }
+
+  /**
+   * Invokes hook_ENTITY_TYPE_update() on behalf the entity.
+   */
+  public function invokeHookUpdate() {
+    if (!empty($this->entityInfo['fieldable'])) {
+      field_attach_update($this->entityType, $this);
+    }
+    module_invoke_all($this->entityType . '_update', $this);
+    module_invoke_all('entity_update', $this, $this->entityType);
+  }
+
+  /**
+   * Invokes hook_ENTITY_TYPE_delete() on behalf the entity.
+   */
+  public function invokeHookDelete() {
+    if (!empty($this->entityInfo['fieldable'])) {
+      field_attach_delete($this->entityType, $this);
+    }
+    module_invoke_all($this->entityType . '_delete', $this);
+    module_invoke_all('entity_delete', $this, $this->entityType);
+  }
+
+  /**
+   * Exports the entity.
+   *
+   * @see entity_export()
+   */
+  public function export($prefix = '') {
+    $vars = get_object_vars($this);
+    unset($vars['is_new']);
+    return entity_var_json_export($vars, $prefix);
+  }
+
+  /**
+   * Applies the export on the entity.
+   *
+   * @param $export
+   *   A serialized string in JSON format as produced by the export() method.
+   */
+  public function import($export) {
+    $vars = drupal_json_decode($export);
+    if (!is_array($vars)) {
+      throw new Exception('Invalid export given.');
+    }
+    foreach ($vars as $key => $value) {
+      $this->$key = $value;
+    }
+  }
+}
diff --git a/modules/entity/entity.controller.inc b/modules/entity/entity.controller.inc
index 8327bc6..a49b20a 100644
--- a/modules/entity/entity.controller.inc
+++ b/modules/entity/entity.controller.inc
@@ -388,3 +388,339 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
     $this->entityCache += $entities;
   }
 }
+
+/**
+ * Interface for EntityControllers compatible with the entity API.
+ */
+interface EntityAPIControllerInterface extends DrupalEntityControllerInterface {
+
+  /**
+   * Deletes permanently saved entities.
+   *
+   * In case of failures, an exception is thrown.
+   *
+   * @param $ids
+   *   An array of entity IDs.
+   */
+  public function delete($ids);
+
+  /**
+   * Saves the entity permanently.
+   *
+   * In case of failures, an exception is thrown.
+   *
+   * @param $entity
+   *   The entity to save.
+   *
+   * @return
+   *   SAVED_NEW or SAVED_UPDATED is returned depending on the operation
+   *   performed.
+   */
+  public function save($entity);
+
+  /**
+   * Builds a structured array representing the entity's content.
+   *
+   * The content built for the entity will vary depending on the $view_mode
+   * parameter.
+   *
+   * @param $entity
+   *   An entity object.
+   * @param $view_mode
+   *   View mode, e.g. 'full', 'teaser'...
+   * @param $langcode
+   *   (optional) A language code to use for rendering. Defaults to the global
+   *   content language of the current request.
+   *
+   * @return
+   *   The renderable array.
+   */
+  public function buildContent($entity, $view_mode = 'full', $langcode = NULL);
+
+  /**
+   * Generate an array for rendering the given entities.
+   *
+   * @param $entities
+   *   An array of entities to render.
+   * @param $view_mode
+   *   View mode, e.g. 'full', 'teaser'...
+   * @param $langcode
+   *   (optional) A language code to use for rendering. Defaults to the global
+   *   content language of the current request.
+   *
+   * @return
+   *   The renderable array, keyed by entity name or numeric id.
+   */
+  public function view($entities, $view_mode = 'full', $langcode = NULL);
+}
+
+/**
+ * A controller implementing EntityAPIControllerInterface for the database.
+ */
+class EntityAPIController extends DrupalDefaultEntityController implements EntityAPIControllerInterface {
+
+  protected $cacheComplete = FALSE;
+  protected $bundleKey;
+
+  /**
+   * Overridden.
+   * @see DrupalDefaultEntityController::__construct()
+   */
+  public function __construct($entityType) {
+    parent::__construct($entityType);
+    // If this is the bundle of another entity, set the bundle key.
+    if (isset($this->entityInfo['bundle of'])) {
+      $info = entity_get_info($this->entityInfo['bundle of']);
+      $this->bundleKey = $info['bundle keys']['bundle'];
+    }
+  }
+
+  /**
+   * Builds and executes the query for loading.
+   *
+   * @return The results in a Traversable object.
+   */
+  public function query($ids, $conditions, $revision_id = FALSE) {
+    // Build the query.
+    $query = $this->buildQuery($ids, $conditions, $revision_id);
+    $result = $query->execute();
+    if (!empty($this->entityInfo['entity class'])) {
+      $result->setFetchMode(PDO::FETCH_CLASS, $this->entityInfo['entity class'], array(array(), $this->entityType));
+    }
+    return $result;
+  }
+
+  /**
+   * Overridden.
+   * @see DrupalDefaultEntityController#load($ids, $conditions)
+   *
+   * In contrast to the parent implementation we factor out query execution, so
+   * fetching can be further customized easily.
+   */
+  public function load($ids = array(), $conditions = array()) {
+    $entities = array();
+
+    // Revisions are not statically cached, and require a different query to
+    // other conditions, so separate the revision id into its own variable.
+    if ($this->revisionKey && isset($conditions[$this->revisionKey])) {
+      $revision_id = $conditions[$this->revisionKey];
+      unset($conditions[$this->revisionKey]);
+    }
+    else {
+      $revision_id = FALSE;
+    }
+
+    // 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. The $ids array is reduced as items are loaded from cache,
+    // and we need to know if it's empty for this reason to avoid querying the
+    // database when all requested entities are loaded from cache.
+    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
+
+    // Try to load entities from the static cache.
+    if ($this->cache && !$revision_id) {
+      $entities = $this->cacheGet($ids, $conditions);
+      // If any entities were loaded, remove them from the ids still to load.
+      if ($passed_ids) {
+        $ids = array_keys(array_diff_key($passed_ids, $entities));
+      }
+    }
+
+    // Load any remaining entities from the database. This is the case if $ids
+    // is set to FALSE (so we load all entities), if there are any ids left to
+    // load or if loading a revision.
+    if (!($this->cacheComplete && $ids === FALSE && !$conditions) && ($ids === FALSE || $ids || $revision_id)) {
+      $queried_entities = array();
+      foreach ($this->query($ids, $conditions, $revision_id) as $record) {
+        // Skip entities already retrieved from cache.
+        if (isset($entities[$record->{$this->idKey}])) {
+          continue;
+        }
+
+        // Take care of serialized columns.
+        $schema = drupal_get_schema($this->entityInfo['base table']);
+
+        foreach ($schema['fields'] as $field => $info) {
+          if (!empty($info['serialize']) && isset($record->$field)) {
+            $record->$field = unserialize($record->$field);
+          }
+        }
+        $queried_entities[$record->{$this->idKey}] = $record;
+      }
+    }
+
+    // Pass all entities loaded from the database through $this->attachLoad(),
+    // which attaches fields (if supported by the entity type) and calls the
+    // entity type specific load callback, for example hook_node_load().
+    if (!empty($queried_entities)) {
+      $this->attachLoad($queried_entities, $revision_id);
+      $entities += $queried_entities;
+    }
+
+    if ($this->cache) {
+      // Add entities to the cache if we are not loading a revision.
+      if (!empty($queried_entities) && !$revision_id) {
+        $this->cacheSet($queried_entities);
+
+        // Remember if we have cached all entities now.
+        if (!$conditions && $ids === FALSE) {
+          $this->cacheComplete = TRUE;
+        }
+      }
+    }
+    // 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 && $passed_ids = array_intersect_key($passed_ids, $entities)) {
+      foreach ($passed_ids as $id => $value) {
+        $passed_ids[$id] = $entities[$id];
+      }
+      $entities = $passed_ids;
+    }
+    return $entities;
+  }
+
+  public function resetCache(array $ids = NULL) {
+    $this->cacheComplete = FALSE;
+    parent::resetCache($ids);
+  }
+
+  /**
+   * Implements EntityAPIControllerInterface.
+   *
+   * @param $transaction
+   *   Optionally a DatabaseTransaction object to use. Allows overrides to pass
+   *   in their transaction object.
+   */
+  public function delete($ids, DatabaseTransaction $transaction = NULL) {
+    $entities = $ids ? $this->load($ids) : FALSE;
+    if (!$entities) {
+      // Do nothing, in case invalid or no ids have been passed.
+      return;
+    }
+    $transaction = isset($transaction) ? $transaction : db_transaction();
+
+    try {
+      $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);
+
+      foreach ($entities as $id => $entity) {
+        $entity->invokeHookDelete();
+      }
+      // Ignore slave server temporarily.
+      db_ignore_slave();
+    }
+    catch (Exception $e) {
+      if (isset($transaction)) {
+        $transaction->rollback();
+      }
+      watchdog_exception($this->entityType, $e);
+      throw $e;
+    }
+  }
+
+  /**
+   * Implements EntityAPIControllerInterface.
+   *
+   * @param $transaction
+   *   Optionally a DatabaseTransaction object to use. Allows overrides to pass
+   *   in their transaction object.
+   */
+  public function save($entity, DatabaseTransaction $transaction = NULL) {
+    $transaction = isset($transaction) ? $transaction : db_transaction();
+    try {
+      // Load the stored entity, if any.
+      if (!empty($entity->{$this->idKey}) && !isset($entity->original)) {
+        // In order to properly work in case of name changes, load the original
+        // entity using the id key if it is available.
+        $entity->original = entity_load_unchanged($this->entityType, $entity->{$this->idKey});
+      }
+
+      $entity->invokeHookPreSave();
+
+      if (!empty($entity->{$this->idKey}) && empty($entity->is_new)) {
+        $return = drupal_write_record($this->entityInfo['base table'], $entity, $this->idKey);
+        $this->resetCache(array($entity->{$this->idKey}));
+        $entity->invokeHookUpdate();
+      }
+      else {
+        $return = drupal_write_record($this->entityInfo['base table'], $entity);
+        $entity->invokeHookInsert();
+      }
+      // 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 $e;
+    }
+  }
+
+  /**
+   * Implements EntityAPIControllerInterface.
+   *
+   * @param $content
+   *   Optionally. Allows pre-populating the built content to ease overridding
+   *   this method.
+   */
+  public function buildContent($entity, $view_mode = 'full', $langcode = NULL, $content = array()) {
+    // Remove previously built content, if exists.
+    $entity->content = $content;
+    $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language;
+
+    // Add in fields.
+    if (!empty($this->entityInfo['fieldable'])) {
+      $entity->content += field_attach_view($this->entityType, $entity, $view_mode, $langcode);
+    }
+    // Invoke hook_ENTITY_view() to allow modules to add their additions.
+    module_invoke_all($this->entityType . '_view', $entity, $view_mode, $langcode);
+    module_invoke_all('entity_view', $entity, $this->entityType, $view_mode, $langcode);
+    $build = $entity->content;
+    unset($entity->content);
+    return $build;
+  }
+
+  /**
+   * Implements EntityAPIControllerInterface.
+   */
+  public function view($entities, $view_mode = 'full', $langcode = NULL) {
+    // For Field API and entity_prepare_view, the entities have to be keyed by
+    // (numeric) id.
+    $entities = entity_key_array_by_property($entities, $this->idKey);
+    if (!empty($this->entityInfo['fieldable'])) {
+      field_attach_prepare_view($this->entityType, $entities, $view_mode);
+    }
+    entity_prepare_view($this->entityType, $entities);
+    $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language;
+
+    $view = array();
+    foreach ($entities as $entity) {
+      $build = entity_build_content($this->entityType, $entity, $view_mode, $langcode);
+      $build += array(
+        // If the entity type provides an implementation, use this instead the
+        // generic one.
+        // @see template_preprocess_entity()
+        '#theme' => 'entity',
+        '#entity_type' => $this->entityType,
+        '#entity' => $entity,
+        '#view_mode' => $view_mode,
+        '#language' => $langcode,
+      );
+      // Allow modules to modify the structured entity.
+      drupal_alter(array($this->entityType . '_view', 'entity_view'), $build, $this->entityType);
+      $key = isset($entity->{$this->idKey}) ? $entity->{$this->idKey} : NULL;
+      $view[$this->entityType][$key] = $build;
+    }
+    return $view;
+  }
+}
+
diff --git a/modules/entity/entity.info b/modules/entity/entity.info
index 3f4591e..07e8d37 100644
--- a/modules/entity/entity.info
+++ b/modules/entity/entity.info
@@ -4,5 +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/modules/entity/entity.module b/modules/entity/entity.module
index 5f95a8d..d0d779d 100644
--- a/modules/entity/entity.module
+++ b/modules/entity/entity.module
@@ -149,19 +149,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;
 }
 
 /**
@@ -232,6 +233,155 @@ function entity_load_unchanged($entity_type, $id) {
 }
 
 /**
+ * Saves an entity permanently.
+ *
+ * In case of failures, an exception is thrown.
+ *
+ * @param $entity
+ *   The entity to save.
+ *
+ * @return
+ *   Either SAVED_NEW or SAVED_UPDATED is returned, depending on the operation
+ *   performed.
+ */
+function entity_save($entity) {
+  return $entity->save();
+}
+
+/**
+ * Deletes an entity permanently.
+ *
+ * In case of failures, an exception is thrown.
+ *
+ * @param $entity_type
+ *   The type of the entity.
+ * @param $id
+ *   The identifier of the entity to delete.
+ */
+function entity_delete($entity_type, $id) {
+  entity_get_controller($entity_type)->delete(array($id));
+}
+
+/**
+ * Permanently delete multiple entities.
+ *
+ * @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);
+}
+
+/**
+ * Creates a new entity object.
+ *
+ * @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 Entity
+ *   A new entity object.
+ */
+function entity_create($entity_type, array $values) {
+  $info = entity_get_info($entity_type) + array('entity class' => 'Entity');
+  // Add is_new property if it is not set.
+  $values += array('is_new' => TRUE);
+  if ($class = $info['entity class']) {
+    return new $class($values, $entity_type);
+  }
+  return (object) $values;
+}
+
+/**
+ * Exports an entity.
+ *
+ * @param $entity
+ *   The entity to export.
+ * @param $prefix
+ *   (optional) A prefix for each line.
+ *
+ * @return
+ *   The exported entity as serialized string. The concrete format may be
+ *   customized by the entity type, while it is JSON by default. The output is
+ *   suitable for use with entity_import().
+ */
+function entity_export($entity, $prefix = '') {
+  return $entity->export($prefix);
+}
+
+/**
+ * Imports an entity.
+ *
+ * For persisting a newly imported entity use entity_save(). In case of
+ * failures, an exception is thrown.
+ *
+ * @param $entity_type
+ *   The type of the entity.
+ * @param string $export
+ *   The string containing the exported entity as produced by entity_export().
+ *
+ * @return Entity
+ *   The imported entity object.
+ */
+function entity_import($entity_type, $export) {
+  $entity = entity_create($entity_type, array());
+  $entity->import($export);
+  return $entity;
+}
+
+/**
+ * Builds a structured array representing the entity's content.
+ *
+ * The content built for the entity will vary depending on the $view_mode
+ * parameter.
+ *
+ * @param $entity_type
+ *   The type of the entity.
+ * @param $entity
+ *   An entity object.
+ * @param $view_mode
+ *   A view mode as used by this entity type, e.g. 'full', 'teaser'...
+ * @param $langcode
+ *   (optional) A language code to use for rendering. Defaults to the global
+ *   content language of the current request.
+ *
+ * @return array
+ *   The renderable array.
+ */
+function entity_build_content($entity_type, $entity, $view_mode = 'full', $langcode = NULL) {
+  return entity_get_controller($entity_type)->buildContent($entity, $view_mode, $langcode);
+}
+
+/**
+ * Generates an array for rendering the given entities.
+ *
+ * Entities being viewed, are generally expected to be fully-loaded entity
+ * objects as returned by entity_load(). However, it is possible to view a
+ * single entity without an id, e.g. for generating a preview during
+ * creation.
+ *
+ * @param $entity_type
+ *   The type of the entity.
+ * @param $entities
+ *   An array of entities to render.
+ * @param $view_mode
+ *   A view mode as used by this entity type, e.g. 'full', 'teaser'...
+ * @param $langcode
+ *   (optional) A language code to use for rendering. Defaults to the global
+ *   content language of the current request.
+ *
+ * @return
+ *   The renderable array, keyed by id.
+ */
+function entity_view($entity_type, $entities, $view_mode = 'full', $langcode = NULL) {
+  return entity_get_controller($entity_type)->view($entities, $view_mode, $langcode);
+}
+
+/**
  * Gets the entity controller class for an entity type.
  */
 function entity_get_controller($entity_type) {
@@ -424,3 +574,120 @@ function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_st
     field_attach_submit($entity_type, $entity, $form, $form_state);
   }
 }
+
+/**
+ * Converts an array of entities to be keyed by the values of a given property.
+ *
+ * @param array $entities
+ *   The array of entities to convert.
+ * @param $property
+ *   The name of entity property, by which the array should be keyed. To get
+ *   reasonable results, the property has to have unique values.
+ *
+ * @return array
+ *   The same entities in the same order, but keyed by their $property values.
+ */
+function entity_key_array_by_property(array $entities, $property) {
+  $ret = array();
+  foreach ($entities as $entity) {
+    $key = isset($entity->$property) ? $entity->$property : NULL;
+    $ret[$key] = $entity;
+  }
+  return $ret;
+}
+
+/**
+ * Export a variable in pretty formatted JSON.
+ *
+ * @param $var
+ *   The variable to export.
+ * @param $prefix
+ *   (optional) A prefix that will be added at the begining of every lines of
+ *   the output.
+ *
+ * @return
+ *   The variable exported in JSON.
+ */
+function entity_var_json_export($var, $prefix = '') {
+  if (is_array($var) && $var) {
+    // Defines whether we use a JSON array or object.
+    $use_array = ($var == array_values($var));
+    $output = $use_array ? "[" : "{";
+
+    foreach ($var as $key => $value) {
+      if ($use_array) {
+        $values[] = entity_var_json_export($value, '  ');
+      }
+      else {
+        $values[] = entity_var_json_export((string) $key, '  ') . ' : ' . entity_var_json_export($value, '  ');
+      }
+    }
+    // Use several lines for long content. However for objects with a single
+    // entry keep the key in the first line.
+    if (strlen($content = implode(", ", $values)) > 70 && ($use_array || count($values) > 1)) {
+      $output .= "\n  " .  implode(",\n  ", $values) . "\n";
+    }
+    elseif (strpos($content, "\n") !== FALSE) {
+      $output .= " " .  $content . "\n";
+    }
+    else {
+      $output .= " " .  $content . " ";
+    }
+    $output .= $use_array ? "]" : "}";
+  }
+  else {
+    $output = drupal_json_encode($var);
+  }
+
+  if ($prefix) {
+    $output = str_replace("\n", "\n$prefix", $output);
+  }
+  return $output;
+}
+
+/**
+ * Processes variables for entity.tpl.php.
+ */
+function template_preprocess_entity(&$variables) {
+  $variables['view_mode'] = $variables['elements']['#view_mode'];
+  $entity_type = $variables['elements']['#entity_type'];
+  $variables['entity_type'] = $entity_type;
+  $entity = $variables['elements']['#entity'];
+  $variables[$variables['elements']['#entity_type']] = $entity;
+  $info = entity_get_info($entity_type);
+
+  $variables['title'] = check_plain(entity_label($entity_type, $entity));
+  $uri = entity_uri($entity_type, $entity);
+  $variables['url'] = $uri ? url($uri['path'], $uri['options']) : FALSE;
+  $variables['page'] = $uri && $uri['path'] == $_GET['q'];
+
+  // Helpful $content variable for templates.
+  $variables['content'] = array();
+  foreach (element_children($variables['elements']) as $key) {
+    $variables['content'][$key] = $variables['elements'][$key];
+  }
+
+  if (!empty($info['fieldable'])) {
+    // Make the field variables available with the appropriate language.
+    field_attach_preprocess($entity_type, $entity, $variables['content'], $variables);
+  }
+  list(, , $bundle) = entity_extract_ids($entity_type, $entity);
+
+  // Gather css classes.
+  $variables['classes_array'][] = drupal_html_class('entity-' . $entity_type);
+  $variables['classes_array'][] = drupal_html_class($entity_type . '-' . $bundle);
+
+  // Add RDF type and about URI.
+  if (module_exists('rdf')) {
+    $variables['attributes_array']['about'] = empty($uri['path']) ? NULL: url($uri['path']);
+    $variables['attributes_array']['typeof'] = empty($entity->rdf_mapping['rdftype']) ? NULL : $entity->rdf_mapping['rdftype'];
+  }
+
+  // Add suggestions.
+  $variables['theme_hook_suggestions'][] = $entity_type;
+  $variables['theme_hook_suggestions'][] = $entity_type . '__' . $bundle;
+  $variables['theme_hook_suggestions'][] = $entity_type . '__' . $bundle . '__' . $variables['view_mode'];
+  if ($id = $entity->identifier()) {
+    $variables['theme_hook_suggestions'][] = $entity_type . '__' . $id;
+  }
+}
diff --git a/modules/entity/tests/entity.test b/modules/entity/tests/entity.test
new file mode 100644
index 0000000..befe386
--- /dev/null
+++ b/modules/entity/tests/entity.test
@@ -0,0 +1,123 @@
+<?php
+
+/**
+ * @file
+ * Entity CRUD API tests.
+ */
+
+/**
+ * Test basic API.
+ */
+class EntityAPITestCase extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Entity CRUD',
+      'description' => 'Tests basic CRUD API functionality.',
+      'group' => 'Entity API',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('entity', 'entity_test');
+  }
+
+  /**
+   * Tests CRUD by making use of the Entity class methods.
+   */
+  function testCRUD() {
+
+    $user1 = $this->drupalCreateUser();
+    // Create test entities for the user1 and unrelated to a user.
+    $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.');
+
+    $results = entity_test_load_multiple(array($entity->pid));
+    $loaded = array_pop($results);
+    $this->assertEqual($loaded->pid, $entity->pid, 'Loaded a single entity by id.');
+
+    $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.');
+
+    $entity->save();
+    $this->assertEqual($entity->pid, $loaded->pid, 'Entity updated.');
+  }
+
+  /**
+   * Tests CRUD API functions: entity_(create|delete|delete_multiple|save)().
+   */
+  function testCRUDAPIfunctions() {
+    $user1 = $this->drupalCreateUser();
+    // Create test entities for the user1 and unrelated to a user.
+    $entity = entity_create('entity_test', array('name' => 'test', 'uid' => $user1->uid));
+    entity_save($entity);
+    $entity = entity_create('entity_test', array('name' => 'test2', 'uid' => $user1->uid));
+    entity_save($entity);
+    $entity = entity_create('entity_test', array('name' => 'test', 'uid' => NULL));
+    entity_save($entity);
+
+    $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.');
+
+    $results = entity_test_load_multiple(array($entity->pid));
+    $loaded = array_pop($results);
+    $this->assertEqual($loaded->pid, $entity->pid, 'Loaded a single entity by id.');
+
+    $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test2')));
+
+    entity_delete('entity_test', $entities[0]->pid);
+    $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test2')));
+    $this->assertEqual($entities, array(), 'Entity deleted.');
+
+    entity_save($entity);
+    $this->assertEqual($entity->pid, $loaded->pid, 'Entity updated.');
+
+    // Try deleting multiple test entities by deleting all.
+    $pids = array_keys(entity_test_load_multiple(FALSE));
+    entity_test_delete_multiple($pids);
+
+    $all = entity_test_load_multiple(FALSE);
+    $this->assertTrue(empty($all), 'Deleted all entities.');
+  }
+
+  /**
+   * Tests viewing entites.
+   */
+  function testEntityView() {
+    $user1 = $this->drupalCreateUser();
+    // Create test entities for the user1 and unrelated to a user.
+    $entity = entity_create('entity_test', array('name' => 'test', 'uid' => $user1->uid));
+
+    $render = $entity->view();
+    $output = drupal_render($render);
+    // The entity class adds the user name to the output. Verify it is there.
+    $this->assertTrue(strpos($output, format_username($user1)) !== FALSE, 'Entity has been rendered');
+  }
+
+  /**
+   * Tests exporting and importing an entity.
+   */
+  function testExport() {
+    // Test exporting an entity to JSON.
+    $entity = entity_create('entity_test', array('name' => 'test', 'uid' => $GLOBALS['user']->uid));
+    $serialized_string = $entity->export();
+    $data = drupal_json_decode($serialized_string);
+    $this->assertNotNull($data, 'Exported entity is valid JSON.');
+
+    $import = entity_import('entity_test', $serialized_string);
+    $this->assertTrue(get_class($import) == get_class($entity) && $entity->name == $import->name, 'Imported entity.');
+  }
+}
+
diff --git a/modules/entity/tests/entity_crud_hook_test.test b/modules/entity/tests/entity_crud_hook_test.test
index 3f18fc8..782201e 100644
--- a/modules/entity/tests/entity_crud_hook_test.test
+++ b/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/modules/entity/tests/entity_test.info b/modules/entity/tests/entity_test.info
new file mode 100644
index 0000000..ae6006b
--- /dev/null
+++ b/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/modules/entity/tests/entity_test.install b/modules/entity/tests/entity_test.install
new file mode 100644
index 0000000..841ad53
--- /dev/null
+++ b/modules/entity/tests/entity_test.install
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the entity_test module.
+ */
+
+/**
+ * Implements hook_install().
+ */
+function entity_test_install() {
+  // Auto-create a field for the full name.
+  $field = array(
+    'field_name' => 'entity_test_fullname',
+    'type' => 'text',
+    'cardinality' => 1,
+    'translatable' => FALSE,
+  );
+  field_create_field($field);
+
+  $instance = array(
+    'entity_type' => 'entity_test',
+    'field_name' => 'entity_test_fullname',
+    'bundle' => 'entity_test',
+    'label' => 'Full name',
+    'description' => 'Specify your first and last name.',
+    '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(
+      'pid' => 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('pid'),
+  );
+  return $schema;
+}
diff --git a/modules/entity/tests/entity_test.module b/modules/entity/tests/entity_test.module
new file mode 100644
index 0000000..f95e619
--- /dev/null
+++ b/modules/entity/tests/entity_test.module
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Test module for the entity API.
+ */
+
+/**
+ * Implements hook_entity_info().
+ */
+function entity_test_entity_info() {
+  $return = array(
+    'entity_test' => array(
+      'label' => t('Test entity'),
+      'entity class' => 'Entity',
+      'controller class' => 'EntityTestController',
+      'base table' => 'entity_test',
+      'fieldable' => TRUE,
+      'entity keys' => array(
+        'id' => 'pid',
+      ),
+    ),
+  );
+  return $return;
+}
+
+/**
+ * Loads multiple test entities based on certain conditions.
+ *
+ * @param $pids
+ *   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 pid.
+ */
+function entity_test_load_multiple($pids = array(), $conditions = array(), $reset = FALSE) {
+  return entity_load('entity_test', $pids, $conditions, $reset);
+}
+
+/**
+ * Deletes multiple test entities.
+ *
+ * @param $pids
+ *   An array of test entity IDs.
+ */
+function entity_test_delete_multiple(array $pids) {
+  entity_get_controller('entity_test')->delete($pids);
+}
+
+/**
+ * Controller class for test entities.
+ */
+class EntityTestController extends EntityAPIController {
+
+  /**
+   * Overrides buildContent() to add the username to the output.
+   */
+  public function buildContent($entity, $view_mode = 'full', $langcode = NULL) {
+    $content['user'] = array(
+      '#markup' => "User: ". format_username(user_load($entity->uid)),
+    );
+    return parent::buildContent($entity, $view_mode, $langcode, $content);
+  }
+}
+
diff --git a/modules/simpletest/simpletest.info b/modules/simpletest/simpletest.info
index 54b020d..926ade6 100644
--- a/modules/simpletest/simpletest.info
+++ b/modules/simpletest/simpletest.info
@@ -15,8 +15,6 @@ files[] = tests/bootstrap.test
 files[] = tests/cache.test
 files[] = tests/common.test
 files[] = tests/database_test.test
-files[] = tests/entity_crud_hook_test.test
-files[] = tests/entity_query.test
 files[] = tests/error.test
 files[] = tests/file.test
 files[] = tests/filetransfer.test
diff --git a/modules/user/user.test b/modules/user/user.test
index 6ecbfac..edd127f 100644
--- a/modules/user/user.test
+++ b/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.'));
 
