viernes, 20 de enero de 2017

Clonando Objectos en .NET Framework Parte II




Cuando publiqué la primera parte del artículo, decidí dejar fuera un modo más de clonación, el impulsado por Reflection. La decisión de dejarlo fuera vino principalmente porque hubiera querido abordarlo profundamente y pensaba que se podía alargar mucho y perder el sentido. A la vez en los comentarios de otras páginas especializadas me sugirieron también el modo  mediante Expressions Trees, algo que no conocía en este planteamiento, pero tratándose de Arboles de Expresiones, no era para nada lo que se puede decir sencillo.

Buscando más información, me topé con 2 fantásticas librerías en Git Hub, accesibles mediante Nuget, que funcionaban perfectamente bien y que hacen el trabajo de forma maravillosa, así que  ¿para qué reinventar la rueda?

En esta segunda parte trataré de explicar ejemplos con Nuclex y CloneExtensions, que nos permiten realizar clonados profundo, tanto en modo Reflection como Expression Trees.









Clase de Ejemplo.

Esta es una variante de las clases Customer y Address.


public class Customer
{
    public int                ID            { get; set; }
    public string             Name          { get; set; }
    public decimal            Sales         { get; set; }
    public DateTime           EntryDate     { get; set; }
    public Address            Adress        { get; set; }
 
    public Collection<string> Mails         { get; set; }
    public List<Address>      Adresses      { get; set; }
 
    protected string          Data1         { get; set; }
    private string            Data2         { get; set; }
 
    public string             ReadOnlyField { get; set; }
 
 
    public Customer()
    {
        Data1         = "data1";
        Data2         = "Data2";
        ReadOnlyField = "readonly_data";
    }
 
}

public class Address
{
    public string Street  { get; set; }
    public string City    { get; set; }
    public int    ZipCode { get; set; }
}





Nuclex


Nuclex.Cloning es una librería muy completa y sencilla de usar con capacidad de realizar clonado mediante Reflection y ExpressionTrees. También permite realizar clonado superficial o en profundidad y copiados a nivel de propiedad o campo.


INSTALACIÓN

La instalaremos a nivel de Nuget:





















Vista de las referencias:











Ejemplo de tipo Reflection:


using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Nuclex.Cloning;
using CloneLib;
using System.Collections.Generic;
using System.Collections.ObjectModel;
 
namespace Clone.Tests
{
    [TestClass]
    public class NuclexReflectionTests
    {
        [TestMethod]
        public void NuclexReflectorClone()
        {
            var customer1 = new Customer
            {
                ID        = 1,
                Name      = "Test",
                EntryDate = DateTime.Today,
                Sales     = 1000m,
 
                Adress = new Address { Street  = "One street", City    = "City", ZipCode = 2222 },
 
                Mails = new Collection<string>() { "a@b.com", "b@c.com" },
 
                Adresses = new List<Address>
                {
                    new Address { City = "aaa", Street = "bbb", ZipCode = 111  },
                    new Address { City = "ddd", Street = "eee", ZipCode = 222  }
                }
            };
 
            var cloneCustomer = ReflectionCloner.DeepFieldClone<Customer>(customer1);
            /// ******  We have other posibilities, for diferents clone depths
            //var cloneCustomer = ReflectionCloner.DeepPropertyClone    (customer1);
            //var cloneCustomer = ReflectionCloner.ShallowFieldClone    (customer1);
            //var cloneCustomer = ReflectionCloner.ShallowPropertyClone (customer1);
 
            cloneCustomer.Adress.City = "New city";
 
            Assert.AreNotEqual(customer1, cloneCustomer);
 
            Assert.AreEqual(customer1.ID       , cloneCustomer.ID);
            Assert.AreEqual(customer1.Name     , cloneCustomer.Name);
            Assert.AreEqual(customer1.EntryDate, cloneCustomer.EntryDate);
            Assert.AreEqual(customer1.Sales    , cloneCustomer.Sales);
 
            Assert.AreEqual(customer1.Mails[0], cloneCustomer.Mails[0]);
            Assert.AreEqual(customer1.Mails[1], cloneCustomer.Mails[1]);
 
            Assert.AreEqual(customer1.Adresses[0].City   , cloneCustomer.Adresses[0].City);
            Assert.AreEqual(customer1.Adresses[0].Street , cloneCustomer.Adresses[0].Street);
            Assert.AreEqual(customer1.Adresses[0].ZipCode, cloneCustomer.Adresses[0].ZipCode);
            Assert.AreEqual(customer1.Adresses[1].City   , cloneCustomer.Adresses[1].City);
            Assert.AreEqual(customer1.Adresses[1].Street , cloneCustomer.Adresses[1].Street);
            Assert.AreEqual(customer1.Adresses[1].ZipCode, cloneCustomer.Adresses[1].ZipCode);
 
            Assert.AreEqual(customer1.ReadOnlyField, cloneCustomer.ReadOnlyField);
 
            /// Change values in clone object for validate objects don't linked
            cloneCustomer.Adress.City = "Changed City";
            Assert.AreNotEqual(customer1.Adress.City, cloneCustomer.Adress.City);
 
            cloneCustomer.Mails[0] = "mailchanged@aa.com";
            Assert.AreNotEqual(customer1.Mails[0], cloneCustomer.Mails[0]);
 
            cloneCustomer.Adresses[0].Street = "New Street";
            Assert.AreNotEqual(customer1.Adresses[0].Street, cloneCustomer.Adresses[0].Street);
        }
    }
}


