sábado, 26 de diciembre de 2015

DetailMapper: Mapping desde DTOs a Entidades de negocio

DetailMapper


En este artículo hablaré de la forma que encontré para hacer el mapeo de details desde DTOs/ViewModels a Entidades persistibles. Para estos ejemplos voy a utilizar interfaces de repositorios, sin embargo, esto también es válido para NHibernate o para EntityFramework.

El mapeo de Details desde Entidades de negocio a DTOs es fácil hacerlo mediante Automapper ya que siempre se deben crear nuevos objetos y aplanar propiedades.

Para el caso inverso no es tan sencillo ya que el mapeo puede implicar varias partes:

  1. Validación
  2. Obtención de entidades relacionadas
  3. Eliminación de entidades que ya no existen más.
  4. Actualización de entidades que ya existían.
  5. Inserción de entidades inexistentes.


Ya desde hace rato que ando buscando una solución estándar para hacer esto.

Mi primer aproximación fue utilizando “RefactorThis.GraphDiff”. El problema que tenía con esta solución es que primero tenía que mapear mi dto a una entidad de negocio de alguna forma, por ejemplo, con AutoMapper. Luego de esta transformación es que recién era posible utilizar este framework para actualizar las colecciones persistentes.

Lo que no me convenció es que no tenía un control más fino de lo que se podía hacer.

Luego, leyendo este artículo “Why mapping DTOs to Entities using AutoMapper and EntityFramework is horrible” se me vinieron un par de ideas a la mente que son las que voy a compartir.

Utilizaré las clases de este último Post (con algunas modificaciones) como ejemplo.

// Agregar las clases
// Example DTO Classes
public class OrderDTO
{
    public int Id { get; set; }
    public List<ItemDTO> Items { get; set; }
}
public class ItemDTO
{
    public int Id { get; set; }
    public decimal Quantity { get; set; }
    public string ProductId { get; set; }
}

// Example Entity Classes
public class Order
{
    public int Id { get; set; }
    private ICollection<ItemOrder> _items = new HashSet<ItemOrder>();
    public virtual ICollection<ItemOrder> Items
    {
        get { return _items; }
        set { _items = value; }
    }
    public virtual void AddItem(ItemOrder detail)
    {
        detail.Order = this;
        _items.Add(detail);
    }
    public virtual void RemoveItem(ItemOrder detail)
    {
        _items.Remove(detail);
    }
}
public class ItemOrder
{
    public int Id { get; set; }
    // ForeignKey of Order
    public int OrderID { get; set; }
    public Order Order { get; set; }
    public decimal Quantity { get; set; }
    public string ProductId { get; set; }
}

// Repositories
public interface IItemRepository
{
    void Insert(Order order, ItemOrder item);
    void Update(ItemOrder item);
    void Delete(ItemOrder item);
}
public interface IOrderRepository
{
    Order Find(int orderId);
}

Creación de Framework de mapping de Details:


Mi intención principal es hacer un framework que haga sencillo el mapeo de colecciones. Mi intención no es hacer un framework que lo pueda hacer todo. Ya existen frameworks de mapeos de datos simples, de validación y también existen diferentes maneras actualizar los datos de un ORM.

Antes de entrar en el código, entendamos qué es lo que se quiere hacer con una colección:

  1. Buscar los registros que no están en el DTO pero sí en la Entidad. Borrarlos.
  2. Buscar los registros que están el DTO pero no están en la entidad. Agregarlos.
  3. Buscar los registros que están tanto en el DTO como en la entidad. Modificarlos.


Para hacer estas 3 operaciones es necesario tener ciertos objetos y métodos definidos:

  1. Clase master DTO que contendrá la colección con cambios a aplicar.
  2. Clase master que contendrá la colección a ser modificada
  3. Colección de DTOs con cambios a aplicar.
  4. Colección de entidades a ser modificada.
  5. Método de creación de Detail. Para crear registros que no existen todavía.
  6. Método de agregado de Detail. Para insertar el Detail creado.
  7. Método de borrado de Detail. Para poder quitarlo de todas las colecciones que existas y ejecutar deletes en cascada si fuese necesario.
  8. Método de testeo de igualdad entre objetos. Permite identificar qué un DetailDTO se corresponde con un Detail.


Configuración:


using DetailMapper;
/**************************/

// Definición de Mapper. (Debería crearse una única vez)
var orderDTOMapperBuilder = DetailMapperBuilder.Create<OrderDTO, Order>();
var itemsMap = orderDTOMapperBuilder.Detail((dto) => dto.Items, (e) => e.Items)
    .WithDependencies<IItemRepository>()
    .AddAction((ctx, item) => ctx.Dependencies.Insert(ctx.Master, item))
    .DeleteAction((ctx, item) => ctx.Dependencies.Delete(item))
    .CreateFunc((ctx) => new ItemOrder())
    .EqualsFunc((itemDTO, item) => itemDTO.Id == item.Id)
    .Build();


En la configuración anterior se definen los métodos base necesarios para poder hacer el mapeo correctamente.

Veamos que representa cada uno:

  1. WithDependencies: Permite setear una un objeto del que dependerá el código. Es preferible que sea una interfaz para poder hacer testeos correctamente. En caso de que el código no tenga dependencias, setear simplemente “object”.
    1. El parámetro “required” permite indicar si es requerido que se pase la dependencia o puede funcionar si se le pasa null. Por defecto es false.
  2. AddAction: Permite setear el método que inserta un Detail.
  3. DeleteAction: Permite setear el método que borra un detail.
  4. CreateFunc: Permite setear la función que crea una instancia de un Detail.
  5. EqualsFunc: Permite identificar qué un DetailDTO se corresponde con un Detail.
  6. Build: Construye el mapper final del Detail




Utilización:


Una vez creado el mapper, el mismo se podrá utilizar de la siguiente manera:

var orderDTO1 = /* Viene desde el cliente */;
var order1 = orderRepository.Find(orderDTO1.Id);

//
itemsMap.Map(orderDTO1, order1, itemRepository, (itemDTO, item) =>
{
    // Mapeo de propiedades internas (Podría hacerse mediante AutoMapper)
    item.ProductId = itemDTO.ProductId;
    item.Quantity = itemDTO.Quantity;
});


Los parámetros que recibe el método Map son los siguientes:

  1. DTO Master. DTO que contiene la colección con modificaciones.
  2. Entidad Master: Entidad que contiene la colección a ser modificada.
  3. Dependencies: Dependencias necesarias para ejecutar el mapping. Puede ser un Repositorio, un DbContext de EntityFramework, una ISession de NHibernate, etc.
  4. Método de mapeo de propiedades de Detail DTO a Detail: Método utilizado por la inserción o modificación de details.


En caso de que el detail tenga internamente otro detail, los pasos serían exáctamente los mismo salvo que el método Map se llamaría dentro del último parámetro.


Instalación


Para simplificar la forma de utilizar creé un paquete de nuget:

https://www.nuget.org/packages/DetailMapper/

Install-Package DetailMapper

El código fuente está subido en GitHub con licencia MIT:

https://github.com/jgquiroga/DetailMapper


Bueno, para no hacer más largo este post, lo dejo hasta acá. Cualquier consulta, pueden comentarla debajo de este post.

Los issues y mejoras los pueden agregar en el repositorio de GitHub.

Gracias por pasar!