En este post explicaré como hacer un uso correcto de los extension attributes. Lo primero de todo es saber para qué sirve esta nueva funcionalidad de Magento 2.
Los extension attributes son usados para añadir nuevos atributos a modelos sin tener que modificar ni la clase ni la interfaz de ellos. Esto es aplicable tanto a modelos propios de Magento como de módulos de terceros. El único requisito es que la interfaz del modelo que vayamos a extender herede de  Magento \ Framework \ Api \ ExtensibleDataInterface y su clase de Magento \ Framework \ Model \ AbstractExtensibleModel.

Para explicar esta nueva funcionalidad, vamos a basarnos en una entidad propia, la cual explicamos en el post Creación de una nueva entidad en Magento 2.

Cabe destacar que todo el módulo sigue las recomendaciones estándar de PHP.

Creación de modelos, recursos y repositorios

Para el uso de los extension attributes tenemos 2 opciones, guardar estos nuevos atributos en la misma tabla de la base de datos o en otra distinta. El ejemplo que vamos a explicar es añadiendo la nueva información en una tabla.

Para ello, crearemos exactamente una nueva entidad con los mismos pasos que en el post Creación de una nueva entidad en Magento 2.. En nuesto caso, en el InstallSchema.php crearemos un campo que almacenará el id del modelo principal el cual será una clave foránea del campo del modelo principal.

app / code / Interactiv4 / CustomPost / Setup / InstallSchema.php

<?php
/**
 * @author Interactiv4 Team
 * @copyright Copyright © Interactiv4 (https://www.interactiv4.com)
 */

namespace Interactiv4\CustomPost\Setup;

use Interactiv4\CustomPost\Api\Data\PostInterface;
use Interactiv4\Post\Api\Data\EntityInterface;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Zend_Db_Exception;

/**
 * Class InstallSchema
 */
class InstallSchema implements InstallSchemaInterface
{
    /**
     * {@inheritdoc}
     */
    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $installer = $setup;

        $installer->startSetup();

        //Custom post
        $installer->getConnection()->dropTable($installer->getTable(PostInterface::SCHEMA_TABLE));
        $this->installTableCustomPost($installer);

        $installer->endSetup();
    }


    /**
     * Create table relations between custom entity and custom post
     *
     * @param SchemaSetupInterface $installer
     * @throws Zend_Db_Exception
     */
    private function installTableCustomPost(SchemaSetupInterface $installer)
    {
        $table = $installer->getConnection()->newTable(
            $installer->getTable(PostInterface::SCHEMA_TABLE)
        )->addColumn(
            PostInterface::FIELD_ID,
            Table::TYPE_INTEGER,
            null,
            ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
            'Custom post Id'
        )->addColumn(
            PostInterface::FIELD_POST_ID,
            Table::TYPE_INTEGER,
            null,
            ['unsigned' => true, 'nullable' => false],
            'Post Id'
        )->addColumn(
            PostInterface::FIELD_SHORT_DESCRIPTION,
            Table::TYPE_TEXT,
            null,
            ['nullable' => false, 'default' => ''],
            'Short description'
        )->addIndex(
            $installer->getIdxName(
                $installer->getTable(PostInterface::SCHEMA_TABLE),
                [PostInterface::FIELD_POST_ID],
                AdapterInterface::INDEX_TYPE_UNIQUE
            ),
            [PostInterface::FIELD_POST_ID],
            ['type' => AdapterInterface::INDEX_TYPE_UNIQUE]
        )->addForeignKey(
            $installer->getFkName(
                PostInterface::SCHEMA_TABLE,
                PostInterface::FIELD_POST_ID,
                EntityInterface::TABLE,
                EntityInterface::ID
            ),
            PostInterface::FIELD_POST_ID,
            $installer->getTable(EntityInterface::TABLE),
            EntityInterface::ID,
            Table::ACTION_CASCADE
        )->setComment(
            'Custom Post'
        );

        $installer->getConnection()->createTable($table);
    }
}

Creación de extension attributes

Para añadir los extension attributes a un modelo ya creado, debemos primero crear un archivo extension_attributes.xml, en el cual especificaremos la interfaz del modelo principal y los campos que vamos a añadir.