Ejemplo de tipo ExpressionTrees:




using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Nuclex.Cloning;
using CloneLib;
using System.Collections.Generic;
using System.Collections.ObjectModel;
 
namespace Clone.Tests
{
    [TestClass]
    public class NeclexExpressionTreeClonerText
    {
        [TestMethod]
        public void NuclexReflectorClone()
        {
            var customer1 = new Customer
            {
                ID = 1,
                Name = "Test",
                EntryDate = DateTime.Today,
                Sales = 1000m,
 
                Adress = new Address { Street = "One street", City = "City", ZipCode = 2222 },
 
                Mails = new Collection<string>() { "a@b.com", "b@c.com" },
 
                Adresses = new List<Address>
                {
                    new Address { City = "aaa", Street = "bbb", ZipCode = 111  },
                    new Address { City = "ddd", Street = "eee", ZipCode = 222  }
                }
            };
 
            var cloneCustomer = ExpressionTreeCloner.DeepFieldClone<Customer>(customer1);
            /// ******  We have other posibilities, for diferents clone depths
            //var cloneCustomer = ExpressionTreeCloner.DeepPropertyClone    (customer1);
            //var cloneCustomer = ExpressionTreeCloner.ShallowFieldClone    (customer1);
            //var cloneCustomer = ExpressionTreeCloner.ShallowPropertyClone (customer1);
 
            cloneCustomer.Adress.City = "New city";
 
            Assert.AreNotEqual(customer1, cloneCustomer);
 
            Assert.AreEqual(customer1.ID       , cloneCustomer.ID);
            Assert.AreEqual(customer1.Name     , cloneCustomer.Name);
            Assert.AreEqual(customer1.EntryDate, cloneCustomer.EntryDate);
            Assert.AreEqual(customer1.Sales    , cloneCustomer.Sales);
 
            Assert.AreEqual(customer1.Mails[0], cloneCustomer.Mails[0]);
            Assert.AreEqual(customer1.Mails[1], cloneCustomer.Mails[1]);
 
            Assert.AreEqual(customer1.Adresses[0].City   , cloneCustomer.Adresses[0].City);
            Assert.AreEqual(customer1.Adresses[0].Street , cloneCustomer.Adresses[0].Street);
            Assert.AreEqual(customer1.Adresses[0].ZipCode, cloneCustomer.Adresses[0].ZipCode);
            Assert.AreEqual(customer1.Adresses[1].City   , cloneCustomer.Adresses[1].City);
            Assert.AreEqual(customer1.Adresses[1].Street , cloneCustomer.Adresses[1].Street);
            Assert.AreEqual(customer1.Adresses[1].ZipCode, cloneCustomer.Adresses[1].ZipCode);
 
            Assert.AreEqual(customer1.ReadOnlyField, cloneCustomer.ReadOnlyField);
 
            /// Change values in clone object for validate objects don't linked
            cloneCustomer.Adress.City = "Changed City";
            Assert.AreNotEqual(customer1.Adress.City, cloneCustomer.Adress.City);
 
            cloneCustomer.Mails[0] = "mailchanged@aa.com";
            Assert.AreNotEqual(customer1.Mails[0], cloneCustomer.Mails[0]);
 
            cloneCustomer.Adresses[0].Street = "New Street";
            Assert.AreNotEqual(customer1.Adresses[0].Street, cloneCustomer.Adresses[0].Street);
        }
    }
}


