diff --git a/entity.api.php b/entity.api.php
index 45f130d..85d396f 100644
--- a/entity.api.php
+++ b/entity.api.php
@@ -119,6 +119,13 @@
  *   Override the controller class to adapt the defaults and to improve and
  *   complete the generated metadata. Set it to FALSE to disable this feature.
  *   Defaults to the EntityDefaultMetadataController class.
+ * - extra fields controller class: (optional) A controller class for providing
+ *   field API extra fields. Defaults to none.
+ *   The class must implement the EntityExtraFieldsControllerInterface. Display
+ *   extra fields that are exposed that way are rendered by default by the
+ *   EntityAPIController. The EntityDefaultExtraFieldsController class may be
+ *   used to generate extra fields based upon property metadata, which in turn
+ *   get rendered by default by the EntityAPIController.
  * - features controller class: (optional) A controller class for providing
  *   Features module integration for exportable entities. The given class has to
  *   inherit from the default class being EntityDefaultFeaturesController. Set
diff --git a/entity.info.inc b/entity.info.inc
index 1f626cc..b86f167 100644
--- a/entity.info.inc
+++ b/entity.info.inc
@@ -179,3 +179,84 @@ function _entity_metadata_convert_schema_type($type) {
       return 'text';
   }
 }
+
+/**
+ * Interface for extra fields controller.
+ *
+ * Note: Displays extra fields exposed by this controller are rendered by
+ * default by the EntityAPIController.
+ */
+interface EntityExtraFieldsControllerInterface {
+
+  /**
+   * Returns extra fields for this entity type.
+   *
+   * @see hook_field_extra_fields().
+   */
+  public function fieldExtraFields();
+}
+
+/**
+ * Default controller for generating extra fields based on property metadata.
+ *
+ * By default a display extra field for each property not being a field, ID or
+ * bundle is generated.
+ */
+class EntityDefaultExtraFieldsController implements EntityExtraFieldsControllerInterface {
+
+  /**
+   * @var string
+   */
+  protected $entityType;
+
+  /**
+   * @var array
+   */
+  protected $entityInfo;
+
+  /**
+   * Constructor.
+   */
+  public function __construct($type) {
+    $this->entityType = $type;
+    $this->entityInfo = entity_get_info($type);
+    $this->propertyInfo = entity_get_property_info($type);
+  }
+
+  /**
+   * Implements EntityExtraFieldsControllerInterface::fieldExtraFields().
+   */
+  public function fieldExtraFields() {
+    $extra = array();
+    foreach ($this->propertyInfo['properties'] as $name => $property_info) {
+      // Skip adding the ID or bundle.
+      if ($this->entityInfo['entity keys']['id'] == $name || $this->entityInfo['entity keys']['bundle'] == $name) {
+        continue;
+      }
+      $extra[$this->entityType][$this->entityType]['display'][$name] = $this->generateExtraFieldInfo($name, $property_info);
+    }
+
+    // Handle bundle properties.
+    $this->propertyInfo += array('bundles' => array());
+    foreach ($this->propertyInfo['bundles'] as $bundle_name => $info) {
+      foreach ($info['properties'] as $name => $property_info) {
+        $extra[$this->entityType][$bundle_name]['display'][$name] = $this->generateExtraFieldInfo($name, $property_info);
+      }
+    }
+    return $extra;
+  }
+
+  /**
+   * Generates the display field info for a given property.
+   */
+  protected function generateExtraFieldInfo($name, $property_info) {
+    $info = array(
+      'label' => $property_info['label'],
+      'weight' => 0,
+    );
+    if (!empty($property_info['description'])) {
+      $info['description'] = $property_info['description'];
+    }
+    return $info;
+  }
+}
diff --git a/entity.module b/entity.module
index 81ad379..30030b4 100644
--- a/entity.module
+++ b/entity.module
@@ -971,11 +971,18 @@ function entity_theme() {
   return array(
     'entity_status' => array(
       'variables' => array('status' => NULL, 'html' => TRUE),
+      'file' => 'theme/entity.theme.inc',
     ),
     'entity' => array(
       'render element' => 'elements',
       'template' => 'entity',
       'pattern' => $pattern,
+      'path' => drupal_get_path('module', 'entity') . '/theme',
+      'file' => 'entity.theme.inc',
+    ),
+    'entity_property' => array(
+      'render element' => 'elements',
+      'file' => 'theme/entity.theme.inc',
     ),
     'entity_ui_overview_item' => array(
       'variables' => array('label' => NULL, 'entity_type' => NULL, 'url' => FALSE, 'name' => FALSE),
@@ -985,90 +992,6 @@ function entity_theme() {
 }
 
 /**
- * Themes the exportable status of an entity.
- */
-function theme_entity_status($variables) {
-  $status = $variables['status'];
-  $html = $variables['html'];
-  if (($status & ENTITY_FIXED) == ENTITY_FIXED) {
-    $label = t('Fixed');
-    $help = t('The configuration is fixed and cannot be changed.');
-    return $html ? "<span class='entity-status-fixed' title='$help'>" . $label . "</span>" : $label;
-  }
-  elseif (($status & ENTITY_OVERRIDDEN) == ENTITY_OVERRIDDEN) {
-    $label = t('Overridden');
-    $help = t('This configuration is provided by a module, but has been changed.');
-    return $html ? "<span class='entity-status-overridden' title='$help'>" . $label . "</span>" : $label;
-  }
-  elseif ($status & ENTITY_IN_CODE) {
-    $label = t('Default');
-    $help = t('A module provides this configuration.');
-    return $html ? "<span class='entity-status-default' title='$help'>" . $label . "</span>" : $label;
-  }
-  elseif ($status & ENTITY_CUSTOM) {
-    $label = t('Custom');
-    $help = t('A custom configuration by a user.');
-    return $html ? "<span class='entity-status-custom' title='$help'>" . $label . "</span>" : $label;
-  }
-}
-
-/**
- * Process 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;
-
-  if (isset($variables['elements']['#page'])) {
-    // If set by the caller, respect the page property.
-    $variables['page'] = $variables['elements']['#page'];
-  }
-  else {
-    // Else, try to automatically detect it.
-    $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_id($entity_type, $entity)) {
-    $variables['theme_hook_suggestions'][] = $entity_type . '__' . $id;
-  }
-}
-
-/**
  * Label callback that refers to the entity classes label method.
  */
 function entity_class_label($entity) {
@@ -1246,6 +1169,39 @@ function entity_views_api() {
 }
 
 /**
+ * Implements hook_field_extra_fields().
+ */
+function entity_field_extra_fields() {
+  // Invoke specified controllers for entity types provided by the CRUD API.
+  $items = array();
+  foreach (entity_crud_get_info() as $type => $info) {
+    if (!empty($info['extra fields controller class'])) {
+      $items = array_merge_recursive($items, entity_get_extra_fields_controller($type)->fieldExtraFields());
+    }
+  }
+  return $items;
+}
+
+/**
+ * Gets the extra field controller class for a given entity type.
+ *
+ * @return EntityExtraFieldsControllerInterface|false
+ *   The controller for the given entity type or FALSE if none is specified.
+ */
+function entity_get_extra_fields_controller($type = NULL) {
+  $static = &drupal_static(__FUNCTION__);
+
+  if (!isset($static[$type])) {
+    $static[$type] = FALSE;
+    $info = entity_get_info($type);
+    if (!empty($info['extra fields controller class'])) {
+      $static[$type] = new $info['extra fields controller class']($type);
+    }
+  }
+  return $static[$type];
+}
+
+/**
  * Returns a property wrapper for the given data.
  *
  * If an entity is wrapped, the wrapper can be used to retrieve further wrappers
diff --git a/entity.tpl.php b/entity.tpl.php
deleted file mode 100644
index cfe94dd..0000000
--- a/entity.tpl.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-/**
- * @file
- * Default theme implementation for entities.
- *
- * Available variables:
- * - $content: An array of comment items. Use render($content) to print them all, or
- *   print a subset such as render($content['field_example']). Use
- *   hide($content['field_example']) to temporarily suppress the printing of a
- *   given element.
- * - $title: The (sanitized) entity label.
- * - $url: Direct url of the current entity if specified.
- * - $page: Flag for the full page state.
- * - $classes: String of classes that can be used to style contextually through
- *   CSS. It can be manipulated through the variable $classes_array from
- *   preprocess functions. By default the following classes are available, where
- *   the parts enclosed by {} are replaced by the appropriate values:
- *   - entity-{ENTITY_TYPE}
- *   - {ENTITY_TYPE}-{BUNDLE}
- *
- * Other variables:
- * - $classes_array: Array of html class attribute values. It is flattened
- *   into a string within the variable $classes.
- *
- * @see template_preprocess()
- * @see template_preprocess_entity()
- * @see template_process()
- */
-?>
-<div class="<?php print $classes; ?> clearfix"<?php print $attributes; ?>>
-
-  <?php if (!$page): ?>
-    <h2<?php print $title_attributes; ?>>
-      <?php if ($url): ?>
-        <a href="<?php print $url; ?>"><?php print $title; ?></a>
-      <?php else: ?>
-        <?php print $title; ?>
-      <?php endif; ?>
-    </h2>
-  <?php endif; ?>
-
-  <div class="content"<?php print $content_attributes; ?>>
-    <?php
-      print render($content);
-    ?>
-  </div>
-</div>
diff --git a/includes/entity.controller.inc b/includes/entity.controller.inc
index 2f87010..c9023c7 100644
--- a/includes/entity.controller.inc
+++ b/includes/entity.controller.inc
@@ -587,6 +587,20 @@ class EntityAPIController extends DrupalDefaultEntityController implements Entit
     $entity->content = $content;
     $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language;
 
+    // By default add in properties for all defined extra fields.
+    if ($extra_field_controller = entity_get_extra_fields_controller($this->entityType)) {
+      $wrapper = entity_metadata_wrapper($this->entityType, $entity);
+      $extra = $extra_field_controller->fieldExtraFields();
+      $type_extra = &$extra[$this->entityType][$this->entityType]['display'];
+      $bundle_extra = &$extra[$this->entityType][$wrapper->getBundle()]['display'];
+
+      foreach ($wrapper as $name => $property) {
+        if (isset($type_extra[$name]) || isset($bundle_extra[$name])) {
+          $this->renderEntityProperty($wrapper, $name, $property, $view_mode, $langcode, $entity->content);
+        }
+      }
+    }
+
     // Add in fields.
     if (!empty($this->entityInfo['fieldable'])) {
       // Perform the preparation tasks if they have not been performed yet.
@@ -609,6 +623,24 @@ class EntityAPIController extends DrupalDefaultEntityController implements Entit
   }
 
   /**
+   * Renders a single entity property.
+   */
+  protected function renderEntityProperty($wrapper, $name, $property, $view_mode, $langcode, &$content) {
+    $info = $property->info();
+
+    $content[$name] = array(
+      '#label_hidden' => FALSE,
+      '#label' => $info['label'],
+      '#entity_wrapped' => $wrapper,
+      '#theme' => 'entity_property',
+      '#property_name' => $name,
+      '#access' => $property->access('view'),
+      '#entity_type' => $this->entityType,
+    );
+    $content['#attached']['css']['entity.theme'] = drupal_get_path('module', 'entity') . '/theme/entity.theme.css';
+  }
+
+  /**
    * Implements EntityAPIControllerInterface.
    */
   public function view($entities, $view_mode = 'full', $langcode = NULL, $page = NULL) {
diff --git a/theme/entity.theme.css b/theme/entity.theme.css
new file mode 100644
index 0000000..a57318d
--- /dev/null
+++ b/theme/entity.theme.css
@@ -0,0 +1,4 @@
+
+.entity-property-label {
+  font-weight: bold;
+}
diff --git a/theme/entity.theme.inc b/theme/entity.theme.inc
new file mode 100644
index 0000000..d365996
--- /dev/null
+++ b/theme/entity.theme.inc
@@ -0,0 +1,212 @@
+<?php
+
+/**
+ * @file
+ * Holds entity module's theme functions.
+ */
+
+/**
+ * Returns HTML for an entity property.
+ *
+ * This is the default theme implementation to display the value of a property.
+ * This function can be overridden with varying levels of specificity. For
+ * example, for a property named 'title' displayed on the 'article' bundle,
+ * any of the following functions will override this default implementation.
+ * The first of these functions that exists is used:
+ * - THEMENAME_property__body__article()
+ * - THEMENAME_property__article()
+ * - THEMENAME_property__body()
+ * - THEMENAME_property()
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - label: A boolean indicating to show or hide the property label.
+ *   - title_attributes: A string containing the attributes for the title.
+ *   - label: The label for the property.
+ *   - content_attributes: A string containing the attributes for the content's
+ *     div.
+ *   - content: The rendered property value.
+ *   - attributes: A string containing the attributes for the wrapping div.
+ *
+ * @ingroup themeable
+ */
+function theme_entity_property($variables) {
+  $output = '';
+
+  // Render the label, if it's not hidden.
+  if (!$variables['label_hidden']) {
+    $output .= '<div' . $variables['title_attributes'] . '>' . $variables['label'] . ':&nbsp;</div>';
+  }
+
+  // Render the content.
+  $content_suffix = '';
+  if (!$variables['label_hidden'] || $variables['content_attributes']) {
+    $output .= '<div' . $variables['content_attributes'] . '>';
+    $content_suffix = '</div>';
+  }
+  $output .= $variables['content'] . $content_suffix;
+
+  // Render the top-level DIV.
+  return '<div' . $variables['attributes'] . '>' . $output . '</div>';
+}
+
+/**
+ * Theme preprocess function for theme_entity_property().
+ *
+ * @see theme_entity_property()
+ */
+function template_preprocess_entity_property(&$variables, $hook) {
+  $element = $variables['elements'];
+
+  $variables += array(
+    'theme_hook_suggestions' => array(),
+    'attributes_array' => array(),
+  );
+  // Generate variables from element properties.
+  foreach (array('label_hidden', 'label', 'property_name') as $name) {
+    $variables[$name] = check_plain($element['#' . $name]);
+  }
+  $variables['title_attributes_array']['class'][] = 'entity-property-label';
+  $variables['attributes_array'] = array_merge($variables['attributes_array'], isset($element['#attributes']) ? $element['#attributes'] : array());
+
+  $variables['property_name_css'] = strtr($element['#property_name'], '_', '-');
+  $variables['attributes_array']['class'][] = 'entity-property';
+  $variables['attributes_array']['class'][] = 'entity-property-' . $variables['property_name_css'];
+
+  // Add specific suggestions that can override the default implementation.
+  $variables['theme_hook_suggestions'] += array(
+    'entity_property__' . $element['#property_name'],
+    'entity_property__' . $element['#entity_type'] . '__' . $element['#property_name'],
+  );
+
+  // Populate the content with sensible defaults.
+  if (!isset($variables['content'])) {
+    $variables['content'] = entity_property_default_render_value_by_type($element['#entity_wrapped']->{$element['#property_name']});
+  }
+}
+
+/**
+ * Renders a property using simple defaults based upon the property type.
+ *
+ * @return string
+ */
+function entity_property_default_render_value_by_type(EntityMetadataWrapper $property) {
+  // If there is an options list or entity label, render that by default.
+  if ($label = $property->label()) {
+    if ($property instanceof EntityDrupalWrapper && $uri = entity_uri($property->type(), $property->value())) {
+      return l($label, $uri['path'], $uri['options']);
+    }
+    else {
+      return check_plain($label);
+    }
+  }
+  switch ($property->type()) {
+    case 'boolean':
+      return $property->value() ? t('yes') : t('no');
+    default:
+      return check_plain($property->value());
+  }
+}
+
+/**
+ * Theme process function for theme_entity_property().
+ *
+ * Taken over from template_process_field()
+ *
+ * @see theme_entity_property()
+ */
+function template_process_entity_property(&$variables, $hook) {
+  $element = $variables['elements'];
+  // The default theme implementation is a function, so template_process() does
+  // not automatically run, so we need to flatten the classes and attributes
+  // here. For best performance, only call drupal_attributes() when needed, and
+  // note that template_preprocess_field() does not initialize the
+  // *_attributes_array variables.
+  $variables['attributes'] = empty($variables['attributes_array']) ? '' : drupal_attributes($variables['attributes_array']);
+  $variables['title_attributes'] = empty($variables['title_attributes_array']) ? '' : drupal_attributes($variables['title_attributes_array']);
+  $variables['content_attributes'] = empty($variables['content_attributes_array']) ? '' : drupal_attributes($variables['content_attributes_array']);
+}
+
+/**
+ * Themes the exportable status of an entity.
+ */
+function theme_entity_status($variables) {
+  $status = $variables['status'];
+  $html = $variables['html'];
+  if (($status & ENTITY_FIXED) == ENTITY_FIXED) {
+    $label = t('Fixed');
+    $help = t('The configuration is fixed and cannot be changed.');
+    return $html ? "<span class='entity-status-fixed' title='$help'>" . $label . "</span>" : $label;
+  }
+  elseif (($status & ENTITY_OVERRIDDEN) == ENTITY_OVERRIDDEN) {
+    $label = t('Overridden');
+    $help = t('This configuration is provided by a module, but has been changed.');
+    return $html ? "<span class='entity-status-overridden' title='$help'>" . $label . "</span>" : $label;
+  }
+  elseif ($status & ENTITY_IN_CODE) {
+    $label = t('Default');
+    $help = t('A module provides this configuration.');
+    return $html ? "<span class='entity-status-default' title='$help'>" . $label . "</span>" : $label;
+  }
+  elseif ($status & ENTITY_CUSTOM) {
+    $label = t('Custom');
+    $help = t('A custom configuration by a user.');
+    return $html ? "<span class='entity-status-custom' title='$help'>" . $label . "</span>" : $label;
+  }
+}
+
+/**
+ * Process 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;
+
+  if (isset($variables['elements']['#page'])) {
+    // If set by the caller, respect the page property.
+    $variables['page'] = $variables['elements']['#page'];
+  }
+  else {
+    // Else, try to automatically detect it.
+    $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_id($entity_type, $entity)) {
+    $variables['theme_hook_suggestions'][] = $entity_type . '__' . $id;
+  }
+}
diff --git a/theme/entity.tpl.php b/theme/entity.tpl.php
new file mode 100644
index 0000000..cfe94dd
--- /dev/null
+++ b/theme/entity.tpl.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Default theme implementation for entities.
+ *
+ * Available variables:
+ * - $content: An array of comment items. Use render($content) to print them all, or
+ *   print a subset such as render($content['field_example']). Use
+ *   hide($content['field_example']) to temporarily suppress the printing of a
+ *   given element.
+ * - $title: The (sanitized) entity label.
+ * - $url: Direct url of the current entity if specified.
+ * - $page: Flag for the full page state.
+ * - $classes: String of classes that can be used to style contextually through
+ *   CSS. It can be manipulated through the variable $classes_array from
+ *   preprocess functions. By default the following classes are available, where
+ *   the parts enclosed by {} are replaced by the appropriate values:
+ *   - entity-{ENTITY_TYPE}
+ *   - {ENTITY_TYPE}-{BUNDLE}
+ *
+ * Other variables:
+ * - $classes_array: Array of html class attribute values. It is flattened
+ *   into a string within the variable $classes.
+ *
+ * @see template_preprocess()
+ * @see template_preprocess_entity()
+ * @see template_process()
+ */
+?>
+<div class="<?php print $classes; ?> clearfix"<?php print $attributes; ?>>
+
+  <?php if (!$page): ?>
+    <h2<?php print $title_attributes; ?>>
+      <?php if ($url): ?>
+        <a href="<?php print $url; ?>"><?php print $title; ?></a>
+      <?php else: ?>
+        <?php print $title; ?>
+      <?php endif; ?>
+    </h2>
+  <?php endif; ?>
+
+  <div class="content"<?php print $content_attributes; ?>>
+    <?php
+      print render($content);
+    ?>
+  </div>
+</div>
