How to Clone Objects in C# .NET Core
In this article
The problem: How do I make a deep copy of an object in C# .NET Core?
Sometimes you may need to make a copy of an object which functions as a Prototype, which you use to build upon or adjust as necessary for use in a particular context. The issue arises when you do not want to mutate the original object but work with only a deep copy or clone of it. In languages like ES6 JavaScript, you have the spread syntax which you can use to quickly make a new copy of an object in memory, however that syntax does not exist in C#. So what are some ways we can create a deep clone of our object?
The solution
Here are some different approaches in C# you can take to create a deep copy of an object. The following section lists the pros and cons of each approach along with an implementation example.
Let's work with an object that we want to make a clone of and then change without mutating the original. The following is an object that includes a property containing a nested object for the purpose of demonstrating that a deep copy is made and references on the original are not retained.
public class MyObject
{
public MyObject(string objectProp, NestedObjectProp nestedObjectProp)
{
this.ObjectProp = objectProp;
this.NestedObjectProp = nestedObjectProp ;
}
public string ObjectProp { get; set; }
public NestedObjectProp NestedObjectProp { get; set; }
}
public class NestedObjectProp
{
public NestedObjectProp(string nestedPropA, string nestedPropB)
{
this.NestedPropB = nestedPropB;
this.NestedPropA = nestedPropA;
}
public string NestedPropA { get; set; }
public string NestedPropB { get; set; }
}
Option 1: Serialize and deserialize the object via an extension method
The pros
- Automatically makes a deep copy of the object for you.
- Flexible implementation: It does not matter what form you serialize it into - you can use a Binary, XML, or JSON Formatter, etc.
- Does not require the creation or use of an interface.
The cons
- Performance: Common serialization approaches are based on Reflection, which can have negative performance implications.
- Runtime errors are possible if the object cannot be cleanly serialized/deserialized.
Tips
- If serializing the object to JSON, use the Microsoft.AspNetCore.Mvc.NewtonsoftJson Nuget package since the built-in JSON serializer that comes with .NET Core 3 requires you to create parameterless constructors, while the NewtonsoftJson serializer does not.
- If using a Binary formatter, you need to use the
[Serializable]
attribute on your classes.
Implementation example
- Create an Extension Method that you can call on Objects. It makes a deep copy by serializing it and then returning a deserialized copy.
using Newtonsoft.Json;
public static class ExtensionMethods
{
public static T DeepCopy<T>(this T self)
{
var serialized = JsonConvert.SerializeObject(self);
return JsonConvert.DeserializeObject<T>(serialized);
}
}
- Now to make a clone, call
.DeepCopy()
on the object. The following example creates a deep copy of an instance ofMyObject
, and then alters properties on the clone. The originalMyObject
instance is therefore not mutated.
static void Main(string[] args)
{
var myObj = new MyObject("original", new NestedObjectProp("nestedPropA", "nestedPropB"));
var myObjClone = myObj.DeepCopy();
myObjClone.ObjectProp = "changed objectProp on clone";
myObjClone.NestedObjectProp.NestedPropB = "changed nestedPropB on clone";
}
Option 2: Implement the ICloneable interface
The ICloneable interface requires that an object which implements it needs to define a Clone()
method.
The pros
- This interface comes built-in with .NET Core.
The cons
- Ambiguous specification: the
Clone()
method is not required to implement a deep copy strategy and therefore it is unclear to the user whether the method is making a deep copy or a shallow copy of the object. - The
ICloneable
Clone()
method returns a weakly typedobject
requiring you to cast it for stronger typing.
Implementation example
- Implement the
ICloneable
interface on the objects.
public class MyObject : ICloneable
{
// ...The properties and constructor remain the same as above.
// Just implement the Clone method:
public object Clone()
{
return new MyObject(ObjectProp, (NestedObjectProp) NestedObjectProp.Clone());
}
}
public class NestedObjectProp : ICloneable
{
// ...The properties and constructor remain the same as above.
// Just implement the Clone method:
public object Clone()
{
return new NestedObjectProp(NestedPropA, NestedPropB);
}
}
- Call the
Clone()
method on the object to make a copy. Note the need to cast the clone for strong typing.
static void Main(string[] args)
{
var myObj = new MyObject("original", new NestedObjectProp("nestedPropA", "nestedPropB"));
var myObjClone = (MyObject) myObj.Clone();
myObjClone.ObjectProp = "changed objectProp on clone";
myObjClone.NestedObjectProp.NestedPropB = "changed nestedPropB on clone";
}
Option 3: Implement an explicit DeepCopy interface
The pros
- Makes it clear to the user what kind of copy is made and can be a better alternative to implementing the more ambiguous
ICloneable
interface.
The cons
- Tedious to implement if you have a deep hierarchy of objects because you need an interface implementation and deep copy logic for every member.
Implementation example
- Create an interface which implements an explicit
DeepCopy()
method.
public interface IPrototype<T>
{
T DeepCopy();
}
- Implement the interface in your objects.
public class MyObject : IPrototype<MyObject>
{
// ...The properties and constructor remain the same as above.
public MyObject DeepCopy()
{
return new MyObject(ObjectProp, NestedObjectProp.DeepCopy());
}
}
public class NestedObjectProp : IPrototype<NestedObjectProp>
{
// ...The properties and constructor remain the same as above.
public NestedObjectProp DeepCopy()
{
return new NestedObjectProp(NestedPropA, NestedPropB);
}
}
- Call
DeepCopy()
on the object to make a deep clone.
static void Main(string[] args)
{
var myObj = new MyObject("original", new NestedObjectProp("nestedPropA", "nestedPropB"));
var myObjClone = myObj.DeepCopy();
myObjClone.ObjectProp = "changed objectProp on clone";
myObjClone.NestedObjectProp.NestedPropB = "changed nestedPropB on clone";
}
Option 4: Use a copy constructor
The pros
- Offers a bit more clarity on usage over implementing an
ICloneable
interface.
The cons
- Copy Constructor is a concept that comes from the C++ language, and few programmers use the concept, preferring to use another method, such as the ones enumerated here.
- Copy Constructors must be created for nested properties as well, and this can get big fast.
Implementation example
- Create a separate constructor which takes in an instance of the same type, copies properties from it and returns a new instance.
public class MyObject
{
public MyObject(string objectProp, NestedObjectProp nestedObjectProp)
{
this.ObjectProp = objectProp;
this.NestedObjectProp = nestedObjectProp ;
}
public MyObject(MyObject other)
{
this.ObjectProp = other.ObjectProp;
this.NestedObjectProp = new NestedObjectProp(other.NestedObjectProp);
}
public string ObjectProp { get; set; }
public NestedObjectProp NestedObjectProp { get; set; }
}
public class NestedObjectProp
{
public NestedObjectProp(string nestedPropA, string nestedPropB)
{
this.NestedPropB = nestedPropB;
this.NestedPropA = nestedPropA;
}
public NestedObjectProp(NestedObjectProp other)
{
this.NestedPropA = other.NestedPropA;
this.NestedPropB = other.NestedPropB;
}
public string NestedPropA { get; set; }
public string NestedPropB { get; set; }
}
- Create a deep copy by calling the Copy Constructor and passing in the instance of the original object.
static void Main(string[] args)
{
var myObj = new MyObject("original", new NestedObjectProp("nestedPropA", "nestedPropB"));
var myObjClone = new MyObject(myObj);
myObjClone.ObjectProp = "changed objectProp on clone";
myObjClone.NestedObjectProp.NestedPropB = "changed nestedPropB on clone";
}
Record Types and their differences from above
A record
is a new object type introduced in C# 9.0. Records are reference types that provide built-in functionality for encapsulating data, much like classes and structs. Records are intended to be used where POCOs (Plain Old CLR Objects) or DTOs (Data Transfer Objects) have traditionally been used.
C# 10.0 introduced reference type records and value type records. These are created by using the record class
and record struct
keywords, respectively.
For a more thorough explanation, you can access the Microsoft documentation on records here.
By and large, record class and struct types behave the same as their non-record forms, but there are differences, due to the unique nature of records. For our purposes, one such difference is that record types have a unique keyword for them to allow quick copying: with { }
. Microsoft calls this a Nondestructive Mutation operator because within the curly braces one can override any of the properties of the record with a new value, and the non-mutated properties are copied over. Thus, for a straight copy of a record, one can simply write:
var newRecord = oldRecord with { };
And the with
operator makes a shallow copy of the original. If the record has only value type properties, then the copy is effectively a deep copy, but not if there are reference type properties in the record, just as in classes and structs.
Options 1, 2, and 4 above are allowed for records, just as if they were regular classes or structs.
For Option 2, however, one cannot import ICloneable
and implement the Clone()
method on any record type. The reason for this is that the record type itself implements a Clone()
method, and the method cannot be overridden or replaced.
Summary
Hopefully, this presents you with some options for when there is a need to clone an object in C# .NET Core and what to consider when choosing one over the other. Special thanks and shoutout to colleagues Greg Todd, Rodney Foley and Erik Muir who helped inform this article and offered their insights on these approaches as well.