app / code / Interactiv4 / CustomPost / etc / extension_attributes.xml

<?xml version="1.0"?>
<!--
~ @author Interactiv4 Team
~ @copyright  Copyright © Interactiv4 (https://www.interactiv4.com)
-->

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
   <extension_attributes for="Interactiv4\Post\Api\Data\EntityInterface">
       <attribute code="short_description" type="string">
           <resources>
               <resource ref="Interactiv4_Post::entity"/>
           </resources>
           <join reference_table="interactiv4_custompost_post" reference_field="post_id" join_on_field="entity_id">
               <field>short_description</field>
           </join>
       </attribute>
   </extension_attributes>
</config>

En el ejemplo de arriba podemos ver, como le indicamos la interfaz del modelo principal, el nombre del nuevo atributo y su tipología (int, string, un objeto, array…).

El parámetro resources, es opcional y restringe el acceso al usuario con un permiso específico.

Cabe destacar la especificación del join, el cual es opcional. En este punto, especificamos nuestra nueva tabla y los campos que están referenciados entre sí entre el modelo principal y el nuestro. Esto sólo funcionará si en el método getList del repositorio principal está incluido el joinProcessor de los extension attributes (Magento \ Framework \ Api \ ExtensionAttribute \ JoinProcessorInterface).

app / code / Interactiv4 / Post / Model / EntityRepository.php

<?php
/**
* @author Interactiv4 Team
* @copyright Copyright (c) 2017 Interactiv4 (https://www.interactiv4.com)
* @package Interactiv4_Post
*/

namespace Interactiv4\Post\Model;

...
use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface;

class EntityRepository implements EntityRepositoryInterface
{

...

   /**
    * EntityRepository constructor.
    *
    * @param EntityFactory $entityFactory
    * @param CollectionFactory $entityCollectionFactory
    * @param EntitySearchResultsInterfaceFactory $entitySearchResultsInterfaceFactory
    * @param CollectionProcessorInterface $collectionProcessor
    * @param JoinProcessorInterface $extensionAttributesJoinProcessor
    */
   public function __construct(
       EntityFactory $entityFactory,
       CollectionFactory $entityCollectionFactory,
       EntitySearchResultsInterfaceFactory $entitySearchResultsInterfaceFactory,
       CollectionProcessorInterface $collectionProcessor,
       JoinProcessorInterface $extensionAttributesJoinProcessor
   ) {
       $this->entityFactory = $entityFactory;
       $this->entityCollectionFactory = $entityCollectionFactory;
       $this->entitySearchResultsInterfaceFactory = $entitySearchResultsInterfaceFactory;
       $this->collectionProcessor = $collectionProcessor;
       $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor;
   }

   ...

   /**
    * @inheritdoc
    */
   public function getList(SearchCriteriaInterface $searchCriteria)
   {
       /** @var Collection $collection */
       $collection = $this->entityCollectionFactory->create();

       $this->extensionAttributesJoinProcessor->process($collection, EntityInterface::class);
       $this->collectionProcessor->process($searchCriteria, $collection);

       /** @var EntitySearchResultsInterface $searchResults */
       $searchResults = $this->entitySearchResultsInterfaceFactory->create();
       $searchResults->setSearchCriteria($searchCriteria);
       $searchResults->setItems($collection->getItems());
       $searchResults->setTotalCount($collection->getSize());

       return $searchResults;
   }
}

Gracias a este joinProcessor podemos definir en nuestro PostRepository.php una función getListPostByShortDescription() la cual obtiene un listado de todas las entidades del modelo principal filtrando por el valor de un extension attributes.

app / code / Interactiv4 / CustomPost / Model / PostRepository.php

<?php
/**
 * @author Interactiv4 Team
 * @copyright Copyright © Interactiv4 (https://www.interactiv4.com)
 */

namespace Interactiv4\CustomPost\Model;

use Exception;
use Interactiv4\CustomPost\Api\Data\PostInterface;
use Interactiv4\CustomPost\Api\Data\PostSearchResultsInterface;
use Interactiv4\CustomPost\Api\Data\PostSearchResultsInterfaceFactory;
use Interactiv4\CustomPost\Api\PostRepositoryInterface;
use Interactiv4\CustomPost\Model\PostFactory;
use Interactiv4\CustomPost\Model\ResourceModel\Post as ResourceCustomPost;
use Interactiv4\CustomPost\Model\ResourceModel\Post\Collection;
use Interactiv4\CustomPost\Model\ResourceModel\Post\CollectionFactory;
use Interactiv4\Post\Api\Data\EntityInterface;
use Interactiv4\Post\Api\EntityRepositoryInterface;
use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface;
use Magento\Framework\Api\FilterBuilder;
use Magento\Framework\Api\Search\SearchCriteria;
use Magento\Framework\Api\Search\SearchCriteriaBuilder;
use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SortOrder;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Exception\StateException;
use Magento\Framework\Exception\ValidatorException;

/**
 * Class PostRepository
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class PostRepository implements PostRepositoryInterface
{
    /**
     * @var ResourceCustomPost
     */
    private $resourceCustomPost;

    /**
     * @var PostFactory
     */
    private $customPostFactory;

    /**
     * @var CollectionFactory
     */
    private $customPostCollectionFactory;

    /**
     * @var PostSearchResultsInterfaceFactory
     */
    private $customPostSearchResultInterfaceFactory;

    /**
     * @var CollectionProcessorInterface
     */
    private $collectionProcessor;

    /**
     * @var JoinProcessorInterface
     */
    private $extensionAttributesJoinProcessor;

    /**
     * @var EntityRepositoryInterface
     */
    private $entityRepository;

    /**
     * @var FilterBuilder
     */
    private $filterBuilder;

    /**
     * @var SearchCriteriaBuilder
     */
    private $searchCriteriaBuilder;

    /**
     * @var SortOrder
     */
    private $sortOrder;

    /**
     * PostRepository constructor.
     *
     * @param ResourceCustomPost $resourceCustomPost
     * @param PostFactory $customPostFactory
     * @param CollectionFactory $customPostCollectionFactory
     * @param PostSearchResultsInterfaceFactory $customPostSearchResultInterfaceFactory
     * @param CollectionProcessorInterface $collectionProcessor
     * @param JoinProcessorInterface $extensionAttributesJoinProcessor
     * @param EntityRepositoryInterface $entityRepository
     * @param FilterBuilder $filterBuilder
     * @param SearchCriteriaBuilder $searchCriteriaBuilder
     * @param SortOrder $sortOrder
     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
     */
    public function __construct(
        ResourceCustomPost $resourceCustomPost,
        PostFactory $customPostFactory,
        CollectionFactory $customPostCollectionFactory,
        PostSearchResultsInterfaceFactory $customPostSearchResultInterfaceFactory,
        CollectionProcessorInterface $collectionProcessor,
        JoinProcessorInterface $extensionAttributesJoinProcessor,
        EntityRepositoryInterface $entityRepository,
        FilterBuilder $filterBuilder,
        SearchCriteriaBuilder $searchCriteriaBuilder,
        SortOrder $sortOrder
    ) {
        $this->resourceCustomPost                     = $resourceCustomPost;
        $this->customPostFactory                      = $customPostFactory;
        $this->customPostCollectionFactory            = $customPostCollectionFactory;
        $this->customPostSearchResultInterfaceFactory = $customPostSearchResultInterfaceFactory;
        $this->collectionProcessor                    = $collectionProcessor;
        $this->extensionAttributesJoinProcessor       = $extensionAttributesJoinProcessor;
        $this->entityRepository                       = $entityRepository;
        $this->filterBuilder                          = $filterBuilder;
        $this->searchCriteriaBuilder                  = $searchCriteriaBuilder;
        $this->sortOrder                              = $sortOrder;
    }

    /**
     * @inheritdoc
     */
    public function save(PostInterface $customPost)
    {
        $this->resourceCustomPost->save($customPost);

        return $customPost;
    }

    /**
     * @inheritdoc
     */
    public function getById($customPostId)
    {
        return $this->get($customPostId);
    }

    /**
     * @inheritdoc
     */
    public function get($value, $attributeCode = null)
    {
        /** @var Post $customPost */
        $customPost = $this->customPostFactory->create()->load($value, $attributeCode);

        if (!$customPost->getId()) {
            throw new NoSuchEntityException(__('Unable to find custom post'));
        }

        return $customPost;
    }

    /**
     * @inheritdoc
     */
    public function delete(PostInterface $customPost)
    {
        $customPostId = $customPost->getId();
        try {
            $this->resourceCustomPost->delete($customPost);
        } catch (Exception $e) {
            throw new StateException(
                __('Unable to remove custom post %1', $customPostId)
            );
        }

        return true;
    }

    /**
     * @inheritdoc
     */
    public function deleteById($customPostId)
    {
        $customPost = $this->getById($customPostId);

        return $this->delete($customPost);
    }

    /**
     * @inheritdoc
     */
    public function getList(SearchCriteriaInterface $searchCriteria)
    {
        /** @var Collection $collection */
        $collection = $this->customPostCollectionFactory->create();

        $this->extensionAttributesJoinProcessor->process($collection, PostInterface::class);
        $this->collectionProcessor->process($searchCriteria, $collection);

        /** @var PostSearchResultsInterface $searchResults */
        $searchResults = $this->customPostSearchResultInterfaceFactory->create();
        $searchResults->setSearchCriteria($searchCriteria);
        $searchResults->setItems($collection->getItems());
        $searchResults->setTotalCount($collection->getSize());

        return $searchResults;
    }

    /**
     * @inheritdoc
     */
    public function getListPostByShortDescription($shortDescription)
    {

        $filter = $this->filterBuilder
            ->setField(PostInterface::FIELD_SHORT_DESCRIPTION)
            ->setValue($shortDescription)
            ->setConditionType('LIKE')
            ->create();

        /** @var SearchCriteria $searchCriteria */
        $searchCriteria = $this->searchCriteriaBuilder->addFilter($filter);

        $sortOrder = $this->sortOrder->setField(EntityInterface::NAME)->setDirection('asc');
        $searchCriteria->setSortOrders([$sortOrder]);
        $entitySearchResults = $this->entityRepository->getList($searchCriteria);

        return $entitySearchResults;
    }
}

