Updated on

In C#, when we create a new object that is a copy of an existing object, we can either create a shallow copy or a deep copy.

Creating a deep copy of an object is often necessary when we need to modify the copy without affecting the original object. In this article, we will explore the different methods for creating a deep copy of an object in C#.

To download the source code for this article, you can visit our GitHub repository.

This article covers copying a single object. If the thing to copy is a collection, cloning a list has its own set of answers.

What Is the Difference Between a Shallow Copy and a Deep Copy in C#?

A shallow copy creates one new object and copies each field across. Value fields are copied by value. Reference fields are copied as references, so the copy and the original end up pointing at the same nested objects.

A deep copy also creates one new object, and then creates new copies of everything that object points at, recursively, so nothing is shared.

The difference only shows up once we change something. Assign to a property on the copy and the original is unaffected either way. Change something inside a nested object and a shallow copy leaks that change back to the original, because there is only one nested object.

C# gives us no built-in deep copy. Every approach below is either code we write ourselves, a serializer we round trip through, or a library that walks the object graph for us.

In the diagram below, originalPerson and shallowCopy both point at one Address object, and deepCopy points at its own.

Three Person objects. The original and the shallow copy both point at one Address object; the deep copy points at its own separate Address object.

Let’s understand this with a class Person:

public class Person
{
    public required string Name { get; set; }
    public required int Age { get; set; }
    public required Address Address { get; set; }
}

The required modifier used in the property declaration was introduced in C# 11. We use it to ensure that all the members of this class are initialized upon instantiation.

The class contains a reference to Address object:

public class Address
{
    public required string Street { get; set; }
    public required string City { get; set; }
    public required string State { get; set; }
}

To create a shallow copy, we can use the MemberwiseClone method in the Person class:

public Person ShallowCopy() => (Person)this.MemberwiseClone();

When we create a new Person object and then copy it by calling this method, we are creating a shallow copy:

var originalPerson = new Person
{
    Name = "Steve Doe",
    Age = 22,
    Address = new Address
    {
        Street = "123 Main St.",
        City = "Anytown",
        State = "AB"
    }
};

var copiedPerson = originalPerson.ShallowCopy();

copiedPerson is a new Person, but its Address is the very same Address the original points at.

Now, let’s modify the copiedPerson object and see its effects:

copiedPerson.Name = "Jack Swallow";
copiedPerson.Address.Street = "456 Elmo St.";

In this case, we modified the Name property of copiedPerson and the Street property of its Address object. However, when we output the Name and Street properties of the originalPerson object:

Console.WriteLine($"Original Name: {originalPerson.Name}");
Console.WriteLine($"Original Street: {originalPerson.Address.Street}");

We can see that the street has changed:

Original Name: Steve Doe
Original Street: 456 Elmo St.

This is a behavior of shallow copy. We can see that the reference type property has another (changed) value.

A deep copy, on the other hand, creates a new object that is identical to the original object in memory, but all the objects referenced by the original object are also copied recursively.

This ensures that changes made to the copied objects do not affect the original object.

Arrays have their own copying options, covered in copying array elements into a new array.

What Does MemberwiseClone() Do in C#?

MemberwiseClone() is a method every type inherits from object. It makes a shallow copy: a new instance of the same type, with every non-static field copied straight across.

It is protected, which is the part that surprises people. We cannot call someObject.MemberwiseClone() from outside. The class itself has to expose it, which is why the Person class above wraps it in a public ShallowCopy() method.

It never calls a constructor, so no validation and no initializer runs, and it copies fields rather than properties, so a computed property is not involved at all.

For a class whose fields are all value types or strings, that is already a complete copy: value types are copied outright, and a string is immutable, so sharing one is harmless. The moment one field is a mutable reference type, MemberwiseClone() alone is a shallow copy and we need one of the techniques below.

Deep Copy With the ICloneable Interface

The most direct way to control a deep copy is to write the copying code ourselves, and ICloneable is the interface C# offers for exposing it.

