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))
{
if (IsNullableType(fi.FieldType))
{
Type argumentValueForTheNullableType = GetFirstArgumentOfGenericType(
fi.FieldType);
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
- 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.
- 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
System.Runtime.Serialization.ISurrogateSelector _nextSelector;
public void ChainSelector(
System.Runtime.Serialization.ISurrogateSelector selector)
{
this._nextSelector = selector;
}
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.
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))
{
if (IsNullableType(fi.FieldType))
{
Type argumentValueForTheNullableType =
GetFirstArgumentOfGenericType(
fi.FieldType);
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.