Gracias a la definición del archivo extension_attributes.xml, Magento generará en la carpeta var / generation (< Magento 2.2) o generated (> Magento 2.2) una clase EntityExtensionInterface.php con los getters y setters de los atributos que hayamos definido en dicho archivo xml.

var / generation / Interactiv4 / Post / Api / Data / EntityExtensionInterface.php ó
generated / code / Interactiv4 / Post / Api / Data / EntityExtensionInterface.php

<?php
/**
* @author Interactiv4 Team
* @copyright  Copyright © Interactiv4 (https://www.interactiv4.com)
*/

namespace Interactiv4\Post\Api\Data;

/**
* ExtensionInterface class for @see \Interactiv4\Post\Api\Data\EntityInterface
*/
interface EntityExtensionInterface extends \Magento\Framework\Api\ExtensionAttributesInterface
{
   /**
    * @return string|null
    */
   public function getShortDescription();

   /**
    * @param string $shortDescription
    * @return $this
    */
   public function setShortDescription($shortDescription);
}

Obtener y almacenar información de extension attributes

Plugins

Magento no almacena ni obtiene la información de los extension attributes de manera automática. Tenemos que obtenerla y guardarla de manera manual haciendo uso de los plugins en las funciones get(), getList() y save() del repositorio principal.