The ICloneable interface defines a single method, Clone(), which we can use to create a new object that is a copy of the original object.

We have to be careful with the ICloneable interface when we want to create a deep copy of an object.

The main problem with ICloneable interface implementation is that it provides a Clone() method, but is vague about whether the method creates a deep copy or a shallow copy. Different classes can have their own way of implementation. Hence, we can not rely on  ICloneable to always create deep copies.

Microsoft’s documentation for the interface says the same in its notes to implementers: “Because callers of Clone() cannot depend on the method performing a predictable cloning operation, we recommend that ICloneable not be implemented in public APIs.”

That said, let’s look at a version of the Clone() method that creates a deep copy.

First, let’s modify our Person class to implement ICloneable interface:

public class Person : ICloneable
{
    public required string Name { get; set; }
    public required int Age { get; set; }
    public required Address Address { get; set; }

    public object Clone()
    {
        var clonedPerson = new Person
        {
            Name = Name,
            Age = Age,
            Address = new Address()
            {
                Street = Address.Street,
                City = Address.City,
                State = Address.State
            }
        };

        return clonedPerson;
    }
}

Here, the Clone() method creates a new instance of the Person class and copies all the properties of the original object to the new object. It creates a new instance of the Address class and copies the Address property recursively by copying all its properties.

Also, we can refactor our code even further and implement the ICloneable interface in the Address class too:

public class Address : ICloneable
{
    public required string Street { get; set; }
    public required string City { get; set; }
    public required string State { get; set; }

    public object Clone()
    {
        return new Address
        {
            Street = Street,
            City = City,
            State = State
        };
    }
}

This allows us to modify the Clone() method in the Person class to remove excess code:

public object Clone()
{
    var clonedPerson = new Person
    {
        Name = Name,
        Age = Age,
        Address = (Address)Address.Clone()
    };

    return clonedPerson;
}

When we create a new Person object using the Clone() method, we get a deep copy of the original object, meaning that any changes made to the copy will not affect the original object:

var copiedPerson = (Person)originalPerson.Clone();

Now, if we modify the Name and Street properties of the copiedPerson object:

copiedPerson.Name = "Jack Swallow";
copiedPerson.Address.Street = "456 Elmo St.";

And output the Name and Street properties of the originalPerson object:

Console.WriteLine($"Original Name: {originalPerson.Name}");
Console.WriteLine($"Original Street: {originalPerson.Address.Street}");

We see that there is no change in the original object’s properties:

Original Name: Steve Doe
Original Street: 123 Main St.

How Do We Write a Copy Constructor in C#?

A copy constructor is an ordinary constructor that takes an instance of its own type and copies the values out of it.

Nothing in C# generates one for a class, and nothing enforces what it does. It is a deep copy exactly to the extent that we make it one: copy a reference field straight across and we have written a shallow copy with extra steps.

The technique is recursive. Person‘s copy constructor calls Address‘s copy constructor, which copies three strings, and that covers the whole graph.

Records are the near miss here. A record does get a compiler-generated copy constructor, and with uses it, but that copy is shallow: the new record points at the same nested objects. with is for changing a value on an immutable record, not for detaching a copy from its original.

We start with Address. Declaring any constructor removes the implicit parameterless one, which object initializers and XmlSerializer both need, so we write it back next to the copy constructor. The [SetsRequiredMembers] attribute, from System.Diagnostics.CodeAnalysis, tells the compiler that the copy constructor sets every required member itself:

public class Address
{
    public required string Street { get; set; }
    public required string City { get; set; }
    public required string State { get; set; }

    public Address() { }

    [SetsRequiredMembers]
    public Address(Address other)
    {
        Street = other.Street;
        City = other.City;
        State = other.State;
    }
}

Then Person copies its own values and hands the nested object to the constructor above:

public Person() { }

[SetsRequiredMembers]
public Person(Person other)
{
    Name = other.Name;
    Age = other.Age;
    Address = new Address(other.Address);
}

Copying a Person is now one constructor call, and the copy gets its own Address:

var copiedPerson = new Person(originalPerson);

