C# – How to copy an object

In this article I’ll explain how to copy an object.

First I’ll explain the difference between shallow and deep copying.

Then I’ll show how to shallow copy and four different ways to deep copy – including manually copying and using serialization.

Finally I’ll show a speed and feature comparison of each approach, and a decision tree to help you decide which object copy method to use.

Shallow copy vs Deep Copy

There are two types of copying: shallow copying and deep copying. Shallow copy creates a new object and copies primitive values and references to the new object. Deep copy creates a new object, copies primitive values, and recursively creates new referenced objects.

The following diagram illustrates the key difference between shallow and deep copy:

Diagram showing the difference between Shallow Copy and Deep Copy

As illustrated above, when you shallow copy, the original NFLTeam object and copy point to the exact same HeadCoach object.

Because they are pointing to the same HeadCoach object, if you change original.HeadCoach.Name, it also changes copy.HeadCoach.Name. This may be undesirable. As long as you understand that shallow copying means your references are shared, then it’s OK. If this is not acceptable, you’ll need to use deep copy.

Whether you choose to do a shallow copy or deep copy will depend on your requirements. The important thing is to be aware of the difference between these two types of copying, and choosing the one that makes sense in your situation.

Example object to copy

I’ll use the following object to demonstrate the different copy methods. This is a sufficient choice for comparing different copy methods because it has references to other objects and has a list of objects.

var team = new NFLTeam() { City = "Detroit", Name = "Lions", Conference = Conferences.NFC, Divison = Divisions.North, HeadCoach = new Person() { FirstName = "Matt", LastName = "Patricia" }, Stats = new Stats() { RegularSeasonWins = 559, RegularSeasonLosses = 658, RegularSeasonTies = 32, PlayoffWins = 7, PlayoffLosses = 13, SuperBowlWins = 0, SuperBowlLosses = 0 }, Players = new List<Player>() { new Player() { FirstName = "Matthew", LastName = "Stafford", Position = PlayerPositions.QB, YearsOfExperience = 12, College = "Georgia" }, new Player() { FirstName = "Kenny", LastName = "Golladay", Position = PlayerPositions.WR, YearsOfExperience = 4, College = "Northern Illinois" }, new Player() { FirstName = "Tracy", LastName = "Walker", Position = PlayerPositions.DB, YearsOfExperience = 3, College = "Louisiana-Lafayette" }, new Player() { FirstName = "T.J.", LastName = "Hockenson", Position = PlayerPositions.TE, YearsOfExperience = 2, College = "Iowa" } } };

How to shallow copy an object

Shallow Copy copies values and references. It’s extremely fast, but doesn’t create new references.

To shallow copy you can call MemberwiseClone() on the object. This is a protected method, so it can only be called from within an instance method.

Luckily, there’s a sneaky way to avoid having to modify your class just to call this method. You can use reflection to invoke MemberwiseClone(). This is especially useful if you’re trying to copy an object from third-party code – which you wouldn’t be able to modify yourself.

Here is the reflective ShallowCopy() method.

public class ObjectCopier { public object ShallowCopy(object o) { return o?.GetType().GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(o, null); } }

The following test calls ShallowCopy().

[TestMethod()] public void ShallowCopyTest() { var team = GetTeam(); var objectCopier = new ObjectCopier(); var copy = (NFLTeam)objectCopier.ShallowCopy(team); Assert.AreNotSame(team, copy); Assert.AreSame(team.HeadCoach, copy.HeadCoach); Assert.AreSame(team.Name, copy.Name); Assert.AreEqual(team.Name, copy.Name); }

Notice the outcome:

  • team.HeadCoach and copy.HeadCoach are pointing to the same object. This is because shallow copy doesn’t create new references.
  • team.Name and copy.Name are pointing to the same string object. This is OK though, because strings are immutable, so it really doesn’t matter that they are pointing to the same string object.

How to deep copy an object

Deep copying creates a new object, copies values, and recursively creates new referenced objects, including strings.

