Home > Back-end >  Deep copy .NET class object
Deep copy .NET class object

Time:07-20

I want to write a generic method that returns a deep copy for a given object:

TTEntity CreateDeepCopy<TTEntity>(TTEntity obj) where TTEntity : MyBaseEntity

There are some constraints:

  • I cannot implement copy constructors or custom interfaces for each new class that derives from MyBaseEntity (or anything that involves writing code in each new class added in project)
  • I cannot use Binary Formatter because is obsolete
  • I tried XmlSerializer but MyBaseEntity has a Dictionary and it throws an error:
Cannot serialize member {...} of type System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Boolean, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], because it implements IDictionary.

CodePudding user response:

Usually I prefer to use a simple Json serialization to deep clone an object because by default you don't have to decorate classes with attribute to make serialization and deserialization working. Protobuf and MessagePack are faster and have a smaller memory footprint but the serialization result is less readable (also MessagePack can be used without decorating classes but with this options it serializes the message in Json format).

Here's an example using System.Text.Json:

TTEntity CreateDeepCopy<TTEntity>(TTEntity obj) where TTEntity : MyBaseEntity
{
    if (obj == null)
    {
        return null;
    }

    var serialized = System.Text.Json.JsonSerializer.Serialize(obj, new System.Text.Json.JsonSerializerOptions()
    {
        DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
        WriteIndented = false,
        Converters = {
            new System.Text.Json.Serialization.JsonStringEnumConverter()
        },
        IncludeFields = true
    });

    return System.Text.Json.JsonSerializer.Deserialize<TTEntity>(serialized);
}

Here using Newtonsoft.Json:

TTEntity CreateDeepCopy<TTEntity>(TTEntity obj) where TTEntity : MyBaseEntity
{
    if (obj == null)
    {
        return null;
    }

    var jsonSettings = new Newtonsoft.Json.JsonSerializerSettings()
    {
        NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore,
        Formatting = Newtonsoft.Json.Formatting.None,
    };
    jsonSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());

    var serialized = Newtonsoft.Json.JsonConvert.SerializeObject(obj, jsonSettings);

    return System.Text.Json.JsonSerializer.Deserialize<TTEntity>(serialized);
}

Note: Newtonsoft.Json.JsonSerializerSettings and System.Text.Json.JsonSerializerOptions are best defined only once: create a singleton class or register your class as a singleton within dependency injection container.

CodePudding user response:

In preference to BinaryFormatter, I would suggest you try MessagePack. https://github.com/neuecc/MessagePack-CSharp

Here is a simple unit test I created to verify that an object replicated via MessagePack serialize/deserialize matched the original object.

[Fact]
public void TestMessagePack()
{
    var options = MessagePack.Resolvers.ContractlessStandardResolver.Options;
    var ms = new MemoryStream();
    
    MessagePackSerializer.Serialize(ms, _testObject, options);
    
    ms.Position = 0;

    var result = MessagePackSerializer.Deserialize<TestObject>(ms, options);
    
    result.Should().BeEquivalentTo(_testObject);
    
    ms.Close();
}

In the above example _testObject was a complex object with multiple arrays, collections and nested objects.

CodePudding user response:

As an alternative to Ben's answer and ale91's answer (the concept of which, i.e using a production ready package, I highly suggest) is handrolling your own using reflection.

Basically, we need to

  1. Get every property we want to clone
  2. Create a copy of that property
  3. Assign it to the (new) object

Step 2 is a pain, since not all types can easily be clone, i.e collections like List and Dictionary are harder to clone as well as reference types having to go through the cloning process as well until we reach only primitives/ value types. Another problem is circular references, i.e a Company has many Employees, and an Employee has one Company leads to Company1 -> Employee -> Company1 -> Employee ... which will result in a stack overflow exception if we don't mitigate against that, I will just use a naïve hashset to keep track of already cloned objects and not clone them again. So here's an implementation of this

static TEntity CreateDeepCopy<TEntity>(TEntity obj, HashSet<object>? seenObjects = default)
{
    if (seenObjects == default)
        seenObjects = new HashSet<object>();

    if (seenObjects.TryGetValue(obj, out _))
        return obj;

    seenObjects.Add(obj);

    var type = obj.GetType();
    if (type.IsCopyType())
        return obj;

    var clone = Activator.CreateInstance<TEntity>();

    var properties = type.GetProperties(
        BindingFlags.Public | BindingFlags.Instance
    );

    foreach (var property in properties)
    {
        var value = property.GetValue(obj);
        if (value is null)
            continue;

        // This is the easy case
        if (property.PropertyType.IsCopyType())
        {
            property.SetValue(clone, value);
            seenObjects.Add(value);
        }
        else if (typeof(IList).IsAssignableFrom(property.PropertyType))
        {
            var list = (IList) Activator.CreateInstance(property.PropertyType)!;
            var listValue = (IEnumerable)value;

            foreach (var element in listValue)
            {
                var targetMethod = typeof(Program).GetMethod(nameof(CreateDeepCopy), BindingFlags.NonPublic | BindingFlags.Static);
                var genericTarget = targetMethod.MakeGenericMethod(element.GetType());
                var subClone = genericTarget.Invoke(null, new[] { element, seenObjects });

                list.Add(subClone);
                seenObjects.Add(element);
            }
            
            property.SetValue(clone, list);
            seenObjects.Add(value);
        }
        else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
        {
            throw new Exception($"Only supports cloning collection types which implement 'IList', {property.PropertyType} is not supported");
        }
        else
        {
            var targetMethod = typeof(Program).GetMethod(nameof(CreateDeepCopy), BindingFlags.NonPublic | BindingFlags.Static);
            var genericTarget = targetMethod.MakeGenericMethod(value.GetType());
            var subClone = genericTarget.Invoke(null, new[] { value, seenObjects });

            property.SetValue(clone, subClone);
            seenObjects.Add(value);
        }
    }

    return clone;
}


static bool IsCopyType(this Type type)
{
    return type.IsPrimitive ||
           type.IsValueType ||
           type == typeof(string);
}

I also wrote some tests as a dotnetfiddle, but again, I highly encourage you to test this implementation much more thoroughly or use some form of serialization.

Plus, this code is quite messy, I would clean up a few places if you were to use this in production

P.S this currently doesn't work on types with collection properties other than IList, if you want to support dictionary and others you'll have to implement that on your own

  • Related