A copy constructor is a constructor that creates a new object by copying the values of the fields and properties of an existing object. It provides a way to create a new object with the same values as an existing object without modifying the original object.

However, we encounter the same problem of vagueness with copy constructors. Whether the constructor creates a deep copy or a shallow copy depends on its implementation and it doesn’t inherently guarantee a deep copy.

Deep Copy With Serialization

Another way to create a deep copy of an object in C# is to use serialization.

Serialization is the process of converting an object to a byte stream that can be saved to a file or transmitted over a network. Deserialization is the process of converting the byte stream back into an object.

None of the three serializers in this section needs the [Serializable] attribute. It was the marker for BinaryFormatter, and the in-box implementation of BinaryFormatter was removed in .NET 9. Each serializer has its own requirements instead: XmlSerializer needs a public parameterless constructor and public members, DataContractSerializer uses [DataContract] and [DataMember] when they are present, and System.Text.Json needs public settable members or a matching constructor.

There are various serialization techniques for deep copying an object, and we will use three of them.

XML Serialization

XML serialization is the process of converting an object into an XML format that can be stored in a file, database, or memory stream:

public static T DeepCopyXML<T>(T input)
{
    ArgumentNullException.ThrowIfNull(input);

    using var stream = new MemoryStream();

    var serializer = new XmlSerializer(typeof(T));
    serializer.Serialize(stream, input);
    stream.Position = 0;

    return (T)serializer.Deserialize(stream)!;
}

Here, we create a new MemoryStream instance, which we use to store the serialized object, and an XmlSerializer instance, which will be used to serialize and deserialize the object.

The Serialize() method call serializes the input object. Next, we set the stream.Position to 0 to read the MemoryStream from the beginning. Finally, the Deserialize() method call deserializes the object from the MemoryStream and returns it as a new object of type T.

The ArgumentNullException.ThrowIfNull() guard rejects a null input up front. With a non-null object going in, the serializer hands a non-null object back, and the ! operator on the return tells the compiler so. The JSON, data contract and Json.NET versions below use the same two lines.

Now, we can create a deep copy of the Person object using the DeepCopyXML() method, which uses XML serialization to create the copy.

JSON Serialization

JSON serialization is the process of converting an object into a JSON format that we can store and transport over the network:

public static T DeepCopyJSON<T>(T input)
{
    ArgumentNullException.ThrowIfNull(input);

    var jsonString = JsonSerializer.Serialize(input);

    return JsonSerializer.Deserialize<T>(jsonString)!;
}

Here, instead of MemoryStream, we use the JsonSerializer class.

Data Contract Serialization

Data contract serialization is another serialization technique that we use to serialize and deserialize objects. It uses the [DataContract] and [DataMember] attributes to mark the objects that should be serialized.

Let’s mark the Person class with those attributes:

[DataContract]
public class Person
{
    [DataMember]
    public required string Name { get; set; }

    [DataMember]
    public required int Age { get; set; }

    [DataMember]
    public required Address Address { get; set; }
}

Data contract serialization is similar to XML serialization in that it creates an XML document to hold the serialized data. Also, it is used for data serialization in Windows Communication Foundation (WCF) messages:

public static T DeepCopyDataContract<T>(T input)
{
    ArgumentNullException.ThrowIfNull(input);

    using var stream = new MemoryStream();

    var serializer = new DataContractSerializer(typeof(T));
    serializer.WriteObject(stream, input);
    stream.Position = 0;

    return (T)serializer.ReadObject(stream)!;
}

The implementation is similar to serialization using XML. The difference is, here, we use a DataContractSerializer instance to serialize and deserialize the object.

Deep Copy With Reflection

Reflection is a powerful feature in C# that allows us to inspect and manipulate objects at runtime.

Let’s understand this technique with a new method:

public static T DeepCopyReflection<T>(T input)
{
    ArgumentNullException.ThrowIfNull(input);

    var type = input.GetType();
    var properties = type.GetProperties();

    T clonedObj = (T)Activator.CreateInstance(type)!;

    foreach (var property in properties)
    {
        if (property.CanWrite)
        {
            object? value = property.GetValue(input);
            if (value != null && value.GetType().IsClass && !value.GetType().FullName!.StartsWith("System."))
            {
                property.SetValue(clonedObj, DeepCopyReflection(value));
            }
            else
            {
                property.SetValue(clonedObj, value);
            }
        }
    }

    return clonedObj;
}

First, we create a new instance of the same type as the original object using the CreateInstance() method from the Activator class.

Then, we iterate over all the properties of the input object and copy their values to the newly created object. If the property is a reference type (i.e. a class) outside the System namespace, the method recursively calls itself to create a deep copy of the object.

Thus, the DeepCopyReflection() method takes an object of type T and returns a deep copy of the object.

Deep Copy With Expression Trees

Expression Tree is a powerful feature in C# that allows us to dynamically create and compile code at runtime.

We can use it to generate code that performs a deep copy of an object. This technique can be useful in scenarios where we don’t have control over the classes we need to copy.

First, let’s create a method that generates an expression tree to deep copy an instance of a class:

private static Func<T, T> GenerateDeepCopy<T>()
{
    var inputParameter = Expression.Parameter(typeof(T), "input");

    var memberBindings = new List<MemberBinding>();
    foreach (var propertyInfo in typeof(T).GetProperties())
    {
        var propertyExpression = Expression.Property(inputParameter, propertyInfo);

        if (propertyInfo.PropertyType.IsClass && propertyInfo.PropertyType != typeof(string))
        {
            var copyMethod = typeof(DeepCopyMaker)
                .GetMethod(nameof(DeepCopyMaker.DeepCopyExpressionTrees))!
                .MakeGenericMethod(propertyInfo.PropertyType);

            var propertyCopyExpression = Expression.Call(copyMethod, propertyExpression);

            memberBindings.Add(Expression.Bind(propertyInfo, propertyCopyExpression));
        }
        else
        {
            memberBindings.Add(Expression.Bind(propertyInfo, propertyExpression));
        }
    }

    var memberInitExpression = Expression.MemberInit(Expression.New(typeof(T)), memberBindings);

    return Expression.Lambda<Func<T, T>>(memberInitExpression, inputParameter).Compile();
}

The GenerateDeepCopy() method iterates over each property of the specified type T, and for each property that is a reference type other than string, generates an expression to deep copy the object using recursion.

Then, we compile the resulting expression tree into a delegate that creates deep copy instances of the specified type.

Let’s create another method to execute the deep copy delegate, with a cache so that each type is compiled only once:

public static T DeepCopyExpressionTrees<T>(T input)
{
    return Cache<T>.Copy(input);
}

private static class Cache<T>
{
    public static readonly Func<T, T> Copy = GenerateDeepCopy<T>();
}

Cache<T> is a static generic class, so the runtime keeps one of it per type, and its field runs GenerateDeepCopy<T>() once, the first time that type is copied. DeepCopyExpressionTrees() only reads the stored delegate and calls it. The generated code copies the nested Address by calling DeepCopyExpressionTrees<Address>(), so nested types go through the same cache.

Compiling an expression tree costs far more than running the delegate it produces. Without the cache, every copy of a Person would compile two of them, one for Person and one for its Address.

Deep Copy With Third-Party Libraries

There are several third-party libraries that provide easy-to-use and efficient methods for deep copying objects in C#.

AutoMapper can also produce a deep copy, but it is built for mapping one type onto another, and our AutoMapper guide for ASP.NET Core covers it.

FastDeepCloner

FastDeepCloner is a library that uses reflection to create a deep copy of an object irrespective of whether it is serializable, including nested reference types. 

The default usage for this library is simpler. All we need to do is refer FastDeepCloner and create a method:

public static T DeepCopyFastDeepCloner<T>(T input)
{
    return (T)DeepCloner.Clone(input);
}

The FastDeepCloner library provides us with the DeepCloner.Clone() method to create a deep copy of any object. The Clone() method takes an object as input and returns a new instance of that object, with all of its properties copied.