Muy fácil, muy sencillo y con un millón de posiblidades.




CloneExtensions

CloneExtensions es una librería de uso extremadamente simple, muy rápido ya que está basada en Expressions Trees. Según la documentación del autor el primer clonado por tipo es un poco más lento ya que tiene recorrer las propiedades con Reflection, pero las siguientes ya son inmediatas. En la práctica va como un tiro, no se aprecia prácticamente.


INSTALACIÓN

La instalaremos mediante Nuget:





















Vista de las referencias:











Ejemplo de CloneExtensions:


using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using CloneLib;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using CloneExtensions;
 
namespace Clone.Tests
{
    [TestClass]
    public class CloneExtensionsTests
    {
        [TestMethod]
        public void NuclexReflectorClone()
        {
            var customer1 = new Customer
            {
                ID        = 1,
                Name      = "Test",
                EntryDate = DateTime.Today,
                Sales     = 1000m,
 
                Adress = new Address { Street = "One street", City = "City", ZipCode = 2222 },
 
                Mails = new Collection<string>() { "a@b.com", "b@c.com" },
 
                Adresses = new List<Address>
                {
                    new Address { City = "aaa", Street = "bbb", ZipCode = 111  },
                    new Address { City = "ddd", Street = "eee", ZipCode = 222  }
                }
            };
 
            var cloneCustomer = customer1.GetClone();
 
            cloneCustomer.Adress.City = "New city";
 
            Assert.AreNotEqual(customer1, cloneCustomer);
 
            Assert.AreEqual(customer1.ID        , cloneCustomer.ID);
            Assert.AreEqual(customer1.Name      , cloneCustomer.Name);
            Assert.AreEqual(customer1.EntryDate , cloneCustomer.EntryDate);
            Assert.AreEqual(customer1.Sales     , cloneCustomer.Sales);
 
            Assert.AreEqual(customer1.Mails[0], cloneCustomer.Mails[0]);
            Assert.AreEqual(customer1.Mails[1], cloneCustomer.Mails[1]);
 
            Assert.AreEqual(customer1.Adresses[0].City   , cloneCustomer.Adresses[0].City);
            Assert.AreEqual(customer1.Adresses[0].Street , cloneCustomer.Adresses[0].Street);
            Assert.AreEqual(customer1.Adresses[0].ZipCode, cloneCustomer.Adresses[0].ZipCode);
            Assert.AreEqual(customer1.Adresses[1].City   , cloneCustomer.Adresses[1].City);
            Assert.AreEqual(customer1.Adresses[1].Street , cloneCustomer.Adresses[1].Street);
            Assert.AreEqual(customer1.Adresses[1].ZipCode, cloneCustomer.Adresses[1].ZipCode);
 
            Assert.AreEqual(customer1.ReadOnlyField, cloneCustomer.ReadOnlyField);
 
            /// Change values in clone object for validate objects don't linked
            cloneCustomer.Adress.City = "Changed City";
            Assert.AreNotEqual(customer1.Adress.City, cloneCustomer.Adress.City);
 
            cloneCustomer.Mails[0] = "mailchanged@aa.com";
            Assert.AreNotEqual(customer1.Mails[0], cloneCustomer.Mails[0]);
 
            cloneCustomer.Adresses[0].Street = "New Street";
            Assert.AreNotEqual(customer1.Adresses[0].Street, cloneCustomer.Adresses[0].Street);
        }
    }
}


De Nuevo otra solución, fácil, rápida y sencilla.





Conclusión

Si tú lo que necesitas es hacer un clonado sencillo y sin partes demasiado especiales, cualquiera de estas dos librerías son tu solución. En caso de necesitar clonar algún tipo con una determinada estructura, yo optaría por los métodos más tradicionales de la primera parte del artículo.



Felicitaciones

Felicitaciones a Marcin Juraszek (Nuclex) y a Jun Wei Lee (CloneExtensions) por su EXTRAORDINARIO TRABAJO.