There are three main ways to do this:

  1. Manually copying
  2. Serialization
  3. Walk the object graph using recursion + reflection. This is what the serializers do, so there is no point doing this yourself. This is out of scope for this article, since I wouldn’t recommend reinventing the wheel with this approach.

The method you choose depends on balancing maintainability, performance, and how general-purpose it needs to be. Furthermore, if you are trying to deep copy a third party object, then your options may be very restricted.

For our purposes here, we’ll define deep copy as copying the public properties of a class. If you need to copy private fields, you’ll need to stick to using the built-in BinaryFormatter approach.

Deep copy an object manually

The first option is to simply manually copy the object. This is simple and very fast. The downside is that any time you add a new property, you’ll need to remember to update the copy method.

Here’s the ManuallyCopy() method.

public class ObjectCopier { public NFLTeam ManuallyCopy(NFLTeam nflTeam) { return new NFLTeam { City = nflTeam.City, Conference = nflTeam.Conference, Divison = nflTeam.Divison, Name = nflTeam.Name, HeadCoach = new Person() { FirstName = nflTeam.HeadCoach.FirstName, LastName = nflTeam.HeadCoach.LastName }, Players = nflTeam.Players.Select(p => new Player() { College = p.College, FirstName = p.FirstName, LastName = p.LastName, Position = p.Position, YearsOfExperience = p.YearsOfExperience }).ToList(), Stats = new Stats() { PlayoffLosses = nflTeam.Stats.PlayoffLosses, PlayoffWins = nflTeam.Stats.PlayoffWins, RegularSeasonLosses = nflTeam.Stats.RegularSeasonLosses, RegularSeasonTies = nflTeam.Stats.RegularSeasonTies, RegularSeasonWins = nflTeam.Stats.RegularSeasonWins, SuperBowlLosses = nflTeam.Stats.SuperBowlLosses, SuperBowlWins = nflTeam.Stats.SuperBowlWins } }; } }

Here’s the test:

[TestMethod()] public void ManualCopyTests() { var team = GetTeam(); var objectCopier = new ObjectCopier(); var copy = objectCopier.ManuallyCopy(team); Assert.AreNotSame(team, copy); Assert.AreNotSame(team.HeadCoach, copy.HeadCoach); Assert.AreSame(team.Name, copy.Name); Assert.AreEqual(team.Name, copy.Name); }

Two things to notice:

  • team.HeadCoach and copy.HeadCoach are not the same, because it was deep copied.
  • team.Name and copy.Name are the same string reference. Strings are immutable, so it doesn’t matter if they are shallow copied.

Deep copy an object with binary serialization using built-in BinaryFormatter

The next option is using the built-in BinaryFormatter to do binary serialization. To create a deep copy with binary serialization, you first serialize an object, then deserialize it. This is quite simple to do using the built-in BinaryFormatter class.

The only downside is you need to add the [Serializable] attribute to every object in the object graph.

[Serializable] public class NFLTeam

If you don’t add this attribute, then you’ll get the following exception:

System.Runtime.Serialization.SerializationException: Type ‘TypeName’ in Assembly ‘AssemblyName, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null’ is not marked as serializable.

If you are in control of all the objects in the object graph, then you can simply add the [Serializable] attribute. However, if there is a third-party object in the object graph without the [Serializable] attribute, you’ll need to use a different approach.

Here’s the code:

public class ObjectCopier { private BinaryFormatter binaryFormatter; public ObjectCopier() { binaryFormatter = new BinaryFormatter(); } public object DeepCopyBinaryFormatter(object o) { using (var ms = new MemoryStream()) { binaryFormatter.Serialize(ms, o); ms.Position = 0; return binaryFormatter.Deserialize(ms); } } }

Here’s the test:

[TestMethod()] public void DeepCopyBinaryFormatterTest() { var team = GetTeam(); var objectCopier = new ObjectCopier(); var copy = (NFLTeam)objectCopier.DeepCopyBinaryFormatter(team); Assert.AreNotSame(team, copy); Assert.AreNotSame(team.HeadCoach, copy.HeadCoach); Assert.AreNotSame(team.Name, copy.Name); Assert.AreEqual(team.Name, copy.Name); }

