6

I'm trying to deep clone an object which contains a System.Random variable. My application must be deterministic and so I need to capture the the random object state. My project is based on .Net Core 2.0.

I'm using some deep clone code from here (How do you do a deep copy of an object in .NET (C# specifically)?) which uses serialization.

The documentation for System.Random is mixed:

Serializable

Not Serializable

and I get the following error.

System.Runtime.Serialization.SerializationException HResult=0x8013150C Message=Type 'System.Random' in Assembly 'System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e' is not marked as serializable. Source=System.Runtime.Serialization.Formatters

Can System.Random it be cloned in the way I want?

I created a small program to illustrate.

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

namespace RandomTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Container C = new Container();
            Container CopyC = DeepClone(C);
        }

        public static T DeepClone<T>(T obj)
        {
            using(var ms = new MemoryStream())
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(ms, obj); //<-- error here
                ms.Position = 0;

                return (T)formatter.Deserialize(ms);
            }
        }
    }

    [Serializable]
    public class Container
    {
        public ObjectType AnObject;
        public Container()
        {
            AnObject = new ObjectType();
        }
    }

    [Serializable]
    public class ObjectType
    {
        //[NonSerialized]  // Uncommenting here removes the error
        internal System.Random R;
    }
}

I probably don't need the Container object, but this structure more closely resembles my application.

Making R [NonSerialized] removes the error but I don't get my Random object back after deserialization. I tried re-creating the random object, but it starts a new random sequence and so breaks the deterministic requirement.

xareth
  • 112
  • 9
  • I dont get an exception (.NET 4.6.2) – Tim Schmelter Dec 11 '17 at 10:09
  • 3
    Why not write your own version of `System.Random` and use that instead? There are plenty of options for writing a PRNG available. You could even directly copy the relevant parts of underlying [C# source code](https://referencesource.microsoft.com/#mscorlib/system/random.cs). – rossum Dec 11 '17 at 10:55
  • I've now copied the Random class from http://referencesource.microsoft.com/#mscorlib/system/random.cs,bb77e610694e64ca and it seems to work, just had to comment out some Environment.GetResourceString errors. Why doesn't it work from the bundled System.Random? – xareth Dec 11 '17 at 10:58
  • 1
    Because in .NET Core it's not marked as serializable – Evk Dec 11 '17 at 11:06
  • @Evk I was having a hard time believing it [but you're right](https://github.com/dotnet/coreclr/blob/6c4172449dd5d1ab55c543dd37843d4decb5aa3f/src/mscorlib/shared/System/Random.cs). Took me forever to find the source on Github – xareth Dec 11 '17 at 11:45
  • Seems it was consciously removed. https://github.com/dotnet/corefx/issues/19119 – xareth Dec 11 '17 at 12:40
  • Well, sounds reasonable from their perspective. Marking Random Serializable makes for them hard to change its internals, because you can serialize it, then they change some private fields, then you will not be able to deserialize back (or worse - deserialize incorrectly). Anyway, I have never ever used BinaryFormatter in real code, and not going to in future. – Evk Dec 11 '17 at 12:47
  • Thanks for the explanation. I guess it makes sense. One wonders how the .Net Framework handles this. Never change the code or don't care about breaking de-serialization. – xareth Dec 11 '17 at 13:02
  • There is not much point in serializing a random number generator. Not like you can't create another one when you deserialize that is just as good. Heck, better, at least it will continue to be random. Applying the [NonSerializable] attribute is plenty good enough. – Hans Passant Dec 11 '17 at 13:22
  • @Hans Passant. My application must be deterministic. That means the process of [Random(seed) > Execute Code > Serialize > De-Serialize > Execute Code > Result] must produce the same result as [Random(seed) > Execute Code > Result]. Saving the random number generator state is crucial to this process. – xareth Dec 11 '17 at 13:49

2 Answers2

1

You could use JSON.NET to do this.

Use Project | Manage NuGet Packages to add "Newtonsoft.Json" (latest stable version 10.0.3) to your project.

Then you can write a Cloner class that uses Json.NET to clone an object:

public static class Cloner
{
    public static T Clone<T>(T source)
    {
        if (ReferenceEquals(source, null))
            return default(T);

        var settings = new JsonSerializerSettings { ContractResolver = new ContractResolver() };

        return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source, settings), settings);
    }

    class ContractResolver : DefaultContractResolver
    {
        protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
        {
            var props = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                .Select(p => base.CreateProperty(p, memberSerialization))
                .Union(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                    .Select(f => base.CreateProperty(f, memberSerialization)))
                .ToList();
            props.ForEach(p => { p.Writable = true; p.Readable = true; });
            return props;
        }
    }
}