DeepCopy

DeepCopy is a simple library that uses IL code generation for deep copying objects in C#. 

The library supports deep copying of complex objects, including objects with circular references, and can handle a wide variety of object types.

Let’s create a method to understand this library:

public static T DeepCopyLibraryDeepCopy<T>(T input)
{
    return DeepCopier.Copy(input);
}

With the DeepCopy library, we can create a deep copy of an object by calling the Copy() method in the DeepCopier class, and passing in the object that we want to copy.

We can use it similarly to deep copying with FastDeepCloner.

Json.NET

Json.NET is a popular third-party library for working with JSON in C#. Also, it provides a way to create deep copies of objects using its serialization and deserialization features.

To create a deep copy of an object using Json.NET, we can serialize the object to a JSON string using the  JsonConvert.SerializeObject() method. Then, we can deserialize the string back into a new object using the JsonConvert.DeserializeObject() method:

public static T DeepCopyJsonDotNet<T>(T input)
{
    ArgumentNullException.ThrowIfNull(input);

    var serialized = Newtonsoft.Json.JsonConvert.SerializeObject(input);

    return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(serialized)!;
}

This approach creates a new object with the same values as the original object, but it is not connected to the original object in any way. This works very similarly to how we previously covered the native JSON serialization in C#.

What Is the Fastest Deep Copy Method?

Hand-written code is fastest, and a cached expression tree runs within 2 nanoseconds of it. A copy constructor, or a Clone() doing the same thing by hand, copies this two-object graph in about 15 nanoseconds because it does exactly the assignments we wrote and nothing else.

After that the order follows how much work each approach does at runtime. The DeepCopy library’s generated code takes about twice as long. A reflection walk reads type metadata every time. The serializer round trips sit at the slow end, with XmlSerializer last, because each one writes the object out as text and parses it back. Compiled on every call, an expression tree is slower than all of them, at over 250,000 nanoseconds per copy.

Speed rarely decides this. Hand-written code is the fastest and the one that silently goes stale when somebody adds a property, so pick on that trade first and measure only if copying turns up in a profile.

Here is every technique from this article side by side, with its typical cost from the benchmark run further down:

MethodCopies nested objectsWhat it needsWatch out forTypical cost
Copy constructorYes, if we write it that wayA constructor per classEvery new property has to be added by hand16 ns
ICloneable.Clone()Yes, if we write it that wayThe interface plus the same hand-written codeMicrosoft recommends against it in public APIs15 ns
record with withNo, shallow onlyA record declarationNested reference members stay sharedNot benchmarked: shallow copy
MemberwiseClone()No, shallow onlyNothing, it is on objectProtected, so it is callable only from inside the classNot benchmarked: shallow copy
System.Text.Json round tripYesPublic settable members, or a matching constructorAnything the serializer skips is silently lost1,110 ns
Json.NET round tripYesJsonConvert from the Json.NET packageThe same round trip as System.Text.Json, with a third-party dependency1,490 ns
XmlSerializer round tripYesA public parameterless constructor, public membersSlowest of the serializers, and no Dictionary support10,200 ns
DataContractSerializer round tripYesA parameterless constructor on an unattributed class; it honours [DataContract] and [DataMember] when they are thereOnce one class in the graph is attributed, each of its members we want copied needs [DataMember]3,790 ns
Reflection walkYesWritable propertiesNo cycle detection, so a circular reference overflows the stack390 ns
Compiled expression treeYesA cached compiled delegateUncached, the compile dominates everything else17 ns
DeepCopy packageYesDeepCopier.Copy()Last published 2017-12-0531 ns
FastDeepCloner packageYesDeepCloner.Clone()Last published 2021-10-162,130 ns

The two shallow rows have no benchmark figure because they never produce a deep copy. They copy the Person and share its Address, as the diagram at the top of the article shows.

Let’s create a DeepCopierBenchmark class to compare the performance of various deep copying methods using BenchmarkDotNet:

public class DeepCopierBenchmark
{
    private Person _person = null!;

    [GlobalSetup]
    public void Setup()
    {
        _person = new Person
        {
            Name = "John",
            Age = 45,
            Address = new Address
            {
                Street = "56 Jump St.",
                City = "Aquatica",
                State = "TY"
            }
        };
    }
}

Next, in the same class, we will implement benchmark methods for all the deep copying techniques. 

The DeepCopierBenchmark class then runs all the methods and compares their performance:

var config = ManualConfig.Create(DefaultConfig.Instance)
                         .WithOptions(ConfigOptions.DisableOptimizationsValidator);

var summary = BenchmarkRunner.Run<DeepCopierBenchmark>(config);
Console.WriteLine(summary);

The DisableOptimizationsValidator option is there because FastDeepCloner 1.3.6 ships an assembly built without optimizations, and BenchmarkDotNet refuses to run while any referenced assembly is non-optimized. The option turns that check off for every assembly, ours included, so we run the project with dotnet run -c Release.

Let’s assess the performance results:

BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6466/22H2/2022Update)
AMD Ryzen 5 3600 3.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  DefaultJob : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

| Method                             | Mean         | Error      | StdDev     |
|----------------------------------- |-------------:|-----------:|-----------:|
| CopyConstructorBenchmark           |     16.07 ns |   0.373 ns |   0.602 ns |
| ICloneableBenchmark                |     15.36 ns |   0.363 ns |   0.789 ns |
| XMLSerializationBenchmark          | 10,238.73 ns | 190.799 ns | 178.474 ns |
| JSONSerializationBenchmark         |  1,109.52 ns |  20.268 ns |  16.925 ns |
| DataContractSerializationBenchmark |  3,789.88 ns |  54.915 ns |  56.394 ns |
| ReflectionBenchmark                |    390.57 ns |   2.534 ns |   2.116 ns |
| ExpressionTreesBenchmark           |     16.80 ns |   0.359 ns |   0.319 ns |
| FastDeepClonerBenchmark            |  2,134.58 ns |  13.023 ns |  11.544 ns |
| DeepCopyLibraryBenchmark           |     30.72 ns |   0.485 ns |   0.405 ns |
| JsonDotNetBenchmark                |  1,491.98 ns |  28.691 ns |  26.837 ns |

Three techniques tie at the top. ICloneable, the copy constructor and the cached expression tree each copy the Person in 15 to 17 nanoseconds. The DeepCopy library’s generated code comes next at about 31 nanoseconds.

The reflection walk takes about 390 nanoseconds, roughly 25 times the hand-written code. Everything that goes through text is slower again: System.Text.Json at about 1,100 nanoseconds, Json.NET at about 1,500, DataContractSerializer at about 3,800 and XmlSerializer at over 10,000. FastDeepCloner, the reflection-based library, lands among the serializers at about 2,100 nanoseconds.

The speed of the ICloneable row says nothing about its contract, and the contract is the reason for the arguments against using it in public APIs: a caller cannot tell whether Clone() makes a deep or a shallow copy.

One row moved for a reason other than the newer runtime. The previous version of this article measured expression trees at about 264,000 nanoseconds, because the sample compiled a new delegate on every call. That figure was the cost of the compiler, and the cached version removes it.

All of these figures come from one machine, an AMD Ryzen 5 3600 running .NET 10.0.10, so the absolute numbers will differ on others.

Conclusion

For classes we own, a copy constructor is the deep copy to reach for. It is as fast as anything in this article, and its cost is maintenance: every new property has to be added to it by hand. For classes we cannot edit, a cached expression tree copies at the same speed without a constructor per class. When maintaining copying code is not worth it, a serializer round trip copies the whole graph for us at about one to ten microseconds per copy, and a library such as DeepCopy handles graphs that are deep and irregular.

MemberwiseClone() and a record’s with expression both make shallow copies, so we reach for them only when a shallow copy is what we want.

Tested with .NET 10.0.10 and BenchmarkDotNet 0.15.8.