Notice that team.HeadCoach and copy.HeadCopy are not the same object. Even team.Name and copy.Name are different string objects. Binary serialization creates entirely new object references, including strings.

Deep copy an object with binary serialization using GroBuf

If you want to do binary serialization, but can’t, or don’t want to add the [Serialization] attribute to everything in the object graph, then you can use GroBuf. This simply binary serializes all public properties in the object graph. Because you don’t need to add any attributes, this is a nice low-maintenance option.

First, install GroBuf using the nuget package console:

Install-Package GroBuf

Here’s the code that uses the GroBuf serializer. Notice that this is creating the Serializer instance ahead of time. This is critical for performance.

public class ObjectCopier { private Serializer groBufSerializer; public ObjectCopier() { groBufSerializer = new Serializer(new PropertiesExtractor(), options: GroBufOptions.WriteEmptyObjects); } public object DeepCopyGroBufBinary(object o) { byte[] data = groBufSerializer.Serialize(o.GetType(), o); return groBufSerializer.Deserialize(o.GetType(), data); } }

Here’s the test:

[TestMethod()] public void DeepCopyGroBufBinaryTest() { var team = GetTeam(); var objectCopier = new ObjectCopier(); var copy = (NFLTeam)objectCopier.DeepCopyGroBufBinary(team); Assert.AreNotSame(team, copy); Assert.AreNotSame(team.HeadCoach, copy.HeadCoach); Assert.AreNotSame(team.Name, copy.Name); Assert.AreEqual(team.Name, copy.Name); }

Warning: GroBuf is very slow on initial use. Use the performance recommendations mentioned below.

Deep copy an object with JSON serialization using the built-in System.Text.Json

If you want a very low-maintenance deep copy option, then JSON serialization is for you. You don’t need to add attributes anywhere, and you can use the built-in System.Text.Json.

Here’s the code:

public class ObjectCopier { private JsonSerializerOptions jsonOptions; public ObjectCopier() { jsonOptions = new JsonSerializerOptions(); jsonOptions.Converters.Add(new JsonStringEnumConverter()); } public object DeepCopyJson(object o) { var json = JsonSerializer.Serialize(o, jsonOptions); return JsonSerializer.Deserialize(json, o.GetType(), jsonOptions); } }

Here’s the test:

[TestMethod()] public void DeepCopyJsonTest() { var team = GetTeam(); var objectCopier = new ObjectCopier(); var copy = (NFLTeam)objectCopier.DeepCopyJson(team); Assert.AreNotSame(team, copy); Assert.AreNotSame(team.HeadCoach, copy.HeadCoach); Assert.AreNotSame(team.Name, copy.Name); Assert.AreEqual(team.Name, copy.Name); }

Warning: System.Text.Json is very slow during initial use. Use the performance recommendations mentioned below.

Performance recommendations – cache serializers and warm them up

To improve performance I recommend the following two things:

1 – Cache serializer objects

During startup, create the serializer objects and options and save them for later. For example, when I use System.Text.Json I did the following:

private JsonSerializerOptions jsonOptions; public ObjectCopier() { jsonOptions = new JsonSerializerOptions(); jsonOptions.Converters.Add(new JsonStringEnumConverter()); }

If you create the serializer objects (or serialization options in this case) each time you serialize, then that adds significant overhead and practically eliminates performance benefits.

2 – Warm up the serializers on startup

Some serializers are relatively slow on their first use compared to subsequent uses.

There are two reasons:

  1. They use reflection. The first time you use reflection to get type info, it’s a cache miss. It caches the type info, so subsequent lookups are very fast.
  2. They build and use internal caches. If you use GroBuf or System.Text.Json, you’ll notice that the first use is incredibly slow. Then subsequent uses are fast. This indicates that they build and use an internal cache.

So if you’re going to use the serializer over and over, it makes sense to warm it up by using it during startup.

Speed comparison of the different object copying methods

I compared the speed of the different copy methods by copying an object 100 times and capturing the min, max, and average times.