Then you can write some code like so:

var inner = new ObjectType {R = new Random(12345)};
var outer = new Container  {AnObject = inner};

var clone = Cloner.Clone(outer);

Console.WriteLine(clone.AnObject.R.Next()); // Prints 143337951
Console.WriteLine(outer.AnObject.R.Next()); // Also prints 143337951
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • This worked for my test program above. As a bonus, I'm already using JSON.NET in my application. Unfortunately, I've run straight into another issue there. But I've got a serialized Random class now. – xareth Dec 11 '17 at 12:20
0

It seems that it' is possible to serialize & deserialize random class by writing custom serializer and using reflection. Please Check.

Here is your code sample. It needs some refactoring of course. I just added here to show it' s working.

class Program
{
    static void Main(string[] args)
    {
        Container C = new Container();

        Console.WriteLine(C.AnObject.R.Next());

        Container CopyC = DeepClone(C);

        Console.WriteLine(C.AnObject.R.Next());
        Console.WriteLine(CopyC.AnObject.R.Next());
    }

    public static T DeepClone<T>(T obj)
    {
        using (var ms = new MemoryStream())
        {
            var formatter = new BinaryFormatter();
            formatter.Serialize(ms, obj); //<-- error here
            ms.Position = 0;

            return (T)formatter.Deserialize(ms);
        }
    }
}

[Serializable]
public class Container
{
    public ObjectType AnObject;
    public Container()
    {
        AnObject = new ObjectType
        {
            R = new Random()
        };
    }
}

[Serializable]
public class ObjectType : ISerializable
{

    internal Random R;

    public ObjectType() { }
    protected ObjectType(SerializationInfo info, StreamingContext context)
    {
        R = new Random();
        var binaryFormatter = new BinaryFormatter();
        using (var temp = new MemoryStream(Encoding.BigEndianUnicode.GetBytes(info.GetString("_seedArr"))))
        {
            var arr = (int[])binaryFormatter.Deserialize(temp);
            R.GetType().GetField("_seedArray", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue(R, arr);               
        }

        R.GetType().GetField("_inext", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue(R, info.GetInt32("_inext"));
        R.GetType().GetField("_inextp", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue(R, info.GetInt32("_inextp"));
    }       

    public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        var binaryFormatter = new BinaryFormatter();

        var arr = R.GetType().GetField("_seedArray", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

        using (var temp = new MemoryStream())
        {
            binaryFormatter.Serialize(temp, arr.GetValue(R));

            info.AddValue("_seedArr", Encoding.BigEndianUnicode.GetString(temp.ToArray())); 
        }

        info.AddValue("_inext", R.GetType().GetField("_inext", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(R));
        info.AddValue("_inextp", R.GetType().GetField("_inextp", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(R));
    }

}
Mutlu Kaya
  • 129
  • 7
  • Note that you should also serialize other fields of Random, not just `_seedArray`. – Evk Dec 11 '17 at 11:48
  • @Evk, you are right, i just wanted to show how it would work. – Mutlu Kaya Dec 11 '17 at 16:31
  • I understand, but this is dangerous in this case, because someone can copy paste this code and in many cases it will work, creating illusion its correct. – Evk Dec 11 '17 at 16:33
  • again, I agree with you :) i will edit my code with fully working version soon. – Mutlu Kaya Dec 11 '17 at 16:35