diff --git a/modules/comment/comment.module b/modules/comment/comment.module
index 37a208f..44a6cdc 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 EntityDefaultStorageController class, adding required
+ * special handling for comment entities.
  */
-class CommentController extends DrupalDefaultEntityController {
+class CommentController extends EntityDefaultStorageController {
 
   protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
     $query = parent::buildQuery($ids, $conditions, $revision_id);
@@ -1691,6 +1528,117 @@ 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 invokeHookPreSave() {
+    global $user;
+
+    if (!isset($this->status)) {
+      $this->status = user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED;
+    }
+    // Make sure we have a proper bundle name.
+    if (!isset($this->node_type) || $this->node_type == 'comment') {
+      $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();
+    }
+
+    parent::invokeHookPreSave();
+  }
+
+  public function invokeHookInsert() {
+    // Update the {node_comment_statistics} table prior to executing the hook.
+    _comment_update_node_statistics($this->nid);
+    parent::invokeHookInsert();
+    if ($this->status == COMMENT_PUBLISHED) {
+      module_invoke_all('comment_publish', $this);
+    }
+  }
+
+  public function invokeHookUpdate() {
+    // Update the {node_comment_statistics} table prior to executing the hook.
+    _comment_update_node_statistics($this->nid);
+    parent::invokeHookUpdate();
+    if ($this->status == COMMENT_PUBLISHED) {
+      module_invoke_all('comment_publish', $this);
+    }
+  }
+
+  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 +1767,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 +2064,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 e5cae5e..246972a 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..ce34bb3
--- /dev/null
+++ b/modules/entity/entity.class.inc
@@ -0,0 +1,342 @@
+<?php
+
+/**
+ * @file
+ * Provides an interface and a base class for entities.
+ */
+
+/**
+ * Interface for all entity objects.
+ */
+interface EntityInterface {
+
+  /**
+   * Creates 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, i.e. the entities machine name or numeric id.
+   *
+   * @return
+   *   The identifier of the entity. In case the entity has no identifier yet,
+   *   it returns NULL.
+   */
+  public function id();
+
+  /**
+   * Returns whether the entity is new; i.e. whether it has been already 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 UUID of the entity.
+   *
+   * @return
+   *   The UUID of the entity, or NULL if the entity type does not provide
+   *   UUIDs.
+   */
+  public function uuid();
+
+  /**
+   * Returns the UUID of the entity's revision.
+   *
+   * @return
+   *   The UUID of the entity's revision, or NULL if the entity type does not
+   *   make use of revisions.
+   */
+  public function revisionUuid();
+
+  /**
+   * 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();
+
+  /**
+   * Returns the value of an entity property.
+   *
+   * @param $property_name
+   *   The name of the property to return; e.g., 'title'.
+   * @param $language
+   *   (optional) In case the property is translatable, the language object of
+   *   the language that should be used for getting the property. If set to
+   *   NULL, the default language is being used.
+   * @todo
+   *   Which default language should be used.
+   *
+   * @return
+   *   The property value, or NULL in case it is not defined.
+   */
+  public function get($property_name, $language = NULL);
+
+  /**
+   * Sets the value of an entity property.
+   *
+   * @param $property_name
+   *   The name of the property to set; e.g., 'title'.
+   * @param $value
+   *   The value to set, or NULL to unset the property.
+   * @param $language
+   *   (optional) In case the property is translatable, the language object of
+   *   the language that should be used for getting the property. If set to
+   *   NULL, the default language is being used.
+   * @todo
+   *   Which default language should be used.
+   */
+  public function set($property_name, $value, $language = NULL);
+
+  /**
+   * Saves an entity permanently.
+   *
+   * @throws EntityStorageException
+   *   In case of failures an exception is thrown.
+   *
+   * @return
+   *   Either SAVED_NEW or SAVED_UPDATED is returned, depending on the operation
+   *   performed.
+   */
+  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();
+
+  /**
+   * @todo Add invokeHook*() methods.
+   */
+}
+
+/**
+ * Exception thrown when storage operations fail.
+ */
+class EntityStorageException extends Exception { }
+
+/**
+ * A class implementing the 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 {
+
+  protected $entityType;
+
+
+  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;
+    }
+    // Apply defaults.
+    if (!empty($this->bundleKey) && !isset($this->{$this->bundleKey})) {
+      $this->{$this->bundleKey} = $this->entityType;
+    }
+  }
+
+  /**
+   * 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'];
+    $this->bundleKey = isset($this->entityInfo['entity keys']['bundle']) ? $this->entityInfo['entity keys']['bundle'] : NULL;
+  }
+
+  public function id() {
+    return isset($this->{$this->idKey}) ? $this->{$this->idKey} : NULL;
+  }
+
+  public function isNew() {
+    return empty($this->{$this->idKey});
+  }
+
+  public function entityType() {
+    return $this->entityType;
+  }
+
+  public function bundle() {
+    return isset($this->bundleKey) ? $this->{$this->bundleKey} : $this->entityType;
+  }
+
+  public function uuid() {
+    return NULL;
+  }
+
+  public function revisionUuid() {
+    return NULL;
+  }
+
+  public function label() {
+    return entity_label($this->entityType, $this);
+  }
+
+  public function uri() {
+    return entity_uri($this->entityType, $this);
+  }
+
+  /**
+   * @todo
+   *   Implement default handling of language.
+   */
+  public function get($property_name, $language = NULL) {
+    $value = isset($this->$property_name) ? $this->$property_name : NULL;
+    if (isset($language)) {
+      return isset($value[$language]) ? $value[$language] : NULL;
+    }
+    return $value;
+  }
+
+  /**
+   * @todo
+   *   Implement default handling of language.
+   */
+  public function set($property_name, $value, $language = NULL) {
+    if (isset($language)) {
+      $this->$property_name[$language] = $value;
+    }
+    else {
+      $this->$property_name = $value;
+    }
+  }
+
+  public function save() {
+    return entity_get_controller($this->entityType)->save($this);
+  }
+
+  public function delete() {
+    if (!$this->isNew()) {
+      entity_get_controller($this->entityType)->delete(array($this->id()));
+    }
+  }
+
+  public function createDuplicate() {
+    $duplicate = clone $this;
+    $duplicate->{$this->idKey} = NULL;
+    return $duplicate;
+  }
+
+  public function entityInfo() {
+    return $this->entityInfo;
+  }
+
+  /**
+   * Magic method to only serialize what is necessary.
+   */
+  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));
+  }
+
+  /**
+   * 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 ($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 ($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 ($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 ($this->entityInfo['fieldable']) {
+      field_attach_delete($this->entityType, $this);
+    }
+    module_invoke_all($this->entityType . '_delete', $this);
+    module_invoke_all('entity_delete', $this, $this->entityType);
+  }
+}
+
diff --git a/modules/entity/entity.controller.inc b/modules/entity/entity.controller.inc
index 8327bc6..6cd13d3 100644
--- a/modules/entity/entity.controller.inc
+++ b/modules/entity/entity.controller.inc
@@ -194,11 +194,13 @@ 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'])) {
+        $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 +390,122 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
     $this->entityCache += $entities;
   }
 }
+
+/**
+ * Interface for entity storage controllers.
+ */
+interface EntityAPIStorageControllerInterface 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 EntityInterface $entity
+   *   The entity to save.
+   *
+   * @return
+   *   SAVED_NEW or SAVED_UPDATED is returned depending on the operation
+   *   performed.
+   */
+  public function save(EntityInterface $entity);
+
+}
+
+/**
+ * A controller implementing EntityAPIStorageControllerInterface for the database.
+ */
+class EntityDefaultStorageController extends DrupalDefaultEntityController implements EntityAPIStorageControllerInterface {
+
+  /**
+   * Implements EntityAPIStorageControllerInterface.
+   *
+   * @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 EntityAPIStorageControllerInterface.
+   *
+   * @param $transaction
+   *   Optionally a DatabaseTransaction object to use. Allows overrides to pass
+   *   in their transaction object.
+   */
+  public function save(EntityInterface $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;
+    }
+  }
+}
+
diff --git a/modules/entity/entity.info b/modules/entity/entity.info
index 31eb720..b15fadd 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 cbde1fe..1bffee0 100644
--- a/modules/entity/entity.module
+++ b/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,38 @@ function entity_load_unchanged($entity_type, $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');
+  if ($class = $info['entity class']) {
+    return new $class($values, $entity_type);
+  }
+  throw new Exception("Entity class is missing");
+}
+
+/**
  * Gets the entity controller class for an entity type.
  */
 function entity_get_controller($entity_type) {
diff --git a/modules/entity/tests/entity.test b/modules/entity/tests/entity.test
new file mode 100644
index 0000000..c435541
--- /dev/null
+++ b/modules/entity/tests/entity.test
@@ -0,0 +1,69 @@
+<?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 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/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..ec2e5bd
--- /dev/null
+++ b/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/modules/entity/tests/entity_test.module b/modules/entity/tests/entity_test.module
new file mode 100644
index 0000000..2a24a18
--- /dev/null
+++ b/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' => 'EntityDefaultStorageController',
+      'base table' => 'entity_test',
+      'fieldable' => TRUE,
+      'entity keys' => array(
+        'id' => 'id',
+      ),
+    ),
+  );
+  return $return;
+}
+
+/**
+ * Load 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.
+ */
+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/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.'));
 
