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!

lunes, 7 de noviembre de 2011

Builders genéricos: Agregando funcionalidad (3)

Esta es la tercera parte de la serie de posts que escribí sobre Builders genéricos.
Escribo los links de los post anteriores:

Builders genéricos: Introducción (1)
Builders genéricos: Implementación (2)


En este post voy a dar ejemplos más avanzados en los cuales me es muy útil utilizar el concepto de Builder. Quizás existan mejores formas de hacer lo que les voy a explicar (escucho sugerencias), pero hasta ahora, este patrón me dio buenos resultados, sobre todo para realizar los Tests de mis aplicaciones.

Primero partamos de la base que nos dejó mi post anterior:

Tenemos una interfaz IBuilder que recibe un TObject como parámetro genérico cuyo método principal es Build. Este método nos permite construir objetos de tipo TObject:

public interface IBuilder<TBuilder, TObject>
    where TBuilder : IBuilder<TBuilder, TObject>
{
    TBuilder With(Action<TObject> setProperties);
    TObject Build();
}


Aparte, también tenemos nuestra implementación abstracta que nos permite construir un objeto de tipo TObject. Para esto tuvimos que agregarle la restricción a TObject de que debía tener como mínimo un constructor público sin parámetros ( new() ):

public abstract class AbstractBuilder<TBuilder, TObject>
    : IBuilder<TBuilder, TObject>
    where TBuilder : AbstractBuilder<TBuilder, TObject>
    where TObject : class, new()
{
    protected TObject obj;
    protected AbstractBuilder()
    {
        obj = new TObject();
    }

    public virtual TBuilder With(Action<TObject> setProperties)
    {
        if (setProperties == null)
            throw new ArgumentNullException("setProperties");
        setProperties(obj);
        return this as TBuilder;
    }

    public virtual TObject Build()
    {
        TObject objAux = obj;
        obj = null;
        return objAux;
    }
}


Ahora comenzaremos a utilizarlo y extenderemos su funcionalidad o la simplificaremos :) cuando veamos que es posible.

Revisemos primero la interfaz IBuilder<TObject>.

Leyendo uno de los posts del Blog de Jorge Rowies que hablaba sobre el Principio de Separación de Interfaces - SOLID se me cruzó por la mente que mi interfaz IBuilder, aunque en apariencia resulte simple estaba haciendo algo más que lo que debería hacer.

¿Qué es lo que dice el principio de separación de interfaces (Interface Segregation Principle o ISP)?
El principio dice: Una clase cliente no debe ser forzada a depender de interfaces que no usa.

