Thursday, 17 December 2009

But Paulo, I don't want to clone, I need to copy properties from one object to another

Always moaning aren't you? Well, we can do that as well....
And hopefully, with minimal fuss... (now would be a good time to read about reflection)

It is a simplistic approach, but what we want to do is to copy the properties in one object to another object that may or may not have said properties. Maybe we can use the property name as a match...

Lets try that approach.
This is what we want to do:
AnotherPerson anotherPerson = person.Copy<Person,AnotherPerson>();
And this is one of the ways we can do it:
public static TTo Copy<TFrom,TTo>(this TFrom toCopyFrom) where TTo : new()
{
Type typeTo = typeof(TTo);
Type typeFrom = typeof(TFrom);
TTo instanceToCopyTo = new TTo();

//DeepClone the from object so you don't get reference copies.
//Came in handy, didn't it?
TFrom cloneOfFrom = toCopyFrom.DeepClone();

//For each property on the destination object.
PropertyInfo[] toProperties = typeTo.GetProperties();
foreach (PropertyInfo toProperty in toProperties.Where(toProperty => typeFrom.GetProperty(toProperty.Name) != null))
{
//set the value on the destination object.
toProperty.SetValue(
instanceToCopyTo,
typeFrom.GetProperty(toProperty.Name).GetValue(cloneOfFrom, null),
null);
}

return instanceToCopyTo;
}

Create these classes on your Test class:
public class Person
{
public string Name { get; set; }

public DateTime DateOfBirth { get; set; }

public List<Person> Siblings { get; set; }
}

public class AnotherPerson
{
public string Name { get; set; }

public List<Person> Siblings { get; set; }

public int ANonMatchingProperty { get; set; }
}

And the Test:
[TestMethod]
public void CopyTest()
{
Person person =
new Person
{
Name = "Paulo Serra",
DateOfBirth = new DateTime(1972, 4, 2),
Siblings = new List<Person>
{
new Person
{
Name = "Andre Serra",
DateOfBirth = new DateTime(2002, 3, 27)
}
}
};

AnotherPerson anotherPerson = person.Copy<Person,AnotherPerson>();

Assert.AreEqual(person.Name, anotherPerson.Name);
Assert.AreNotSame(person.Siblings[0], anotherPerson.Siblings[0]);
Assert.AreEqual(person.Siblings[0].Name, anotherPerson.Siblings[0].Name);
}

PS: Thanks to the Resharper 5 team to remind me I can do a Where inline on a foreach :P

For those that are paying attention, at the moment the copier only works because the AnotherPerson class defines the Siblings as Person, not AnotherPerson.

Well, guess we can solve that tomorrow, time to go for drinks.

Easy, type safe, Object deep cloning

I tend to find amazing the pain that some people go thru to deep clone objects. From non generic methods that copy only one object, to recursive reflection based function calls (these tend to be more generic than the previous), implementing IClonable (so .Net 1.1), to other sightly more obscure methods....

One of the easiest ways to deep clone a object is to (try to) serialize it and immediately desirialize it. But, as explained in this excellent post, this approach seems to also have disadvantages.

.Net 3.5 to the rescue. Using the DataContractSerializer actually gets rid of some of the mentioned problems. The object does not have to be marked as serializable, and you can always mark your object as a DataContract if you feel the need to set non public properties or fields.
Since .Net 3.5, the data contract serializer will accept any POCO, not just DataContracts as MSDN claims.

Ok, 2 problems solved, but can we make type safe and generic? Yes, we can. Extension Methods anyone?
Using extension methods and generics we can easily create a type safe cloner that can attach itself to any type.

This is what we want to do:
Person toClone = new Person()

// Set stuff here.

Person cloned = toClone.DeepClone();

A very simple approach:
using System.IO;
using System.Runtime.Serialization;

namespace Extensions
{
    public static class CloningExtensions
    {
        public static T DeepClone<T>(this T toClone)
        {
            using (MemoryStream stream = new MemoryStream())
            {
                DataContractSerializer serializer = new DataContractSerializer(typeof(T));
                serializer.WriteObject(stream, toClone);
                stream.Position = 0;
                return (T)serializer.ReadObject(stream);
            }
        }
    }
}
The test:
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Extensions;

namespace CloningExtensionsTests
{
    [TestClass]
    public class ExtensionsTests
    {
        public class Person
        {
            public string Name { get; set; }

            public DateTime DateOfBirth { get; set; }

            public List<Person> Siblings { get; set; }
        }

        [TestMethod]
        public void DeepCloneTest()
        {
            Person toClone =
                new Person
                    {
                        Name = "Paulo Serra",
                        DateOfBirth = new DateTime(1972, 4, 2),
                        Siblings = new List<Person>
                                       {
                                           new Person
                                               {
                                                   Name = "Andre Serra",
                                                   DateOfBirth = new DateTime(2002, 3, 27)
                                               }
                                       }
                    };

            Person cloned = toClone.DeepClone();

            // Object is not an object reference
            Assert.AreNotSame(toClone, cloned);

            // Properties were cloned
            Assert.AreEqual(toClone.Name, cloned.Name);
            Assert.AreEqual(toClone.DateOfBirth, cloned.DateOfBirth);

            // Inner object is not an object reference.
            Assert.AreNotSame(toClone.Siblings, cloned.Siblings);

            // Inner object properties were cloned.
            Assert.AreEqual(toClone.Siblings[0].Name, cloned.Siblings[0].Name);
            Assert.AreEqual(toClone.Siblings[0].DateOfBirth, cloned.Siblings[0].DateOfBirth);
        }
    }
}

Hope this post has been useful to somebody, be back shortly for another one ;)