MethodMin (ms)Max (ms)Avg (ms)
ShallowCopy0.00170.00850.00206
DeepCopyBinaryFormatter0.12490.29530.15459
DeepCopyJson0.03750.25420.04968
ManuallyCopy0.00190.01170.00253
DeepCopyGroBufBinary0.00430.02770.00648

Note: The reflective methods are always slower the first time they are executed. This is because type metadata gotten from reflection is lazy loaded. So in order to have a fair comparison, I “warmed up” all of the methods, so the reflective methods would not incur the penalty of the initial cache miss.

Here is the code I used to compare the speeds.

[TestMethod()] public void SpeedTest() { var team = GetTeam(); var objectCopier = new ObjectCopier(); Stopwatch sw = new Stopwatch(); //Warm up - so reflective methods don't get reflection penalty objectCopier.ShallowCopy(team); objectCopier.DeepCopyBinaryFormatter(team); objectCopier.DeepCopyJson(team); objectCopier.ManuallyCopy(team); objectCopier.DeepCopyGroBufBinary(team); Dictionary<string, List<double>> times = new Dictionary<string, List<double>>(); times.Add(nameof(objectCopier.ShallowCopy), new List<double>()); times.Add(nameof(objectCopier.DeepCopyBinaryFormatter), new List<double>()); times.Add(nameof(objectCopier.DeepCopyJson), new List<double>()); times.Add(nameof(objectCopier.ManuallyCopy), new List<double>()); times.Add(nameof(objectCopier.DeepCopyGroBufBinary), new List<double>()); for (int i = 0; i < 100; i++) { sw.Start(); objectCopier.ShallowCopy(team); sw.Stop(); times[nameof(objectCopier.ShallowCopy)].Add(sw.Elapsed.TotalMilliseconds); sw.Restart(); objectCopier.DeepCopyBinaryFormatter(team); sw.Stop(); times[nameof(objectCopier.DeepCopyBinaryFormatter)].Add(sw.Elapsed.TotalMilliseconds); sw.Restart(); objectCopier.DeepCopyJson(team); sw.Stop(); times[nameof(objectCopier.DeepCopyJson)].Add(sw.Elapsed.TotalMilliseconds); sw.Restart(); objectCopier.ManuallyCopy(team); sw.Stop(); times[nameof(objectCopier.ManuallyCopy)].Add(sw.Elapsed.TotalMilliseconds); sw.Restart(); objectCopier.DeepCopyGroBufBinary(team); sw.Stop(); times[nameof(objectCopier.DeepCopyGroBufBinary)].Add(sw.Elapsed.TotalMilliseconds); } foreach(var kvp in times) { Console.WriteLine($"Method={kvp.Key} Min={kvp.Value.Min()} Max={kvp.Value.Max()} Avg={kvp.Value.Average()}"); } }

Feature and performance comparison table

The following table shows a comparison of all the features and performance discussed in this article.

MethodShallow or Deep Copy?Creates new strings?Copies private fields?General-purpose?Built-in?Relative speed of initial useAverage speed (ms)Works on third-party objectsRelative coding effort
MemberwiseClone()ShallowNoNoYesYesMedium0.00206YesVery little.

Add a one-line utility method.
Manual deep copyDeepNoNoNoYesFast0.00253YesVery high.

Have to implement the manual copying, then remember to update it whenever classes change.
Binary serialization using BinaryFormatterDeepYesYesYesYesMedium0.15459Must have [Serializable] attributePretty high.

You have to add the serializer logic + add [Serializable] to all objects in the object graph.
Binary serialization using GroBufDeepYesNoYesNoVery slow0.00648YesPractically no effort if you don’t care about speed.

Low effort relatively if you want great performance. You just need to initialize it during startup and make it available everywhere.

(See Performance Recommendation section)
JSON serialization using System.Text.JsonDeepYesNoYesYesSlow0.04968YesSame as above.

Which object copy method should I use?

If you’re having trouble deciding which object copy method to use, then ask yourself the questions in the decision tree below.

Decision tree to figure out which object copy method to use

Leave a Comment