En nuestro caso, tenemos una interfaz de un Builder que contiene un método Build para generar un objeto :) pero aparte tiene el método With que recibe un Action para setear sus propiedades :(.

Planteemos un ejemplo para ver que es lo que no está del todo bien:

Tenemos la siguiente clase que representa un número aleatorio cuyo único constructor recibe un número entero:

public class RandomNumber
{
    public RandomNumber(int num)
    {
        IntegerValue = num;
    }
    public int IntegerValue { get; private set; }
}

El Builder genérico no nos sirve porque la clase RandomNumber no cumple la restricción de tener al menos un constructor público sin parámetros. Por lo tanto nos creamos una implementación customizada que nos devuelva esta clase pero que implemente IBuilder:

public class RandomNumberBuilder : IBuilder<RandomNumberBuilder,RandomNumber>
{
    public RandomNumber Build()
    {
        // Implementación sólo de ejemplo
        return new RandomNumber(new Random(Guid.NewGuid().GetHashCode()).Next());
    }
    public RandomNumberBuilder With(Action<RandomNumber> setProperties)
    {
        throw new NotSupportedException("Este builder no permite setear propiedades!");
    }
}


A simple vista nos damos cuenta que hay 2 cosas que nos hacen ruido:

  1. Debemos implementar el método With aún cuando ya sabemos de antemano que la clase RandomNumber no permite el seteo de propiedades. Como primer solución, en el cuerpo de este método lanzamos una excepción de tipo "método no soportado".
  2. Por otro lado, como el método With debe devolver la instancia del builder, debemos pasarle por parámetro al propio Builder que nunca se utilizará dentro de la clase.

¿Cómo debería estar implementada nuestra clase RandomNumberBuilder?

public class RandomNumberBuilder : IBuilder<RandomNumber>
{
    public RandomNumber Build()
    {
        return new RandomNumber(new Random(Guid.NewGuid().GetHashCode()).Next());
    }
}

Utilizando el principio de ISP simplemente separamos en 2 la interfaz:

public interface IBuilder<TObject>
{
    TObject Build();
}

public interface IBuilderWithSetters<TBuilder,TObject>
    : IBuilder<TObject>
    where TBuilder : IBuilderWithSetters<TBuilder, TObject>
{
    TBuilder With(Action<TObject> setProperties);
}

De esta forma queda más reusable la interfaz IBuilder y es mucho más clara la función que debe cumplir.

Con este cambio, nuestro Generic Builder quedaría igual que antes sólo que implementaría la interfaz IBuilderWithSetters<TBuilder,TObject>:

public abstract class AbstractBuilder<TBuilder, TObject> 
        : IBuilderWithSetters<TBuilder,TObject>
    where TBuilder : AbstractBuilder<TBuilder, TObject>
    where TObject : class, new()
{
    // Misma implementación que antes
}


Otro ejemplo. Utilizaremos la clase Person definida en el post anterior junto con el siguiente builder:

public class Person
{
    public string Name { get; set; }
    public int? Age { get; set; }
}

public class PersonBuilder : AbstractBuilder<PersonBuilder,Person>
{
}

Supongamos que en lugar de querer crear un objeto, queremos crear una colección de objetos Person con valores iguales(que no es lo mismo que una colección de instancias iguales). Para crear 10 elementos la solución sería la siguiente:

[Test]
public void TestBuildCollection()
{
    var people = new List<Person>();
    for (int i = 0; i < 10; i++)
    {
        people.Add(
            new PersonBuilder()
                .With(person => person.Name = "Juan")
                .With(person => person.Age = 30)
                .Build());
    }
    Assert.That(people.Count(), Is.EqualTo(10));
    foreach (Person p in people)
    {
        Assert.That(p, Is.Not.Null);
        Assert.That(p.Name, Is.EqualTo("Juan"));
        Assert.That(p.Age, Is.EqualTo(30));
    }
}

Esta solución funciona correctamente, sin embargo estamos creando 10 builders exactamente iguales, 1 por cada objeto Person que queremos crear.

Una segunda posible solución podría haber sido la siguiente:

[Test]
public void TestBuildCollection()
{
    var people = new List<Person>();
    var personBuilder = new PersonBuilder()
        .With(person => person.Name = "Juan")
        .With(person => person.Age = 30);
    for (int i = 0; i < 10; i++)
    {
        people.Add(personBuilder.Build());
    }
    Assert.That(people.Count(), Is.EqualTo(10));
    foreach (Person p in people)
    {
        Assert.That(p, Is.Not.Null);
        Assert.That(p.Name, Is.EqualTo("Juan"));
        Assert.That(p.Age, Is.EqualTo(30));
    }
}

Sin embargo, cada vez que hacemos build, el campo "obj" del builder se setea a null ya que sino, una nueva llamada al método Build devolvería la misma instancia.

public virtual TObject Build()
{
   TObject objAux = obj;
   obj = null;   // Para prevenir utilizar una instancia creada previamente
   return objAux;
}

Por otro lado, si en vez de setear esa propiedad en null, le seteamos new TObject(), al correr el test nos encontraremos con un resultado raro. El primer objeto creado tendría todas sus propiedades con valores pero los demás estarían vacíos. Esto se debe a que cada llamada al método With se ejecuta inmediatamente y por lo tanto, en el nuevo objeto a crear este seteo no estaría registrado:

public virtual TObject Build()
{
   TObject objAux = obj;
   obj = new TObject();   // NO!!!.. Se están perdiendo todos los seteos
   return objAux;
}


¿Cómo podemos modificar nuestro Builder genérico para que el segundo test corra correctamente? 

En lugar de ejecutar inmediatamente las acciones las guardamos en una lista para luego ejecutarlas en cada llamada al método Build. Para no complicar la clase AbstractBuilder crearía una nueva llamada AbstractTemplateBuilder (le agregué la palabra Template porque lo que estoy haciendo es crear una plantilla de objetos).

Las clases AbstractTemplateBuilder y PersonBuilder quedarían de la siguiente manera:

public abstract class AbstractTemplateBuilder<TBuilder, TObject>
        : IBuilderWithSetters<TBuilder, TObject>
    where TBuilder : AbstractTemplateBuilder<TBuilder, TObject>
    where TObject : class, new()
{
    protected readonly IList<Action<TObject>> actions;
    protected AbstractTemplateBuilder()
    {
        actions = new List<Action<TObject>>();
    }

    public virtual TBuilder With(Action<TObject> setProperties)
    {
        if (setProperties == null)
            throw new ArgumentNullException("setProperties");
        actions.Add(setProperties);
        return (TBuilder)this;
    }

    public virtual TObject Build()
    {
        TObject toBuild = new TObject();
        foreach (Action<TObject> action in actions)
        {
            if (action != null)
                action(toBuild);
        }
        return toBuild;
    }
}

public class PersonBuilder : AbstractTemplateBuilder<TPersonbuilder, Person>
{
}

Ahora que podemos generar las clases mediante un template. ¿Podemos generalizar la obtención de la colección de objetos? La respuesta es Sí :)
¿Cómo quedaría el test?

