Click here to Skip to main content
13,193,551 members (50,061 online)
Click here to Skip to main content
Add your own
alternative version

Stats

58.5K views
508 downloads
39 bookmarked
Posted 9 Jan 2009

A Generic Method for Deep Cloning in C# 3.0

, 9 Jan 2009
Rate this:
Please Sign up or sign in to vote.
A method for deep cloning objects in C#, by using Surrogates and serialization formatters

Introduction

Recently I was asked to provide a serialization mechanism for objects that are not marked as serializable. The solution is quite simple when using SurrogateSelectors. This solution is also useful in providing a deep cloning utility.

Solution

All that's needed is, as mentioned above, a SurrogateSelector.

The .NET Remoting framework has a lot of extension points. By using an ISerializationSurrogate we are able to create an object that can tell the formmater what kind of object should be serialized.

An ISerializationSurrogate offers the following methods

Name Description
GetObjectData Populates the provided SerializationInfo with the data needed to serialize the object.
SetObjectData Populates the object using the information in the SerializationInfo.

We implement this method by using reflection on the objects we are asked to serialize.

public void GetObjectData(object obj,
    System.Runtime.Serialization.SerializationInfo info,
    System.Runtime.Serialization.StreamingContext context)
{
    FieldInfo[] fieldInfos = obj.GetType().GetFields(BindingFlags.Instance |
        BindingFlags.Public | BindingFlags.NonPublic);
    foreach (var fi in fieldInfos)
    {
        if (IsKnownType(fi.FieldType)
            )
        {
            info.AddValue(fi.Name, fi.GetValue(obj));
        }
        else
            if (fi.FieldType.IsClass)
            {
                info.AddValue(fi.Name, fi.GetValue(obj));
            }
    }
}

All I'm doing is to get all the fields and call info.AddValue. Saving the object's information is the easy part. The tricky part is retrieving it. I'm calling it tricky because we need to pay special care for Nullable types. I'm showing you the implmentation, and I'll try to expand a bit on it.