app / etc / di.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
~ @author Interactiv4 Team
~ @copyright  Copyright © Interactiv4 (https://www.interactiv4.com)
-->

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
   <preference for="Interactiv4\CustomPost\Api\Data\PostInterface" type="Interactiv4\CustomPost\Model\Post" />
   <preference for="Interactiv4\CustomPost\Api\Data\PostSearchResultsInterface" type="Interactiv4\CustomPost\Model\PostSearchResults" />
   <preference for="Interactiv4\CustomPost\Api\PostRepositoryInterface" type="Interactiv4\CustomPost\Model\PostRepository" />
   <type name="Interactiv4\Post\Api\EntityRepositoryInterface">
       <plugin name="interactiv4_custompost_plugin_entity_repository" type="Interactiv4\CustomPost\Plugin\EntityRepositoryPlugin"/>
   </type>
</config>

app / code / Plugin / EntityRepositoryPlugin.php

<?php
/**
 * @author Interactiv4 Team
 * @copyright Copyright © Interactiv4 (https://www.interactiv4.com)
 */

namespace Interactiv4\CustomPost\Plugin;

use Interactiv4\CustomPost\Api\Data\PostInterface;
use Interactiv4\CustomPost\Api\Data\PostInterfaceFactory;
use Interactiv4\CustomPost\Api\PostRepositoryInterface;
use Interactiv4\Post\Api\Data\EntityExtensionFactory;
use Interactiv4\Post\Api\Data\EntityExtensionInterface;
use Interactiv4\Post\Api\Data\EntityInterface;
use Interactiv4\Post\Api\Data\EntitySearchResultsInterface;
use Interactiv4\Post\Api\EntityRepositoryInterface;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;

/**
 * Class EntityRepositoryPlugin
 */
class EntityRepositoryPlugin
{
    /**
     * @var EntityExtensionFactory
     */
    private $entityExtensionFactory;

    /**
     * @var PostInterfaceFactory
     */
    private $customPostFactory;

    /**
     * @var PostRepositoryInterface
     */
    private $customPostRepository;

    /**
     * PostGet constructor.
     *
     * @param EntityExtensionFactory $entityExtensionFactory
     * @param PostInterfaceFactory $customPostFactory
     * @param PostRepositoryInterface $customPostRepository
     */
    public function __construct(
        EntityExtensionFactory $entityExtensionFactory,
        PostInterfaceFactory $customPostFactory,
        PostRepositoryInterface $customPostRepository
    ) {
        $this->entityExtensionFactory = $entityExtensionFactory;
        $this->customPostFactory      = $customPostFactory;
        $this->customPostRepository   = $customPostRepository;
    }

    /**
     * @param EntityRepositoryInterface $subject
     * @param EntityInterface $result
     * @return EntityInterface
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function afterGet(EntityRepositoryInterface $subject, EntityInterface $result)
    {
        /** @var EntityExtensionInterface $extensionAttributes */
        $extensionAttributes = $result->getExtensionAttributes() ?: $this->entityExtensionFactory->create();

        try {
            /** @var PostInterface $customPost */
            $customPost = $this->customPostRepository->get($result->getId(), PostInterface::FIELD_POST_ID);
        } catch (NoSuchEntityException $e) {
            $result->setExtensionAttributes($extensionAttributes);

            return $result;
        }
        $extensionAttributes->setShortDescription($customPost->getShortDescription());

        $result->setExtensionAttributes($extensionAttributes);

        return $result;
    }

    /**
     * @param EntityRepositoryInterface $subject
     * @param EntitySearchResultsInterface $entities
     * @return EntitySearchResultsInterface
     */
    public function afterGetList(EntityRepositoryInterface $subject, EntitySearchResultsInterface $entities)
    {
        foreach ($entities->getItems() as $entity) {
            $this->afterGet($subject, $entity);
        }

        return $entities;
    }

    /**
     * @param EntityRepositoryInterface $subject
     * @param EntityInterface $result
     * @return EntityInterface
     * @throws CouldNotSaveException
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function afterSave(EntityRepositoryInterface $subject, EntityInterface $result)
    {
        $extensionAttributes = $result->getExtensionAttributes() ?: $this->entityExtensionFactory->create();
        if ($extensionAttributes !== null &&
            $extensionAttributes->getShortDescription() !== null
        ) {
            /** @var PostInterface $customPost */
            try {
                $customPost = $this->customPostRepository->get($result->getId(), PostInterface::FIELD_POST_ID);
            } catch (NoSuchEntityException $e) {
                $customPost = $this->customPostFactory->create();
            }
            $customPost->setPostId($result->getId());
            $customPost->setShortDescription($extensionAttributes->getShortDescription());
            $this->customPostRepository->save($customPost);
        }

        return $result;
    }
}

De esta manera conseguimos que cada vez que se cargue un modelo o un listado, contenga la información adicional de los extension attributes. A su vez, cada vez que guardemos la información de un producto, también guardaremos la información adicional.

Espero que os haya sido de ayuda y cualquier duda podéis dejar un comentario.

En este link podéis descargar el código de esta explicación y en este otrolink la entidad principal de la que vamos a extender.