[Test]
public void TestBuildCollection()
{
    var personBuilder = new PersonBuilder()
        .With(person => person.Name = "Juan")
        .With(person => person.Age = 30);

    var people = personBuilder.BuildCollection(10);

    Assert.That(people.Count(), Is.EqualTo(10));
    foreach (Person p in people)
    {
        Assert.That(p, Is.Not.Null);
        Assert.That(p.Name, Is.EqualTo("Juan"));
        Assert.That(p.Age, Is.EqualTo(30));
    }
}


Para que esto funcione, creamos la interfaz ICollectionBuilder:

public interface ICollectionBuilder<TObject>
{
    IEnumerable<TObject> BuildCollection(int length);
}

La implementación del método sería la siguiente:

public virtual IEnumerable<TObject> BuildCollection(int length)
{
    TObject toBuild = Build();
    for (int i = 0; i < length; i++)
    {
        yield return toBuild;
    }
}

Y para finalizar, ya que creamos una colección, ¿Por qué no permitir que se seteen valores específicos a cada elemento teniendo en cuenta la posición del elemento a agregar? Otra vez podemos utilizar los actions para permitir esto.

Por ejemplo, si quiero devolver una colección de personas con el mismo nombre pero con edades de 0 a 10 el test sería el siguiente:

[Test]
public void TestBuildCollection()
{
    var personBuilder = new PersonBuilder()
        .With(person => person.Name = "Juan")
        .With(person => person.Age = 30);

    var people = personBuilder
        .BuildCollection(10, (person, index) => person.Age = index);

    Assert.That(people.Count(), Is.EqualTo(10));
    int aux = 0;
    foreach (Person p in people)
    {
        Assert.That(p, Is.Not.Null);
        Assert.That(p.Name, Is.EqualTo("Juan"));
        Assert.That(p.Age, Is.EqualTo(aux++));
    }
}

Para no extenderme más, les dejo como quedarían la interfaz ICollectionBuilder y la implementación completa de AbstractTemplateBuilder:

public interface ICollectionBuilder<TObject>
{
    IEnumerable<TObject> BuildCollection(int length);
    IEnumerable<TObject> BuildCollection(int length, Action<TObject,int> setProperties);
}

public abstract class AbstractTemplateBuilder<TBuilder, TObject>
        : IBuilderWithSetters<TBuilder, TObject>,
            ICollectionBuilder<TObject>
    where TBuilder : AbstractTemplateBuilder<TBuilder, TObject>
    where TObject : class, new()
{
    protected readonly IList<Action<TObject>> actions;
    protected AbstractTemplateBuilder()
    {
        actions = new List<Action<TObject>>();
    }

    public virtual TBuilder With(Action<TObject> setProperties)
    {
        if (setProperties == null)
            throw new ArgumentNullException("setProperties");
        actions.Add(setProperties);
        return (TBuilder)this;
    }

    public virtual TObject Build()
    {
        TObject toBuild = new TObject();
        foreach (Action<TObject> action in actions)
        {
            if (action != null)
                action(toBuild);
        }
        return toBuild;
    }

    public virtual IEnumerable<TObject> BuildCollection(int length)
    {
        for (int i = 0; i < length; i++)
        {
            TObject toBuild = Build();
            yield return toBuild;
        }
    }

    public virtual IEnumerable<TObject> BuildCollection(int length, Action<TObject, int> setProperties)
    {
        for (int i = 0; i < length; i++)
        {
            TObject toBuild = Build();
            if (setProperties != null)
                setProperties(toBuild, i);
            yield return toBuild;
        }
    }
}

Todavía me quedaron unos ejemplos más avanzados pero ya me extendí demasiado. Quizás haya un 4° post si lo amerita.

Cualquier duda, consulta o sugerencia como siempre será bienvenida.
Hasta luego.

27/12/2015
Fix: BuildCollection tenía un error. Se estaba creando el objeto por fuera del foreach cuando debería haber sido dentro del mismo.