public object SetObjectData(object obj,
    System.Runtime.Serialization.SerializationInfo info,
    System.Runtime.Serialization.StreamingContext context,
    System.Runtime.Serialization.ISurrogateSelector selector)
{
    FieldInfo[] fieldInfos = obj.GetType().GetFields(BindingFlags.Instance |
        BindingFlags.Public | BindingFlags.NonPublic);
    foreach (var fi in fieldInfos)
    {
        if (IsKnownType(fi.FieldType))
        {
            //var value = info.GetValue(fi.Name, fi.FieldType);

            if (IsNullableType(fi.FieldType))
            {
                // Nullable<argumentValue>
                Type argumentValueForTheNullableType = GetFirstArgumentOfGenericType(
                   fi.FieldType);//fi.FieldType.GetGenericArguments()[0];
                fi.SetValue(obj, info.GetValue(fi.Name, argumentValueForTheNullableType));
            }
            else

            {
                fi.SetValue(obj, info.GetValue(fi.Name, fi.FieldType));
            }

        }
        else
            if (fi.FieldType.IsClass)

            {
                fi.SetValue(obj, info.GetValue(fi.Name, fi.FieldType));
            }
    }

    return obj;


    private Type GetFirstArgumentOfGenericType(Type type)
    {
        return type.GetGenericArguments()[0];
    }

    private bool IsNullableType(Type type)
    {
        if (type.IsGenericType)
            return type.GetGenericTypeDefinition() == typeof(Nullable<>);
        return false;
    }

What the last two functions do is to

  1. Determine if the type is an extension of Nullable<>

    By default when we write int? the compiler generates a Nullable<int> in the background for us.

  2. Get the type information for what's contained in that Nullable<> instance and load it's value from the serializationInfo object we were passed by the formmater.

From the implementation of SetObjectData you can see that I'm using reflection like before to retrieve the values stored in the SerializationInfo object. This way we should, and are able, to deserialize any object that doesn't have the SerializableAttribute.

All we need now is a way to use our surrogate for any and all types. And so we need to provide a way of choosing our implementation of the surrogate selector for all types without explicitly specifying it. And the way we are doing it, is by means of an ISurrogateSelector. The ISurrogateSelector interface exposes the following methods

Methods

Name Description
ChainSelector Specifies the next ISurrogateSelectorfor surrogates to examine if the current instance does not have a surrogate for the specified type and assembly in the specified context.
GetNextSelector Returns the next surrogate selector in the chain.
GetSurrogate Finds the surrogate that represents the specified object's type, starting with the specified surrogate selector for the specified serialization context.

The following piece of code shows how I choose to implement this interface.

#region ISurrogateSelector Members
            // This is what we'll use to hold the _nextSelector in the chain
            System.Runtime.Serialization.ISurrogateSelector _nextSelector;
            /// <span class="code-SummaryComment"><summary></span>
            /// Sets the selector
            /// <span class="code-SummaryComment"></summary></span>
            
            public void ChainSelector(
                System.Runtime.Serialization.ISurrogateSelector selector)
            {
                  this._nextSelector = selector;
            }
            
/// <span class="code-SummaryComment"><summary></span>
/// Gets the next selectr from the chain
/// <span class="code-SummaryComment"></summary></span>
public System.Runtime.Serialization.ISurrogateSelector GetNextSelector()
            {
                  return _nextSelector;
            }

Now we need to choose wich objects we are going to seralize. IsKnownType basically asks if the object is an object that has the SerializableAttribute applied, or whatever known types you don't want to save.

The next part of the GetSurrogate method chooses the instance of this class as the serialization selector. If the return value is set to null, than the next selector in the chain is chosen by the formatter (be it Soap, Binary, or other open source implementations like OpenNX or CompactFrameworkFormmater).

public System.Runtime.Serialization.ISerializationSurrogate GetSurrogate(
    Type type, System.Runtime.Serialization.StreamingContext context,
    out System.Runtime.Serialization.ISurrogateSelector selector)
{
      if (IsKnownType(type))
      {
            selector = null;
            return null;
      }
      else if (type.IsClass || type.IsValueType)
      {
            selector = this;
            return this;
      }
      else
      {
            selector = null;
            return null;
      }
}

#endregion

Follwing is the complete code for the NonSerialiazableTypeSurrogateSelector that I've been explaining.

/// <span class="code-SummaryComment"><summary></span>
/// This class offers the ability to save the fields
/// of types that don't have the <span class="code-SummaryComment"><c ref="System.SerializableAttribute"></span>
/// SerializableAttribute<span class="code-SummaryComment"></c>.</span>
/// <span class="code-SummaryComment"></summary></span>

      public class NonSerialiazableTypeSurrogateSelector : 
          System.Runtime.Serialization.ISerializationSurrogate,
          System.Runtime.Serialization.ISurrogateSelector
      {
#region ISerializationSurrogate Members

          public void GetObjectData(object obj,
              System.Runtime.Serialization.SerializationInfo info,
              System.Runtime.Serialization.StreamingContext context)
          {
              FieldInfo[] fieldInfos = obj.GetType().GetFields(BindingFlags.Instance |
                  BindingFlags.Public | BindingFlags.NonPublic);
              foreach (var fi in fieldInfos)
              {
                  if (IsKnownType(fi.FieldType)
                      )
                  {
                      info.AddValue(fi.Name, fi.GetValue(obj));
                  }
                  else
                      if (fi.FieldType.IsClass)
                      {
                          info.AddValue(fi.Name, fi.GetValue(obj));
                      }
              }
          }

          public object SetObjectData(object obj,
              System.Runtime.Serialization.SerializationInfo info,
              System.Runtime.Serialization.StreamingContext context,
              System.Runtime.Serialization.ISurrogateSelector selector)
          {
              FieldInfo[] fieldInfos = obj.GetType().GetFields(
                  BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

              foreach (var fi in fieldInfos)
              {
                  if (IsKnownType(fi.FieldType))
                  {
                      //var value = info.GetValue(fi.Name, fi.FieldType);

                      if (IsNullableType(fi.FieldType))
                      {
                          // Nullable<argumentValue>
                          Type argumentValueForTheNullableType = 
                              GetFirstArgumentOfGenericType(
                              fi.FieldType);//fi.FieldType.GetGenericArguments()[0];
                          fi.SetValue(obj, info.GetValue(fi.Name,
                              argumentValueForTheNullableType));
                      }
                      else
                      {
                          fi.SetValue(obj, info.GetValue(fi.Name, fi.FieldType));
                      }

                  }
                  else
                      if (fi.FieldType.IsClass)
                      {
                          fi.SetValue(obj, info.GetValue(fi.Name, fi.FieldType));
                      }
              }

              return obj;
          }
          private Type GetFirstArgumentOfGenericType(Type type)
          {
              return type.GetGenericArguments()[0];
          }
          private bool IsNullableType(Type type)
          {
              if (type.IsGenericType)

                  return type.GetGenericTypeDefinition() == typeof(Nullable<>);
              return false;
          }
          private bool IsKnownType(Type type)
          {
              return
                  type == typeof(string)
                  || type.IsPrimitive
                  || type.IsSerializable
                  ;
          }
#endregion

#region ISurrogateSelector Members
          System.Runtime.Serialization.ISurrogateSelector _nextSelector;
          public void ChainSelector(
              System.Runtime.Serialization.ISurrogateSelector selector)
          {
              this._nextSelector = selector;
          }

          public System.Runtime.Serialization.ISurrogateSelector GetNextSelector()
          {
              return _nextSelector;
          }

          public System.Runtime.Serialization.ISerializationSurrogate GetSurrogate(
              Type type, System.Runtime.Serialization.StreamingContext context,
              out System.Runtime.Serialization.ISurrogateSelector selector)
          {
              if (IsKnownType(type))
              {
                  selector = null;
                  return null;
              }
              else if (type.IsClass || type.IsValueType)
              {
                  selector = this;
                  return this;
              }
              else
              {
                  selector = null;
                  return null;
              }
          }

#endregion
      }

Surrogates are used by the serializer to provide information about how/what should be serialized from a certain instance of an object. So the last piece for cloning the object is this, a clone method extension.

The clone method is a generic method, but since C# 3.0 nows how to do type inference, you can use int a = 10, a.Clone() without explicitly asking for the result to be an int like so a.Clone<int>().

using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using CloningExtension;

namespace System
{
    public static class DeepCloning
    {
        public static T Clone<T>(this T obj)
        {
            IFormatter formatter = new BinaryFormatter();
            formatter.SurrogateSelector = new SurrogateSelector();
            formatter.SurrogateSelector.ChainSelector(
                new NonSerialiazableTypeSurrogateSelector());
            var ms = new MemoryStream();
            formatter.Serialize(ms, obj);
            ms.Position = 0;
            return (T)formatter.Deserialize(ms);
        }
    }
}

Now I'm tieing together the last pieces. I've used BinaryFormatter, but using an IoC container should work too if you need it to provide another formatter like the ones I specified at the beginning (OpenNX, CF, Soap) by means of configuration.

As you can see the first selector in the chain is SurrogateSelector. This is used to prevent calling on the NonSerialiazableTypeSurrogateSelector everytime we hit a class. It also gives you the ability to register other Surrogates before reaching the NonSerialiazableTypeSurrogateSelector.

Testing

Nothing is bulletproof, so I've tested this class on some simple to intermediate cases. Please test this against your own code, before using it.

[TestMethod]
public void Should_clone_primitive_types()
{
    int aInt = 10;
    int bInt = aInt.Clone();
    bInt.ShouldEqual(aInt);
    bInt.ShouldNotBeSameAs(aInt);

    string aString = "Another string";
    string bString = aString.Clone<string>();

    bString.ShouldEqual(aString);
    bString.ShouldNotBeSameAs(aString);

    float aFloat = 1.0f;
    float bFloat = aFloat.Clone();
    bFloat.ShouldEqual(aFloat);
    bFloat.ShouldNotBeSameAs(aFloat);

}

[TestMethod]
public void Should_clone_nullable_types()
{

    decimal? aDecimal = 1.2m;
    decimal? bDecimal = aDecimal.Clone();
    bDecimal.ShouldEqual(aDecimal);
    bDecimal.ShouldNotBeSameAs(aDecimal);
}

public class UsernameExample
{
    public string Username { get; set; }
}
public class UsernameExampleHolder
{
    public UsernameExample UsernameExample { get; set; }
}

[TestMethod]
public void Should_clone_a_graph_of_objects()
{
    var aUeh = new UsernameExampleHolder
    {
        UsernameExample = new UsernameExample
        {
            Username = "someUser"
        }
    };
    var bUeh = aUeh.Clone();
    bUeh.UsernameExample.Username.ShouldEqual(aUeh.UsernameExample.Username);
    bUeh.ShouldNotBeSameAs(aUeh);

}

[TestMethod]
public void Should_serialiaze_a_simple_list_of_primitives()
{
    var aList = new List<double>();
    for (var step = 0.0; step < 100.0; step += 0.1)
    {
        aList.Add(step);
    }
    var bList = aList.Clone();
    bList.ShouldNotBeSameAs(aList);
    for (var listIndex = 0; listIndex < aList.Count; listIndex++)
    {
        aList[listIndex].ShouldEqual(bList[listIndex]);
    }
}

[TestMethod]
public void Should_serialize_a_dictionary_of_values()
{
    var aDict = new Dictionary<string, List<char>>();

    aDict.Add("a", new char[] { 'a', 'b', 'c' }.ToList());
    aDict.Add("abra", new char[] { 'a', 'b', 'c' }.ToList());
    aDict.Add("cadabra", new char[] { 'a', 'b', 'c' }.ToList());

    var bDict = aDict.Clone();
    bDict.ShouldNotBeSameAs(aDict);
    bDict.Keys.Count.ShouldBe(aDict.Keys.Count);

}

Notes

The surrogate uses reflection. Even if you add dynamic invocation using the delegate method, withouth some sort of caching I doubt you'll achieve much from a performance point of view. It's still a simple method and can be improved.

Bibliography

http://msdn.microsoft.com/en-us/library/system.runtime.serialization.surrogateselector.aspx

If you find this article useful, like or dislike something please comment upon it. It is the best way for everybody to learn something new.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Sharpoverride
Software Developer (Junior)
Romania Romania
I'm an average programmer, that hopes to be of some help to others.

Moto
------------------------------------------------------
The road to becoming a good programmer is paved with bad scripts.

You may also be interested in...

Pro

Comments and Discussions

 
GeneralProblem with a class containing events [modified] Pin
Anindya Chatterjee3-Jan-11 7:14
memberAnindya Chatterjee3-Jan-11 7:14 
GeneralRe: Problem with a class containing events Pin
Anindya Chatterjee3-Jan-11 7:46
memberAnindya Chatterjee3-Jan-11 7:46 
GeneralProblems Pin
Andy Lang9-Sep-09 22:02
memberAndy Lang9-Sep-09 22:02 
GeneralRe: Problems Pin
aleha12314-Jan-14 2:03
memberaleha12314-Jan-14 2:03 
GeneralGetFields won't work with nested types Pin
kamyk7-Jun-09 22:56
memberkamyk7-Jun-09 22:56 
GeneralRe: GetFields won't work with nested types Pin
Sharpoverride8-Jun-09 1:36
memberSharpoverride8-Jun-09 1:36 
QuestionThe object with ID...was referenced in a fixup but does not exist. Pin
BarrySchulz11116-May-09 8:32
memberBarrySchulz11116-May-09 8:32 
AnswerRe: The object with ID...was referenced in a fixup but does not exist. [modified] Pin
Sharpoverride16-May-09 19:34
memberSharpoverride16-May-09 19:34 
AnswerRe: The object with ID...was referenced in a fixup but does not exist. Pin
Sharpoverride27-May-09 0:50
memberSharpoverride27-May-09 0:50 
GeneralRe: The object with ID...was referenced in a fixup but does not exist. Pin
BarrySchulz11127-May-09 5:30
memberBarrySchulz11127-May-09 5:30 
GeneralRe: The object with ID...was referenced in a fixup but does not exist. [modified] Pin
Sharpoverride28-May-09 8:43
memberSharpoverride28-May-09 8:43 
QuestionBUG ? Pin
kamyk17-Apr-09 8:53
memberkamyk17-Apr-09 8:53 
AnswerRe: BUG ? Pin
Sharpoverride23-Apr-09 2:04
memberSharpoverride23-Apr-09 2:04 
AnswerRe: BUG ? Pin
kamyk23-Apr-09 6:42
memberkamyk23-Apr-09 6:42 
GeneralIt unfortunately does not work :-( Pin
Reiner Block12-Mar-09 3:08
memberReiner Block12-Mar-09 3:08 
GeneralRe: It unfortunately does not work :-( [modified] Pin
Sharpoverride12-Mar-09 3:42
memberSharpoverride12-Mar-09 3:42 
GeneralRe: It unfortunately does not work :-( Pin
Sharpoverride23-Apr-09 1:57
memberSharpoverride23-Apr-09 1:57 
GeneralInteresting! Pin
hughd11-Jan-09 1:07
memberhughd11-Jan-09 1:07 
GeneralRe: Interesting! Pin
Sharpoverride11-Jan-09 7:38
memberSharpoverride11-Jan-09 7:38 
GeneralRe: Interesting! Pin
Nate Trimble13-Jan-09 8:58
memberNate Trimble13-Jan-09 8:58 
GeneralRe: Interesting! Pin
Sharpoverride13-Jan-09 23:29
memberSharpoverride13-Jan-09 23:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.171018.2 | Last Updated 9 Jan 2009
Article Copyright 2009 by Sharpoverride
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid